Expand description
Shared chain-event log streaming for the on-chain watchers.
Every watcher registers a Route on one multiplexed_poller, a single
eth_getLogs polling loop that merges all routes into one call per tick and
unifies historical backfill and the live tail into one resumable,
cursor-persisting loop (#1092). The node makes no watch_logs/eth_newFilter
call: the default public Arbitrum Sepolia RPC and most keyless endpoints
reject those with -32601 (#1106).
§Watchers scan to head, deliberately
Every watcher’s scan upper bound is the head block itself. There is no confirmation lag and no knob for one.
Two distinct things can go wrong at the unstable tip, and re-scan idempotency covers only one of them:
- Re-delivery of a canonical log. Covered. The retry and reorg-rewind
paths already re-deliver, so
resumable_watcher’s module doc does not merely observe sink idempotency, it requires it (dedup by id / set insertion / authoritative re-read). A re-mined event is re-applied harmlessly. - A log that never becomes canonical. Not covered, and not the same
property: idempotency makes a repeated apply cheap, whereas a confirmation
lag would prevent the apply happening at all.
eth_getLogsreturns only canonical logs, so no sink ever observes a removal and none has an un-apply path. An event from an orphaned block therefore stays applied.
That second case is an accepted residual, not a solved problem. It is
accepted because reorgs on the target chain are shallow and rare (see
backfill’s REORG_MARGIN_BLOCKS, which carries the sizing rationale for an
Arbitrum-Sepolia-class L2 and is the single place that claim lives), and
because the sinks holding an authoritative on-chain enumeration source
re-read it and self-heal. The one without one is where an orphan sticks:
settlement’s register_open_channel (durable — an fsynced phantom channel,
reclaimed only by the expiry sweep). It predates #1227 and is unchanged by
it — deleting an always-zero field cannot alter what a lag never did.
For the settlement watcher specifically, a lag also carries a concrete cost, which is what makes zero an active choice there rather than an inherited one: it would delay channel registration, so a client’s first request on a freshly-opened channel would be rejected as unknown.
If a future chain needs a lag, reintroduce it as a config knob with a non-zero default and a test asserting a live production config engages it — not a silently-zero field whose unit test exercises the arithmetic rather than the wiring.
Modules§
- boot_
retry - In-process retry for the boot-time chain reads (#2159).
- multiplexed_
poller - Multiplexed
eth_getLogspolling: one poll tick, oneget_logscall per backfill window, demuxed by(address, topic0)into per-route sinks. - shared_
head - One shared, TTL-cached
eth_blockNumberread for every chain watcher.
Structs§
- Watcher
Handle - Owns a running watcher task and the shutdown token that stops it. The token
is minted inside
super::multiplexed_poller::spawnand never escapes except throughshutdown, so no call site can conjure or drop an inert token — the mistake #1230 fixed by convention, made unrepresentable (#1236).shutdownis the graceful path (the loop flushes every route’s cursor and returns); the wrappedAbortOnDropHandleguard is the hard safety net when the handle drops without a priorshutdown.