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}