decdn_node/chain_events/mod.rs
1//! Shared chain-event log streaming for the on-chain watchers.
2//!
3//! Every watcher registers a `Route` on one `multiplexed_poller`, a single
4//! `eth_getLogs` polling loop that merges all routes into one call per tick and
5//! unifies historical backfill and the live tail into one resumable,
6//! cursor-persisting loop (#1092). The node makes no `watch_logs`/`eth_newFilter`
7//! call: the default public Arbitrum Sepolia RPC and most keyless endpoints
8//! reject those with `-32601` (#1106).
9//!
10//! # Watchers scan to head, deliberately
11//!
12//! Every watcher's scan upper bound is the head block itself. There is no
13//! confirmation lag and no knob for one.
14//!
15//! Two distinct things can go wrong at the unstable tip, and re-scan idempotency
16//! covers only one of them:
17//!
18//! - **Re-delivery of a canonical log.** Covered. The retry and reorg-rewind
19//! paths already re-deliver, so `resumable_watcher`'s module doc does not
20//! merely observe sink idempotency, it *requires* it (dedup by id / set
21//! insertion / authoritative re-read). A re-mined event is re-applied
22//! harmlessly.
23//! - **A log that never becomes canonical.** *Not* covered, and not the same
24//! property: idempotency makes a repeated apply cheap, whereas a confirmation
25//! lag would prevent the apply happening at all. `eth_getLogs` returns only
26//! canonical logs, so no sink ever observes a removal and none has an un-apply
27//! path. An event from an orphaned block therefore stays applied.
28//!
29//! That second case is an accepted residual, not a solved problem. It is
30//! accepted because reorgs on the target chain are shallow and rare (see
31//! `backfill`'s `REORG_MARGIN_BLOCKS`, which carries the sizing rationale for an
32//! Arbitrum-Sepolia-class L2 and is the single place that claim lives), and
33//! because the sinks holding an authoritative on-chain enumeration source
34//! re-read it and self-heal. The one without one is where an orphan sticks:
35//! settlement's `register_open_channel` (durable — an fsynced phantom channel,
36//! reclaimed only by the expiry sweep). It predates #1227 and is unchanged by
37//! it — deleting an always-zero field cannot alter what a lag never did.
38//!
39//! For the settlement watcher specifically, a lag also carries a concrete cost,
40//! which is what makes zero an active choice there rather than an inherited one:
41//! it would delay channel registration, so a client's first request on a
42//! freshly-opened channel would be rejected as unknown.
43//!
44//! If a future chain needs a lag, reintroduce it as a config knob with a
45//! **non-zero default and a test asserting a live production config engages
46//! it** — not a silently-zero field whose unit test exercises the arithmetic
47//! rather than the wiring.
48
49pub(crate) mod backfill;
50// Public because `BootRetry` appears in the `pub` bootstrap signatures that
51// external integration tests (`tests/anvil_settlement_e2e.rs`) call.
52pub mod boot_retry;
53// Public because `Route` appears in the `pub` watcher `bootstrap` signatures and
54// the builder/`spawn` are driven by external integration tests
55// (`tests/anvil_settlement_e2e.rs`) to run the settlement route.
56pub mod multiplexed_poller;
57pub(crate) mod resumable_watcher;
58// Public because it appears in the `pub` watcher `bootstrap` signatures, which
59// external integration tests (`tests/anvil_settlement_e2e.rs`) call.
60pub mod shared_head;
61
62// `resumable_watcher` is crate-private, but `WatcherHandle` is the return type of
63// the `pub multiplexed_poller::spawn`, so external callers must be able to name
64// it. Re-export it on a public path rather than widen the whole module.
65pub use resumable_watcher::WatcherHandle;
66
67pub(crate) use backfill::{REORG_MARGIN_BLOCKS, Shrunk, WindowSpan, window_end};
68
69use std::future::IntoFuture;
70use std::time::Duration;
71
72use anyhow::Result;
73
74/// Default first backoff after a failing poll tick, doubled (bounded by
75/// [`WATCHER_MAX_BACKOFF`]) on each successive failure and reset on a clean
76/// tick. The one shared multiplexed poller drives every route on this schedule,
77/// so the recovery cadence is uniform by construction (#1092).
78pub(crate) const WATCHER_INITIAL_BACKOFF: Duration = Duration::from_secs(1);
79
80/// Default ceiling for the per-tick exponential backoff. See
81/// [`WATCHER_INITIAL_BACKOFF`].
82pub(crate) const WATCHER_MAX_BACKOFF: Duration = Duration::from_mins(1);
83
84/// Fallback per-RPC-call timeout when a caller does not set its own. The alloy
85/// HTTP provider has no request timeout of its own, so a provider that keeps the
86/// connection open but never responds would otherwise wedge the tick forever —
87/// silently stopping event processing and blocking graceful shutdown. A bounded
88/// default makes such a call fail fast into the retry/backoff path instead.
89pub(crate) const DEFAULT_RPC_CALL_TIMEOUT: Duration = Duration::from_secs(10);
90
91/// Apply a per-call timeout to an RPC future, folding the elapsed case and the
92/// call's own error into one `anyhow::Error`. A `None` config uses
93/// [`DEFAULT_RPC_CALL_TIMEOUT`].
94///
95/// **Every** chain read on a watcher's path routes through here: the loop's own
96/// `get_logs`, `shared_head`'s `get_block_number`, and each follow-up RPC a
97/// [`resumable_watcher::LogSink`] issues from `apply` / `on_tick_complete`
98/// (`getChannel`, `getOrigins`, `nodeIdOf`, `get_block`,
99/// `isHashBlacklistedForOperator`). This is not automatic — the loop cannot see
100/// a sink's own reads, so a *new* sink read that does not wrap itself here
101/// reintroduces the wedge this exists to prevent: the alloy HTTP provider has no
102/// request timeout, so a provider that holds the connection open and never
103/// responds stalls the tick forever, with no backoff, no metric, and no
104/// graceful-shutdown path.
105///
106/// This helper guarantees only that a call is *bounded*. What a timeout **means**
107/// is the call site's decision, and each documents its own — the two watcher shapes
108/// being fail-the-tick-and-back-off (`getChannel`, origin's `getOrigins`) and
109/// degrade-and-continue (`slash`'s `get_block`, the registry's `nodeIdOf`,
110/// origin's `nodeIdOf`). The serve-admission reads in `ResolvingPoolView`
111/// (`getPool`, `getAuthorization`) refuse the request.
112///
113/// `nodeIdOf` is the same read at two sites under one policy: the registry and
114/// origin both count-and-skip because their projections self-heal on the
115/// operator's next event.
116///
117/// Takes `IntoFuture`, not `Future`, so an alloy `.call()` (which returns an
118/// `EthCall`, not a future) can be wrapped directly rather than each caller
119/// spelling `.into_future()`. Every `Future` is an `IntoFuture`, so this is a
120/// strict widening.
121pub(crate) async fn timed<T, E, F>(timeout: Option<Duration>, what: &str, fut: F) -> Result<T>
122where
123 F: IntoFuture<Output = std::result::Result<T, E>>,
124 E: std::error::Error + Send + Sync + 'static,
125{
126 let d = timeout.unwrap_or(DEFAULT_RPC_CALL_TIMEOUT);
127 tokio::time::timeout(d, fut)
128 .await
129 .map_err(|_| anyhow::anyhow!("{what} timed out after {d:?}"))?
130 .map_err(anyhow::Error::new)
131}
132
133/// A provider whose RPCs never resolve, for proving a read is bounded.
134///
135/// Every `timed` call site is a claim that *this* read cannot wedge the tick.
136/// The only way to test that claim is to drive the production read path against
137/// a provider that stalls — asserting the pure helper (as
138/// `timed_bounds_a_hanging_call_at_the_default` does) proves the mechanism but
139/// not the wiring, which is exactly the gap that let `confirmations` ship
140/// documented-but-disabled.
141///
142/// alloy's `Asserter` cannot express a stall: `MockTransport::map_request` pops
143/// a queued response or errors immediately on an empty queue — it never pends.
144/// So this is the smallest transport that can: `poll_ready` is always ready and
145/// `call` returns `pending()`, so a request is dispatched and then hangs with no
146/// I/O involved. That last part matters, but not for the reason you might
147/// expect. Under `#[tokio::test(start_paused = true)]` the clock auto-advances
148/// whenever the runtime goes idle, and waiting on I/O does *not* inhibit that —
149/// only `spawn_blocking` does (it is the sole caller of tokio's
150/// `inhibit_auto_advance`; `park_thread_timeout` otherwise polls the I/O driver
151/// with a zero timeout and then advances). So a wiremock server with a long
152/// response delay does not hang the test — it fails it *vacuously*: the clock
153/// races to the 10s deadline while the HTTP round-trip is still in flight, so
154/// `timed` fires on virtual time rather than on the stall being tested, and the
155/// test no longer distinguishes a bounded read from a slow one. A never-resolving
156/// in-process future has nothing to race: the deadline is the only pending timer,
157/// so firing it is the assertion.
158#[cfg(test)]
159#[allow(clippy::panic)]
160pub(crate) mod test_support;
161
162#[cfg(test)]
163#[allow(
164 clippy::unwrap_used,
165 clippy::expect_used,
166 clippy::panic,
167 clippy::indexing_slicing
168)]
169mod tests;