bp_core/network/
state.rs

1//! Distributed network state maintained via gossip.
2//!
3//! Each node in a BillPouch network periodically announces its `NodeInfo` over
4//! gossipsub.  All nodes accumulate received announcements in a local
5//! `NetworkState` map.  The `metadata` field on `NodeInfo` is intentionally
6//! open (`HashMap<String, Value>`) so that future versions can extend the
7//! gossipped information without a breaking protocol change.
8
9use crate::service::ServiceType;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Gossipped information about a single node (one peer = one service instance).
14///
15/// Multiple nodes can belong to the same user (same `user_fingerprint`).
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct NodeInfo {
18    /// libp2p PeerId (base58 string).
19    pub peer_id: String,
20    /// SHA-256(pubkey)[0..8] hex — ties multiple nodes to one user.
21    pub user_fingerprint: String,
22    /// Optional alias chosen at login.
23    pub user_alias: Option<String>,
24    pub service_type: ServiceType,
25    /// UUID of the local service instance.
26    pub service_id: String,
27    /// Network this node belongs to.
28    pub network_id: String,
29    /// Multiaddrs the node is listening on.
30    pub listen_addrs: Vec<String>,
31    /// Unix timestamp (seconds) when this announcement was created.
32    pub announced_at: u64,
33    /// Extensible key-value metadata (storage size, capabilities, …).
34    pub metadata: HashMap<String, serde_json::Value>,
35}
36
37impl NodeInfo {
38    /// Gossipsub topic name for the given network.
39    pub fn topic_name(network_id: &str) -> String {
40        format!("billpouch/v1/{}/nodes", network_id)
41    }
42}
43
44/// Local in-memory view of all known nodes in all joined networks.
45#[derive(Default)]
46pub struct NetworkState {
47    /// peer_id (string) → latest NodeInfo
48    nodes: HashMap<String, NodeInfo>,
49}
50
51impl NetworkState {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Upsert (or insert) a `NodeInfo`, keeping the newest announcement.
57    pub fn upsert(&mut self, info: NodeInfo) {
58        let entry = self
59            .nodes
60            .entry(info.peer_id.clone())
61            .or_insert_with(|| info.clone());
62        if info.announced_at >= entry.announced_at {
63            *entry = info;
64        }
65    }
66
67    /// Remove a node by PeerId string.
68    pub fn remove(&mut self, peer_id: &str) {
69        self.nodes.remove(peer_id);
70    }
71
72    /// All known nodes.
73    pub fn all(&self) -> Vec<&NodeInfo> {
74        self.nodes.values().collect()
75    }
76
77    /// All nodes in a specific network.
78    pub fn in_network<'a>(&'a self, network_id: &str) -> Vec<&'a NodeInfo> {
79        self.nodes
80            .values()
81            .filter(|n| n.network_id == network_id)
82            .collect()
83    }
84
85    /// Evict nodes whose last announcement is older than `max_age_secs`.
86    pub fn evict_stale(&mut self, max_age_secs: u64) {
87        let now = chrono::Utc::now().timestamp() as u64;
88        self.nodes
89            .retain(|_, n| now.saturating_sub(n.announced_at) < max_age_secs);
90    }
91
92    /// Total node count.
93    pub fn len(&self) -> usize {
94        self.nodes.len()
95    }
96
97    /// Returns true if there are no known nodes.
98    pub fn is_empty(&self) -> bool {
99        self.nodes.is_empty()
100    }
101}