fix: anchor stream from_block at creation to close startup polling gap#577
fix: anchor stream from_block at creation to close startup polling gap#577agentotto[bot] wants to merge 16 commits intomainfrom
Conversation
Remove corrupt commit hash fields from staging.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace has_pending() with should_propagate() that compares WorldIDSource.LATEST_ROOT vs WorldIDRegistry.getLatestRoot() - Remove pending_roots (roots propagate via ChainCommitted) - Remove dead code: reset_chain, with_chain, kind() - Remove first flag in satellite, use mark_changed() instead - Flatten satellite None branch into resync_head() helper - Start live event stream before backfill to avoid missed events - Backfill only fetches ChainCommitted (skip 3 wasted RPC calls) - Downgrade per-tick polling log to debug - Remove unused reqwest/tokio-stream deps - Remove NothingChanged string matching hack Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix notify race condition: Add AtomicBool flag to prevent satellites from hanging when mark_ready() is called before they start waiting - Fix partial get_logs failure: Only advance from_block when all filters succeed to prevent permanent event loss
- Fix notify race condition: Add AtomicBool flag to prevent satellites from hanging when mark_ready() is called before they start waiting - Fix partial get_logs failure: Only advance from_block when all filters succeed to prevent permanent event loss Applied via @cursor push command
…d-id-protocol into karan-relay-local
The polling stream in services/relay previously initialised from_block lazily on the first poll cycle by calling eth_blockNumber at that moment. engine.rs calls registry_stream() *before* backfill, so the stream exists but has not yet fired a poll when backfill_commitments() runs. Gap window: t0: registry_stream() called — stream created, from_block = None t1: backfill_commitments() fetches logs from [0 .. chain-head-at-t1] t2: backfill completes, mark_ready() signals satellites t3: POLL_INTERVAL expires, first poll fires → eth_blockNumber() = N Any ChainCommitted event emitted in blocks (t1-chain-head .. N] is never fetched: backfill stopped at t1-chain-head and the poll loop starts querying from N+1. Missing even a single ChainCommitted breaks keccak-chain continuity — every subsequent commit references the wrong predecessor hash, causing satellites to reject or stall permanently. Fix: fetch the current chain head in registry_stream() (before backfill) and pass it as anchor_block to poll_events(). The unfold state is now initialised with a concrete u64 rather than None, so the first poll queries [anchor_block+1 .. latest], guaranteeing gapless coverage from the moment the stream is created. Backfill and live-poll now overlap at anchor_block (both may process that block), but commit_chained deduplicates by chain head so this is harmless. Test: poll_stream_captures_event_emitted_during_backfill_window — simulates a ChainCommitted at block 6 landing during a backfill window (anchor = 5), advances mock time past POLL_INTERVAL, and asserts the event is yielded by the stream.
- Clarify that backfill covers [0, latest_at_rpc_time] (not strictly [0, anchor]) and that the overlap is deduplicated by commit_chained; previous wording would have misled a reader into adding an artificial to_block(anchor) cap that could re-introduce the gap. - Add a note that get_block_number failure propagates immediately because anchoring safely requires a concrete block number. - Derive CHAIN_COMMITTED_TOPIC in the test from the generated ABI binding instead of a hardcoded literal, so the test breaks visibly if the event signature is ever renamed.
Code Review SummaryFix correctness ✅
Test quality ✅
One remaining gap (non-blocking)The wiring in |
Problem
poll_eventsinitialisedfrom_blocklazily on the first poll cycle by callingeth_blockNumber()at that moment. Inengine.rs,registry_stream()is called beforebackfill_commitments(), so the stream exists but hasn't fired its first poll when backfill runs.Gap window
Any
ChainCommittedevent emitted in blocks(t1-head .. N]is never fetched: backfill stopped att1-headand the poll loop starts querying fromN+1.Why this is fatal
The keccak chain is a hash chain: each commit's
keccakChainfield must equalkeccak256(prev_head || new_commits). Missing even oneChainCommittedmeans the relay's in-memorylocal_chaindiverges from the on-chain chain. Every subsequent commit will produce the wrongkeccakChainand be rejected by satellites, permanently stalling the log.Fix
Fetch the current chain head in
registry_stream()(before backfill hands off) and pass it asanchor_blocktopoll_events(). Theunfoldstate is initialised with that concreteu64instead ofNone, so the first poll queries[anchor_block+1 .. latest].Backfill and live-poll overlap at
anchor_block(both may process that exact block), butcommit_chaineddeduplicates by chain head — no double-counting.Test
poll_stream_captures_event_emitted_during_backfill_window— uses alloy'sMockTransportand tokio's paused time to simulate aChainCommittedevent landing at block 6 during the backfill window (anchor = 5), advances the mock clock pastPOLL_INTERVAL, and asserts the event is yielded by the stream.