Skip to main content

agent_uri_dht/
node.rs

1//! Node identity and peer addressing.
2
3use std::fmt;
4
5/// The identity of a node participating in the DHT.
6///
7/// Opaque and backend-defined: a Kademlia backend derives it from the node's
8/// public key, while the in-process simulator synthesizes one. Length varies
9/// by backend, so this holds bytes rather than a fixed-size array.
10#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub struct NodeId(Vec<u8>);
12
13impl NodeId {
14    /// Creates a node identity from raw bytes.
15    #[must_use]
16    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
17        Self(bytes.into())
18    }
19
20    /// Returns the raw bytes of this identity.
21    #[must_use]
22    pub fn as_bytes(&self) -> &[u8] {
23        &self.0
24    }
25
26    /// Returns true if this identity carries no bytes.
27    #[must_use]
28    pub fn is_empty(&self) -> bool {
29        self.0.is_empty()
30    }
31}
32
33impl fmt::Display for NodeId {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        // Truncated hex, matching DhtKey's display convention.
36        for byte in self.0.iter().take(8) {
37            write!(f, "{byte:02x}")?;
38        }
39        if self.0.len() > 8 {
40            write!(f, "...")?;
41        }
42        Ok(())
43    }
44}
45
46impl AsRef<[u8]> for NodeId {
47    fn as_ref(&self) -> &[u8] {
48        &self.0
49    }
50}
51
52/// The address of a peer used to join the overlay.
53///
54/// Held as an opaque string because the encoding is the backend's business: a
55/// libp2p backend parses a multiaddr, the simulator ignores it entirely.
56#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
57pub struct PeerAddr(String);
58
59impl PeerAddr {
60    /// Creates a peer address from its backend-specific string form.
61    #[must_use]
62    pub fn new(addr: impl Into<String>) -> Self {
63        Self(addr.into())
64    }
65
66    /// Returns the address string.
67    #[must_use]
68    pub fn as_str(&self) -> &str {
69        &self.0
70    }
71}
72
73impl fmt::Display for PeerAddr {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(&self.0)
76    }
77}
78
79/// The outcome of a write that had to reach a quorum of replicas.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct WriteReceipt {
82    acknowledged: usize,
83    required: usize,
84}
85
86impl WriteReceipt {
87    /// Records a write acknowledged by `acknowledged` replicas against a
88    /// quorum of `required`.
89    #[must_use]
90    pub const fn new(acknowledged: usize, required: usize) -> Self {
91        Self {
92            acknowledged,
93            required,
94        }
95    }
96
97    /// Returns how many replicas acknowledged the write.
98    #[must_use]
99    pub const fn acknowledged(&self) -> usize {
100        self.acknowledged
101    }
102
103    /// Returns how many acknowledgements the requested quorum needed.
104    #[must_use]
105    pub const fn required(&self) -> usize {
106        self.required
107    }
108
109    /// Returns true if the write reached more replicas than the quorum needed.
110    ///
111    /// Useful for callers that want to know a write is durable beyond the
112    /// minimum they asked for.
113    #[must_use]
114    pub const fn exceeded_quorum(&self) -> bool {
115        self.acknowledged > self.required
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn node_id_round_trips_bytes() {
125        let id = NodeId::from_bytes(vec![1, 2, 3]);
126        assert_eq!(id.as_bytes(), &[1, 2, 3]);
127        assert!(!id.is_empty());
128    }
129
130    #[test]
131    fn node_id_display_is_truncated_hex() {
132        let id = NodeId::from_bytes(vec![0xab; 16]);
133        let shown = id.to_string();
134        assert_eq!(shown, "abababababababab...");
135    }
136
137    #[test]
138    fn short_node_id_display_has_no_ellipsis() {
139        let id = NodeId::from_bytes(vec![0xab; 4]);
140        assert_eq!(id.to_string(), "abababab");
141    }
142
143    #[test]
144    fn peer_addr_keeps_its_string_form() {
145        let addr = PeerAddr::new("/ip4/127.0.0.1/tcp/4001");
146        assert_eq!(addr.as_str(), "/ip4/127.0.0.1/tcp/4001");
147        assert_eq!(addr.to_string(), "/ip4/127.0.0.1/tcp/4001");
148    }
149
150    #[test]
151    fn receipt_reports_acknowledgements() {
152        let r = WriteReceipt::new(11, 11);
153        assert_eq!(r.acknowledged(), 11);
154        assert_eq!(r.required(), 11);
155        assert!(!r.exceeded_quorum());
156    }
157
158    #[test]
159    fn receipt_detects_writes_beyond_quorum() {
160        assert!(WriteReceipt::new(20, 11).exceeded_quorum());
161    }
162}