Files
chia-blockchain/chia/cmds/db_upgrade_func.py
Matt Hauff 40db4635a8 [LABS-245] Enable PEP604 Ruff rules (#20269)
* Enable PEP604 Ruff rules

* Fix harcoded signature in test

* Hack CLVMStreamable test with note to fast follow
2025-11-18 12:34:00 -08:00

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")