From b966c5c96b8f9bed393f03dbd577ac19d4cfef59 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 27 Jul 2026 12:40:06 -0700 Subject: [PATCH] Add BlockStoreProtocol; drop consensus dependency on chia.full_node (#21169) * Add BlockStoreProtocol; drop consensus dependency on chia.full_node Co-authored-by: Cursor * Assert isinstance directly on bc.block_store Review feedback: drop the intermediate block_store assignment; mypy narrows the attribute expression after the isinstance assert. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../blockchain/blockchain_test_utils.py | 5 +- chia/consensus/block_store_protocol.py | 67 +++++++++++++++++++ chia/consensus/blockchain.py | 6 +- chia/full_node/block_store.py | 17 +++-- tach.toml | 1 - 5 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 chia/consensus/block_store_protocol.py diff --git a/chia/_tests/blockchain/blockchain_test_utils.py b/chia/_tests/blockchain/blockchain_test_utils.py index ebf61f6a67..cac867b14a 100644 --- a/chia/_tests/blockchain/blockchain_test_utils.py +++ b/chia/_tests/blockchain/blockchain_test_utils.py @@ -8,11 +8,14 @@ from chia.consensus.block_body_validation import ForkInfo from chia.consensus.blockchain import AddBlockResult, Blockchain from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty from chia.consensus.multiprocess_validation import PreValidationResult, pre_validate_block +from chia.full_node.block_store import BlockStore from chia.types.validation_state import ValidationState from chia.util.errors import Err async def check_block_store_invariant(bc: Blockchain) -> None: + # this checks sqlite-level invariants, so it needs the concrete store + assert isinstance(bc.block_store, BlockStore) db_wrapper = bc.block_store.db_wrapper if db_wrapper.db_version == 1: @@ -20,7 +23,7 @@ async def check_block_store_invariant(bc: Blockchain) -> None: in_chain = set() max_height = -1 - async with bc.block_store.transaction() as conn: + async with db_wrapper.writer() as conn: async with conn.execute("SELECT height, in_main_chain FROM full_blocks") as cursor: rows = await cursor.fetchall() for row in rows: diff --git a/chia/consensus/block_store_protocol.py b/chia/consensus/block_store_protocol.py new file mode 100644 index 0000000000..762589b8f8 --- /dev/null +++ b/chia/consensus/block_store_protocol.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from contextlib import AbstractAsyncContextManager +from typing import Protocol + +from chia_rs import BlockRecord, FullBlock, SubEpochChallengeSegment +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint32 + + +class BlockStoreProtocol(Protocol): + """ + The block store interface used by `chia.consensus`. + This is a substitute for importing from chia.full_node.block_store directly. + + The concrete `BlockStore` has a larger surface (block blobs by range, + compactification queries, etc.), but those methods serve peer sync and + RPCs, not consensus, so they are not part of this protocol. + """ + + async def add_full_block(self, header_hash: bytes32, block: FullBlock, block_record: BlockRecord) -> None: ... + + async def get_block_record(self, header_hash: bytes32) -> BlockRecord | None: ... + + async def get_block_records_by_hash(self, header_hashes: list[bytes32]) -> list[BlockRecord]: ... + + async def get_block_records_in_range(self, start: int, stop: int) -> dict[bytes32, BlockRecord]: ... + + async def get_block_records_close_to_peak( + self, blocks_n: int + ) -> tuple[dict[bytes32, BlockRecord], bytes32 | None]: ... + + async def get_prev_hash(self, header_hash: bytes32) -> bytes32: ... + + async def get_full_block(self, header_hash: bytes32) -> FullBlock | None: ... + + async def get_blocks_by_hash(self, header_hashes: list[bytes32]) -> list[FullBlock]: ... + + async def get_generator(self, header_hash: bytes32) -> bytes | None: ... + + async def get_generators_at(self, heights: set[uint32]) -> dict[uint32, bytes]: ... + + async def rollback(self, height: int) -> None: ... + + async def set_in_chain(self, header_hashes: list[tuple[bytes32]]) -> None: ... + + async def set_peak(self, header_hash: bytes32) -> None: ... + + def transaction(self) -> AbstractAsyncContextManager[None]: + """ + A write transaction scope. Store methods called within the scope are + atomic. The context manager deliberately yields None: the underlying + database connection is an implementation detail of the store. + """ + ... + + def get_block_from_cache(self, header_hash: bytes32) -> FullBlock | None: ... + + def rollback_cache_block(self, header_hash: bytes32) -> None: ... + + async def persist_sub_epoch_challenge_segments( + self, ses_block_hash: bytes32, segments: list[SubEpochChallengeSegment] + ) -> None: ... + + async def get_sub_epoch_challenge_segments( + self, ses_block_hash: bytes32 + ) -> list[SubEpochChallengeSegment] | None: ... diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index e5b5189f54..28af79fc89 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -28,6 +28,7 @@ from chia_rs.sized_ints import uint16, uint32, uint64, uint128 from chia.consensus.block_body_validation import ForkInfo, validate_block_body from chia.consensus.block_header_validation import validate_unfinished_header_block from chia.consensus.block_height_map import BlockHeightMap +from chia.consensus.block_store_protocol import BlockStoreProtocol from chia.consensus.blockchain_interface import MMRManagerProtocol from chia.consensus.blockchain_mmr import BlockchainMMRManager from chia.consensus.coin_store_protocol import CoinStoreProtocol @@ -38,7 +39,6 @@ from chia.consensus.generator_tools import get_block_header from chia.consensus.get_block_challenge import pre_sp_tx_block_height from chia.consensus.get_block_generator import get_block_generator from chia.consensus.multiprocess_validation import PreValidationResult -from chia.full_node.block_store import BlockStore from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.vdf import VDFInfo from chia.types.generator_types import BlockGenerator @@ -105,7 +105,7 @@ class Blockchain: # Unspent Store coin_store: CoinStoreProtocol # Store - block_store: BlockStore + block_store: BlockStoreProtocol mmr_manager: MMRManagerProtocol # Used to verify blocks in parallel pool: Executor @@ -124,7 +124,7 @@ class Blockchain: @staticmethod async def create( coin_store: CoinStoreProtocol, - block_store: BlockStore, + block_store: BlockStoreProtocol, height_map: BlockHeightMap, consensus_constants: ConsensusConstants, pool: Executor, diff --git a/chia/full_node/block_store.py b/chia/full_node/block_store.py index e923913a86..d37da83fe1 100644 --- a/chia/full_node/block_store.py +++ b/chia/full_node/block_store.py @@ -3,15 +3,17 @@ from __future__ import annotations import dataclasses import logging import sqlite3 -from contextlib import AbstractAsyncContextManager +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, ClassVar, cast -import aiosqlite import typing_extensions import zstd from chia_rs import BlockRecord, FullBlock, SubEpochChallengeSegment, SubEpochSegments from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint32 +from chia.consensus.block_store_protocol import BlockStoreProtocol from chia.full_node.full_block_utils import GeneratorBlockInfo, block_info_from_block, generator_from_block from chia.util.batches import to_batches from chia.util.db_wrapper import DBWrapper2, execute_fetchone @@ -38,6 +40,9 @@ def decompress_blob(block_bytes: bytes) -> bytes: @typing_extensions.final @dataclasses.dataclass class BlockStore: + if TYPE_CHECKING: + _protocol_check: ClassVar[BlockStoreProtocol] = cast("BlockStore", None) + block_cache: LRUCache[bytes32, FullBlock] db_wrapper: DBWrapper2 ses_challenge_cache: LRUCache[bytes32, list[SubEpochChallengeSegment]] @@ -191,8 +196,12 @@ class BlockStore: return challenge_segments return None - def transaction(self) -> AbstractAsyncContextManager[aiosqlite.Connection]: - return self.db_wrapper.writer() + @asynccontextmanager + async def transaction(self) -> AsyncIterator[None]: + # the database connection is deliberately not exposed: callers only + # get an atomic scope, not access to the underlying database + async with self.db_wrapper.writer(): + yield def get_block_from_cache(self, header_hash: bytes32) -> FullBlock | None: return self.block_cache.get(header_hash) diff --git a/tach.toml b/tach.toml index a5f6597559..2a9fd3d089 100644 --- a/tach.toml +++ b/tach.toml @@ -22,7 +22,6 @@ path = "chia.consensus" depends_on = [ "chia.types", "chia.util", - { path = "chia.full_node", deprecated = false }, ] [[modules]]