Files
fips/src/proto/bloom/state.rs
T
Johnathan Corgan 83c4e800a5 Merge branch 'maint'
# Conflicts:
#	src/node/retry.rs
2026-07-29 02:17:34 +00:00

293 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! FIPS-specific Bloom filter announcement state management.
use alloc::collections::{BTreeMap, BTreeSet};
use super::BloomFilter;
use crate::NodeAddr;
/// State for managing Bloom filter announcements.
///
/// Tracks local filter state and what needs to be sent to peers.
#[derive(Clone, Debug)]
pub struct BloomState {
/// This node's NodeAddr (always included in outgoing filters).
own_node_addr: NodeAddr,
/// Leaf-only nodes we speak for (included in our filter).
leaf_dependents: BTreeSet<NodeAddr>,
/// Whether this node operates in leaf-only mode.
is_leaf_only: bool,
/// Rate limiting: minimum interval between outgoing updates (milliseconds).
update_debounce_ms: u64,
/// Timestamp of last update sent (per peer, in milliseconds).
last_update_sent: BTreeMap<NodeAddr, u64>,
/// Peers that need a filter update.
pending_updates: BTreeSet<NodeAddr>,
/// Current sequence number for outgoing filters.
sequence: u64,
/// Last outgoing filter sent to each peer (for change detection).
last_sent_filters: BTreeMap<NodeAddr, BloomFilter>,
}
impl BloomState {
/// Create new Bloom state for a node.
pub fn new(own_node_addr: NodeAddr) -> Self {
Self {
own_node_addr,
leaf_dependents: BTreeSet::new(),
is_leaf_only: false,
update_debounce_ms: 500,
last_update_sent: BTreeMap::new(),
pending_updates: BTreeSet::new(),
sequence: 0,
last_sent_filters: BTreeMap::new(),
}
}
/// Create state for a leaf-only node.
pub fn leaf_only(own_node_addr: NodeAddr) -> Self {
let mut state = Self::new(own_node_addr);
state.is_leaf_only = true;
state
}
/// Get the node's own ID.
pub fn own_node_addr(&self) -> &NodeAddr {
&self.own_node_addr
}
/// Check if this is a leaf-only node.
pub fn is_leaf_only(&self) -> bool {
self.is_leaf_only
}
/// Get the current sequence number.
pub fn sequence(&self) -> u64 {
self.sequence
}
/// Increment and return the next sequence number.
pub fn next_sequence(&mut self) -> u64 {
self.sequence += 1;
self.sequence
}
/// Get the update debounce interval in milliseconds.
pub fn update_debounce_ms(&self) -> u64 {
self.update_debounce_ms
}
/// Set the update debounce interval.
pub fn set_update_debounce_ms(&mut self, ms: u64) {
self.update_debounce_ms = ms;
}
/// Add a leaf dependent that we'll include in our filter.
pub fn add_leaf_dependent(&mut self, node_addr: NodeAddr) {
self.leaf_dependents.insert(node_addr);
}
/// Remove a leaf dependent.
pub fn remove_leaf_dependent(&mut self, node_addr: &NodeAddr) -> bool {
self.leaf_dependents.remove(node_addr)
}
/// Get the set of leaf dependents.
pub fn leaf_dependents(&self) -> &BTreeSet<NodeAddr> {
&self.leaf_dependents
}
/// Number of leaf dependents.
pub fn leaf_dependent_count(&self) -> usize {
self.leaf_dependents.len()
}
/// Mark that a peer needs an update.
pub fn mark_update_needed(&mut self, peer_id: NodeAddr) {
self.pending_updates.insert(peer_id);
}
/// Mark all peers as needing updates.
pub fn mark_all_updates_needed(&mut self, peer_ids: impl IntoIterator<Item = NodeAddr>) {
self.pending_updates.extend(peer_ids);
}
/// Check if a peer needs an update.
pub fn needs_update(&self, peer_id: &NodeAddr) -> bool {
self.pending_updates.contains(peer_id)
}
/// Check if we should send an update to a peer (respecting debounce).
pub fn should_send_update(&self, peer_id: &NodeAddr, current_time_ms: u64) -> bool {
if !self.pending_updates.contains(peer_id) {
return false;
}
match self.last_update_sent.get(peer_id) {
Some(&last_time) => current_time_ms >= last_time + self.update_debounce_ms,
None => true,
}
}
/// Record that we sent an update to a peer.
pub fn record_update_sent(&mut self, peer_id: NodeAddr, current_time_ms: u64) {
self.last_update_sent.insert(peer_id, current_time_ms);
self.pending_updates.remove(&peer_id);
}
/// Clear all pending updates.
pub fn clear_pending_updates(&mut self) {
self.pending_updates.clear();
}
/// Record the outgoing filter that was sent to a peer.
pub fn record_sent_filter(&mut self, peer_id: NodeAddr, filter: BloomFilter) {
self.last_sent_filters.insert(peer_id, filter);
}
/// Read back the last outgoing filter actually sent to a peer, if any.
///
/// Returns the filter recorded by [`record_sent_filter`](Self::record_sent_filter)
/// — i.e. what the peer currently holds for us — or `None` when no announce
/// has been sent to that peer yet (or the node is root, with no parent to
/// send to).
pub fn last_sent_filter(&self, peer_id: &NodeAddr) -> Option<&BloomFilter> {
self.last_sent_filters.get(peer_id)
}
/// Remove stored filter state for a peer that was removed.
pub fn remove_peer_state(&mut self, peer_id: &NodeAddr) {
self.last_sent_filters.remove(peer_id);
self.last_update_sent.remove(peer_id);
self.pending_updates.remove(peer_id);
}
/// Mark only peers whose outgoing filter has actually changed.
///
/// Computes the outgoing filter for each peer and compares it
/// against what was last sent. Only marks peers where the filter
/// differs. This prevents cascading update loops in steady state.
pub fn mark_changed_peers(
&mut self,
exclude_from: &NodeAddr,
peer_addrs: &[NodeAddr],
peer_filters: &BTreeMap<NodeAddr, BloomFilter>,
) {
let targets: Vec<NodeAddr> = peer_addrs
.iter()
.filter(|addr| *addr != exclude_from)
.copied()
.collect();
for (peer_addr, new_filter) in self.compute_outgoing_filters(&targets, peer_filters) {
let changed = match self.last_sent_filters.get(&peer_addr) {
Some(last) => *last != new_filter,
None => true, // never sent → must send
};
if changed {
self.pending_updates.insert(peer_addr);
}
}
}
/// Compute the outgoing filter for many peers in one pass.
///
/// Equivalent to calling [`compute_outgoing_filter`](Self::compute_outgoing_filter)
/// once per target, but linear in the number of contributing peer
/// filters instead of quadratic. The per-peer call rebuilds the whole
/// union from scratch, so computing it for every peer costs
/// O(targets × filters) 1 KB merges; announce fan-out on a
/// large node does exactly that, once per tick and again on every
/// inbound announce.
///
/// The split-horizon exclusion is the only thing that differs between
/// targets, so the union of "everything except peer i" is assembled
/// from a running prefix union and a precomputed suffix union. Merging
/// is a bytewise OR, which is commutative and associative, so the
/// result is bit-identical to the per-peer computation.
pub fn compute_outgoing_filters(
&self,
targets: &[NodeAddr],
peer_filters: &BTreeMap<NodeAddr, BloomFilter>,
) -> BTreeMap<NodeAddr, BloomFilter> {
let base = self.base_filter();
let keys: Vec<NodeAddr> = peer_filters.keys().copied().collect();
let n = keys.len();
// suffix[i] = union of peer_filters[keys[i..]]; suffix[n] is empty.
let mut suffix = vec![BloomFilter::new(); n + 1];
for i in (0..n).rev() {
let mut acc = suffix[i + 1].clone();
// Size mismatches are skipped, exactly as in the per-peer path.
let _ = acc.merge(&peer_filters[&keys[i]]);
suffix[i] = acc;
}
// Filter for a target that contributes nothing: everything merged.
let mut all = base.clone();
let _ = all.merge(&suffix[0]);
let mut per_key: BTreeMap<NodeAddr, BloomFilter> = BTreeMap::new();
let mut prefix = BloomFilter::new();
for i in 0..n {
let mut outgoing = base.clone();
let _ = outgoing.merge(&prefix);
let _ = outgoing.merge(&suffix[i + 1]);
per_key.insert(keys[i], outgoing);
let _ = prefix.merge(&peer_filters[&keys[i]]);
}
targets
.iter()
.map(|target| {
let filter = per_key.get(target).cloned().unwrap_or_else(|| all.clone());
(*target, filter)
})
.collect()
}
/// Compute the outgoing filter for a specific peer.
///
/// The filter includes:
/// - This node's own ID
/// - All leaf dependents
/// - Entries from other peers' inbound filters (excluding the destination peer)
///
/// The `peer_filters` map contains inbound filters from each peer.
/// The filter for `exclude_peer` is excluded to prevent routing loops.
pub fn compute_outgoing_filter(
&self,
exclude_peer: &NodeAddr,
peer_filters: &BTreeMap<NodeAddr, BloomFilter>,
) -> BloomFilter {
let mut filter = BloomFilter::new();
// Always include ourselves
filter.insert(&self.own_node_addr);
// Include leaf dependents
for dep in &self.leaf_dependents {
filter.insert(dep);
}
// Merge filters from other peers
for (peer_id, peer_filter) in peer_filters {
if peer_id != exclude_peer {
// Ignore merge errors (size mismatches) - just skip that filter
let _ = filter.merge(peer_filter);
}
}
filter
}
/// Create a base filter containing just this node and its dependents.
pub fn base_filter(&self) -> BloomFilter {
let mut filter = BloomFilter::new();
filter.insert(&self.own_node_addr);
for dep in &self.leaf_dependents {
filter.insert(dep);
}
filter
}
}