Skip to content

Lookup sync with blocks_by_head#9541

Open
dapplion wants to merge 28 commits into
sigp:unstablefrom
dapplion:lookup-ancestor-blocks
Open

Lookup sync with blocks_by_head#9541
dapplion wants to merge 28 commits into
sigp:unstablefrom
dapplion:lookup-ancestor-blocks

Conversation

@dapplion

@dapplion dapplion commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Issue Addressed

Part of tree sync, #7678 the consumer + requester side of beacon_blocks_by_head. The responder landed in #9237; this is the other half.

What do we gain with this PR?

Given a capable by_head peer, lookup sync discovers the common ancestor of an unknown block much faster: requiring up to 32 times less roundtrips.

Proposed Changes

  • One response seeds the whole chain. A by_head response is delivered as AncestorBlocks { first_block, ancestor_blocks }; the ancestors are cached and threaded through parent-lookup creation, so a single request resolves the chain with zero follow-up downloads.
  • Peer selection prefers by_head-capable peers. Block lookups pick a by_head peer when one is available and otherwise fall back to by_root.
  • Adaptive request count. A lone lookup requests only a few ancestors (the common shallow case shouldn't over-fetch)

The whole block-lookup test suite runs against both by_root- and by_head-capable peers (the rig serves beacon_blocks_by_head), plus dedicated tests for the count escalation and the peer-fallback-on-failure behaviour.

AI disclosure

Original design by hand, completed by Claude (Opus 4.8), reviewed line by line.

dapplion added 22 commits June 11, 2026 15:47
Block lookups now prefer peers that advertise beacon_blocks_by_head and
fetch the block (plus a run of its ancestors) via that protocol, falling
back to beacon_blocks_by_root otherwise. on_block_download_response now
receives a Vec of blocks; the ancestors beyond the requested root are not
yet consumed (header-backfill step).
Implement Serialize for the rpc Protocol enum (as its canonical name) so
PeerInfo.supported_protocols is surfaced in the peers API, matching how
subnet fields are serialized, rather than skipped.
Move the identify protocol-id parsing into a standalone
rpc_protocol_from_id function and add a unit test covering ReqResp,
non-ReqResp, and unknown protocol ids.
- AncestorBlocks::from_vec consumes an iterator (no remove(0))
- BlocksByHeadRequestItems::add uses if-let instead of match
- reduce rpc_protocol_from_id test to two cases
- Split block_lookup_request send paths into send_block_by_root_request
  and send_blocks_by_head, both taking the selected PeerId
- on_{single_block,blocks_by_head}_response return AncestorBlocks so the
  sync manager does no transformation
- query peer_supports_blocks_by_head after selection instead of carrying it
Add the beacon_blocks_by_head support flag as the first tuple element and
keep unstable's .min()/peer-in-tuple shape instead of restructuring.
Strictly de-prioritize peers already at MAX_CONCURRENT_REQUESTS for the
protocol we would use (by_head if supported, else by_root), counting only
that protocol's in-flight requests per the spec's per-protocol-ID limit.
Expose MAX_CONCURRENT_REQUESTS and add ActiveRequests::active_request_count_of_peer.
Build per-peer blocks_by_root / blocks_by_head count maps once instead of
scanning per candidate peer; gate selection on the relevant protocol's count
>= MAX_CONCURRENT_REQUESTS.
…ctors

- send_block_by_root_request/send_blocks_by_head take lookup_id, allocate
  their own req_id, and return it
- replace the two per-peer count helpers with an ActiveRequestsPerPeer struct
  exposing at_concurrency_limit(peer)
- give AncestorBlocks from_single/from_vec constructors and inline
  into_ancestor_blocks into the response handlers
…elper

get_parent_import_status now reports an already-imported block (genesis, or a
finalized block whose parent was pruned) as having an imported parent, so a
lookup for such an anchor is processed rather than awaiting a missing parent.
Block lookups use cx.conflicts_with_finality (returning a reason String) to
drop blocks at/below finalization that are off the finalized chain.
Pass known_blocks down search_parent_of_child -> new_current_lookup ->
on_lookup_result so every ancestor seeded from a single beacon_blocks_by_head
response is served from the cache, rather than only the first parent level.
- size PARENT_DEPTH_TOLERANCE to BLOCKS_BY_HEAD_REQUEST_COUNT + 1 so one
  by-head batch can seed a full chain without tripping the depth limit
- rig: serve BlocksByHead (walk parent chain) and a peerdb test setter +
  rig helper to mark a peer as advertising beacon_blocks_by_head
- add a test asserting a 32-block chain resolves with 1 by-head request and
  0 blocks-by-root, all blocks cached
is_parent_imported now also returns true when the block is itself in fork
choice (contains_block), covering genesis and finalized blocks whose anchor
proto-node isn't queryable via get_block. This lets a lookup for such a block
be processed instead of dropped as conflicting with finality.
Serve beacon_blocks_by_head in the test rig (honouring the empty/wrong-block
failure strategies), add a PeerSupport enum threaded into TestRig::default /
new_after_fulu, and nest the depth macro so every lookup test runs as a
by_root and a by_head variant.
… fetch a batch

A lookup that nothing is waiting on is a lone island and requests a small count
(4); once a child is awaiting it the lookup is part of a chain proven deep and
requests a large batch (32). Derived from the lookup graph at creation, no
escalation state. Tune the counts against mainnet metrics.
The subject (beacon_blocks_by_head) now lives in the type name, so the variants
read clearly on their own instead of the ambiguous DoesNotSupport.
SingleLookupRequestState now records the peers that failed a download for the
request, and block_lookup_request de-prioritizes them in peer selection (just
below the concurrency-limit check). This stops a faulty by_head peer from being
re-picked on every retry in a mixed peer set, letting the lookup fall back to a
by_root peer instead of exhausting its attempts and dropping. Found in review.
Add a rig test asserting a faulty by_head peer's failed download forces the
retry to fall back to a by_root peer (1 BlocksByHead + 1 BlocksByRoot, lookup
succeeds). Also restore the set_peer_supports_by_head helper name, mangled by
the earlier ByHeadSupport rename.
# Conflicts:
#	beacon_node/network/src/sync/block_lookups/single_block_lookup.rs
#	beacon_node/network/src/sync/manager.rs
#	beacon_node/network/src/sync/network_context.rs
#	beacon_node/network/src/sync/tests/lookups.rs
#	beacon_node/network/src/sync/tests/range.rs
@dapplion dapplion requested a review from jxs as a code owner June 25, 2026 15:50
…chor handling into get_parent_import_status

- Remove the out-of-scope failed_peers peer-fallback (block lookups no longer
  de-prioritize peers that failed a download); it can be a separate PR.
- Reject a downloaded block more than SLOT_IMPORT_TOLERANCE ahead of the current
  slot before creating a parent lookup, so a peer can't make us walk ancestors or
  force range sync to a bogus future head (cx.block_too_far_in_future).
- Move the genesis/anchor 'block is itself in fork choice' case into
  get_parent_import_status via a proto-less ParentImportStatus::AlreadyImported,
  so is_parent_imported is a plain matches!.
@dapplion dapplion added ready-for-review The code is ready for review syncing work-in-progress PR is a work-in-progress and removed ready-for-review The code is ready for review labels Jun 25, 2026
dapplion added 2 commits June 26, 2026 11:51
Revert the consensus-side move from the previous commit: get_parent_import_status
returns to pure parent-only semantics (no AlreadyImported variant) and
block_verification.rs is restored byte-identical to upstream. The genesis/anchor
'block is itself imported' case lives only in the sync-facing is_parent_imported
via '|| contains_block', so consensus block-import paths are untouched.
…aker, finality uses Failed, lookup tidy-ups

- SyncRequestId::BlocksByHead uses a tuple variant.
- Drop the per-peer request-count tiebreaker from lookup peer selection
  (and the now-unused active_request_count_by_peer helper).
- Reuse LookupRequestError::Failed for finality-conflicting blocks; remove
  the unused ConflictsWithFinality variant.
- Collect known ancestors via iterator; tidy two comments.
dapplion added 2 commits June 26, 2026 12:33
…tream

is_parent_imported goes back to the upstream-pure wrapper of get_parent_import_status
(no contains_block, no block hashing on the consensus block-verification path). The
genesis/anchor case (a looked-up block already in fork choice, or whose parent was
pruned by finalization) is handled in the lookup eager path via
block_is_known_to_fork_choice, keyed on the root the lookup already holds.
- libp2p_peers_supporting_blocks_by_head: connected peers advertising the protocol,
  counted alongside the other per-peer gauges.
- sync_blocks_by_head_requested_ancestors: histogram of the requested-ancestor count.
- sync_lookups_known_parent_reused_total: blocks reused from a prior by-head response
  to seed a parent lookup instead of re-downloading (KnownParents optimization).

(by-head request timings already exist via sync_rpc_request_duration_sec{protocol}.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

syncing work-in-progress PR is a work-in-progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant