return RejectAdditionsRequest to wallets instead of raising an exception (#20555)

* return RejectAdditionsRequest to wallets instead of raising an exception

* review comments
This commit is contained in:
Arvid Norberg
2026-02-27 12:38:42 -06:00
committed by GitHub
parent e48de3871c
commit 7ddc900bf7
2 changed files with 101 additions and 15 deletions
+68 -8
View File
@@ -40,14 +40,17 @@ from chia.consensus.augmented_chain import AugmentedBlockchain
from chia.consensus.block_body_validation import ForkInfo
from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty
from chia.full_node.full_node_api import FullNodeAPI
from chia.full_node.full_node_api import MAX_COIN_HASHES_PER_REQUEST, FullNodeAPI
from chia.full_node.weight_proof import WeightProofHandler
from chia.protocols import full_node_protocol, wallet_protocol
from chia.protocols.outbound_message import Message, make_msg
from chia.protocols.protocol_message_types import ProtocolMessageTypes
from chia.protocols.shared_protocol import Capability
from chia.protocols.wallet_protocol import (
RejectAdditionsRequest,
RejectRemovalsRequest,
RequestAdditions,
RequestRemovals,
RespondAdditions,
RespondBlockHeader,
RespondBlockHeaders,
@@ -635,16 +638,44 @@ async def test_request_additions_errors(simulator_and_wallet: OldSimulatorsAndWa
last_block: BlockRecord | None = full_node_api.full_node.blockchain.get_peak()
assert last_block is not None
# Invalid height
with pytest.raises(ValueError):
await full_node_api.request_additions(RequestAdditions(uint32(100), last_block.header_hash, [ph]))
# Invalid height (with header_hash specified)
res = await full_node_api.request_additions(RequestAdditions(uint32(100), last_block.header_hash, [ph]))
assert res is not None
reject = RejectAdditionsRequest.from_bytes(res.data)
assert reject.height == 100
assert reject.header_hash == last_block.header_hash
# Invalid height (no header_hash, returns zeros)
res = await full_node_api.request_additions(RequestAdditions(uint32(100), None, [ph]))
assert res is not None
reject = RejectAdditionsRequest.from_bytes(res.data)
assert reject.height == 100
assert reject.header_hash == bytes32.zeros
# Invalid header hash
with pytest.raises(ValueError):
await full_node_api.request_additions(RequestAdditions(last_block.height, std_hash(b""), [ph]))
res = await full_node_api.request_additions(RequestAdditions(last_block.height, std_hash(b""), [ph]))
assert res is not None
reject = RejectAdditionsRequest.from_bytes(res.data)
assert reject.height == last_block.height
assert reject.header_hash == std_hash(b"")
# Too many puzzle hashes
too_many = [bytes32.random() for _ in range(MAX_COIN_HASHES_PER_REQUEST + 1)]
res = await full_node_api.request_additions(RequestAdditions(last_block.height, last_block.header_hash, too_many))
assert res is not None
reject = RejectAdditionsRequest.from_bytes(res.data)
assert reject.height == last_block.height
assert reject.header_hash == last_block.header_hash
# Exactly at the limit is allowed
at_limit = [bytes32.random() for _i in range(MAX_COIN_HASHES_PER_REQUEST)]
res = await full_node_api.request_additions(RequestAdditions(last_block.height, last_block.header_hash, at_limit))
assert res is not None
response = RespondAdditions.from_bytes(res.data)
assert response.height == last_block.height
# No results
fake_coin = std_hash(b"")
fake_coin = bytes32.random()
assert ph != fake_coin
res1 = await full_node_api.request_additions(
RequestAdditions(last_block.height, last_block.header_hash, [fake_coin])
@@ -667,7 +698,7 @@ async def test_request_additions_errors(simulator_and_wallet: OldSimulatorsAndWa
# all coin names are concatenated and hashed into one entry in the merkle set for proof_2
# the response contains the list of coins so you can check the proof_2
assert response.proofs[0][0] == std_hash(b"")
assert response.proofs[0][0] == fake_coin
assert response.proofs[0][1] is not None
assert response.proofs[0][2] is None
@@ -763,6 +794,35 @@ async def test_request_additions_success(simulator_and_wallet: OldSimulatorsAndW
assert len(response.coins) == 0
@pytest.mark.anyio
async def test_request_removals_too_many_coin_names(
simulator_and_wallet: OldSimulatorsAndWallets, self_hostname: str
) -> None:
full_nodes, wallets, _ = simulator_and_wallet
wallet_node, wallet_server = wallets[0]
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
full_node_api = full_nodes[0]
await wallet_server.start_client(PeerInfo(self_hostname, full_node_api.full_node.server.get_port()), None)
for _ in range(2):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
last_block = full_node_api.full_node.blockchain.get_peak()
assert last_block is not None
too_many = [bytes32.random() for _ in range(MAX_COIN_HASHES_PER_REQUEST + 1)]
res = await full_node_api.request_removals(RequestRemovals(last_block.height, last_block.header_hash, too_many))
assert res is not None
reject = RejectRemovalsRequest.from_bytes(res.data)
assert reject.height == last_block.height
assert reject.header_hash == last_block.header_hash
@pytest.mark.anyio
async def test_get_wp_fork_point(
default_10000_blocks: list[FullBlock], blockchain_constants: ConsensusConstants
+33 -7
View File
@@ -83,6 +83,9 @@ if TYPE_CHECKING:
else:
FullNode = object
MAX_COIN_HASHES_PER_REQUEST = 50
MAX_COINS_MAP_SIZE = 100
async def tx_request_and_timeout(full_node: FullNode, transaction_id: bytes32, task_id: bytes32) -> None:
"""
@@ -1303,18 +1306,31 @@ class FullNodeAPI:
@metadata.request()
async def request_additions(self, request: wallet_protocol.RequestAdditions) -> Message | None:
if request.puzzle_hashes is not None and len(request.puzzle_hashes) > MAX_COIN_HASHES_PER_REQUEST:
reject = wallet_protocol.RejectAdditionsRequest(
request.height, request.header_hash if request.header_hash is not None else bytes32.zeros
)
return make_msg(ProtocolMessageTypes.reject_additions_request, reject)
if request.header_hash is None:
header_hash: bytes32 | None = self.full_node.blockchain.height_to_hash(request.height)
else:
header_hash = request.header_hash
if header_hash is None:
raise ValueError(f"Block at height {request.height} not found")
# Note: this might return bad data if there is a reorg in this time
additions = await self.full_node.coin_store.get_coins_added_at_height(request.height)
reject = wallet_protocol.RejectAdditionsRequest(request.height, bytes32.zeros)
return make_msg(ProtocolMessageTypes.reject_additions_request, reject)
if self.full_node.blockchain.height_to_hash(request.height) != header_hash:
raise ValueError(f"Block {header_hash} no longer in chain, or invalid header_hash")
reject = wallet_protocol.RejectAdditionsRequest(request.height, header_hash)
return make_msg(ProtocolMessageTypes.reject_additions_request, reject)
additions = await self.full_node.coin_store.get_coins_added_at_height(request.height)
# Note: this might return bad data if there is a reorg while waiting for
# the DB. So check the height-to-hash again
if self.full_node.blockchain.height_to_hash(request.height) != header_hash:
reject = wallet_protocol.RejectAdditionsRequest(request.height, header_hash)
return make_msg(ProtocolMessageTypes.reject_additions_request, reject)
puzzlehash_coins_map: dict[bytes32, list[Coin]] = {}
for coin_record in additions:
@@ -1327,6 +1343,9 @@ class FullNodeAPI:
proofs_map: list[tuple[bytes32, bytes, bytes | None]] = []
if request.puzzle_hashes is None:
if len(puzzlehash_coins_map) > MAX_COINS_MAP_SIZE:
reject = wallet_protocol.RejectAdditionsRequest(request.height, header_hash)
return make_msg(ProtocolMessageTypes.reject_additions_request, reject)
for puzzle_hash, coins in puzzlehash_coins_map.items():
coins_map.append((puzzle_hash, coins))
response = wallet_protocol.RespondAdditions(request.height, header_hash, coins_map, None)
@@ -1360,6 +1379,10 @@ class FullNodeAPI:
@metadata.request()
async def request_removals(self, request: wallet_protocol.RequestRemovals) -> Message | None:
if request.coin_names is not None and len(request.coin_names) > MAX_COIN_HASHES_PER_REQUEST:
reject = wallet_protocol.RejectRemovalsRequest(request.height, request.header_hash)
return make_msg(ProtocolMessageTypes.reject_removals_request, reject)
block: FullBlock | None = await self.full_node.block_store.get_full_block(request.header_hash)
# We lock so that the coin store does not get modified
@@ -1377,11 +1400,14 @@ class FullNodeAPI:
assert block is not None and block.foliage_transaction_block is not None
# Note: this might return bad data if there is a reorg in this time
all_removals: list[CoinRecord] = await self.full_node.coin_store.get_coins_removed_at_height(block.height)
# Note: this might return bad data if there is a reorg while waiting for
# the DB. So check the height-to-hash again
if self.full_node.blockchain.height_to_hash(block.height) != request.header_hash:
raise ValueError(f"Block {block.header_hash} no longer in chain")
reject = wallet_protocol.RejectRemovalsRequest(request.height, request.header_hash)
msg = make_msg(ProtocolMessageTypes.reject_removals_request, reject)
return msg
all_removals_dict: dict[bytes32, Coin] = {}
for coin_record in all_removals: