-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add db validate function to check consistency of blockchain database (#…
- Loading branch information
Showing
6 changed files
with
398 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
from pathlib import Path | ||
from typing import Any, Dict, Optional | ||
|
||
from chia.consensus.block_record import BlockRecord | ||
from chia.consensus.default_constants import DEFAULT_CONSTANTS | ||
from chia.types.blockchain_format.sized_bytes import bytes32 | ||
from chia.types.full_block import FullBlock | ||
from chia.util.config import load_config | ||
from chia.util.path import path_from_root | ||
|
||
|
||
def db_validate_func( | ||
root_path: Path, | ||
in_db_path: Optional[Path] = None, | ||
*, | ||
validate_blocks: bool, | ||
) -> None: | ||
if in_db_path is None: | ||
config: Dict[str, Any] = load_config(root_path, "config.yaml")["full_node"] | ||
selected_network: str = config["selected_network"] | ||
db_pattern: str = config["database_path"] | ||
db_path_replaced: str = db_pattern.replace("CHALLENGE", selected_network) | ||
in_db_path = path_from_root(root_path, db_path_replaced) | ||
|
||
validate_v2(in_db_path, validate_blocks=validate_blocks) | ||
|
||
print(f"\n\nDATABASE IS VALID: {in_db_path}\n") | ||
|
||
|
||
def validate_v2(in_path: Path, *, validate_blocks: bool) -> None: | ||
import sqlite3 | ||
from contextlib import closing | ||
|
||
import zstd | ||
|
||
if not in_path.exists(): | ||
print(f"input file doesn't exist. {in_path}") | ||
raise RuntimeError(f"can't find {in_path}") | ||
|
||
print(f"opening file for reading: {in_path}") | ||
with closing(sqlite3.connect(in_path)) as in_db: | ||
|
||
# read the database version | ||
try: | ||
with closing(in_db.execute("SELECT * FROM database_version")) as cursor: | ||
row = cursor.fetchone() | ||
if row is None or row == []: | ||
raise RuntimeError("Database is missing version field") | ||
if row[0] != 2: | ||
raise RuntimeError(f"Database has the wrong version ({row[0]} expected 2)") | ||
except sqlite3.OperationalError: | ||
raise RuntimeError("Database is missing version table") | ||
|
||
try: | ||
with closing(in_db.execute("SELECT hash FROM current_peak WHERE key = 0")) as cursor: | ||
row = cursor.fetchone() | ||
if row is None or row == []: | ||
raise RuntimeError("Database is missing current_peak field") | ||
peak = bytes32(row[0]) | ||
except sqlite3.OperationalError: | ||
raise RuntimeError("Database is missing current_peak table") | ||
|
||
print(f"peak hash: {peak}") | ||
|
||
with closing(in_db.execute("SELECT height FROM full_blocks WHERE header_hash = ?", (peak,))) as cursor: | ||
peak_row = cursor.fetchone() | ||
if peak_row is None or peak_row == []: | ||
raise RuntimeError("Database is missing the peak block") | ||
peak_height = peak_row[0] | ||
|
||
print(f"peak height: {peak_height}") | ||
|
||
print("traversing the full chain") | ||
|
||
current_height = peak_height | ||
# we're looking for a block with this hash | ||
expect_hash = peak | ||
# once we find it, we know what the next block to look for is, which | ||
# this is set to | ||
next_hash = None | ||
|
||
num_orphans = 0 | ||
height_to_hash = bytearray(peak_height * 32) | ||
|
||
with closing( | ||
in_db.execute( | ||
f"SELECT header_hash, prev_hash, height, in_main_chain" | ||
f"{', block, block_record' if validate_blocks else ''} " | ||
"FROM full_blocks ORDER BY height DESC" | ||
) | ||
) as cursor: | ||
|
||
for row in cursor: | ||
|
||
hh = row[0] | ||
prev = row[1] | ||
height = row[2] | ||
in_main_chain = row[3] | ||
|
||
# if there are blocks being added to the database, just ignore | ||
# the ones added since we picked the peak | ||
if height > peak_height: | ||
continue | ||
|
||
if validate_blocks: | ||
block = FullBlock.from_bytes(zstd.decompress(row[4])) | ||
block_record = BlockRecord.from_bytes(row[5]) | ||
actual_header_hash = block.header_hash | ||
actual_prev_hash = block.prev_header_hash | ||
if actual_header_hash != hh: | ||
raise RuntimeError( | ||
f"Block {hh.hex()} has a blob with mismatching " f"hash: {actual_header_hash.hex()}" | ||
) | ||
if block_record.header_hash != hh: | ||
raise RuntimeError( | ||
f"Block {hh.hex()} has a block record with mismatching " | ||
f"hash: {block_record.header_hash.hex()}" | ||
) | ||
if block_record.total_iters != block.total_iters: | ||
raise RuntimeError( | ||
f"Block {hh.hex()} has a block record with mismatching total " | ||
f"iters: {block_record.total_iters} expected {block.total_iters}" | ||
) | ||
if block_record.prev_hash != actual_prev_hash: | ||
raise RuntimeError( | ||
f"Block {hh.hex()} has a block record with mismatching " | ||
f"prev_hash: {block_record.prev_hash} expected {actual_prev_hash.hex()}" | ||
) | ||
if block.height != height: | ||
raise RuntimeError( | ||
f"Block {hh.hex()} has a mismatching " f"height: {block.height} expected {height}" | ||
) | ||
|
||
if height != current_height: | ||
# we're moving to the next level. Make sure we found the block | ||
# we were looking for at the previous level | ||
if next_hash is None: | ||
raise RuntimeError( | ||
f"Database is missing the block with hash {expect_hash} at height {current_height}" | ||
) | ||
expect_hash = next_hash | ||
next_hash = None | ||
current_height = height | ||
|
||
if hh == expect_hash: | ||
if next_hash is not None: | ||
raise RuntimeError(f"Database has multiple blocks with hash {hh.hex()}, " f"at height {height}") | ||
if not in_main_chain: | ||
raise RuntimeError( | ||
f"block {hh.hex()} (height: {height}) is part of the main chain, " | ||
f"but in_main_chain is not set" | ||
) | ||
|
||
if validate_blocks: | ||
if actual_prev_hash != prev: | ||
raise RuntimeError( | ||
f"Block {hh.hex()} has a blob with mismatching " | ||
f"prev-hash: {actual_prev_hash}, expected {prev}" | ||
) | ||
|
||
next_hash = prev | ||
|
||
height_to_hash[height * 32 : height * 32 + 32] = hh | ||
|
||
print(f"\r{height} orphaned blocks: {num_orphans} ", end="") | ||
|
||
else: | ||
if in_main_chain: | ||
raise RuntimeError( | ||
f"block {hh.hex()} (height: {height}) is orphaned, " "but in_main_chain is set" | ||
) | ||
num_orphans += 1 | ||
print("") | ||
|
||
if current_height != 0: | ||
raise RuntimeError(f"Database is missing blocks below height {current_height}") | ||
|
||
# make sure the prev_hash pointer of block height 0 is the genesis | ||
# challenge | ||
if next_hash != DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA: | ||
raise RuntimeError( | ||
f"Blockchain has invalid genesis challenge {next_hash}, expected " | ||
f"{DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA.hex()}" | ||
) | ||
|
||
if num_orphans > 0: | ||
print(f"{num_orphans} orphaned blocks") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.