Skip to main content

unb_core/
node_core.rs

1use std::collections::BTreeMap;
2
3use serde_json::{json, Value};
4
5use crate::discover::NodeCatalogSnapshot;
6use crate::route_control::{RouteAdvertisement, RouteSnapshot};
7use crate::route_table::RouteTable;
8use crate::{Envelope, Resolution, RouteError, TargetPath};
9
10#[derive(Clone)]
11pub struct NodeCore {
12    routes: RouteTable,
13    catalog: BTreeMap<String, Value>,
14    revision: u64,
15    route_revision: u64,
16    instance_id: String,
17    epoch: u64,
18    proof: Value,
19}
20
21impl NodeCore {
22    pub fn new(node: &str) -> NodeCore {
23        NodeCore {
24            routes: RouteTable::new(node),
25            catalog: BTreeMap::new(),
26            revision: 0,
27            route_revision: 0,
28            instance_id: node.to_string(),
29            epoch: 0,
30            proof: Value::Null,
31        }
32    }
33
34    pub fn set_identity(&mut self, instance_id: &str, epoch: u64) {
35        self.instance_id = instance_id.to_string();
36        self.epoch = epoch;
37    }
38
39    pub fn set_node_identity(&mut self, identity: crate::NodeIdentity) {
40        self.instance_id = identity.instance_id;
41        self.epoch = identity.epoch;
42        self.proof = identity.proof;
43    }
44
45    pub fn node(&self) -> &str {
46        self.routes.node()
47    }
48
49    pub fn identity(&self) -> crate::NodeIdentity {
50        crate::NodeIdentity {
51            node_id: self.node().to_string(),
52            instance_id: self.instance_id.clone(),
53            epoch: self.epoch,
54            proof: self.proof.clone(),
55        }
56    }
57
58    pub fn catalog_revision(&self) -> u64 {
59        self.revision
60    }
61
62    pub fn fingerprint(&self) -> String {
63        fingerprint_of(self.catalog.keys().map(String::as_str))
64    }
65
66    pub fn catalog_subjects(&self, detail_full: bool) -> Vec<Value> {
67        self.catalog
68            .iter()
69            .map(|(subject, described)| {
70                let one_line = described.get("one_line").cloned().unwrap_or(Value::Null);
71                let target_path = TargetPath::application(self.node(), subject)
72                    .expect("installed catalog subjects are path-safe")
73                    .to_string();
74                if detail_full {
75                    let mut entry = described.clone();
76                    entry["subject"] = Value::String(subject.clone());
77                    entry["target_path"] = Value::String(target_path);
78                    entry
79                } else {
80                    json!({
81                        "subject": subject,
82                        "target_path": target_path,
83                        "one_line": one_line,
84                    })
85                }
86            })
87            .collect()
88    }
89
90    pub fn catalog(&self, detail_full: bool) -> Value {
91        json!({ "node": self.node(), "subjects": self.catalog_subjects(detail_full) })
92    }
93
94    pub fn catalog_snapshot(&self, detail_full: bool) -> NodeCatalogSnapshot {
95        NodeCatalogSnapshot {
96            node: self.node().to_string(),
97            instance_id: self.instance_id.clone(),
98            revision: self.revision,
99            fingerprint: self.fingerprint(),
100            subjects: self.catalog_subjects(detail_full),
101        }
102    }
103
104    pub fn install_local_capabilities(&mut self, capabilities: BTreeMap<String, Value>) -> bool {
105        let capabilities: BTreeMap<_, _> = capabilities
106            .into_iter()
107            .filter(|(subject, _)| TargetPath::application(self.node(), subject).is_ok())
108            .collect();
109        if self.catalog == capabilities {
110            return false;
111        }
112        self.catalog = capabilities;
113        self.revision = self
114            .revision
115            .checked_add(1)
116            .expect("catalog revision overflow");
117        true
118    }
119
120    pub fn resolve(&self, destination: &str) -> Resolution {
121        self.routes.resolve(destination)
122    }
123
124    pub fn apply_snapshot(
125        &mut self,
126        session: &str,
127        advertiser: &str,
128        snapshot: &RouteSnapshot,
129    ) -> Result<Vec<String>, RouteError> {
130        self.routes.apply_snapshot(session, advertiser, snapshot)
131    }
132
133    pub fn apply_delta(
134        &mut self,
135        session: &str,
136        advertiser: &str,
137        delta: &crate::route_control::RouteDelta,
138    ) -> Result<Vec<String>, RouteError> {
139        self.routes.apply_delta(session, advertiser, delta)
140    }
141
142    pub fn leave(&mut self, session: &str) -> Vec<String> {
143        self.routes.leave(session)
144    }
145
146    pub fn applied_generation(&self, session: &str) -> Option<u64> {
147        self.routes.applied_generation(session)
148    }
149
150    pub fn export_for(&self, peer_node: &str) -> Vec<RouteAdvertisement> {
151        let node = self.routes.node().to_string();
152        let mut routes = vec![RouteAdvertisement {
153            destination: node.clone(),
154            owner: node.clone(),
155            owner_instance: self.instance_id.clone(),
156            owner_epoch: self.epoch,
157            owner_revision: self.route_revision,
158            distance: 0,
159            path: vec![node.clone()],
160        }];
161        routes.extend(self.routes.selected_transit().filter_map(|candidate| {
162            let advertisement = &candidate.advertisement;
163            if advertisement.path.iter().any(|hop| hop == peer_node) {
164                return None;
165            }
166            if advertisement.path.len() >= crate::route_control::MAX_ROUTE_PATH {
167                return None;
168            }
169            let mut path = advertisement.path.clone();
170            path.push(node.clone());
171            Some(RouteAdvertisement {
172                destination: advertisement.destination.clone(),
173                owner: advertisement.owner.clone(),
174                owner_instance: advertisement.owner_instance.clone(),
175                owner_epoch: advertisement.owner_epoch,
176                owner_revision: advertisement.owner_revision,
177                distance: advertisement.distance.checked_add(1)?,
178                path,
179            })
180        }));
181        routes.sort();
182        routes
183    }
184
185    pub fn reachable_names(&self) -> Vec<String> {
186        self.routes.reachable_names()
187    }
188
189    pub fn forward(&self, envelope: Envelope) -> Result<(String, Envelope), RouteError> {
190        self.routes.forward(envelope)
191    }
192
193    pub fn annotate_error(&self, envelope: Envelope) -> Envelope {
194        self.routes.annotate_error(envelope)
195    }
196}
197
198fn fingerprint_of<'a>(names: impl Iterator<Item = &'a str>) -> String {
199    let mut hash: u64 = 0xcbf29ce484222325;
200    for name in names {
201        for byte in name.bytes() {
202            hash ^= u64::from(byte);
203            hash = hash.wrapping_mul(0x100000001b3);
204        }
205        hash ^= 0xff;
206        hash = hash.wrapping_mul(0x100000001b3);
207    }
208    format!("{hash:016x}")
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::route_control::MAX_ROUTE_PATH;
215    use serde_json::json;
216
217    fn advertisement(destination: &str, owner: &str, path: &[&str]) -> RouteAdvertisement {
218        RouteAdvertisement {
219            destination: destination.into(),
220            owner: owner.into(),
221            owner_instance: format!("{owner}-inst"),
222            owner_epoch: 1,
223            owner_revision: 0,
224            distance: (path.len() - 1) as u32,
225            path: path.iter().map(|s| s.to_string()).collect(),
226        }
227    }
228
229    #[test]
230    fn session_close_removes_exactly_its_routes() {
231        let mut core = NodeCore::new("hub");
232        core.apply_snapshot(
233            "sess-1",
234            "leaf-a",
235            &RouteSnapshot::canonical(1, vec![advertisement("leaf-a", "leaf-a", &["leaf-a"])]),
236        )
237        .unwrap();
238        core.apply_snapshot(
239            "sess-2",
240            "leaf-b",
241            &RouteSnapshot::canonical(1, vec![advertisement("leaf-b", "leaf-b", &["leaf-b"])]),
242        )
243        .unwrap();
244        core.leave("sess-1");
245        assert_eq!(core.resolve("leaf-a"), Resolution::Unknown);
246        assert_eq!(core.resolve("leaf-b"), Resolution::Route("leaf-b".into()));
247    }
248
249    #[test]
250    fn exports_carry_the_local_identity_and_revision() {
251        let mut core = NodeCore::new("hub");
252        core.set_identity("hub-7", 3);
253        core.install_local_capabilities(BTreeMap::from([("chess".into(), json!({}))]));
254        let export = core.export_for("anyone");
255        assert_eq!(export.len(), 1);
256        assert_eq!(export[0].owner, "hub");
257        assert_eq!(export[0].owner_instance, "hub-7");
258        assert_eq!(export[0].owner_epoch, 3);
259        assert_eq!(export[0].owner_revision, 0);
260        assert_eq!(export[0].path, vec!["hub"]);
261    }
262
263    #[test]
264    fn a_transit_route_at_the_path_limit_is_not_exported() {
265        let mut core = NodeCore::new("hub");
266        let mut path: Vec<String> = (0..MAX_ROUTE_PATH - 1).map(|i| format!("n{i}")).collect();
267        path.insert(0, "owner-far".to_string());
268        let path_refs: Vec<&str> = path.iter().map(String::as_str).collect();
269        let mut long = advertisement("owner-far", "owner-far", &path_refs);
270        long.path[MAX_ROUTE_PATH - 1] = "adv".into();
271        core.apply_snapshot("sess-1", "adv", &RouteSnapshot::canonical(1, vec![long]))
272            .unwrap();
273        assert!(matches!(core.resolve("owner-far"), Resolution::Route(_)));
274        let export = core.export_for("elsewhere");
275        assert_eq!(export.len(), 1, "the overlong transit route is omitted");
276        assert_eq!(export[0].destination, "hub");
277    }
278}