Skip to main content

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;