Repository navigation
fix(sync): judge the sync gate against peers' Status heads - #791
MegaRedHand wants to merge 4 commits into
Conversation
A node reported itself synced while far behind (#559): the tracker took the freshest block the network knows from the node's own imported chain, which trails the head during a catch-up, so its network-stall override fired and the node attested and proposed on a stale head. The beacon follower had the same bug and reported synced through whole catch-ups. Follow Lighthouse and Prysm instead: a node is syncing only when its head trails the wall clock and the head its peers report in Status is ahead of its own. P2P keeps each connected peer's latest Status head (re-requested every few slots, dropped on disconnect, a head past the clock ignored) and publishes the second-highest of three or more on the Store. Thresholds are per chain: leanSpec's 4/2 on lean, Lighthouse's two epochs of clock lag and one epoch of peer lead on beacon.
The beacon tick is a message in the same mailbox range sync forwards a catch-up into, and each import holds the actor for an engine round trip, so on a catch-up the tick waited behind every block of it. The gate was judged only there, so it kept answering `Synced` for the whole catch-up: on a kurtosis devnet a node reconnecting 100 slots behind under a slow EL read synced for 53 s and its validator client proposed on a stale head. Judge it in the head recompute every beacon arrival, tick and envelope release ends in, ahead of the forkchoiceUpdated call, and after each lean arrival, whose tick waits the same way.
…urst The refresh timer re-asked every connected peer for its Status in the same instant: 200 peers on a beacon node with the default target. The bytes are small (about 0.5 KB per round trip), but 200 streams opened together, and 200 answers handled together, are a needless burst. Each peer is now due one interval after its own last Status exchange in either direction, and the timer ticks every slot and asks at most ceil(peers / interval) of the due peers, longest-waiting first, the way Lighthouse and Prysm track a status time per peer. 200 beacon peers that connected together are re-asked at most 13 per slot and stay spread afterwards.
🤖 Codex Code Review
Automated review by OpenAI Codex · gpt-6.1-sol · custom prompt |
🤖 Grok Code ReviewI'll inspect the sync-status and peer-head changes in context, especially ordering, overflow, and how Automated review by Grok (xAI) · grok-4.7 · custom prompt |
🤖 Claude Code ReviewReview: PR 791, peer-reported network head for the sync gateThe direction is right. Comparing against peers' Correctness and security
Performance
Rust and maintainability
Spec
Overall the PR is solid. Item 1 (no delivery feedback on claimed heads) and Item 2 (post-disconnect insert) are the ones I'd want addressed or explicitly accepted before merge. Automated review by Claude · claude-opus-5-5 · custom prompt |
🤖 DeepSeek Code ReviewThe PR substantially improves sync gating by comparing against peer-reported heads rather than imported blocks. The logic is well-structured and documented. A few issues remain: High priority
Medium priority
Low priority / suggestions
Overall the core design is sound and the tests cover the important cases. The lean path deserves immediate attention. Automated review by DeepSeek · deepseek-v4-pro · custom prompt |
🤖 Gemini Code ReviewThe changes look solid. Moving the sync tracker from One minor doc nit:
Automated review by Gemini · gemini-4-argon · custom prompt |
🤖 Kimi Code Review
Otherwise, the threshold arithmetic, startup handling, cross-actor atomic state, and refresh batching look sound and are well covered by tests. Automated review by Kimi (Moonshot AI) · kimi-k3.1-pro · custom prompt |
🗒️ Description / Motivation
The sync gate reported Synced while a node was far behind (#559), so it attested and proposed on a stale head. It took the freshest block the network knows from the node's own imported chain, which trails the head during a catch-up, so the network-stall override fired. The beacon follower had the same bug:
docs/beacon_wire.mdrecords it reporting synced through a 10-minute catch-up up to 98 slots behind.This follows Lighthouse and Prysm: a node is syncing only when its head trails the wall clock and the head its peers report in
Statusis ahead of its own. Peers decide "behind vs. stalled"; the clock keeps any peer claim from idling a node that is current.A kurtosis run found a second cause on the beacon follower: the gate was judged only on the actor's slot tick, which waits behind a whole catch-up in the actor's mailbox. It is now judged after every block arrival too (second commit).
What Changed
crates/blockchain/src/sync_status.rs: new rule with per-chainSyncThresholds. Lean keeps leanSpec ci: add a Commit-Boost check to the kurtosis battery #708's 4/2. Beacon uses Lighthouse's numbers: 2 epochs of clock lag (DEFAULT_SYNC_TOLERANCE_EPOCHS, also Prysm'sshouldReSync) and 1 epoch of peer lead (SLOT_IMPORT_TOLERANCE). There is also a bounded startup hold before the firstStatus(lean 8 slots, beacon 1 epoch).crates/net/p2p/src/peer_heads.rs(new): each connected peer's latestStatushead, dropped on disconnect.FUTURE_SLOT_TOLERANCE).Store::network_head.Statusevery 4 slots (lean) or every half epoch (beacon, Prysm's cadence), counted from that peer's own last exchange in either direction.crates/blockchain/src/lib.rs:Store::network_head, and the beacon fork guard is kept.recompute_beacon_head(which every arrival, tick and envelope release ends in, ahead of theforkchoiceUpdatedcall), on lean at the end ofon_block.Synceduntil the catch-up was over.spec_deviations.mdentry (lean departs from leanSpec ci: add a Commit-Boost check to the kurtosis battery #708), plus updates toarchitecture.md,rpc.mdandbeacon_wire.md.Correctness / Behavior Guarantees
Stalledpassesnot_while_syncing_filter).is_syncingis the decision that gates duties, since its validator client readsis_syncing./eth/v1/node/syncingderivesis_syncingfrom its sync state (!is_synced(), except with zero target peers), so it reads true while range-syncing even within the 2-epoch tolerance, and whileStalled.not_while_syncing_filter) refuse only while range-syncing beyond the tolerance, and letStalledthrough. That is what ethlambda's single flag does.Tests Added / Run
Store::network_headsharing, and the beacon follower cases.a_block_arrival_judges_the_gate_without_waiting_for_the_tickdrives the real arrival path (on_block) with no tick. It fails on db61388 (status staysIdle) and passes now. The lean half of the second commit is a one-line mirror with no lean-specific test.make fmt,make lintandmake testpass on 5e0a9d1 (2,935 tests, 0 failed).Two tools:
iptablesdrops on the CL p2p ports 9000–9001 in cl-3's network namespace ("cut"), and anetemdelay on cl-3's outbound engine-API traffic (TCP 8551) to model a slow EL.Results below are from 5e0a9d1, on a fresh enclave. Of the later commits, d907b60 is docs-only. 6f08c98 spreads the
Statusrefresh over the interval and is covered by unit tests; the devnet scenarios were not rerun on it. They exercise the handshakeStatus, not the refresh.The slow catch-up is simulated with the
netemdelay. Lean was not run on a live network: kurtosis runs the beacon chain.Synced: synced from genesis, 2 peers, duty endpoints 200, and the VC attests, aggregates and does sync-committee duties every slot.
Cut off with the VC stopped, then reconnected under a slow EL: the VC was stopped so the node's own head stalls, as a lone cut-off node's does. The cut ran from slot 12 to slot 112, and reconnect happened with a 1 s engine delay, so each import takes about 2 s.
is_syncing=falsewith duty endpoints at 200 throughout, up to 100 slots behind.Sync status changed current_slot=112 head_slot=14 network_head=111 clock_lag=98 peer_lead=97 starting_up=false syncing=true. The network head came from peers'Status, not from the startup hold.clock_lag=64 peer_lead=59 … syncing=false, 48 s later.Failed to refresh duties … beacon node is syncingevery slot and published nothing (no block, attestation or sync-committee message) until the gate reopened.Restart after downtime: cl-3 was stopped at slot 121 and started at slot 201 (
Resuming from existing DB head_slot=120 current_slot=201 gap=81).starting_up=true).Short cut with the VC running: 12 slots, from slot 207 to 219.
Before the second commit (db61388), same cut-and-slow-EL scenario:
is_syncing=falsefor about 53 s while 101 to 64 slots behind with a peer back, and no "Sync status changed" line was logged.target epoch is too far from this node's head).Expected, within tolerance: once the gate reopens at 64 slots behind, the VC works again while the node finishes catching up. The node refused its attestations there (
target epoch is too far …), and sync-committee messages went out. This is the 2-epoch tolerance Lighthouse's duty filter applies too.Follow-ups (not in this PR)
Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(test-consensusplustest-node, atrelease-fast) — all passing