Expand description
Chain-backed StakerSet implementation.
This module owns the projection only: the cached NodeId → active? set
and its accessors. The CapacityBond enumeration and
the event watcher that keep it current live in
crate::dht::capacity_bond_registry (#1110), which builds this via
ChainStakerSet::from_parts — it shares one getRegisteredNodes read and one
eth_getLogs loop with the node-address directory, since both are derived
from the same contract.
The cached set is seeded from CapacityBond.getRegisteredNodes(), keyed on the
parallel active[] the contract computes per entry — the full isActive
predicate: registered, bond ≥ minBond, no unbonding, not ejected. The page
itself is the un-filtered _registeredAddrs array (the node-address directory
keeps every entry), so the contract returns the predicate alongside it rather
than filtering the page. It then follows the five membership-mutating events,
applying a StakerChange
for every observed transition. (Not an intra-doc link: StakerChange is
private to dht since #1231, and this module doc is public.)
§Spec mapping
- ADR 022 §STORE Flow: the receiver checks
holderis in the cached active-staker set populated fromCapacityBond.getRegisteredNodes(). - ADR 019 § Step 3.3: bootstrap pattern (initial paginated
getRegisteredNodes+ event follow — implemented as aneth_getLogspoll, #1106). - The event set the watcher follows is grounded in
CapacityBond.sol’s own write-paths — every contract write that flips the canonicalisActivepredicate is mirrored by an event there.
§Failure model
Bootstrap RPC failure → the registry bootstrap propagates the error (the runtime treats it as fatal; the DHT cannot function without a staker set).
Watcher RPC failure (mid-run) → the shared loop logs at warn!, sleeps for an
exponentially-growing backoff (1s → 60s cap), and re-polls. The cursor is
retained across the backoff, so the next eth_getLogs tick re-scans
[cursor, head] and re-applies any membership event that landed during the
outage — no stream-level drift window.
A narrower drift source: an operator-indexed event whose follow-up
nodeIdOf(operator) RPC fails is dropped (the membership change is
lost) without tripping the stream backoff. That case bumps no
restart/down-seconds metric, so it is surfaced separately by
decdn_staker_set_watcher_resolve_failures_total (#788).
This drift window is dashboard-visible and alertable via four
metrics (#783, #788), distinct from decdn_rpc_healthy (which tracks
the reachability watchdog, not this task):
decdn_staker_set_watcher_restarts_total— distinct drift windows; bumped once on the edge from a healthy cycle into the error state, not once per backoff iteration of one continuous outage.decdn_staker_set_watcher_down_seconds— true downtime: reads0for the whole life of any established cycle (however long/quiet) and climbs only while the loop is in error/backoff between a failed cycle and the next success, so an alert fires on a sustained outage.decdn_staker_set_watcher_resolve_failures_total— operator-indexed events (Reinstated/UnbondingRequested) dropped because the follow-upnodeIdOfRPC failed; a nonzero rate is silent-drift risk that no stream-level restart would otherwise surface.decdn_staker_set_active_count— current cached active-set size, to spot a frozen or collapsed cache.
Two more cover the repair rather than the drift:
decdn_capacity_bond_registry_resync_failures_total— re-enumerations that could not read chain state and kept the previous projections.decdn_capacity_bond_registry_last_resync_timestamp_seconds— when the last one succeeded; the signal that catches a repair being skipped, which emits nothing at all.
Since #1110 these describe the one shared capacity-bond loop, which also
feeds the bindings projection — so they are that loop’s health for both, and
there is no separate node-address watcher family to correlate against
(#1231).
Re-enumeration is what repairs the drift sources above:
capacity_bond_registry re-derives the whole projection from
getRegisteredNodes, build-then-swap, so a dropped nodeIdOf change is
corrected without waiting for the next event for that operator. Two triggers
drive it. The cadence runs it every REGISTRY_RESYNC_INTERVAL. The recovery
edge runs it on the tick the route comes back from an errored one, through
LogSink::on_recovered, which is what covers the outage case the cadence
handles worst: reconcile_routes skips a route that errored this tick, so
during the outage the cadence repair does not run at all, and a resync whose
own read fails stamps the clock anyway and defers a further interval.
Neither trigger is a ceiling, for two reasons worth knowing before reading
the metrics as one. The recovery edge fires on a route-level outage — a tick
that errored — and a dropped nodeIdOf does not error the route, since
on_operator_change returns Ok by design; a change lost that way with the
poll otherwise healthy still waits for the cadence. And the re-enumeration
the edge forces can itself fail, which stamps the clock and defers the next
attempt a further interval. So the window closes on the first successful
re-enumeration after recovery, not on recovery itself. The metrics above size
that window; the two registry metrics report whether the repair is landing.
Structs§
- Chain
Staker Set - Chain-backed staker set. Cheap to clone via the shared inner
Arc; the runtime holds oneArc<dyn StakerSet>and consumers access the cached set through it. The background loop that keeps the cache fresh is the single shared multiplexed-poller task the runtime owns, so a node-restart cycle never leaks chain-poll tasks.