mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-09-27 17:46:28 -04:00
* Enable PEP604 Ruff rules * Fix harcoded signature in test * Hack CLVMStreamable test with note to fast follow
453 lines
18 KiB
Python
453 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
import textwrap
|
|
from contextlib import closing
|
|
from pathlib import Path
|
|
from time import monotonic
|
|
from typing import Any
|
|
|
|
import zstd
|
|
|
|
from chia.util.config import load_config, lock_and_load_config, save_config
|
|
from chia.util.db_wrapper import get_host_parameter_limit
|
|
from chia.util.path import path_from_root
|
|
|
|
|
|
# if either the input database or output database file is specified, the
|
|
# configuration file will not be updated to use the new database. Only when using
|
|
# the currently configured db file, and writing to the default output file will
|
|
# the configuration file also be updated
|
|
def db_upgrade_func(
|
|
root_path: Path,
|
|
in_db_path: Path | None = None,
|
|
out_db_path: Path | None = None,
|
|
*,
|
|
no_update_config: bool = False,
|
|
force: bool = False,
|
|
) -> None:
|
|
update_config: bool = in_db_path is None and out_db_path is None and not no_update_config
|
|
|
|
config: dict[str, Any]
|
|
selected_network: str
|
|
db_pattern: str
|
|
if in_db_path is None or out_db_path is None:
|
|
config = load_config(root_path, "config.yaml")["full_node"]
|
|
selected_network = config["selected_network"]
|
|
db_pattern = config["database_path"]
|
|
|
|
db_path_replaced: str
|
|
if in_db_path is None:
|
|
db_path_replaced = db_pattern.replace("CHALLENGE", selected_network)
|
|
in_db_path = path_from_root(root_path, db_path_replaced)
|
|
|
|
if out_db_path is None:
|
|
db_path_replaced = db_pattern.replace("CHALLENGE", selected_network).replace("_v1_", "_v2_")
|
|
out_db_path = path_from_root(root_path, db_path_replaced)
|
|
out_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
_, _, free = shutil.disk_usage(out_db_path.parent)
|
|
in_db_size = in_db_path.stat().st_size
|
|
if free < in_db_size:
|
|
no_free: bool = free < in_db_size * 0.6
|
|
strength: str
|
|
if no_free:
|
|
strength = "probably not enough"
|
|
else:
|
|
strength = "very little"
|
|
print(f"there is {strength} free space on the volume where the output database will be written:")
|
|
print(f" {out_db_path}")
|
|
print(
|
|
f"free space: {free / 1024 / 1024 / 1024:0.2f} GiB expected about "
|
|
f"{in_db_size / 1024 / 1024 / 1024:0.2f} GiB"
|
|
)
|
|
if no_free and not force:
|
|
print("to override this check and convert anyway, pass --force")
|
|
return
|
|
|
|
try:
|
|
convert_v1_to_v2(in_db_path, out_db_path)
|
|
|
|
if update_config:
|
|
print("updating config.yaml")
|
|
with lock_and_load_config(root_path, "config.yaml") as config:
|
|
new_db_path = db_pattern.replace("_v1_", "_v2_")
|
|
config["full_node"]["database_path"] = new_db_path
|
|
print(f"database_path: {new_db_path}")
|
|
save_config(root_path, "config.yaml", config)
|
|
|
|
except RuntimeError as e:
|
|
print(f"conversion failed with error: {e}.")
|
|
except Exception as e:
|
|
print(
|
|
textwrap.dedent(
|
|
f"""\
|
|
conversion failed with error: {e}.
|
|
The target v2 database is left in place (possibly in an incomplete state)
|
|
{out_db_path}
|
|
If the failure was caused by a full disk, ensure the volumes of your
|
|
temporary- and target directory have sufficient free space."""
|
|
)
|
|
)
|
|
if platform.system() == "Windows":
|
|
temp_dir = None
|
|
# this is where GetTempPath() looks
|
|
# https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha
|
|
if "TMP" in os.environ:
|
|
temp_dir = os.environ["TMP"]
|
|
elif "TEMP" in os.environ:
|
|
temp_dir = os.environ["TEMP"]
|
|
elif "USERPROFILE" in os.environ:
|
|
temp_dir = os.environ["USERPROFILE"]
|
|
if temp_dir is not None:
|
|
print(f"your temporary directory may be {temp_dir}")
|
|
temp_env = "TMP"
|
|
else:
|
|
temp_env = "SQLITE_TMPDIR"
|
|
print(f'you can specify the "{temp_env}" environment variable to control the temporary directory to be used')
|
|
|
|
print(f"\n\nLEAVING PREVIOUS DB FILE UNTOUCHED {in_db_path}\n")
|
|
|
|
|
|
def convert_v1_to_v2(in_path: Path, out_path: Path) -> None:
|
|
BATCH_SIZE = 300_000
|
|
if not in_path.exists():
|
|
raise RuntimeError(f"Input file doesn't exist. {in_path}")
|
|
if in_path == out_path:
|
|
raise RuntimeError(f"Output file is the same as the input {in_path}")
|
|
if out_path.exists():
|
|
raise RuntimeError(f"Output file already exists. {out_path}")
|
|
|
|
print(f"-- Opening file for reading: {in_path}")
|
|
with sqlite3.connect(in_path) as conn:
|
|
try:
|
|
with closing(conn.execute("SELECT version FROM database_version LIMIT 1")) as cursor:
|
|
row = cursor.fetchone()
|
|
if row is not None and row[0] != 1:
|
|
raise RuntimeError(f"Blockchain database already version {row[0]}. Won't convert")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
try:
|
|
conn.execute("SELECT unhex('00')")
|
|
except sqlite3.OperationalError:
|
|
print("-- No built-in unhex(), falling back to bytes.fromhex()")
|
|
conn.create_function("unhex", 1, bytes.fromhex)
|
|
conn.create_function("zstd_compress", 1, zstd.compress)
|
|
conn.execute("PRAGMA synchronous=off")
|
|
temp_dir = tempfile.gettempdir()
|
|
_, _, free = shutil.disk_usage(temp_dir)
|
|
if free < 50 * 1024 * 1024 * 1024:
|
|
print(f"-- Setting temp_store_directory to: {out_path.parent}")
|
|
conn.execute(f"PRAGMA temp_store_directory = '{out_path.parent}'")
|
|
print(f"-- Opening file for writing: {out_path}")
|
|
conn.execute("ATTACH DATABASE ? AS out_db", (str(out_path),))
|
|
conn.execute("PRAGMA out_db.journal_mode=off")
|
|
conn.execute("PRAGMA out_db.synchronous=off")
|
|
conn.execute("PRAGMA out_db.locking_mode=exclusive")
|
|
conn.execute("PRAGMA out_db.cache_size=131072")
|
|
print("-- Initializing v2 version")
|
|
conn.execute("CREATE TABLE out_db.database_version(version int)")
|
|
conn.execute("INSERT INTO out_db.database_version VALUES(?)", (2,))
|
|
print("-- Initializing current_peak")
|
|
conn.execute("CREATE TABLE out_db.current_peak(key int PRIMARY KEY, hash blob)")
|
|
with closing(conn.execute("SELECT header_hash, height FROM block_records WHERE is_peak = 1 LIMIT 1")) as cursor:
|
|
peak_row = cursor.fetchone()
|
|
if peak_row is None:
|
|
raise RuntimeError("v1 database does not have a peak block, there is no blockchain to convert")
|
|
peak_hash, peak_height = peak_row
|
|
print(f"-- Peak: {peak_hash} Height: {peak_height}")
|
|
conn.execute("INSERT INTO out_db.current_peak VALUES(?, ?)", (0, bytes.fromhex(peak_hash)))
|
|
conn.commit()
|
|
print("-- DB v1 to v2 conversion started")
|
|
print("-- [1/4] Converting full_blocks")
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE out_db.full_blocks(
|
|
header_hash blob PRIMARY KEY,
|
|
prev_hash blob,
|
|
height bigint,
|
|
sub_epoch_summary blob,
|
|
is_fully_compactified tinyint,
|
|
in_main_chain tinyint,
|
|
block blob,
|
|
block_record blob
|
|
)
|
|
"""
|
|
)
|
|
conn.commit()
|
|
parameter_limit = get_host_parameter_limit()
|
|
start_time = monotonic()
|
|
block_start_time = start_time
|
|
rowids: list[int] = []
|
|
small_batch_size = BATCH_SIZE <= parameter_limit
|
|
small_chain = peak_height <= parameter_limit
|
|
current_header_hash = peak_hash
|
|
current_height = peak_height
|
|
insertions_vs_batch = 0
|
|
rate = 1.0
|
|
while current_height >= 0:
|
|
while len(rowids) < parameter_limit:
|
|
if current_height < 0:
|
|
break
|
|
with closing(
|
|
conn.execute(
|
|
"SELECT rowid, prev_hash FROM block_records WHERE header_hash = ? AND height = ? LIMIT 1",
|
|
(current_header_hash, current_height),
|
|
)
|
|
) as cursor:
|
|
row = cursor.fetchone()
|
|
if row is None:
|
|
raise RuntimeError(f"rowid not found for {current_header_hash} at height {current_height}")
|
|
rowid, prev_hash = row
|
|
rowids.append(rowid)
|
|
current_header_hash = prev_hash
|
|
current_height -= 1
|
|
conn.execute(
|
|
f"""
|
|
INSERT INTO out_db.full_blocks
|
|
SELECT
|
|
unhex(br.header_hash),
|
|
unhex(br.prev_hash),
|
|
br.height, br.sub_epoch_summary,
|
|
fb.is_fully_compactified,
|
|
1 AS in_main_chain,
|
|
zstd_compress(fb.block),
|
|
br.block
|
|
FROM block_records br
|
|
JOIN full_blocks fb ON br.header_hash = fb.header_hash
|
|
WHERE br.rowid IN ({",".join("?" * len(rowids))})
|
|
""",
|
|
rowids,
|
|
)
|
|
insertions_vs_batch += len(rowids)
|
|
rowids = []
|
|
if insertions_vs_batch >= BATCH_SIZE or small_batch_size or small_chain:
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
rate = BATCH_SIZE / (end_time - start_time)
|
|
print(
|
|
f"\r{current_height:10d} {(peak_height - current_height) * 100 / peak_height:.3f}% "
|
|
f"{rate:0.1f} blocks/s ETA: {current_height // rate} s ",
|
|
end="",
|
|
)
|
|
sys.stdout.flush()
|
|
start_time = end_time
|
|
insertions_vs_batch = 0
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [1/4] Converting full_blocks SUCCEEDED in "
|
|
f"{end_time - block_start_time:.2f} seconds "
|
|
)
|
|
print("-- [1/4] Creating full_blocks height index")
|
|
height_index_start_time = monotonic()
|
|
conn.execute("CREATE INDEX out_db.height ON full_blocks(height)")
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [1/4] Creating full_blocks height index SUCCEEDED in "
|
|
f"{end_time - height_index_start_time:.2f} seconds "
|
|
)
|
|
print("-- [1/4] Creating full_blocks is_fully_compactified index")
|
|
ifc_index_start_time = monotonic()
|
|
conn.execute(
|
|
"""
|
|
CREATE INDEX out_db.is_fully_compactified
|
|
ON full_blocks(is_fully_compactified, in_main_chain)
|
|
WHERE in_main_chain=1
|
|
"""
|
|
)
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [1/4] Creating full_blocks is_fully_compactified index SUCCEEDED in "
|
|
f"{end_time - ifc_index_start_time:.2f} seconds "
|
|
)
|
|
print("-- [1/4] Creating full_blocks main_chain index")
|
|
main_chain_index_start_time = monotonic()
|
|
conn.execute(
|
|
"""
|
|
CREATE INDEX out_db.main_chain
|
|
ON full_blocks(height, in_main_chain)
|
|
WHERE in_main_chain=1
|
|
"""
|
|
)
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [1/4] Creating full_blocks main_chain index SUCCEEDED in "
|
|
f"{end_time - main_chain_index_start_time:.2f} seconds "
|
|
)
|
|
print("-- [2/4] Converting sub_epoch_segments_v3")
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE out_db.sub_epoch_segments_v3(
|
|
ses_block_hash blob PRIMARY KEY,
|
|
challenge_segments blob
|
|
)
|
|
"""
|
|
)
|
|
conn.commit()
|
|
ses_start_time = monotonic()
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO out_db.sub_epoch_segments_v3
|
|
SELECT
|
|
unhex(ses_block_hash),
|
|
challenge_segments
|
|
FROM sub_epoch_segments_v3
|
|
"""
|
|
)
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [2/4] Converting sub_epoch_segments_v3 SUCCEEDED in "
|
|
f"{end_time - ses_start_time:.2f} seconds "
|
|
)
|
|
print("-- [3/4] Converting hints")
|
|
conn.execute("CREATE TABLE out_db.hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))")
|
|
conn.commit()
|
|
start_time = monotonic()
|
|
hint_start_time = start_time
|
|
hints_count = 0
|
|
rate = 1.0
|
|
range_start = 1
|
|
while True:
|
|
with closing(
|
|
conn.execute("SELECT id FROM hints WHERE id >= ? LIMIT ?", (range_start, parameter_limit))
|
|
) as cursor:
|
|
rows = cursor.fetchall()
|
|
if len(rows) == 0:
|
|
break
|
|
conn.execute(
|
|
f"""
|
|
INSERT OR IGNORE INTO out_db.hints
|
|
SELECT
|
|
coin_id,
|
|
hint
|
|
FROM hints
|
|
WHERE id IN ({",".join("?" * len(rows))})
|
|
""",
|
|
[id_tuple[0] for id_tuple in rows],
|
|
)
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
rate = parameter_limit / (end_time - start_time)
|
|
start_time = end_time
|
|
hints_count += len(rows)
|
|
print(f"\r{hints_count // 1000:10d}k hints {rate:0.1f} hints/s ", end="")
|
|
sys.stdout.flush()
|
|
range_start += parameter_limit
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [3/4] Converting hints SUCCEEDED in "
|
|
f"{end_time - hint_start_time:.2f} seconds "
|
|
)
|
|
print("-- [3/4] Creating hints hint_index index")
|
|
hint_index_start_time = monotonic()
|
|
conn.execute("CREATE INDEX out_db.hint_index on hints(hint)")
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [3/4] Creating hints hint_index index SUCCEEDED in "
|
|
f"{end_time - hint_index_start_time:.2f} seconds "
|
|
)
|
|
print("-- [4/4] Converting coin_record")
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE out_db.coin_record(
|
|
coin_name blob PRIMARY KEY,
|
|
confirmed_index bigint,
|
|
spent_index bigint,
|
|
coinbase int,
|
|
puzzle_hash blob,
|
|
coin_parent blob,
|
|
amount blob,
|
|
timestamp bigint
|
|
)
|
|
"""
|
|
)
|
|
conn.commit()
|
|
start_time = monotonic()
|
|
coin_start_time = start_time
|
|
coins_count = 0
|
|
rate = 1.0
|
|
print("-- [4/4] Creating temp table (slow, patience)", end="\r")
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE temp.coin_record AS
|
|
SELECT
|
|
unhex(coin_name),
|
|
confirmed_index,
|
|
CASE WHEN spent_index <= ? THEN spent_index ELSE 0 END AS spent_index,
|
|
coinbase,
|
|
unhex(puzzle_hash),
|
|
unhex(coin_parent),
|
|
amount,
|
|
timestamp
|
|
FROM coin_record
|
|
WHERE confirmed_index <= ?
|
|
ORDER BY unhex(coin_name)
|
|
""",
|
|
(peak_height, peak_height),
|
|
)
|
|
with closing(conn.execute("SELECT * FROM temp.coin_record")) as cursor:
|
|
while True:
|
|
rows = cursor.fetchmany(BATCH_SIZE)
|
|
if len(rows) == 0:
|
|
break
|
|
conn.executemany("INSERT INTO out_db.coin_record VALUES (?, ?, ?, ?, ?, ?, ?, ?)", rows)
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
rate = len(rows) / (end_time - start_time)
|
|
start_time = end_time
|
|
coins_count += len(rows)
|
|
print(f"\r{coins_count // 1000:10d}k coins {rate:0.1f} coins/s ", end="")
|
|
sys.stdout.flush()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [4/4] Converting coin_record SUCCEEDED in "
|
|
f"{end_time - coin_start_time:.2f} seconds "
|
|
)
|
|
print("-- [4/4] Creating coin_record coin_confirmed_index index")
|
|
coin_confirmed_index_start_time = monotonic()
|
|
conn.execute("CREATE INDEX out_db.coin_confirmed_index ON coin_record(confirmed_index)")
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [4/4] Creating coin_record coin_confirmed_index index SUCCEEDED in "
|
|
f"{end_time - coin_confirmed_index_start_time:.2f} seconds "
|
|
)
|
|
print("-- [4/4] Creating coin_record coin_spent_index index")
|
|
coin_spent_index_start_time = monotonic()
|
|
conn.execute("CREATE INDEX out_db.coin_spent_index ON coin_record(spent_index)")
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [4/4] Creating coin_record coin_spent_index index SUCCEEDED in "
|
|
f"{end_time - coin_spent_index_start_time:.2f} seconds "
|
|
)
|
|
print("-- [4/4] Creating coin_record coin_puzzle_hash index")
|
|
coin_puzzle_hash_index_start_time = monotonic()
|
|
conn.execute("CREATE INDEX out_db.coin_puzzle_hash ON coin_record(puzzle_hash)")
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [4/4] Creating coin_record coin_puzzle_hash index SUCCEEDED in "
|
|
f"{end_time - coin_puzzle_hash_index_start_time:.2f} seconds "
|
|
)
|
|
print("-- [4/4] Creating coin_record coin_parent_index index")
|
|
coin_parent_index_start_time = monotonic()
|
|
conn.execute("CREATE INDEX out_db.coin_parent_index ON coin_record(coin_parent)")
|
|
conn.commit()
|
|
end_time = monotonic()
|
|
print(
|
|
"\r-- [4/4] Creating coin_record coin_parent_index index SUCCEEDED in "
|
|
f"{end_time - coin_parent_index_start_time:.2f} seconds "
|
|
)
|
|
conn.execute("DETACH DATABASE out_db")
|