1use std::sync::{Arc, Mutex};
37
38use clawft_types::placement::{
39 CapabilityState, FactsDelta, NodeFacts, Provenance, StateChange, TrustTier,
40};
41use ed25519_dalek::SigningKey;
42use serde::{Deserialize, Serialize};
43
44use crate::cluster::ClusterMembership;
45use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
46use crate::mesh_delivery::PeerCtx;
47use crate::mesh_discovery::MeshPeerEvent;
48pub use crate::mesh_runtime::{FACTS_TOPIC, PeerControlSink};
49use crate::mesh_runtime::MeshRuntime;
50use crate::node_facts::{CacheError, InsertOutcome};
51use crate::node_facts_advert::{
52 NodeFactsAdvertError, SignedFactsDelta, SignedNodeFacts, sign_facts_delta, sign_node_facts,
53 verify_facts_delta,
54};
55use crate::node_registry::node_id_from_pubkey;
56
57pub const MAX_DISCOVERED_PER_CONN: usize = 4;
60pub const FRAMES_PER_SEC: f64 = 20.0;
62pub const BYTES_PER_SEC: f64 = 256.0 * 1024.0;
64const MAX_TRACKED_CONNS: usize = 1024;
66
67struct Budget {
69 frames: f64,
70 bytes: f64,
71 last: std::time::Instant,
72}
73
74const REBASE_NUMERATOR: u64 = 4;
77const REBASE_DENOMINATOR: u64 = 5;
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(tag = "op", rename_all = "snake_case")]
82pub enum FactsWire {
83 Facts {
85 signed: SignedNodeFacts,
87 },
88 Delta {
90 signed: SignedFactsDelta,
92 },
93 Request,
95}
96
97impl FactsWire {
98 fn to_value(&self) -> Option<serde_json::Value> {
99 serde_json::to_value(self).ok()
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct FactsTrustPolicy {
106 pub verified_tier: TrustTier,
111 pub unverified_tier: TrustTier,
113 pub verified_cap: Provenance,
115 pub unverified_cap: Provenance,
117}
118
119impl Default for FactsTrustPolicy {
120 fn default() -> Self {
121 Self {
122 verified_tier: TrustTier::Paired,
123 unverified_tier: TrustTier::Discovered,
124 verified_cap: Provenance::Probed,
125 unverified_cap: Provenance::Claimed,
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub enum IngestOutcome {
133 Accepted {
135 node_id: String,
137 tier: TrustTier,
139 outcome: InsertOutcome,
141 },
142 DeltaApplied {
144 node_id: String,
146 },
147 Requested,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
153pub enum IngestError {
154 #[error("malformed facts message: {0}")]
156 Malformed(String),
157 #[error("facts for {subject} sent by {sender}")]
159 NotSender {
160 subject: String,
162 sender: String,
164 },
165 #[error("refusing remote facts for the local node")]
167 LocalNode,
168 #[error("connection {conn} already supplied {limit} unverified nodes")]
170 Quota {
171 conn: u64,
173 limit: usize,
175 },
176 #[error(transparent)]
178 Cache(#[from] CacheError),
179}
180
181struct Local {
183 base: Option<SignedNodeFacts>,
184 facts: Option<NodeFacts>,
185 delta: Option<SignedFactsDelta>,
186 delta_seq: u64,
187 last_seq: u64,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum Published {
193 Base(u64),
195 Delta {
197 seq: u64,
199 changes: usize,
201 },
202 Nothing,
204}
205
206pub struct FactsExchange {
208 node_id: String,
209 key: SigningKey,
210 membership: Arc<ClusterMembership>,
211 runtime: Arc<MeshRuntime>,
212 policy: FactsTrustPolicy,
213 local: Mutex<Local>,
214 budgets: dashmap::DashMap<u64, Budget>,
215}
216
217impl FactsExchange {
218 pub fn new(
221 key: SigningKey,
222 membership: Arc<ClusterMembership>,
223 runtime: Arc<MeshRuntime>,
224 policy: FactsTrustPolicy,
225 ) -> Arc<Self> {
226 let node_id = node_id_from_pubkey(&key.verifying_key().to_bytes());
227 Arc::new(Self {
228 node_id,
229 key,
230 membership,
231 runtime,
232 policy,
233 local: Mutex::new(Local {
234 base: None,
235 facts: None,
236 delta: None,
237 delta_seq: 0,
238 last_seq: 0,
239 }),
240 budgets: dashmap::DashMap::new(),
241 })
242 }
243
244 pub fn start(self: &Arc<Self>) {
247 self.runtime.set_control_sink(FACTS_TOPIC, self.clone());
248 let me = self.clone();
249 let mut events = self.runtime.subscribe_peer_events();
250 tokio::spawn(async move {
251 loop {
252 match events.recv().await {
253 Ok(MeshPeerEvent::Joined { node_id, .. })
254 | Ok(MeshPeerEvent::Recovered { node_id, .. }) => {
255 me.send_current(&node_id).await;
256 }
257 Ok(MeshPeerEvent::Left { node_id }) => {
260 me.on_peer_gone(&node_id);
261 }
262 Ok(_) => {}
263 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
264 Err(_) => break,
265 }
266 }
267 });
268 }
269
270 pub fn on_peer_gone(&self, node_id: &str) -> bool {
273 self.membership.facts().demote_mesh_tier(node_id)
274 }
275
276 fn allow(&self, conn: u64, bytes: usize) -> bool {
278 if self.budgets.len() >= MAX_TRACKED_CONNS && !self.budgets.contains_key(&conn) {
279 let oldest = self.budgets.iter().min_by_key(|b| b.last).map(|b| *b.key());
280 if let Some(k) = oldest {
281 self.budgets.remove(&k);
282 }
283 }
284 let now = std::time::Instant::now();
285 let mut b = self.budgets.entry(conn).or_insert(Budget {
286 frames: 2.0 * FRAMES_PER_SEC,
287 bytes: 4.0 * BYTES_PER_SEC,
288 last: now,
289 });
290 let dt = now.duration_since(b.last).as_secs_f64();
291 b.last = now;
292 b.frames = (b.frames + dt * FRAMES_PER_SEC).min(2.0 * FRAMES_PER_SEC);
293 b.bytes = (b.bytes + dt * BYTES_PER_SEC).min(4.0 * BYTES_PER_SEC);
294 if b.frames < 1.0 || b.bytes < bytes as f64 {
295 return false;
296 }
297 b.frames -= 1.0;
298 b.bytes -= bytes as f64;
299 true
300 }
301
302 pub fn node_id(&self) -> &str {
304 &self.node_id
305 }
306
307 pub fn membership(&self) -> &Arc<ClusterMembership> {
309 &self.membership
310 }
311
312 fn now() -> u64 {
313 std::time::SystemTime::now()
314 .duration_since(std::time::UNIX_EPOCH)
315 .map(|d| d.as_secs())
316 .unwrap_or(0)
317 }
318
319 fn lock(&self) -> std::sync::MutexGuard<'_, Local> {
320 self.local.lock().unwrap_or_else(|p| p.into_inner())
321 }
322
323 pub async fn publish(&self, facts: NodeFacts, now: u64) -> Result<u64, NodeFactsAdvertError> {
326 let (seq, signed) = self.install_base(facts, now)?;
327 self.broadcast(FactsWire::Facts { signed }).await;
328 Ok(seq)
329 }
330
331 fn install_base(
332 &self,
333 mut facts: NodeFacts,
334 now: u64,
335 ) -> Result<(u64, SignedNodeFacts), NodeFactsAdvertError> {
336 let mut local = self.lock();
337 let seq = (local.last_seq + 1).max(now);
338 facts.node_id = self.node_id.clone();
339 facts.issued_at = now;
340 facts.seq = seq;
341 let signed = sign_node_facts(&facts, &self.key)?;
342 self.membership
343 .facts()
344 .insert(signed.clone(), TrustTier::Pinned, now)
345 .map_err(|e| NodeFactsAdvertError::Malformed(e.to_string()))?;
346 local.last_seq = seq;
347 local.base = Some(signed.clone());
348 local.facts = Some(facts);
349 local.delta = None;
350 local.delta_seq = 0;
351 Ok((seq, signed))
352 }
353
354 pub async fn update_live(
359 &self,
360 probed: NodeFacts,
361 now: u64,
362 ) -> Result<Published, NodeFactsAdvertError> {
363 enum Step {
364 Base(NodeFacts),
365 Delta(SignedFactsDelta, usize),
366 Nothing,
367 }
368 let step = {
369 let mut local = self.lock();
370 match local.facts.clone() {
371 Some(base)
372 if now < base.issued_at + base.ttl_secs * REBASE_NUMERATOR / REBASE_DENOMINATOR
373 && same_shape(&base, &probed) =>
374 {
375 let delta = live_delta(&base, &probed, local.delta_seq + 1, now);
376 if delta.changes.is_empty() && delta.mem_free.is_none() {
377 Step::Nothing
378 } else {
379 let n = delta.changes.len();
380 let signed = sign_facts_delta(&delta, &self.key)?;
381 local.delta_seq = delta.seq;
382 local.delta = Some(signed.clone());
383 if let Some(f) = local.facts.as_mut() {
385 f.apply_delta(&delta).map_err(NodeFactsAdvertError::Facts)?;
386 }
387 let _ = self.membership.facts().apply_delta(&signed, now);
388 Step::Delta(signed, n)
389 }
390 }
391 _ => Step::Base(probed),
392 }
393 };
394 match step {
395 Step::Nothing => Ok(Published::Nothing),
396 Step::Base(f) => self.publish(f, now).await.map(Published::Base),
397 Step::Delta(signed, changes) => {
398 let seq = {
399 let (d, _) = verify_facts_delta(&signed, now)?;
400 d.seq
401 };
402 self.broadcast(FactsWire::Delta { signed }).await;
403 Ok(Published::Delta { seq, changes })
404 }
405 }
406 }
407
408 async fn broadcast(&self, wire: FactsWire) {
409 for peer in self.runtime.peer_ids() {
410 self.send_wire(&peer, &wire).await;
411 }
412 }
413
414 async fn send_wire(&self, peer: &str, wire: &FactsWire) {
415 let Some(value) = wire.to_value() else { return };
416 let msg = KernelMessage::new(
417 0,
418 MessageTarget::Topic(FACTS_TOPIC.to_string()),
419 MessagePayload::Json(value),
420 );
421 if let Err(e) = self.runtime.route_to_remote(peer, msg).await {
422 tracing::debug!(peer, error = %e, "node facts send failed");
423 }
424 }
425
426 fn current(&self) -> Vec<FactsWire> {
427 let local = self.lock();
428 let mut out = Vec::new();
429 if let Some(signed) = local.base.clone() {
430 out.push(FactsWire::Facts { signed });
431 }
432 if let Some(signed) = local.delta.clone() {
433 out.push(FactsWire::Delta { signed });
434 }
435 out
436 }
437
438 async fn send_current(&self, peer: &str) {
439 for wire in self.current() {
440 self.send_wire(peer, &wire).await;
441 }
442 }
443
444 pub fn ingest(
446 &self,
447 ctx: &PeerCtx,
448 wire: FactsWire,
449 now: u64,
450 replies: &mut Vec<FactsWire>,
451 ) -> Result<IngestOutcome, IngestError> {
452 self.ingest_from(ctx, 0, wire, now, replies)
453 }
454
455 pub fn ingest_from(
463 &self,
464 ctx: &PeerCtx,
465 conn: u64,
466 wire: FactsWire,
467 now: u64,
468 replies: &mut Vec<FactsWire>,
469 ) -> Result<IngestOutcome, IngestError> {
470 let cache = self.membership.facts();
471 let (tier, cap) = if ctx.node_verified {
472 (self.policy.verified_tier, self.policy.verified_cap)
473 } else {
474 (self.policy.unverified_tier, self.policy.unverified_cap)
475 };
476 match wire {
477 FactsWire::Request => {
478 replies.extend(self.current());
479 Ok(IngestOutcome::Requested)
480 }
481 FactsWire::Facts { signed } => {
482 let facts = crate::node_facts_advert::verify_node_facts(&signed, now)
483 .map_err(CacheError::from)?;
484 self.check_subject(ctx, &facts.node_id)?;
485 if tier == TrustTier::Discovered
488 && conn != 0
489 && cache.get(&facts.node_id, now).is_none_or(|h| h.origin != conn)
490 && cache.discovered_from(conn, now) >= MAX_DISCOVERED_PER_CONN
491 {
492 return Err(IngestError::Quota {
493 conn,
494 limit: MAX_DISCOVERED_PER_CONN,
495 });
496 }
497 let outcome = cache.insert_remote(signed, tier, cap, conn, now)?;
498 let tier = cache.get(&facts.node_id, now).map_or(tier, |h| h.trust_tier);
500 Ok(IngestOutcome::Accepted {
501 node_id: facts.node_id,
502 tier,
503 outcome,
504 })
505 }
506 FactsWire::Delta { signed } => {
507 let (delta, _) = verify_facts_delta(&signed, now).map_err(CacheError::from)?;
508 self.check_subject(ctx, &delta.node_id)?;
509 if let Err(e) = cache.apply_delta(&signed, now) {
510 let behind = cache
512 .get(&delta.node_id, now)
513 .is_none_or(|c| c.facts.seq < delta.base_seq);
514 if behind {
515 replies.push(FactsWire::Request);
516 }
517 return Err(e.into());
518 }
519 Ok(IngestOutcome::DeltaApplied {
520 node_id: delta.node_id,
521 })
522 }
523 }
524 }
525
526 fn check_subject(&self, ctx: &PeerCtx, subject: &str) -> Result<(), IngestError> {
527 if subject == self.node_id {
528 return Err(IngestError::LocalNode);
529 }
530 if subject != ctx.peer_id {
531 return Err(IngestError::NotSender {
532 subject: subject.to_string(),
533 sender: ctx.peer_id.clone(),
534 });
535 }
536 Ok(())
537 }
538}
539
540impl PeerControlSink for FactsExchange {
541 fn on_peer_control(
542 &self,
543 ctx: &PeerCtx,
544 conn: u64,
545 payload: &serde_json::Value,
546 ) -> Vec<serde_json::Value> {
547 let size = json_size(payload);
551 if !self.allow(conn, size) {
552 tracing::debug!(peer = %ctx.peer_id, conn, "node facts frame dropped: over budget");
553 return Vec::new();
554 }
555 let wire: FactsWire = match serde_json::from_value(payload.clone()) {
556 Ok(w) => w,
557 Err(e) => {
558 tracing::warn!(peer = %ctx.peer_id, error = %e, "malformed node facts message");
559 return Vec::new();
560 }
561 };
562 let mut replies = Vec::new();
563 if let Err(e) = self.ingest_from(ctx, conn, wire, Self::now(), &mut replies) {
564 if matches!(
565 &e,
566 IngestError::Cache(CacheError::Verify(NodeFactsAdvertError::Facts(
567 clawft_types::placement::FactsError::FromFuture { .. }
568 )))
569 ) {
570 tracing::warn!(peer = %ctx.peer_id, error = %e,
571 "node facts refused: issued in the future, the peer's clock is ahead of ours \
572 by more than the allowed skew");
573 } else {
574 tracing::warn!(peer = %ctx.peer_id, verified = ctx.node_verified, error = %e,
575 "node facts refused");
576 }
577 }
578 replies.iter().filter_map(FactsWire::to_value).collect()
579 }
580}
581
582fn json_size(v: &serde_json::Value) -> usize {
585 match v {
586 serde_json::Value::String(s) => s.len() + 2,
587 serde_json::Value::Array(a) => 2 + a.iter().map(|x| json_size(x) + 1).sum::<usize>(),
588 serde_json::Value::Object(o) => {
589 2 + o.iter().map(|(k, x)| k.len() + 4 + json_size(x)).sum::<usize>()
590 }
591 _ => 8,
592 }
593}
594
595fn same_shape(a: &NodeFacts, b: &NodeFacts) -> bool {
598 let norm = |f: &NodeFacts| {
599 let mut c = f.capabilities.clone();
600 for cap in &mut c {
601 cap.state = CapabilityState::Available;
602 if matches!(cap.id.as_str(), "mem.unified" | "mem.system") {
603 cap.attrs.remove("free");
604 }
605 }
606 (c, f.ttl_secs)
607 };
608 norm(a) == norm(b)
609}
610
611fn live_delta(base: &NodeFacts, now_state: &NodeFacts, seq: u64, now: u64) -> FactsDelta {
613 let changes = base
614 .capabilities
615 .iter()
616 .zip(&now_state.capabilities)
617 .enumerate()
618 .filter(|(_, (b, n))| b.state != n.state)
619 .map(|(i, (_, n))| StateChange {
620 index: i as u32,
621 id: n.id.clone(),
622 state: n.state,
623 })
624 .collect();
625 let free = |f: &NodeFacts| f.load().mem_free;
626 let mem_free = (free(base) != free(now_state)).then(|| free(now_state)).flatten();
627 FactsDelta {
628 node_id: base.node_id.clone(),
629 base_seq: base.seq,
630 seq,
631 issued_at: now,
632 changes,
633 mem_free,
634 }
635}
636
637#[cfg(test)]
638#[path = "node_facts_exchange_tests.rs"]
639mod tests;