decdn_node/dht/staker_set.rs
1//! Active-staker set abstraction (ADR 022 §STORE Flow, §FIND\_VALUE Flow).
2//!
3//! The DHT handler consults the staker set on every `Store` admission
4//! (reject records from non-staked publishers per ADR 022 §STORE Flow
5//! line 140) and the requester-side iterative lookup consults it on
6//! every response (ADR 022 §Lookup integrity step 2 — drop non-staked
7//! `NodeId`s from `providers` and `closer_nodes`). This module owns the
8//! trait + an in-memory implementation suitable for tests / the no-op
9//! runtime default; the chain-backed `ChainStakerSet` lives next to it
10//! in [`super::chain_staker_set`] and is wired by the runtime once the
11//! alloy `Provider` is available.
12
13use std::collections::HashSet;
14
15use crate::dht::routing::NodeId;
16
17/// Read-only view of the cached active-staker set.
18///
19/// Implementations MUST be cheap to clone (typically `Arc<inner>`); the
20/// handler holds a long-lived `Arc<dyn StakerSet>` and every admitted
21/// `Store` / responded-`FindValue` request consults it on the hot path.
22///
23/// `is_active` is the only call the handler makes per request.
24/// `active_nodes` is exposed for the bootstrap path (seed the routing
25/// table from it) and for operator tooling.
26///
27/// There is deliberately no change-subscription seam. Consumers read the
28/// cached set directly; add a subscription only alongside the code that
29/// needs it.
30pub trait StakerSet: Send + Sync + std::fmt::Debug {
31 /// Whether `node_id` is in the active-staker set.
32 fn is_active(&self, node_id: &NodeId) -> bool;
33
34 /// Snapshot of the active-staker set. Order is unspecified; the caller
35 /// MUST randomize before any selection step that an attacker could
36 /// influence (e.g. bootstrap target picking).
37 fn active_nodes(&self) -> Vec<NodeId>;
38
39 /// Cardinality of the active set. Default implementation walks
40 /// [`Self::active_nodes`]; production impls (chain-backed cache, etc.)
41 /// SHOULD override with an O(1) counter.
42 fn len(&self) -> usize {
43 self.active_nodes().len()
44 }
45
46 /// Convenience over [`Self::len`].
47 fn is_empty(&self) -> bool {
48 self.len() == 0
49 }
50}
51
52/// In-memory [`StakerSet`] implementation, built from a parsed
53/// `NodeId` set or empty.
54///
55/// The runtime wires the chain-backed `ChainStakerSet` (reads
56/// `CapacityBond.getRegisteredNodes()` once at startup, then follows the registry's
57/// membership events via the shared `eth_getLogs` poll) — see
58/// `capacity_bond_registry::bootstrap`, #1110. This impl is what tests and
59/// explicit operator-supplied sets use: construct via [`Self::new`] with a known
60/// `HashSet`, or [`Self::empty`]. It implements the same `StakerSet` trait as
61/// the chain-backed impl, so the two are interchangeable.
62#[derive(Debug)]
63pub struct ConfigStakerSet {
64 active: HashSet<NodeId>,
65}
66
67impl ConfigStakerSet {
68 /// Build from a parsed `NodeId` set.
69 #[must_use]
70 pub const fn new(active: HashSet<NodeId>) -> Self {
71 Self { active }
72 }
73
74 /// Empty set — useful in tests where no staker check is desired
75 /// (paired with a permissive handler config). Production code MUST
76 /// NOT use this without an explicit operator opt-in; an empty
77 /// staker set means every `Store` is rejected.
78 #[must_use]
79 pub fn empty() -> Self {
80 Self::new(HashSet::new())
81 }
82}
83
84impl StakerSet for ConfigStakerSet {
85 fn is_active(&self, node_id: &NodeId) -> bool {
86 self.active.contains(node_id)
87 }
88
89 fn active_nodes(&self) -> Vec<NodeId> {
90 self.active.iter().copied().collect()
91 }
92
93 fn len(&self) -> usize {
94 self.active.len()
95 }
96}
97
98#[cfg(test)]
99#[allow(
100 clippy::unwrap_used,
101 clippy::expect_used,
102 clippy::panic,
103 clippy::indexing_slicing
104)]
105mod tests;