Add estimated mesh size from bloom filter cardinality

Compute network size estimate by summing bloom filter estimated entry
counts from the spanning tree parent (upward) and children (downward),
leveraging the tree's non-overlapping partition property. Uses the
standard formula n = -(m/k) * ln(1 - X/m) already in BloomFilter.

Surfaces the estimate in three places:
- show_status JSON: "estimated_mesh_size" field
- Periodic info log at MMP log interval (default 30s)
- fipstop Node dashboard State section as "mesh: ~N"
This commit is contained in:
Johnathan Corgan
2026-03-09 00:10:31 +00:00
parent 0bb6e70fb5
commit 2dc466f359
4 changed files with 89 additions and 0 deletions
+7
View File
@@ -130,6 +130,11 @@ fn draw_state(frame: &mut Frame, data: &serde_json::Value, area: Rect) {
let links = helpers::u64_field(data, "link_count");
let transports = helpers::u64_field(data, "transport_count");
let connections = helpers::u64_field(data, "connection_count");
let mesh_size = data
.get("estimated_mesh_size")
.and_then(|v| v.as_u64())
.map(|n| format!("~{n}"))
.unwrap_or_else(|| "-".into());
let lines = vec![
Line::from(vec![
@@ -153,6 +158,8 @@ fn draw_state(frame: &mut Frame, data: &serde_json::Value, area: Rect) {
Span::styled(transports, count),
Span::styled(" connections: ", label),
Span::styled(connections, count),
Span::styled(" mesh: ", label),
Span::styled(mesh_size, count),
]),
];
+1
View File
@@ -58,6 +58,7 @@ pub fn show_status(node: &Node) -> Value {
"pid": pid,
"exe_path": exe_path,
"uptime_secs": uptime_secs,
"estimated_mesh_size": node.estimated_mesh_size(),
"forwarding": serde_json::to_value(&fwd).unwrap_or_default(),
})
}
+1
View File
@@ -114,6 +114,7 @@ impl Node {
self.process_pending_retries(now_ms).await;
self.check_tree_state().await;
self.check_bloom_state().await;
self.compute_mesh_size();
self.check_mmp_reports().await;
self.check_session_mmp_reports().await;
self.check_link_heartbeats().await;
+80
View File
@@ -378,6 +378,12 @@ pub struct Node {
/// Timestamp of last congestion detection log (rate-limited to 5s).
last_congestion_log: Option<std::time::Instant>,
// === Mesh Size Estimate ===
/// Cached estimated mesh size (computed once per tick from bloom filters).
estimated_mesh_size: Option<u64>,
/// Timestamp of last mesh size log emission.
last_mesh_size_log: Option<std::time::Instant>,
// === Display Names ===
/// Human-readable names for configured peers (alias or short npub).
/// Populated at startup from peer config.
@@ -496,6 +502,8 @@ impl Node {
retry_pending: HashMap::new(),
last_parent_reeval: None,
last_congestion_log: None,
estimated_mesh_size: None,
last_mesh_size_log: None,
peer_aliases: HashMap::new(),
host_map,
})
@@ -596,6 +604,8 @@ impl Node {
retry_pending: HashMap::new(),
last_parent_reeval: None,
last_congestion_log: None,
estimated_mesh_size: None,
last_mesh_size_log: None,
peer_aliases: HashMap::new(),
host_map,
}
@@ -852,6 +862,76 @@ impl Node {
&mut self.bloom_state
}
// === Mesh Size Estimate ===
/// Get the cached estimated mesh size.
pub fn estimated_mesh_size(&self) -> Option<u64> {
self.estimated_mesh_size
}
/// Compute and cache the estimated mesh size from bloom filters.
///
/// Uses the spanning tree partition: parent's filter covers nodes reachable
/// upward, children's filters cover disjoint subtrees downward. The sum
/// of estimated entry counts plus one (self) approximates total network size.
pub(crate) fn compute_mesh_size(&mut self) {
let my_addr = *self.tree_state.my_node_addr();
let parent_id = *self.tree_state.my_declaration().parent_id();
let is_root = self.tree_state.is_root();
let mut total: f64 = 1.0; // count self
let mut child_count: u32 = 0;
let mut has_data = false;
// Parent's filter: nodes reachable upward through the tree
if !is_root
&& let Some(parent) = self.peers.get(&parent_id)
&& let Some(filter) = parent.inbound_filter()
{
total += filter.estimated_count();
has_data = true;
}
// Children's filters: each child's subtree is disjoint
for (peer_addr, peer) in &self.peers {
if let Some(decl) = self.tree_state.peer_declaration(peer_addr)
&& *decl.parent_id() == my_addr
{
child_count += 1;
if let Some(filter) = peer.inbound_filter() {
total += filter.estimated_count();
has_data = true;
}
}
}
if !has_data {
self.estimated_mesh_size = None;
return;
}
let size = total.round() as u64;
self.estimated_mesh_size = Some(size);
// Periodic logging (reuse MMP default interval: 30s)
let now = std::time::Instant::now();
let should_log = match self.last_mesh_size_log {
None => true,
Some(last) => now.duration_since(last) >= std::time::Duration::from_secs(
self.config.node.mmp.log_interval_secs,
),
};
if should_log {
tracing::info!(
estimated_mesh_size = size,
peers = self.peers.len(),
children = child_count,
"Mesh size estimate"
);
self.last_mesh_size_log = Some(now);
}
}
// === Coord Cache ===
/// Get the coordinate cache.