From 7c84f8ef05e10383a07fea9e47e3c9f901ae14b6 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Tue, 7 Jan 2025 16:34:45 +0100 Subject: [PATCH] Clarify cases when transactions filter is computed with just reward coins. --- chia/_tests/blockchain/test_blockchain.py | 16 ++++++++++++++++ chia/consensus/blockchain.py | 16 ++++++++++------ chia/consensus/multiprocess_validation.py | 14 ++++++++------ chia/full_node/full_node_api.py | 14 ++++++++------ chia/util/generator_tools.py | 10 ++++++---- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/chia/_tests/blockchain/test_blockchain.py b/chia/_tests/blockchain/test_blockchain.py index 98ecd040c3bc..0ce70a5647cf 100644 --- a/chia/_tests/blockchain/test_blockchain.py +++ b/chia/_tests/blockchain/test_blockchain.py @@ -4184,3 +4184,19 @@ async def get_fork_info(blockchain: Blockchain, block: FullBlock, peak: BlockRec ) return fork_info + + +@pytest.mark.anyio +async def test_get_header_blocks_in_range_tx_filter_non_tx_block(empty_blockchain: Blockchain, bt: BlockTools) -> None: + """ + Covers the case of calling `get_header_blocks_in_range`, requesting + transactions filter, on a non transaction block. + """ + b = empty_blockchain + blocks = bt.get_consecutive_blocks(2) + for block in blocks: + await _validate_and_add_block(b, block) + non_tx_block = next(block for block in blocks if not block.is_transaction_block()) + blocks_with_filter = await b.get_header_blocks_in_range(0, 42, tx_filter=True) + empty_tx_filter = b"\x00" + assert blocks_with_filter[non_tx_block.header_hash].transactions_filter == empty_tx_filter diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 13ca8cf845ed..a218cbf9e899 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -902,18 +902,22 @@ async def get_header_blocks_in_range( raise ValueError(f"Block at {block.header_hash} is no longer in the blockchain (it's in a fork)") if tx_filter is False: header = get_block_header(block) - elif block.transactions_generator is None: - # There is no point in getting additions and removals for - # blocks that do not have transactions. - header = get_block_header(block) - else: + elif block.transactions_generator is not None: added_coins_records, removed_coins_records = await asyncio.gather( self.coin_store.get_coins_added_at_height(block.height), self.coin_store.get_coins_removed_at_height(block.height), ) tx_additions = [cr.coin for cr in added_coins_records if not cr.coinbase] removed = [cr.coin.name() for cr in removed_coins_records] - header = get_block_header(block, (tx_additions, removed)) + header = get_block_header(block, (removed, tx_additions)) + elif block.is_transaction_block(): + # This is a transaction block with just reward coins. + # We're sending empty additions and removals to signal that we + # want the transactions filter to be computed. + header = get_block_header(block, ([], [])) + else: + # Non transaction block. + header = get_block_header(block) header_blocks[header.header_hash] = header return header_blocks diff --git a/chia/consensus/multiprocess_validation.py b/chia/consensus/multiprocess_validation.py index aecf162b8346..8c247c7280f2 100644 --- a/chia/consensus/multiprocess_validation.py +++ b/chia/consensus/multiprocess_validation.py @@ -5,7 +5,7 @@ import logging import time import traceback -from collections.abc import Awaitable +from collections.abc import Awaitable, Collection from concurrent.futures import Executor from dataclasses import dataclass from typing import Optional @@ -94,12 +94,11 @@ def _pre_validate_block( try: validation_start = time.monotonic() - tx_additions: list[Coin] = [] - removals: list[bytes32] = [] + removals_and_additions: Optional[tuple[Collection[bytes32], Collection[Coin]]] = None if conds is not None: assert conds.validated_signature is True assert block.transactions_generator is not None - removals, tx_additions = tx_removals_and_additions(conds) + removals_and_additions = tx_removals_and_additions(conds) elif block.transactions_generator is not None: assert prev_generators is not None assert block.transactions_info is not None @@ -118,10 +117,13 @@ def _pre_validate_block( return PreValidationResult(uint16(err), None, None, uint32(validation_time * 1000)) assert conds is not None assert conds.validated_signature is True - removals, tx_additions = tx_removals_and_additions(conds) + removals_and_additions = tx_removals_and_additions(conds) + elif block.is_transaction_block(): + # This is a transaction block with just reward coins. + removals_and_additions = ([], []) assert conds is None or conds.validated_signature is True - header_block = get_block_header(block, (tx_additions, removals)) + header_block = get_block_header(block, removals_and_additions) required_iters, error = validate_finished_header_block( constants, blockchain, diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index 22cf52f72a72..f687de8d3a98 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -4,6 +4,7 @@ import logging import time import traceback +from collections.abc import Collection from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timezone from typing import TYPE_CHECKING, ClassVar, Optional, cast @@ -1209,8 +1210,7 @@ async def request_block_header(self, request: wallet_protocol.RequestBlockHeader if block is None: return None - tx_removals: list[bytes32] = [] - tx_additions: list[Coin] = [] + removals_and_additions: Optional[tuple[Collection[bytes32], Collection[Coin]]] = None if block.transactions_generator is not None: block_generator: Optional[BlockGenerator] = await get_block_generator( @@ -1233,10 +1233,12 @@ async def request_block_header(self, request: wallet_protocol.RequestBlockHeader ) # strip the hint from additions, and compute the puzzle hash for # removals - tx_additions = [add[0] for add in additions] - tx_removals = [rem.name() for rem in removals] + removals_and_additions = ([r.name() for r in removals], [a[0] for a in additions]) + elif block.is_transaction_block(): + # This is a transaction block with just reward coins. + removals_and_additions = ([], []) - header_block = get_block_header(block, (tx_additions, tx_removals)) + header_block = get_block_header(block, removals_and_additions) msg = make_msg( ProtocolMessageTypes.respond_block_header, wallet_protocol.RespondBlockHeader(header_block), @@ -1528,7 +1530,7 @@ async def request_header_blocks(self, request: wallet_protocol.RequestHeaderBloc ) added_coins = [record.coin for record in added_coins_records if not record.coinbase] removal_names = [record.coin.name() for record in removed_coins_records] - header_block = get_block_header(block, (added_coins, removal_names)) + header_block = get_block_header(block, (removal_names, added_coins)) header_blocks.append(header_block) msg = make_msg( diff --git a/chia/util/generator_tools.py b/chia/util/generator_tools.py index b2c6ec739d28..31730f080ccf 100644 --- a/chia/util/generator_tools.py +++ b/chia/util/generator_tools.py @@ -14,18 +14,20 @@ def get_block_header( - block: FullBlock, additions_and_removals: Optional[tuple[Collection[Coin], Collection[bytes32]]] = None + block: FullBlock, removals_and_additions: Optional[tuple[Collection[bytes32], Collection[Coin]]] = None ) -> HeaderBlock: """ Returns a HeaderBlock from a FullBlock. - If `additions_and_removals` is not None, account for them, as well as + If `removals_and_additions` is not None, account for them, as well as reward coins, in the creation of the transactions filter, otherwise create an empty one. """ + # Guard against passing removals and additions for a non-transaction block. + assert removals_and_additions is None or block.is_transaction_block() # Create an empty filter to begin with byte_array_tx: list[bytearray] = [] - if additions_and_removals is not None and block.is_transaction_block(): - tx_addition_coins, removals_names = additions_and_removals + if removals_and_additions is not None and block.is_transaction_block(): + removals_names, tx_addition_coins = removals_and_additions for coin in tx_addition_coins: byte_array_tx.append(bytearray(coin.puzzle_hash)) for coin in block.get_included_reward_coins():