mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 01:27:32 +00:00
Validate bloom filter fill ratio on FilterAnnounce ingress
A malformed FilterAnnounce whose fill ratio produces an implausibly high false-positive rate is mostly useless for routing and, once merged into our outgoing filter via bitwise OR, propagates the saturated state to tree peers one hop per announce tick. A saturated filter also made estimated_count() return f64::INFINITY, which compute_mesh_size summed into its cached estimate. handle_filter_announce now rejects inbound FilterAnnounce whose derived FPR exceeds `node.bloom.max_inbound_fpr` (new config field, default 0.05 ≈ fill 0.549 at k=5). Rejection is silent on the wire, logs at WARN, and increments a new `bloom.fill_exceeded` counter. The peer's prior stored filter and filter_sequence are left unchanged so a single rejected announce does not wipe the peer's existing contribution to aggregation. After a successful outgoing FilterAnnounce send, a rate-limited WARN fires if our own filter's FPR exceeds the same cap, surfacing aggregation drift. Limited to once per 60 seconds via a new Node.last_self_warn field. BloomFilter::estimated_count() now takes max_fpr and returns Option<f64>. Returns None for saturated filters (regardless of cap) or when the filter's FPR exceeds max_fpr. Callers updated: debug logs render None as "—", the Debug impl uses f64::INFINITY as "no cap" and prints "saturated" instead of inf, control-socket JSON emits null, and compute_mesh_size propagates None into the already- Option<u64> estimated_mesh_size field.
This commit is contained in:
+35
-5
@@ -430,6 +430,13 @@ pub struct Node {
|
||||
/// Timestamp of last mesh size log emission.
|
||||
last_mesh_size_log: Option<std::time::Instant>,
|
||||
|
||||
// === Bloom Self-Plausibility ===
|
||||
/// Rate-limit state for the self-plausibility WARN. Fires at most
|
||||
/// once per 60s globally when our own outgoing FilterAnnounce has
|
||||
/// an FPR above `node.bloom.max_inbound_fpr`, signalling either
|
||||
/// aggregation drift or an ingress bypass.
|
||||
last_self_warn: Option<std::time::Instant>,
|
||||
|
||||
// === Display Names ===
|
||||
/// Human-readable names for configured peers (alias or short npub).
|
||||
/// Populated at startup from peer config.
|
||||
@@ -558,6 +565,7 @@ impl Node {
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
last_mesh_size_log: None,
|
||||
last_self_warn: None,
|
||||
peer_aliases: HashMap::new(),
|
||||
host_map,
|
||||
})
|
||||
@@ -663,6 +671,7 @@ impl Node {
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
last_mesh_size_log: None,
|
||||
last_self_warn: None,
|
||||
peer_aliases: HashMap::new(),
|
||||
host_map,
|
||||
}
|
||||
@@ -952,17 +961,30 @@ impl Node {
|
||||
let parent_id = *self.tree_state.my_declaration().parent_id();
|
||||
let is_root = self.tree_state.is_root();
|
||||
|
||||
let max_fpr = self.config.node.bloom.max_inbound_fpr;
|
||||
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
|
||||
// Parent's filter: nodes reachable upward through the tree.
|
||||
// If any contributing filter is above the FPR cap, we refuse to
|
||||
// estimate rather than substitute a partial/biased aggregate —
|
||||
// Node.estimated_mesh_size is already Option<u64> and consumers
|
||||
// (control socket, fipstop, periodic debug log) handle None.
|
||||
if !is_root
|
||||
&& let Some(parent) = self.peers.get(&parent_id)
|
||||
&& let Some(filter) = parent.inbound_filter()
|
||||
{
|
||||
total += filter.estimated_count();
|
||||
has_data = true;
|
||||
match filter.estimated_count(max_fpr) {
|
||||
Some(n) => {
|
||||
total += n;
|
||||
has_data = true;
|
||||
}
|
||||
None => {
|
||||
self.estimated_mesh_size = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Children's filters: each child's subtree is disjoint
|
||||
@@ -972,8 +994,16 @@ impl Node {
|
||||
{
|
||||
child_count += 1;
|
||||
if let Some(filter) = peer.inbound_filter() {
|
||||
total += filter.estimated_count();
|
||||
has_data = true;
|
||||
match filter.estimated_count(max_fpr) {
|
||||
Some(n) => {
|
||||
total += n;
|
||||
has_data = true;
|
||||
}
|
||||
None => {
|
||||
self.estimated_mesh_size = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user