Skip to main content

rings_core/dht/
virtual_node.rs

1#![deny(missing_docs)]
2//! Chord-style virtual positions for storage ownership.
3//!
4//! These positions are not independent signing identities. They are derived
5//! ring locations owned by an authenticated physical DID and are used only by
6//! storage owner selection.
7
8use std::collections::BTreeSet;
9
10use ethereum_types::H160;
11use sha1::Digest;
12use sha1::Sha1;
13
14use crate::dht::topology;
15use crate::dht::Did;
16use crate::dht::DEFAULT_FINGER_TABLE_SIZE;
17
18const VIRTUAL_NODE_DOMAIN: &[u8] = b"rings:vnode";
19
20/// Maximum virtual storage positions derived per physical owner.
21pub const MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER: u16 = 256;
22
23/// Default virtual storage positions derived per physical owner.
24///
25/// The Chord paper recommends mapping each real node to O(log N) virtual nodes.
26/// A joining node does not know the stable network size N before it enters the
27/// DHT, so Rings uses the Chord finger-table width as the network-wide default
28/// O(log N) operating point. Operators can still set this to zero to disable
29/// virtual storage ownership for a network.
30pub const DEFAULT_STORAGE_VIRTUAL_POSITIONS_PER_OWNER: u16 = DEFAULT_FINGER_TABLE_SIZE as u16;
31
32/// Return the default virtual storage positions derived per physical owner.
33pub const fn default_storage_virtual_positions_per_owner() -> u16 {
34    DEFAULT_STORAGE_VIRTUAL_POSITIONS_PER_OWNER
35}
36
37/// Configuration for storage virtual nodes.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct VirtualNodeConfig {
40    /// Network namespace mixed into derived virtual positions.
41    network_id: u32,
42    /// Number of virtual positions derived for each physical owner.
43    positions_per_owner: u16,
44}
45
46impl VirtualNodeConfig {
47    /// Disable virtual-node storage ownership.
48    pub const fn disabled() -> Self {
49        Self {
50            network_id: 0,
51            positions_per_owner: 0,
52        }
53    }
54
55    /// Build a virtual-node configuration.
56    ///
57    /// Post: `positions_per_owner <= MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER`.
58    pub const fn new(network_id: u32, positions_per_owner: u16) -> Self {
59        Self {
60            network_id,
61            positions_per_owner: Self::bounded_positions_per_owner(positions_per_owner),
62        }
63    }
64
65    /// Returns whether this configuration enables virtual storage ownership.
66    pub const fn is_enabled(self) -> bool {
67        self.positions_per_owner > 0
68    }
69
70    /// Return the network namespace mixed into derived virtual positions.
71    pub const fn network_id(self) -> u32 {
72        self.network_id
73    }
74
75    /// Return the bounded number of positions derived for each physical owner.
76    pub const fn positions_per_owner(self) -> u16 {
77        self.positions_per_owner
78    }
79
80    /// Returns whether `positions_per_owner` is inside the configured cost bound.
81    pub const fn positions_per_owner_within_limit(positions_per_owner: u16) -> bool {
82        positions_per_owner <= MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER
83    }
84
85    const fn bounded_positions_per_owner(positions_per_owner: u16) -> u16 {
86        if positions_per_owner > MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER {
87            MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER
88        } else {
89            positions_per_owner
90        }
91    }
92}
93
94/// One derived Chord ring position owned by a physical peer.
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub struct VirtualNode {
97    /// Physical peer DID that signs messages and owns the transport.
98    pub owner_did: Did,
99    /// Derived Chord ring position used for storage ownership.
100    pub vnode_did: Did,
101    /// Owner-local virtual-node index.
102    pub index: u16,
103}
104
105impl VirtualNode {
106    /// Derive one virtual position.
107    pub fn derive(network_id: u32, owner_did: Did, index: u16) -> Self {
108        let mut hasher = Sha1::new();
109        hasher.update(VIRTUAL_NODE_DOMAIN);
110        hasher.update(network_id.to_be_bytes());
111        hasher.update(owner_did.as_bytes());
112        hasher.update(index.to_be_bytes());
113        let bytes: [u8; 20] = hasher.finalize().into();
114        Self {
115            owner_did,
116            vnode_did: Did::from(H160::from(bytes)),
117            index,
118        }
119    }
120}
121
122/// Storage-owner registry for derived virtual positions.
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct StorageVirtualNodes {
125    config: VirtualNodeConfig,
126    owners: BTreeSet<Did>,
127}
128
129impl StorageVirtualNodes {
130    /// Create an empty registry.
131    pub fn new(config: VirtualNodeConfig) -> Self {
132        Self {
133            config,
134            owners: BTreeSet::new(),
135        }
136    }
137
138    /// Create a registry from known physical owners.
139    pub fn from_owners(config: VirtualNodeConfig, owners: impl IntoIterator<Item = Did>) -> Self {
140        let mut registry = Self::new(config);
141        for owner in owners {
142            registry.register_owner(owner);
143        }
144        registry
145    }
146
147    /// Return the active configuration.
148    pub const fn config(&self) -> VirtualNodeConfig {
149        self.config
150    }
151
152    /// Returns whether this registry enables virtual storage ownership.
153    pub const fn is_enabled(&self) -> bool {
154        self.config.is_enabled()
155    }
156
157    /// Register one physical owner.
158    ///
159    /// Post: if virtual nodes are disabled, the registry is unchanged.
160    pub fn register_owner(&mut self, owner_did: Did) {
161        if self.is_enabled() {
162            self.owners.insert(owner_did);
163        }
164    }
165
166    /// Remove one physical owner.
167    pub fn unregister_owner(&mut self, owner_did: Did) {
168        self.owners.remove(&owner_did);
169    }
170
171    /// Returns whether this physical owner is registered.
172    pub fn contains_owner(&self, owner_did: Did) -> bool {
173        self.owners.contains(&owner_did)
174    }
175
176    /// Return all virtual positions for `owner_did`.
177    pub fn positions_for_owner(&self, owner_did: Did) -> Vec<VirtualNode> {
178        if !self.contains_owner(owner_did) {
179            return Vec::new();
180        }
181        self.derive_owner_positions(owner_did)
182    }
183
184    /// Return all registered virtual positions.
185    pub fn positions(&self) -> Vec<VirtualNode> {
186        let mut positions = Vec::new();
187        for owner in self.owners.iter().copied() {
188            positions.extend(self.derive_owner_positions(owner));
189        }
190        positions.sort_by_key(|position| (position.vnode_did, position.owner_did, position.index));
191        positions
192    }
193
194    /// Resolve the physical owner responsible for `key`.
195    ///
196    /// This uses Chord successor ownership: the selected virtual position is
197    /// the registered position with minimum clockwise distance from `key`.
198    pub fn owner_for_key(&self, key: Did) -> Option<Did> {
199        self.positions()
200            .into_iter()
201            .min_by(|left, right| {
202                topology::dist(key, left.vnode_did)
203                    .cmp(&topology::dist(key, right.vnode_did))
204                    .then_with(|| left.owner_did.cmp(&right.owner_did))
205                    .then_with(|| left.index.cmp(&right.index))
206            })
207            .map(|position| position.owner_did)
208    }
209
210    fn derive_owner_positions(&self, owner_did: Did) -> Vec<VirtualNode> {
211        (0..self.config.positions_per_owner())
212            .map(|index| VirtualNode::derive(self.config.network_id(), owner_did, index))
213            .collect()
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_virtual_node_derivation_is_domain_separated_by_network() {
223        let owner = Did::from(42u32);
224        let first = VirtualNode::derive(7, owner, 0);
225        let repeated = VirtualNode::derive(7, owner, 0);
226        let other_network = VirtualNode::derive(8, owner, 0);
227
228        assert_eq!(first, repeated);
229        assert_ne!(first.vnode_did, owner);
230        assert_ne!(first.vnode_did, other_network.vnode_did);
231    }
232
233    #[test]
234    fn test_virtual_node_config_caps_positions_at_cost_bound() {
235        let config =
236            VirtualNodeConfig::new(1, MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER.saturating_add(1));
237
238        assert_eq!(
239            config.positions_per_owner(),
240            MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER
241        );
242    }
243
244    #[test]
245    fn test_storage_virtual_nodes_resolve_owner_from_virtual_position() -> crate::error::Result<()>
246    {
247        let owner_a = Did::from(10u32);
248        let owner_b = Did::from(20u32);
249        let registry =
250            StorageVirtualNodes::from_owners(VirtualNodeConfig::new(1, 2), [owner_a, owner_b]);
251
252        let owner_b_position = registry
253            .positions_for_owner(owner_b)
254            .into_iter()
255            .next()
256            .map(|position| position.vnode_did)
257            .ok_or_else(|| {
258                crate::error::Error::InvalidMessage(
259                    "owner should have virtual positions".to_string(),
260                )
261            })?;
262
263        assert_eq!(registry.owner_for_key(owner_b_position), Some(owner_b));
264        Ok(())
265    }
266
267    #[test]
268    fn test_storage_virtual_nodes_resolve_successor_owner_for_interval_key(
269    ) -> crate::error::Result<()> {
270        let owners = [Did::from(10u32), Did::from(20u32), Did::from(30u32)];
271        let registry = StorageVirtualNodes::from_owners(VirtualNodeConfig::new(1, 2), owners);
272        let positions = registry.positions();
273
274        let Some((left, successor)) =
275            positions
276                .iter()
277                .zip(positions.iter().cycle().skip(1))
278                .find(|(left, successor)| {
279                    let key = left.vnode_did + Did::from(1u32);
280                    key != successor.vnode_did
281                })
282        else {
283            return Err(crate::error::Error::InvalidMessage(
284                "expected a non-empty virtual interval".to_string(),
285            ));
286        };
287
288        let key = left.vnode_did + Did::from(1u32);
289        assert_eq!(registry.owner_for_key(key), Some(successor.owner_did));
290        Ok(())
291    }
292
293    #[test]
294    fn test_unregister_owner_removes_virtual_positions() {
295        let owner = Did::from(10u32);
296        let mut registry = StorageVirtualNodes::new(VirtualNodeConfig::new(1, 3));
297        registry.register_owner(owner);
298        registry.unregister_owner(owner);
299
300        assert!(registry.positions().is_empty());
301        assert_eq!(registry.owner_for_key(Did::from(11u32)), None);
302    }
303}