mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 02:24:23 -05:00
CHIA-4218 Return None when anchored wallet timestamp lookup misses the peak header (#21274)
* Treat missing anchored peak header as not-synced in wallet timestamp lookup Follow-up to the wallet timestamp backtracking bound. When get_timestamp_for_height_from_peer runs in anchored mode (an expected_header_hash supplied by new_peak_wallet) and the peer returns no header block for the starting peak height, the loop previously decremented the height without advancing the anchor and then validated lower heights against the stale peak hash. Return None (peer treated as not synced) instead, so the anchor chain is never extended from a missing block. Non-anchored lookups (get_timestamp_for_height) are unchanged and still backtrack past a missing peak height. * Clarify anchored timestamp lookup comment
This commit is contained in:
@@ -625,6 +625,75 @@ async def test_get_timestamp_for_height_from_peer_rejects_wrong_response_height(
|
||||
add_to_blocks.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("peak_response", [None, []])
|
||||
async def test_get_timestamp_for_height_from_peer_rejects_missing_peak_response(
|
||||
root_path_populated_with_config: Path, monkeypatch: pytest.MonkeyPatch, peak_response: list[object] | None
|
||||
) -> None:
|
||||
config = load_config(root_path_populated_with_config, "config.yaml", "wallet")
|
||||
wallet_node = WalletNode(config, root_path_populated_with_config, test_constants)
|
||||
expected_hash = bytes32(b"\x01" * 32)
|
||||
peer = cast(WSChiaConnection, types.SimpleNamespace(get_peer_info=lambda: "peer"))
|
||||
add_to_blocks = Mock()
|
||||
cache = types.SimpleNamespace(
|
||||
get_height_timestamp=Mock(return_value=None),
|
||||
get_block=Mock(return_value=None),
|
||||
add_to_blocks=add_to_blocks,
|
||||
)
|
||||
requested_heights: list[int] = []
|
||||
|
||||
async def missing_response(peer: WSChiaConnection, start_height: uint32, end_height: uint32) -> list[object] | None:
|
||||
requested_heights.append(int(start_height))
|
||||
return peak_response
|
||||
|
||||
monkeypatch.setattr(wallet_node, "get_cache_for_peer", lambda peer: cache)
|
||||
monkeypatch.setattr("chia.wallet.wallet_node.request_header_blocks", missing_response)
|
||||
|
||||
# An anchored lookup whose peak height returns no block (empty or timeout) must not
|
||||
# backtrack with a stale anchor; it returns None after the single failed peak request.
|
||||
assert await wallet_node.get_timestamp_for_height_from_peer(uint32(100), peer, expected_hash) is None
|
||||
assert requested_heights == [100]
|
||||
add_to_blocks.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_timestamp_for_height_from_peer_unanchored_backtracks_past_missing_peak(
|
||||
root_path_populated_with_config: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
config = load_config(root_path_populated_with_config, "config.yaml", "wallet")
|
||||
wallet_node = WalletNode(config, root_path_populated_with_config, test_constants)
|
||||
timestamp = uint64(12_345)
|
||||
peer = cast(WSChiaConnection, types.SimpleNamespace(get_peer_info=lambda: "peer"))
|
||||
add_to_blocks = Mock()
|
||||
cache = types.SimpleNamespace(
|
||||
get_height_timestamp=Mock(return_value=None),
|
||||
get_block=Mock(return_value=None),
|
||||
add_to_blocks=add_to_blocks,
|
||||
)
|
||||
requested_heights: list[int] = []
|
||||
|
||||
async def response(peer: WSChiaConnection, start_height: uint32, end_height: uint32) -> list[object]:
|
||||
requested_heights.append(int(start_height))
|
||||
if int(start_height) == 100:
|
||||
return []
|
||||
return [
|
||||
types.SimpleNamespace(
|
||||
height=uint32(start_height),
|
||||
header_hash=bytes32(b"\x05" * 32),
|
||||
prev_header_hash=bytes32(b"\x06" * 32),
|
||||
foliage_transaction_block=types.SimpleNamespace(timestamp=timestamp),
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr(wallet_node, "get_cache_for_peer", lambda peer: cache)
|
||||
monkeypatch.setattr("chia.wallet.wallet_node.request_header_blocks", response)
|
||||
|
||||
# Without an anchor an empty peak response is not fatal: the lookup keeps backtracking
|
||||
# toward an older transaction block, so the anchored short-circuit must not apply here.
|
||||
assert await wallet_node.get_timestamp_for_height_from_peer(uint32(100), peer) == timestamp
|
||||
assert requested_heights == [100, 99]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_timestamp_for_height_from_peer_rejects_multiple_response_blocks(
|
||||
root_path_populated_with_config: Path, monkeypatch: pytest.MonkeyPatch
|
||||
|
||||
@@ -1267,6 +1267,15 @@ class WalletNode:
|
||||
elif request_height < height:
|
||||
# The peer might be slightly behind but still synced, so we should allow fetching one more block
|
||||
break
|
||||
elif expected_hash is not None:
|
||||
# Only reached on the starting height. In anchored mode, this block establishes
|
||||
# the hash chain used to validate any backtracked headers. Without it, continuing
|
||||
# would only compare lower headers against the original peak hash and fail later.
|
||||
# Treat the peer as not synced instead.
|
||||
self.log.warning(
|
||||
f"missing header block response for height {request_height} from Peer {peer.get_peer_info()}."
|
||||
)
|
||||
return None
|
||||
else:
|
||||
self.log.debug(f"get_timestamp_for_height_from_peer use cached block for height {request_height}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user