Skip to content
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 1 commit into from
Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions chia/cmds/db.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from pathlib import Path
import click
from chia.cmds.db_upgrade_func import db_upgrade_func
from chia.cmds.db_validate_func import db_validate_func


@click.group("db", short_help="Manage the blockchain database")
def db_cmd() -> None:
pass


@db_cmd.command("upgrade", short_help="EXPERIMENTAL: upgrade a v1 database to v2")
@db_cmd.command("upgrade", short_help="upgrade a v1 database to v2")
@click.option("--input", default=None, type=click.Path(), help="specify input database file")
@click.option("--output", default=None, type=click.Path(), help="specify output database file")
@click.option(
Expand All @@ -34,7 +35,22 @@ def db_upgrade_cmd(ctx: click.Context, no_update_config: bool, **kwargs) -> None
print(f"FAILED: {e}")


if __name__ == "__main__":
from chia.util.default_root import DEFAULT_ROOT_PATH

db_upgrade_func(DEFAULT_ROOT_PATH)
@db_cmd.command("validate", short_help="validate the (v2) blockchain database. Does not verify proofs")
@click.option("--db", default=None, type=click.Path(), help="Specifies which database file to validate")
@click.option(
"--validate-blocks",
default=False,
is_flag=True,
help="validate consistency of properties of the encoded blocks and block records",
)
@click.pass_context
def db_validate_cmd(ctx: click.Context, validate_blocks: bool, **kwargs) -> None:
try:
in_db_path = kwargs.get("input")

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?

Copy link
Contributor Author

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

db_validate_func(
Path(ctx.obj["root_path"]),
None if in_db_path is None else Path(in_db_path),
validate_blocks=validate_blocks,
)
except RuntimeError as e:
print(f"FAILED: {e}")
187 changes: 187 additions & 0 deletions chia/cmds/db_validate_func.py
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")
2 changes: 1 addition & 1 deletion chia/util/db_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ async def rollback_transaction(self):
cursor = await self.db.execute("ROLLBACK")
await cursor.close()

async def commit_transaction(self):
async def commit_transaction(self) -> None:
await self.db.commit()
15 changes: 1 addition & 14 deletions tests/core/test_db_conversion.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import pytest
import pytest_asyncio
import aiosqlite
import tempfile
import random
import asyncio
from pathlib import Path
from typing import List, Tuple

from tests.setup_nodes import test_constants
from tests.util.temp_file import TempFile

from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.ints import uint32, uint64
Expand All @@ -20,19 +20,6 @@
from chia.consensus.multiprocess_validation import PreValidationResult


class TempFile:
def __init__(self):
self.path = Path(tempfile.NamedTemporaryFile().name)

def __enter__(self) -> Path:
if self.path.exists():
self.path.unlink()
return self.path

def __exit__(self, exc_t, exc_v, exc_tb):
self.path.unlink()


def rand_bytes(num) -> bytes:
ret = bytearray(num)
for i in range(num):
Expand Down
Loading