Repository navigation
Bound the unknown-parent block buffer - #941
Merged
Merged
Conversation
This was referenced Oct 8, 2026
A block whose parent had no index entry was parked in the block processor's buffer keyed by its prev hash: up to 8192 entries with no byte bound, no per-peer bound and no expiry, whether or not this node had asked for the block, before any proof-of-work or size check. Each entry kept its sender's in-flight guard, so that peer's pongs waited for the 60-second bound until the block's parent connected, and the block went through the per-input fee pass before accept_block refused it. Bitcoin Core keeps no such block: AcceptBlockHeader runs CheckBlockHeader (validation.cpp:4206) and then refuses an unknown parent as prev-blk-not-found (validation.cpp:4216). satd answers a block inv with a getdata, so a block it asked for can arrive before the parent it is still learning about, and keeping that one saves a second download. Now: - Only a block this node asked for (Core's IsBlockRequested, read in handle_block before the request record is cleared) may wait. - A waiting block must pass check_proof_of_work_bounded and be at most MAX_BLOCK_SERIALIZED_SIZE (4,000,000) bytes. - At most 4 per peer, 64 blocks and 32 MB in all; the sender's oldest goes first when it is at its share, then the oldest of all. Each block expires after 5 minutes. - The in-flight guard is counted out when the block is set aside, as Core is done with it once refused. - A block whose parent is unknown skips the fee pass and accept_block. - The sender is asked for headers, as satd already does for a cmpctblock on an unknown parent. - A block on a parent marked invalid is not kept. - Two blocks on the same parent can both wait; when one fails to connect the next is tried. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bkeroack
force-pushed
the
fix/orphan-block-buffer-bounds
branch
from
October 8, 2026 18:13
1ce59e0 to
8061ef1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #936 (merge that first).
Problem
Outside IBD, a
blockmessage whose parent has no index entry goes to the block processor, whereaccept_blockrefuses it withPrevBlockNotFound. The processor then kept it in an in-memory buffer keyed by itsprev_blockhash(net/manager.rs,block_processor):accept_blockreturnsPrevBlockNotFoundbefore either,chain/state.rs);pingfrom that peer waited for the 60-second bound until the block's parent connected, or forever if it never did;compute_block_fee_rates, a coin lookup per input), which runs beforeaccept_block.A block whose parent was marked invalid (
BadPrevBlock) was kept the same way, though nothing can connect on it.Bitcoin Core keeps no such block.
AcceptBlockHeaderrunsCheckBlockHeader(src/validation.cpp:4206) and then refuses an unknown parent asprev-blk-not-found(src/validation.cpp:4216); the block is gone. satd differs on purpose in one place: it answers a blockinvwith agetdataas well as agetheaders(handle_inv, and thesendheadersrow inCORE_DIFFERENCES.md), so a block it asked for can arrive before the parent it is still learning about. Keeping that block saves fetching it again once the parent connects.Core references are to master
16613c9de9.Fix
A new
net/orphan_blocks.rsholds the waiting blocks and enforces the bounds. The block processor andhandle_blockdecide what goes in.handle_blockreads whether any peer was asked for the hash (Core'sIsBlockRequested, read at the same point,src/net_processing.cpp:4800) beforenote_block_arrivedclears the request record, and passes it to the processor with the block.check_proof_of_work_bounded(Core'sCheckProofOfWork, the checkCheckBlockHeadermakes) and be at mostMAX_BLOCK_SERIALIZED_SIZE, 4,000,000 bytes (src/consensus/consensus.h:13). No valid block is larger.pongbehind it is answered then.accept_block's lock.maybe_send_getheaders, rate-limited per peer), as satd already does for acmpctblockon an unknown parent and Core does for headers that do not connect. A block the node did not ask for is otherwise dropped.bad-prevblk).The sender of a block with an unknown parent is still not penalised, as
an_invalid_block_from_a_peer_does_not_ban_or_drop_the_sessioninnet/manager.rsrequires. Core discourages it (MaybePunishNodeForBlock,BLOCK_MISSING_PREV,src/net_processing.cpp:1940); peer punishment is out of scope here.CORE_DIFFERENCES.mdgains a row for the difference that remains: satd keeps a requested block whose parent is unknown, within these bounds, where Core keeps none.Tests
Unit tests, new file
node/src/net/manager_orphanbuf_tests.rs(throughhandle_messageand the real block processor thread):an_unrequested_block_with_an_unknown_parent_is_not_kept: block 5, not requested, then block 4. Block 4 connects and block 5 does not follow it; sent again afterwards, block 5 connects.a_requested_block_waits_for_its_parent_and_connects_after_it: the counterpart. Block 5, requested, then block 4: block 5 connects from the buffer.a_block_waiting_for_its_parent_does_not_hold_the_senders_pong: a requested block 5 is set aside and the sender's in-flight count goes to zero; block 4 then arrives and block 5 follows it in.Unit tests in
node/src/net/orphan_blocks.rs: valid PoW kept and returned to its parent only; hash missing its target, target above regtest's powLimit, and regtest work on mainnet all refused; over 4,000,000 bytes refused and just under kept; the per-peer share evicts that peer's oldest and leaves other peers' blocks; the byte cap and the block cap evict the oldest; expiry; siblings both wait and a second copy is not kept twice; the default bounds fit a full-size block and keep one peer below the total.Integration tests, new module
satd/tests/compact_blocks/unknown_parent.rs(raw P2P against a regtest node):an_unrequested_block_on_an_unknown_parent_is_dropped_and_headers_are_asked_for: the node answers withgetheaders, and when the parent then connects the unrequested child does not.a_requested_block_waits_for_its_parent_without_holding_the_pong: afterinv→getdata, the child is sent, apingbehind it is answered within 10 s, and the child connects after its parent.node/src/net/manager_blockaccept_tests.rs(#936):an_empty_body_buffered_before_its_parent_does_not_bar_the_real_blocknow records a request for block 5 first, so the empty body still takes the buffered route that test covers.Fails without the fix:
an_unrequested_block_with_an_unknown_parent_is_not_kept: yes (block 4 connects and block 5 was not kept, tip was block 5).a_block_waiting_for_its_parent_does_not_hold_the_senders_pong: yes (the waiting block must not hold the peer's in-flight count (1 in flight)).an_unrequested_block_on_an_unknown_parent_is_dropped_and_headers_are_asked_for: yes (a block on an unknown parent must be answered with getheaders).a_requested_block_waits_for_its_parent_without_holding_the_pong: yes (the pong must not wait for the child's parent).a_requested_block_waits_for_its_parent_and_connects_after_itpasses with and without the fix; it guards the behaviour that must stay.Each guard on its own (removed, named test run, restored):
requestedgate removed:an_unrequested_block_with_an_unknown_parent_is_not_keptfails.a_block_waiting_for_its_parent_does_not_hold_the_senders_pongfails (1 in flight).getheadersrequest removed:an_unrequested_block_on_an_unknown_parent_is_dropped_and_headers_are_asked_forfails.a_block_without_proof_of_work_within_the_limit_is_not_keptfails.a_block_over_the_serialized_size_limit_is_not_keptfails.one_peer_keeps_at_most_its_share_and_loses_its_oldest_firstfails.the_byte_cap_drops_the_oldest_blocks_firstfails.the_block_cap_drops_the_oldest_blocks_firstfails.a_block_is_dropped_once_it_has_waited_the_expiryfails.Skipping the fee pass and
accept_blockfor a block with an unknown parent saves work only; no test observes it.Local:
cargo clippy --all-targets --all-features -D warnings,cargo clippy -p node --no-default-features --all-targets -D warnings,cargo test -p node --lib(2169 passed),cargo test -p satd --test compact_blocks(33 passed), and the regtest testsa_pong_answers_for_the_block_that_arrived_before_the_pingandblocknotify_runs_for_a_block_the_stored_tail_drain_connects.Notes
submitblock, the stored-tail drain) leaves the child waiting until it expires or is fetched again; this is unchanged, and the bound now frees it.Release notes
A block received before its parent is now kept only if the node asked for it, and only within fixed bounds: valid proof of work, at most 4 MB, at most 4 blocks per peer and 32 MB in all, for at most 5 minutes. Other such blocks are dropped and the sender is asked for headers, which is how the node learns the missing parent. A peer's
pingis no longer held while one of its blocks waits for a parent.🤖 Generated with Claude Code