Slim CoinStoreProtocol down to the methods consensus uses (#21168)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Richard Kiss
2026-07-27 12:40:33 -07:00
committed by GitHub
co-authored by Cursor
parent b966c5c96b
commit 6d5b52062b
4 changed files with 32 additions and 122 deletions
+18 -8
View File
@@ -15,6 +15,7 @@ import pytest
from chia_rs import (
AugSchemeMPL,
BlockRecord,
CoinRecord,
ConsensusConstants,
EndOfSubSlotBundle,
FullBlock,
@@ -54,6 +55,7 @@ from chia.consensus.generator_tools import get_block_header
from chia.consensus.get_block_generator import get_block_generator
from chia.consensus.multiprocess_validation import PreValidationResult, pre_validate_block
from chia.consensus.pot_iterations import is_overflow_block
from chia.full_node.coin_store import CoinStore
from chia.simulator.block_tools import BlockTools, create_block_tools_async
from chia.simulator.keyring import TempKeyring
from chia.simulator.vdf_prover import get_vdf_info_and_proof
@@ -86,6 +88,14 @@ log = logging.getLogger(__name__)
bad_element = ClassgroupElement.create(b"\x00")
async def get_coin_record(b: Blockchain, coin_id: bytes32) -> CoinRecord | None:
# single-record lookup is not part of the consensus coin store protocol,
# but tests know the concrete store
coin_store = b.coin_store
assert isinstance(coin_store, CoinStore)
return await coin_store.get_coin_record(coin_id)
@asynccontextmanager
async def make_empty_blockchain(constants: ConsensusConstants) -> AsyncIterator[Blockchain]:
"""
@@ -2166,7 +2176,7 @@ class TestBodyValidation:
if expected == AddBlockResult.NEW_PEAK:
# ensure coin was in fact spent
c = await b.coin_store.get_coin_record(coin.name())
c = await get_coin_record(b, coin.name())
assert c is not None and c.spent
@pytest.mark.anyio
@@ -2372,10 +2382,10 @@ class TestBodyValidation:
if expected == AddBlockResult.NEW_PEAK:
# ensure coin1 was in fact spent
c = await b.coin_store.get_coin_record(coin1.name())
c = await get_coin_record(b, coin1.name())
assert c is not None and c.spent
# ensure coin2 was NOT spent
c = await b.coin_store.get_coin_record(coin2.name())
c = await get_coin_record(b, coin2.name())
assert c is not None and not c.spent
@pytest.mark.anyio
@@ -3182,9 +3192,9 @@ class TestBodyValidation:
)
# ephemeral coin is spent
first_coin = await b.coin_store.get_coin_record(new_coin.name())
first_coin = await get_coin_record(b, new_coin.name())
assert first_coin is not None and first_coin.spent
second_coin = await b.coin_store.get_coin_record(tx_2.additions()[0].name())
second_coin = await get_coin_record(b, tx_2.additions()[0].name())
assert second_coin is not None and not second_coin.spent
farmer_coin = create_farmer_coin(
@@ -3200,7 +3210,7 @@ class TestBodyValidation:
)
await _validate_and_add_block(b, blocks_reorg[-1])
farmer_coin_record = await b.coin_store.get_coin_record(farmer_coin.name())
farmer_coin_record = await get_coin_record(b, farmer_coin.name())
assert farmer_coin_record is not None and farmer_coin_record.spent
@pytest.mark.anyio
@@ -4059,11 +4069,11 @@ async def test_chain_failed_rollback(empty_blockchain: Blockchain, bt: BlockTool
await _validate_and_add_block(b, block, expected_result=AddBlockResult.ADDED_AS_ORPHAN, fork_info=fork_info)
# Incorrectly set the height as spent in DB to trigger an error
print(f"{await b.coin_store.get_coin_record(spend_bundle.coin_spends[0].coin.name())}")
print(f"{await get_coin_record(b, spend_bundle.coin_spends[0].coin.name())}")
print(spend_bundle.coin_spends[0].coin.name())
# await b.coin_store._set_spent([spend_bundle.coin_spends[0].coin.name()], 8)
await b.coin_store.rollback_to_block(2)
print(f"{await b.coin_store.get_coin_record(spend_bundle.coin_spends[0].coin.name())}")
print(f"{await get_coin_record(b, spend_bundle.coin_spends[0].coin.name())}")
fork_block = blocks_reorg_chain[10 - 1]
# fork_info = ForkInfo(fork_block.height, fork_block.height, fork_block.header_hash)
+6 -105
View File
@@ -3,18 +3,21 @@ from __future__ import annotations
from collections.abc import Collection
from typing import Protocol
from chia_rs import CoinRecord, CoinState
from chia_rs import CoinRecord
from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint32, uint64
from chia.types.blockchain_format.coin import Coin
from chia.types.mempool_item import UnspentLineageInfo
class CoinStoreProtocol(Protocol):
"""
Protocol defining the interface for CoinStore.
The coin store interface used by `chia.consensus`.
This is a substitute for importing from chia.full_node.coin_store directly.
The concrete `CoinStore` has a much larger surface (puzzle hash queries,
coin states, lineage lookups, etc.), but those methods serve RPCs and the
wallet protocol, not consensus, so they are not part of this protocol.
"""
async def new_block(
@@ -29,11 +32,6 @@ class CoinStoreProtocol(Protocol):
Add a new block to the coin store
"""
async def get_coin_record(self, coin_id: bytes32) -> CoinRecord | None:
"""
Returns the coin record for the specified coin id
"""
async def get_coin_records(self, coin_ids: Collection[bytes32]) -> list[CoinRecord]:
"""
Returns the coin records for the specified coin ids
@@ -49,104 +47,7 @@ class CoinStoreProtocol(Protocol):
Returns the coins removed at a specific height
"""
async def get_coin_records_by_puzzle_hash(
self,
include_spent_coins: bool,
puzzle_hash: bytes32,
start_height: uint32 = ...,
end_height: uint32 = ...,
) -> list[CoinRecord]:
"""
Returns the coin records for a specific puzzle hash
"""
async def get_coin_records_by_puzzle_hashes(
self,
coins: bool,
puzzle_hashes: list[bytes32],
start_height: uint32 = ...,
end_height: uint32 = ...,
) -> list[CoinRecord]:
"""
Returns the coin records for a list of puzzle hashes
"""
async def get_coin_records_by_names(
self,
include_spent_coins: bool,
names: list[bytes32],
start_height: uint32 = ...,
end_height: uint32 = ...,
) -> list[CoinRecord]:
"""
Returns the coin records for a list of coin names
"""
async def get_coin_states_by_puzzle_hashes(
self,
include_spent_coins: bool,
puzzle_hashes: set[bytes32],
min_height: uint32 = uint32(0),
*,
max_items: int = ...,
) -> set[CoinState]:
"""
Returns the coin states for a set of puzzle hashes
"""
async def get_coin_records_by_parent_ids(
self,
include_spent_coins: bool,
parent_ids: list[bytes32],
start_height: uint32 = ...,
end_height: uint32 = ...,
*,
max_items: int = ...,
) -> list[CoinRecord]:
"""
Returns the coin records for a list of parent ids
"""
async def get_coin_states_by_ids(
self,
include_spent_coins: bool,
coin_ids: Collection[bytes32],
min_height: uint32 = uint32(0),
*,
max_height: uint32 = ...,
max_items: int = ...,
) -> list[CoinState]:
"""
Returns the coin states for a collection of coin ids
"""
async def batch_coin_states_by_puzzle_hashes(
self,
puzzle_hashes: list[bytes32],
*,
min_height: uint32 = ...,
include_spent: bool = ...,
include_unspent: bool = ...,
include_hinted: bool = ...,
min_amount: uint64 = ...,
max_items: int = ...,
) -> tuple[list[CoinState], uint32 | None]:
"""
Returns the coin states, as well as the next block height (or `None` if finished).
"""
async def get_unspent_lineage_info_for_puzzle_hash(self, puzzle_hash: bytes32) -> UnspentLineageInfo | None:
"""
Lookup the most recent unspent lineage that matches a puzzle hash
"""
async def rollback_to_block(self, block_index: int) -> dict[bytes32, CoinRecord]:
"""
Rolls back the blockchain to the specified block index
"""
# DEPRECATED: do not use in new code
async def is_empty(self) -> bool:
"""
Returns True if the coin store is empty
"""
+2 -3
View File
@@ -44,7 +44,6 @@ from chia.consensus.block_creation import unfinished_block_to_full_block_with_mm
from chia.consensus.block_height_map import BlockHeightMap
from chia.consensus.blockchain import AddBlockResult, Blockchain, BlockchainMutexPriority, StateChangeSummary
from chia.consensus.blockchain_interface import BlockchainInterface
from chia.consensus.coin_store_protocol import CoinStoreProtocol
from chia.consensus.condition_tools import pkm_pairs
from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty
from chia.consensus.get_block_challenge import post_hard_fork2
@@ -171,7 +170,7 @@ class FullNode:
_db_wrapper: DBWrapper2 | None = None
_hint_store: HintStore | None = None
_block_store: BlockStore | None = None
_coin_store: CoinStoreProtocol | None = None
_coin_store: CoinStore | None = None
_mempool_manager: MempoolManager | None = None
_init_weight_proof: asyncio.Task[None] | None = None
_blockchain: Blockchain | None = None
@@ -456,7 +455,7 @@ class FullNode:
return self._pool
@property
def coin_store(self) -> CoinStoreProtocol:
def coin_store(self) -> CoinStore:
assert self._coin_store is not None
return self._coin_store
+6 -6
View File
@@ -705,7 +705,7 @@ class FullNodeRpcApi:
if "include_spent_coins" in request:
kwargs["include_spent_coins"] = request["include_spent_coins"]
coin_records = await self.service.blockchain.coin_store.get_coin_records_by_puzzle_hash(**kwargs)
coin_records = await self.service.coin_store.get_coin_records_by_puzzle_hash(**kwargs)
return {"coin_records": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in coin_records]}
@@ -727,7 +727,7 @@ class FullNodeRpcApi:
if "include_spent_coins" in request:
kwargs["include_spent_coins"] = request["include_spent_coins"]
coin_records = await self.service.blockchain.coin_store.get_coin_records_by_puzzle_hashes(**kwargs)
coin_records = await self.service.coin_store.get_coin_records_by_puzzle_hashes(**kwargs)
return {"coin_records": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in coin_records]}
@@ -739,7 +739,7 @@ class FullNodeRpcApi:
raise RpcError.simple(RpcErrorCodes.NAME_NOT_IN_REQUEST, "Name not in request")
name = bytes32.from_hexstr(request["name"])
coin_record: CoinRecord | None = await self.service.blockchain.coin_store.get_coin_record(name)
coin_record: CoinRecord | None = await self.service.coin_store.get_coin_record(name)
if coin_record is None:
raise RpcError(
RpcErrorCodes.COIN_RECORD_NOT_FOUND,
@@ -768,7 +768,7 @@ class FullNodeRpcApi:
if "include_spent_coins" in request:
kwargs["include_spent_coins"] = request["include_spent_coins"]
coin_records = await self.service.blockchain.coin_store.get_coin_records_by_names(**kwargs)
coin_records = await self.service.coin_store.get_coin_records_by_names(**kwargs)
return {"coin_records": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in coin_records]}
@@ -790,7 +790,7 @@ class FullNodeRpcApi:
if "include_spent_coins" in request:
kwargs["include_spent_coins"] = request["include_spent_coins"]
coin_records = await self.service.blockchain.coin_store.get_coin_records_by_parent_ids(**kwargs)
coin_records = await self.service.coin_store.get_coin_records_by_parent_ids(**kwargs)
return {"coin_records": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in coin_records]}
@@ -819,7 +819,7 @@ class FullNodeRpcApi:
if "include_spent_coins" in request:
kwargs["include_spent_coins"] = request["include_spent_coins"]
coin_records = await self.service.blockchain.coin_store.get_coin_records_by_names(**kwargs)
coin_records = await self.service.coin_store.get_coin_records_by_names(**kwargs)
return {"coin_records": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in coin_records]}