-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add db validate function to check consistency of blockchain database #10398
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't match the
--db
argument, so it silently discards the filename given on cmdline.Discovered when checking manually the incomplete upgrade, and the config was still pointing to the v1 file.
Change to
kwargs.get("db")
or revert the flag to--input
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the report! fixed here: #10716