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}