Skip to main content

rings_node/
online.rs

1//! Signed online-node descriptors stored in the DHT.
2
3use rings_core::dht::Did;
4use rings_core::ecc::PublicKey;
5use rings_core::ecc::VerificationPublicKey;
6use rings_core::error::Error;
7use rings_core::error::Result;
8use rings_core::message::Decoder;
9use rings_core::message::DhtProtocolMode;
10use rings_core::message::Encoded;
11use rings_core::message::Encoder;
12use rings_core::message::MessageVerification;
13use rings_core::session::SessionSk;
14use serde::Deserialize;
15use serde::Serialize;
16
17use crate::descriptor::decode_descriptor;
18use crate::descriptor::encode_descriptor;
19use crate::descriptor::latest_valid_by_did;
20use crate::descriptor::sign_descriptor_body;
21use crate::descriptor::SignedDescriptor;
22use crate::descriptor::SignedDescriptorBody;
23
24/// DHT topic used for online-node registry descriptors.
25pub const ONLINE_NODES_TOPIC: &str = "online_nodes";
26/// Capability label for nodes that provide DHT storage.
27pub const ONLINE_NODE_CAPABILITY_STORAGE: &str = "storage";
28
29/// Runtime family advertised by a node descriptor.
30#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
31pub enum OnlineNodeType {
32    /// Browser runtime.
33    Browser,
34    /// Native node runtime.
35    Native,
36    /// FFI runtime.
37    Ffi,
38}
39
40/// Descriptor fields covered by the online-node signature.
41#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
42pub struct OnlineNodeDescriptorBody {
43    /// DID of the node/account.
44    pub did: Did,
45    /// Account public key corresponding to `did`.
46    pub public_key: VerificationPublicKey,
47    /// Session public key used for encrypted onion relay frames.
48    pub session_public_key: PublicKey<33>,
49    /// Runtime family of this node.
50    pub node_type: OnlineNodeType,
51    /// Network identifier.
52    pub network_id: u32,
53    /// Storage redundancy required by this DHT protocol mode.
54    pub storage_redundancy: u16,
55    /// Storage virtual-node positions required by this DHT protocol mode.
56    pub dht_virtual_nodes: u16,
57    /// Optional capability labels.
58    pub capabilities: Vec<String>,
59    /// Optional endpoint hint, controlled by node policy/configuration.
60    pub endpoint_hint: Option<String>,
61    /// Process start timestamp in milliseconds since Unix epoch.
62    pub started_at_ms: u128,
63    /// Heartbeat timestamp in milliseconds since Unix epoch.
64    pub heartbeat_at_ms: u128,
65    /// Expiry timestamp in milliseconds since Unix epoch.
66    pub expires_at_ms: u128,
67    /// Node software version.
68    pub version: String,
69}
70
71impl OnlineNodeDescriptorBody {
72    fn body_ref(&self) -> OnlineNodeDescriptorBodyRef<'_> {
73        OnlineNodeDescriptorBodyRef {
74            did: self.did,
75            public_key: &self.public_key,
76            session_public_key: &self.session_public_key,
77            node_type: &self.node_type,
78            network_id: self.network_id,
79            storage_redundancy: self.storage_redundancy,
80            dht_virtual_nodes: self.dht_virtual_nodes,
81            capabilities: &self.capabilities,
82            endpoint_hint: &self.endpoint_hint,
83            started_at_ms: self.started_at_ms,
84            heartbeat_at_ms: self.heartbeat_at_ms,
85            expires_at_ms: self.expires_at_ms,
86            version: self.version.as_str(),
87        }
88    }
89
90    fn signing_data(&self) -> Result<Vec<u8>> {
91        self.body_ref().signing_data()
92    }
93}
94
95impl SignedDescriptorBody for OnlineNodeDescriptorBody {
96    type Descriptor = OnlineNodeDescriptor;
97
98    fn body_did(&self) -> Did {
99        self.did
100    }
101
102    fn body_public_key(&self) -> &VerificationPublicKey {
103        &self.public_key
104    }
105
106    fn body_signing_data(&self) -> Result<Vec<u8>> {
107        self.signing_data()
108    }
109
110    fn into_signed_descriptor(self, signature: MessageVerification) -> Self::Descriptor {
111        OnlineNodeDescriptor {
112            did: self.did,
113            public_key: self.public_key,
114            session_public_key: self.session_public_key,
115            node_type: self.node_type,
116            network_id: self.network_id,
117            storage_redundancy: self.storage_redundancy,
118            dht_virtual_nodes: self.dht_virtual_nodes,
119            capabilities: self.capabilities,
120            endpoint_hint: self.endpoint_hint,
121            started_at_ms: self.started_at_ms,
122            heartbeat_at_ms: self.heartbeat_at_ms,
123            expires_at_ms: self.expires_at_ms,
124            version: self.version,
125            signature,
126        }
127    }
128}
129
130#[derive(Serialize)]
131struct OnlineNodeDescriptorBodyRef<'a> {
132    did: Did,
133    public_key: &'a VerificationPublicKey,
134    session_public_key: &'a PublicKey<33>,
135    node_type: &'a OnlineNodeType,
136    network_id: u32,
137    storage_redundancy: u16,
138    dht_virtual_nodes: u16,
139    capabilities: &'a [String],
140    endpoint_hint: &'a Option<String>,
141    started_at_ms: u128,
142    heartbeat_at_ms: u128,
143    expires_at_ms: u128,
144    version: &'a str,
145}
146
147impl OnlineNodeDescriptorBodyRef<'_> {
148    fn signing_data(&self) -> Result<Vec<u8>> {
149        rings_codec::serialize(self).map_err(Error::CodecSerialize)
150    }
151}
152
153/// Signed descriptor published by online nodes.
154#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
155pub struct OnlineNodeDescriptor {
156    /// DID of the node/account.
157    pub did: Did,
158    /// Account public key corresponding to `did`.
159    pub public_key: VerificationPublicKey,
160    /// Session public key used for encrypted onion relay frames.
161    pub session_public_key: PublicKey<33>,
162    /// Runtime family of this node.
163    pub node_type: OnlineNodeType,
164    /// Network identifier.
165    pub network_id: u32,
166    /// Storage redundancy required by this DHT protocol mode.
167    pub storage_redundancy: u16,
168    /// Storage virtual-node positions required by this DHT protocol mode.
169    pub dht_virtual_nodes: u16,
170    /// Optional capability labels.
171    pub capabilities: Vec<String>,
172    /// Optional endpoint hint, controlled by node policy/configuration.
173    pub endpoint_hint: Option<String>,
174    /// Process start timestamp in milliseconds since Unix epoch.
175    pub started_at_ms: u128,
176    /// Heartbeat timestamp in milliseconds since Unix epoch.
177    pub heartbeat_at_ms: u128,
178    /// Expiry timestamp in milliseconds since Unix epoch.
179    pub expires_at_ms: u128,
180    /// Node software version.
181    pub version: String,
182    /// Signature covering every descriptor field above.
183    pub signature: MessageVerification,
184}
185
186impl OnlineNodeDescriptor {
187    /// Create and sign a descriptor.
188    pub fn new_signed(body: OnlineNodeDescriptorBody, session_sk: &SessionSk) -> Result<Self> {
189        sign_descriptor_body(
190            body,
191            session_sk,
192            "online node descriptor DID/public key/session mismatch",
193        )
194    }
195
196    fn body_ref(&self) -> OnlineNodeDescriptorBodyRef<'_> {
197        let Self {
198            did,
199            public_key,
200            session_public_key,
201            node_type,
202            network_id,
203            storage_redundancy,
204            dht_virtual_nodes,
205            capabilities,
206            endpoint_hint,
207            started_at_ms,
208            heartbeat_at_ms,
209            expires_at_ms,
210            version,
211            signature: _,
212        } = self;
213
214        OnlineNodeDescriptorBodyRef {
215            did: *did,
216            public_key,
217            session_public_key,
218            node_type,
219            network_id: *network_id,
220            storage_redundancy: *storage_redundancy,
221            dht_virtual_nodes: *dht_virtual_nodes,
222            capabilities,
223            endpoint_hint,
224            started_at_ms: *started_at_ms,
225            heartbeat_at_ms: *heartbeat_at_ms,
226            expires_at_ms: *expires_at_ms,
227            version: version.as_str(),
228        }
229    }
230
231    fn signing_data(&self) -> Result<Vec<u8>> {
232        self.body_ref().signing_data()
233    }
234
235    /// Return this descriptor's DHT protocol mode.
236    pub const fn dht_protocol_mode(&self) -> DhtProtocolMode {
237        DhtProtocolMode::new(
238            self.network_id,
239            self.storage_redundancy,
240            self.dht_virtual_nodes,
241        )
242    }
243
244    /// Return whether this descriptor belongs to the local DHT protocol mode.
245    pub fn matches_dht_protocol(&self, expected: DhtProtocolMode) -> bool {
246        self.dht_protocol_mode().matches(expected)
247    }
248
249    /// Verify the descriptor signature and DID/public-key binding.
250    ///
251    /// This does not apply the embedded [`MessageVerification`] timestamp/TTL
252    /// as a liveness rule. Online-node liveness is defined solely by the
253    /// signed `expires_at_ms` descriptor field; use [`Self::is_live_at`] when
254    /// expiry should be enforced.
255    pub fn verify_signature(&self) -> bool {
256        self.descriptor_verify_signature()
257    }
258
259    /// Returns whether this descriptor is expired at `now_ms`.
260    pub fn is_expired_at(&self, now_ms: u128) -> bool {
261        self.descriptor_is_expired_at(now_ms)
262    }
263
264    /// Returns whether this descriptor has a valid signature and is not expired.
265    pub fn is_live_at(&self, now_ms: u128) -> bool {
266        self.descriptor_is_live_at(now_ms)
267    }
268
269    /// Select the newest valid descriptor per DID.
270    pub fn latest_valid_by_did(
271        descriptors: impl IntoIterator<Item = Self>,
272        now_ms: u128,
273        include_expired: bool,
274    ) -> Vec<Self> {
275        latest_valid_by_did(descriptors, now_ms, include_expired)
276    }
277}
278
279impl SignedDescriptor for OnlineNodeDescriptor {
280    fn descriptor_did(&self) -> Did {
281        self.did
282    }
283
284    fn descriptor_public_key(&self) -> &VerificationPublicKey {
285        &self.public_key
286    }
287
288    fn descriptor_signature(&self) -> &MessageVerification {
289        &self.signature
290    }
291
292    fn descriptor_heartbeat_at_ms(&self) -> u128 {
293        self.heartbeat_at_ms
294    }
295
296    fn descriptor_expires_at_ms(&self) -> u128 {
297        self.expires_at_ms
298    }
299
300    fn descriptor_signing_data(&self) -> Result<Vec<u8>> {
301        self.signing_data()
302    }
303}
304
305impl Encoder for OnlineNodeDescriptor {
306    fn encode(&self) -> Result<Encoded> {
307        encode_descriptor(self)
308    }
309}
310
311impl Decoder for OnlineNodeDescriptor {
312    fn from_encoded(encoded: &Encoded) -> Result<Self> {
313        decode_descriptor(encoded)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use rings_core::ecc::SecretKey;
320    use rings_core::session::SessionSk;
321
322    use super::*;
323
324    fn descriptor_at(heartbeat_at_ms: u128, expires_at_ms: u128) -> Result<OnlineNodeDescriptor> {
325        let key = SecretKey::random();
326        let session_sk = SessionSk::new_with_seckey(&key)?;
327        let did = session_sk.account_did();
328        OnlineNodeDescriptor::new_signed(
329            OnlineNodeDescriptorBody {
330                did,
331                public_key: session_sk.session().account_verification_pubkey()?,
332                session_public_key: session_sk.session_public_key(),
333                node_type: OnlineNodeType::Native,
334                network_id: 1,
335                storage_redundancy: 6,
336                dht_virtual_nodes: 0,
337                capabilities: vec![ONLINE_NODE_CAPABILITY_STORAGE.to_string()],
338                endpoint_hint: None,
339                started_at_ms: 10,
340                heartbeat_at_ms,
341                expires_at_ms,
342                version: "test".to_string(),
343            },
344            &session_sk,
345        )
346    }
347
348    #[test]
349    fn test_descriptor_signature_covers_mutable_fields() -> Result<()> {
350        let mut descriptor = descriptor_at(20, 30)?;
351        assert!(descriptor.verify_signature());
352
353        descriptor.node_type = OnlineNodeType::Browser;
354        assert!(!descriptor.verify_signature());
355        descriptor = descriptor_at(20, 30)?;
356        descriptor.storage_redundancy = 7;
357        assert!(!descriptor.verify_signature());
358        Ok(())
359    }
360
361    #[test]
362    fn test_descriptor_round_trips_through_dht_encoding() -> Result<()> {
363        let descriptor = descriptor_at(20, 30)?;
364        let encoded = descriptor.encode()?;
365        let decoded = OnlineNodeDescriptor::from_encoded(&encoded)?;
366
367        assert_eq!(decoded, descriptor);
368        assert!(decoded.verify_signature());
369        Ok(())
370    }
371
372    #[test]
373    fn test_latest_valid_by_did_filters_expired_and_keeps_newest() -> Result<()> {
374        let key = SecretKey::random();
375        let session_sk = SessionSk::new_with_seckey(&key)?;
376        let did = session_sk.account_did();
377        let public_key = session_sk.session().account_verification_pubkey()?;
378
379        let older = OnlineNodeDescriptor::new_signed(
380            OnlineNodeDescriptorBody {
381                did,
382                public_key: public_key.clone(),
383                session_public_key: session_sk.session_public_key(),
384                node_type: OnlineNodeType::Native,
385                network_id: 1,
386                storage_redundancy: 6,
387                dht_virtual_nodes: 0,
388                capabilities: vec![],
389                endpoint_hint: None,
390                started_at_ms: 1,
391                heartbeat_at_ms: 10,
392                expires_at_ms: 100,
393                version: "old".to_string(),
394            },
395            &session_sk,
396        )?;
397        let newer = OnlineNodeDescriptor::new_signed(
398            OnlineNodeDescriptorBody {
399                did,
400                public_key,
401                session_public_key: session_sk.session_public_key(),
402                node_type: OnlineNodeType::Native,
403                network_id: 1,
404                storage_redundancy: 6,
405                dht_virtual_nodes: 0,
406                capabilities: vec![],
407                endpoint_hint: None,
408                started_at_ms: 1,
409                heartbeat_at_ms: 20,
410                expires_at_ms: 100,
411                version: "new".to_string(),
412            },
413            &session_sk,
414        )?;
415        let other_live = descriptor_at(25, 100)?;
416        let expired = descriptor_at(30, 40)?;
417
418        let descriptors = OnlineNodeDescriptor::latest_valid_by_did(
419            vec![
420                older.clone(),
421                newer.clone(),
422                other_live.clone(),
423                expired.clone(),
424            ],
425            50,
426            false,
427        );
428
429        assert_eq!(descriptors.len(), 2);
430        assert!(descriptors.iter().any(|descriptor| descriptor == &newer));
431        assert!(descriptors
432            .iter()
433            .any(|descriptor| descriptor == &other_live));
434
435        let with_expired = OnlineNodeDescriptor::latest_valid_by_did(
436            vec![older, newer, other_live, expired],
437            50,
438            true,
439        );
440        assert_eq!(with_expired.len(), 3);
441        Ok(())
442    }
443}