mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
proto/bloom: relocate bloom filter into sans-IO layout
Migrate the v1 bloom filter subsystem into src/proto/bloom/, matching the discovery/routing/fmp/mmp/stp reference layout and completing the proto/ relocation series for the data/wire subsystems. - wire.rs: the FilterAnnounce (0x20) codec, moved from protocol/filter.rs - core.rs: the pure BloomFilter algorithm (hash/insert/contains/merge/ as_bytes/from_bytes/estimated_count), moved from bloom/filter.rs - state.rs: BloomState (per-peer inbound store, compute_outgoing_filter, the injected-clock send debounce), moved from bloom/state.rs - limits.rs: the v1 sizing constants - mod.rs: module wiring + the BloomError enum - tests/: the unit suite split by target (core/state/wire), no inline tests no_std+alloc hygiene: core::fmt over std::fmt, the tracing dependency dropped from the pure filter, and std collections replaced with BTreeMap/BTreeSet (NodeAddr: Ord) for deterministic iteration. The pure filter combination stays a BloomState method; the two irreducible shell gathers (peer_inbound_filters, build_filter_announce) remain in the async shell. Wire bytes and observable behavior are unchanged; full local CI green (36/36) including the bloom-storm chaos gate.
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
//! Bloom Filter Implementation
|
||||
//!
|
||||
//! 1KB Bloom filters for reachability in FIPS routing. Each node
|
||||
//! maintains filters that summarize which destinations are reachable
|
||||
//! through each peer, enabling efficient routing decisions without
|
||||
//! global network knowledge.
|
||||
//!
|
||||
//! ## v1 Parameters
|
||||
//!
|
||||
//! - Size: 1 KB (8,192 bits) - sized for actual ~400-800 entry occupancy
|
||||
//! - Hash functions: k=5 - optimal at ~1,200 entries, good for 800-1,600
|
||||
//! - Bandwidth: 1 KB/announce (75% reduction from original 4KB design)
|
||||
//!
|
||||
//! These parameters are right-sized for typical network occupancy of
|
||||
//! ~250-800 entries per node.
|
||||
|
||||
mod filter;
|
||||
mod state;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use filter::BloomFilter;
|
||||
pub use state::BloomState;
|
||||
|
||||
/// Default filter size in bits (1KB = 8,192 bits).
|
||||
///
|
||||
/// Sized for ~800-1,600 entries. FPR ~0.05% at 400 entries, ~0.9% at 800.
|
||||
/// This is v1 protocol default (size_class=1).
|
||||
pub const DEFAULT_FILTER_SIZE_BITS: usize = 8192;
|
||||
|
||||
/// Default filter size in bytes (1KB).
|
||||
pub const DEFAULT_FILTER_SIZE_BYTES: usize = DEFAULT_FILTER_SIZE_BITS / 8;
|
||||
|
||||
/// Default number of hash functions.
|
||||
///
|
||||
/// k=5 is optimal at ~1,200 entries and a good compromise for 800-1,600.
|
||||
/// At 400 entries: FPR ~0.05%. At 800 entries: FPR ~0.9%.
|
||||
pub const DEFAULT_HASH_COUNT: u8 = 5;
|
||||
|
||||
/// Size class for v1 protocol (1 KB filters).
|
||||
pub const V1_SIZE_CLASS: u8 = 1;
|
||||
|
||||
/// Filter sizes by size_class: bytes = 512 << size_class
|
||||
pub const SIZE_CLASS_BYTES: [usize; 4] = [512, 1024, 2048, 4096];
|
||||
|
||||
/// Errors related to Bloom filter operations.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BloomError {
|
||||
#[error("invalid filter size: expected {expected} bits, got {got}")]
|
||||
InvalidSize { expected: usize, got: usize },
|
||||
|
||||
#[error("filter size must be a multiple of 8, got {0}")]
|
||||
SizeNotByteAligned(usize),
|
||||
|
||||
#[error("hash count must be positive")]
|
||||
ZeroHashCount,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+7
-5
@@ -9,7 +9,6 @@
|
||||
// distance to extracting the pure cores into a `no_std` crate later.
|
||||
extern crate alloc;
|
||||
|
||||
pub mod bloom;
|
||||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod control;
|
||||
@@ -47,8 +46,8 @@ pub use discovery::{BootstrapHandoffResult, EstablishedTraversal};
|
||||
// Re-export tree types (relocated from tree:: to proto::stp)
|
||||
pub use proto::stp::{CoordEntry, ParentDeclaration, TreeCoordinate, TreeError, TreeState};
|
||||
|
||||
// Re-export bloom filter types
|
||||
pub use bloom::{BloomError, BloomFilter, BloomState};
|
||||
// Re-export bloom filter types (relocated from bloom:: to proto::bloom)
|
||||
pub use proto::bloom::{BloomError, BloomFilter, BloomState};
|
||||
|
||||
// Re-export transport types
|
||||
pub use transport::udp::UdpTransport;
|
||||
@@ -60,13 +59,16 @@ pub use transport::{
|
||||
|
||||
// Re-export protocol types
|
||||
pub use protocol::{
|
||||
FilterAnnounce, LinkMessageType, ProtocolError, SessionAck, SessionDatagram, SessionFlags,
|
||||
SessionMessageType, SessionSetup,
|
||||
LinkMessageType, ProtocolError, SessionAck, SessionDatagram, SessionFlags, SessionMessageType,
|
||||
SessionSetup,
|
||||
};
|
||||
|
||||
// Re-export STP wire types (relocated from protocol:: to proto::stp)
|
||||
pub use proto::stp::TreeAnnounce;
|
||||
|
||||
// Re-export bloom wire types (relocated from protocol:: to proto::bloom)
|
||||
pub use proto::bloom::FilterAnnounce;
|
||||
|
||||
// Re-export discovery wire types (relocated from protocol:: to proto::discovery)
|
||||
pub use proto::discovery::{LookupRequest, LookupResponse};
|
||||
|
||||
|
||||
+5
-5
@@ -4,12 +4,12 @@
|
||||
//! including debounced propagation to peers.
|
||||
|
||||
use crate::NodeAddr;
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::protocol::FilterAnnounce;
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
use crate::proto::bloom::FilterAnnounce;
|
||||
|
||||
use super::reject::BloomReject;
|
||||
use super::{Node, NodeError};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
impl Node {
|
||||
@@ -17,8 +17,8 @@ impl Node {
|
||||
///
|
||||
/// Returns a map of (peer_node_addr -> filter) for peers that
|
||||
/// have sent us a FilterAnnounce.
|
||||
pub(super) fn peer_inbound_filters(&self) -> HashMap<NodeAddr, BloomFilter> {
|
||||
let mut filters = HashMap::new();
|
||||
pub(super) fn peer_inbound_filters(&self) -> BTreeMap<NodeAddr, BloomFilter> {
|
||||
let mut filters = BTreeMap::new();
|
||||
for (addr, peer) in &self.peers {
|
||||
if self.is_tree_peer(addr)
|
||||
&& let Some(filter) = peer.inbound_filter()
|
||||
|
||||
+1
-1
@@ -40,10 +40,10 @@ use self::wire::{
|
||||
ESTABLISHED_HEADER_SIZE, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, build_encrypted,
|
||||
build_established_header, prepend_inner_header,
|
||||
};
|
||||
use crate::bloom::{BloomFilter, BloomState};
|
||||
use crate::cache::CoordCache;
|
||||
use crate::node::session::SessionEntry;
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use crate::proto::bloom::{BloomFilter, BloomState};
|
||||
use crate::proto::discovery::{Discovery, DiscoveryBackoff, DiscoveryForwardRateLimiter};
|
||||
use crate::proto::fmp::Fmp;
|
||||
use crate::proto::mmp::Mmp;
|
||||
|
||||
@@ -455,8 +455,8 @@ async fn test_bloom_filter_split_horizon() {
|
||||
/// counted once, not the double-count fingerprint.
|
||||
#[test]
|
||||
fn compute_mesh_size_counts_each_peer_filter_once() {
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::peer::ActivePeer;
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
use crate::proto::stp::ParentDeclaration;
|
||||
|
||||
let mut node = make_node();
|
||||
@@ -550,8 +550,8 @@ fn compute_mesh_size_counts_each_peer_filter_once() {
|
||||
/// `estimated_mesh_size` carries.
|
||||
#[test]
|
||||
fn compute_mesh_size_unions_overlapping_filters() {
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::peer::ActivePeer;
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
|
||||
let mut node = make_node();
|
||||
|
||||
@@ -632,8 +632,8 @@ fn compute_mesh_size_unions_overlapping_filters() {
|
||||
/// removes the parent, and asserts the estimate does not collapse.
|
||||
#[test]
|
||||
fn compute_mesh_size_stable_across_parent_drop_with_cross_link() {
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::peer::ActivePeer;
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
|
||||
let mut node = make_node();
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
//! bloom.rs.
|
||||
|
||||
use super::*;
|
||||
use crate::bloom::{BloomFilter, DEFAULT_FILTER_SIZE_BITS, DEFAULT_HASH_COUNT};
|
||||
use crate::peer::ActivePeer;
|
||||
use crate::protocol::FilterAnnounce;
|
||||
use crate::proto::bloom::FilterAnnounce;
|
||||
use crate::proto::bloom::{BloomFilter, DEFAULT_FILTER_SIZE_BITS, DEFAULT_HASH_COUNT};
|
||||
|
||||
/// Inject a synthetic active peer into the node with a known NodeAddr.
|
||||
/// Returns the peer's NodeAddr.
|
||||
|
||||
@@ -1038,8 +1038,8 @@ async fn test_open_discovery_sweep_queues_eligible_skips_filtered() {
|
||||
/// that `initiate_lookup` ran fresh on each attempt.
|
||||
#[tokio::test]
|
||||
async fn test_check_pending_lookups_default_sequence_unreachable() {
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::peer::ActivePeer;
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
use crate::proto::discovery::PendingLookup;
|
||||
use crate::transport::LinkId;
|
||||
use std::sync::mpsc;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! filter priority, greedy tree routing, and tie-breaking.
|
||||
|
||||
use super::*;
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
use crate::proto::stp::{ParentDeclaration, TreeCoordinate};
|
||||
use spanning_tree::{
|
||||
TestNode, cleanup_nodes, drain_all_packets, generate_random_edges, initiate_handshake,
|
||||
|
||||
+1
-1
@@ -3,10 +3,10 @@
|
||||
//! Represents a fully authenticated peer after successful Noise handshake.
|
||||
//! ActivePeer holds tree state, Bloom filter, and routing information.
|
||||
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::mmp::MmpConfig;
|
||||
use crate::node::REKEY_JITTER_SECS;
|
||||
use crate::noise::{HandshakeState as NoiseHandshakeState, NoiseError, NoiseSession};
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
use crate::proto::mmp::MmpPeerState;
|
||||
use crate::proto::stp::{ParentDeclaration, TreeCoordinate};
|
||||
use crate::transport::{LinkId, LinkStats, TransportAddr, TransportId};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Generic Bloom filter data structure.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use tracing::trace;
|
||||
use core::fmt;
|
||||
|
||||
use super::{BloomError, DEFAULT_FILTER_SIZE_BITS, DEFAULT_HASH_COUNT};
|
||||
use crate::NodeAddr;
|
||||
@@ -164,7 +162,6 @@ impl BloomFilter {
|
||||
let fill = x / m;
|
||||
let fpr = fill.powi(self.hash_count as i32);
|
||||
if fpr > max_fpr {
|
||||
trace!(fill, fpr, max_fpr, "estimated_count: filter above cap");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//! v1 bloom filter sizing constants (the tunables).
|
||||
|
||||
/// Default filter size in bits (1KB = 8,192 bits).
|
||||
///
|
||||
/// Sized for ~800-1,600 entries. FPR ~0.05% at 400 entries, ~0.9% at 800.
|
||||
/// This is v1 protocol default (size_class=1).
|
||||
pub const DEFAULT_FILTER_SIZE_BITS: usize = 8192;
|
||||
|
||||
/// Default filter size in bytes (1KB).
|
||||
///
|
||||
/// Retained for completeness of the v1 tunable set; no current consumer.
|
||||
#[allow(dead_code)]
|
||||
pub const DEFAULT_FILTER_SIZE_BYTES: usize = DEFAULT_FILTER_SIZE_BITS / 8;
|
||||
|
||||
/// Default number of hash functions.
|
||||
///
|
||||
/// k=5 is optimal at ~1,200 entries and a good compromise for 800-1,600.
|
||||
/// At 400 entries: FPR ~0.05%. At 800 entries: FPR ~0.9%.
|
||||
pub const DEFAULT_HASH_COUNT: u8 = 5;
|
||||
|
||||
/// Size class for v1 protocol (1 KB filters).
|
||||
pub const V1_SIZE_CLASS: u8 = 1;
|
||||
|
||||
/// Filter sizes by size_class: bytes = 512 << size_class
|
||||
///
|
||||
/// Retained for completeness of the v1 tunable set; no current consumer.
|
||||
#[allow(dead_code)]
|
||||
pub const SIZE_CLASS_BYTES: [usize; 4] = [512, 1024, 2048, 4096];
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Sans-IO bloom filter subsystem.
|
||||
//!
|
||||
//! 1KB Bloom filters for reachability in FIPS routing. Each node maintains
|
||||
//! filters that summarize which destinations are reachable through each peer,
|
||||
//! enabling efficient routing decisions without global network knowledge.
|
||||
//!
|
||||
//! ## v1 Parameters
|
||||
//!
|
||||
//! - Size: 1 KB (8,192 bits) - sized for actual ~400-800 entry occupancy
|
||||
//! - Hash functions: k=5 - optimal at ~1,200 entries, good for 800-1,600
|
||||
//! - Bandwidth: 1 KB/announce (75% reduction from original 4KB design)
|
||||
//!
|
||||
//! - `core.rs` — the pure `BloomFilter` data structure.
|
||||
//! - `state.rs` — `BloomState` (per-peer inbound store + outgoing filter
|
||||
//! computation + the send-debounce decision).
|
||||
//! - `limits.rs` — the v1 sizing constants.
|
||||
//! - `wire.rs` — `FilterAnnounce` + `encode`/`decode` (the std-tethered file).
|
||||
//! It imports the shared `ProtocolError` and `LinkMessageType` downward from
|
||||
//! `crate::protocol`.
|
||||
|
||||
mod core;
|
||||
mod limits;
|
||||
mod state;
|
||||
mod wire;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use core::BloomFilter;
|
||||
pub use limits::{DEFAULT_FILTER_SIZE_BITS, DEFAULT_HASH_COUNT, V1_SIZE_CLASS};
|
||||
pub use state::BloomState;
|
||||
pub use wire::FilterAnnounce;
|
||||
|
||||
/// Errors related to Bloom filter operations.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BloomError {
|
||||
#[error("invalid filter size: expected {expected} bits, got {got}")]
|
||||
InvalidSize { expected: usize, got: usize },
|
||||
|
||||
#[error("filter size must be a multiple of 8, got {0}")]
|
||||
SizeNotByteAligned(usize),
|
||||
|
||||
#[error("hash count must be positive")]
|
||||
ZeroHashCount,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! FIPS-specific Bloom filter announcement state management.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::BloomFilter;
|
||||
use crate::NodeAddr;
|
||||
@@ -13,19 +13,19 @@ 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: HashSet<NodeAddr>,
|
||||
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: HashMap<NodeAddr, u64>,
|
||||
last_update_sent: BTreeMap<NodeAddr, u64>,
|
||||
/// Peers that need a filter update.
|
||||
pending_updates: HashSet<NodeAddr>,
|
||||
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: HashMap<NodeAddr, BloomFilter>,
|
||||
last_sent_filters: BTreeMap<NodeAddr, BloomFilter>,
|
||||
}
|
||||
|
||||
impl BloomState {
|
||||
@@ -33,13 +33,13 @@ impl BloomState {
|
||||
pub fn new(own_node_addr: NodeAddr) -> Self {
|
||||
Self {
|
||||
own_node_addr,
|
||||
leaf_dependents: HashSet::new(),
|
||||
leaf_dependents: BTreeSet::new(),
|
||||
is_leaf_only: false,
|
||||
update_debounce_ms: 500,
|
||||
last_update_sent: HashMap::new(),
|
||||
pending_updates: HashSet::new(),
|
||||
last_update_sent: BTreeMap::new(),
|
||||
pending_updates: BTreeSet::new(),
|
||||
sequence: 0,
|
||||
last_sent_filters: HashMap::new(),
|
||||
last_sent_filters: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ impl BloomState {
|
||||
}
|
||||
|
||||
/// Get the set of leaf dependents.
|
||||
pub fn leaf_dependents(&self) -> &HashSet<NodeAddr> {
|
||||
pub fn leaf_dependents(&self) -> &BTreeSet<NodeAddr> {
|
||||
&self.leaf_dependents
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ impl BloomState {
|
||||
&mut self,
|
||||
exclude_from: &NodeAddr,
|
||||
peer_addrs: &[NodeAddr],
|
||||
peer_filters: &HashMap<NodeAddr, BloomFilter>,
|
||||
peer_filters: &BTreeMap<NodeAddr, BloomFilter>,
|
||||
) {
|
||||
for peer_addr in peer_addrs {
|
||||
if peer_addr == exclude_from {
|
||||
@@ -199,7 +199,7 @@ impl BloomState {
|
||||
pub fn compute_outgoing_filter(
|
||||
&self,
|
||||
exclude_peer: &NodeAddr,
|
||||
peer_filters: &HashMap<NodeAddr, BloomFilter>,
|
||||
peer_filters: &BTreeMap<NodeAddr, BloomFilter>,
|
||||
) -> BloomFilter {
|
||||
let mut filter = BloomFilter::new();
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
//! Tests for the `BloomFilter` data structure.
|
||||
|
||||
use crate::proto::bloom::{BloomError, BloomFilter, DEFAULT_FILTER_SIZE_BITS, DEFAULT_HASH_COUNT};
|
||||
use crate::testutil::make_node_addr;
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_new() {
|
||||
let filter = BloomFilter::new();
|
||||
assert_eq!(filter.num_bits(), DEFAULT_FILTER_SIZE_BITS);
|
||||
assert_eq!(filter.hash_count(), DEFAULT_HASH_COUNT);
|
||||
assert_eq!(filter.count_ones(), 0);
|
||||
assert!(filter.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_insert_contains() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
|
||||
assert!(!filter.contains(&node1));
|
||||
assert!(!filter.contains(&node2));
|
||||
|
||||
filter.insert(&node1);
|
||||
|
||||
assert!(filter.contains(&node1));
|
||||
// node2 might have false positive, but very unlikely with single insert
|
||||
assert!(!filter.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_multiple_inserts() {
|
||||
let mut filter = BloomFilter::new();
|
||||
|
||||
for i in 0..100 {
|
||||
let node = make_node_addr(i);
|
||||
filter.insert(&node);
|
||||
}
|
||||
|
||||
// All inserted items should be found
|
||||
for i in 0..100 {
|
||||
let node = make_node_addr(i);
|
||||
assert!(filter.contains(&node), "Node {} not found", i);
|
||||
}
|
||||
|
||||
// Fill ratio should be reasonable
|
||||
let fill = filter.fill_ratio();
|
||||
assert!(fill > 0.0 && fill < 0.5, "Unexpected fill ratio: {}", fill);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_merge() {
|
||||
let mut filter1 = BloomFilter::new();
|
||||
let mut filter2 = BloomFilter::new();
|
||||
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
|
||||
filter1.insert(&node1);
|
||||
filter2.insert(&node2);
|
||||
|
||||
filter1.merge(&filter2).unwrap();
|
||||
|
||||
assert!(filter1.contains(&node1));
|
||||
assert!(filter1.contains(&node2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_union() {
|
||||
let mut filter1 = BloomFilter::new();
|
||||
let mut filter2 = BloomFilter::new();
|
||||
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
|
||||
filter1.insert(&node1);
|
||||
filter2.insert(&node2);
|
||||
|
||||
let union = filter1.union(&filter2).unwrap();
|
||||
|
||||
assert!(union.contains(&node1));
|
||||
assert!(union.contains(&node2));
|
||||
// Original filters unchanged
|
||||
assert!(!filter1.contains(&node2));
|
||||
assert!(!filter2.contains(&node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_clear() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let node = make_node_addr(1);
|
||||
|
||||
filter.insert(&node);
|
||||
assert!(!filter.is_empty());
|
||||
|
||||
filter.clear();
|
||||
assert!(filter.is_empty());
|
||||
assert_eq!(filter.count_ones(), 0);
|
||||
assert!(!filter.contains(&node));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_merge_size_mismatch() {
|
||||
let mut filter1 = BloomFilter::with_params(1024, 7).unwrap();
|
||||
let filter2 = BloomFilter::with_params(2048, 7).unwrap();
|
||||
|
||||
let result = filter1.merge(&filter2);
|
||||
assert!(matches!(result, Err(BloomError::InvalidSize { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_custom_params() {
|
||||
let filter = BloomFilter::with_params(1024, 5).unwrap();
|
||||
assert_eq!(filter.num_bits(), 1024);
|
||||
assert_eq!(filter.num_bytes(), 128);
|
||||
assert_eq!(filter.hash_count(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_invalid_params() {
|
||||
// Not byte-aligned (1001 is not divisible by 8)
|
||||
assert!(matches!(
|
||||
BloomFilter::with_params(1001, 7),
|
||||
Err(BloomError::SizeNotByteAligned(1001))
|
||||
));
|
||||
|
||||
// Zero size
|
||||
assert!(matches!(
|
||||
BloomFilter::with_params(0, 7),
|
||||
Err(BloomError::SizeNotByteAligned(0))
|
||||
));
|
||||
|
||||
// Zero hash count
|
||||
assert!(matches!(
|
||||
BloomFilter::with_params(1024, 0),
|
||||
Err(BloomError::ZeroHashCount)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_bytes() {
|
||||
let original = BloomFilter::new();
|
||||
let bytes = original.as_bytes().to_vec();
|
||||
|
||||
let restored = BloomFilter::from_bytes(bytes, original.hash_count()).unwrap();
|
||||
|
||||
assert_eq!(original, restored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_estimated_count() {
|
||||
let mut filter = BloomFilter::new();
|
||||
|
||||
// Empty filter
|
||||
assert_eq!(filter.estimated_count(f64::INFINITY), Some(0.0));
|
||||
|
||||
// Insert some items
|
||||
for i in 0..50 {
|
||||
filter.insert(&make_node_addr(i));
|
||||
}
|
||||
|
||||
// Estimate should be reasonably close to 50
|
||||
let estimate = filter.estimated_count(f64::INFINITY).unwrap();
|
||||
assert!(
|
||||
estimate > 30.0 && estimate < 100.0,
|
||||
"Unexpected estimate: {}",
|
||||
estimate
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_equality() {
|
||||
let mut filter1 = BloomFilter::new();
|
||||
let mut filter2 = BloomFilter::new();
|
||||
|
||||
assert_eq!(filter1, filter2);
|
||||
|
||||
filter1.insert(&make_node_addr(1));
|
||||
assert_ne!(filter1, filter2);
|
||||
|
||||
filter2.insert(&make_node_addr(1));
|
||||
assert_eq!(filter1, filter2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_bytes_empty() {
|
||||
let result = BloomFilter::from_bytes(vec![], 5);
|
||||
assert!(matches!(result, Err(BloomError::SizeNotByteAligned(0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_bytes_zero_hash_count() {
|
||||
let result = BloomFilter::from_bytes(vec![0u8; 128], 0);
|
||||
assert!(matches!(result, Err(BloomError::ZeroHashCount)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_slice() {
|
||||
let mut original = BloomFilter::new();
|
||||
original.insert(&make_node_addr(42));
|
||||
let bytes = original.as_bytes();
|
||||
|
||||
let restored = BloomFilter::from_slice(bytes, original.hash_count()).unwrap();
|
||||
assert_eq!(original, restored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_insert_bytes_contains_bytes() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let data1 = b"hello world";
|
||||
let data2 = b"goodbye";
|
||||
|
||||
assert!(!filter.contains_bytes(data1));
|
||||
|
||||
filter.insert_bytes(data1);
|
||||
assert!(filter.contains_bytes(data1));
|
||||
assert!(!filter.contains_bytes(data2));
|
||||
|
||||
filter.insert_bytes(data2);
|
||||
assert!(filter.contains_bytes(data1));
|
||||
assert!(filter.contains_bytes(data2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_estimated_count_saturated() {
|
||||
// Create a small filter with all bits set
|
||||
let bytes = vec![0xFF; 8]; // all bits set
|
||||
let filter = BloomFilter::from_bytes(bytes, 3).unwrap();
|
||||
|
||||
// Saturated filter returns None regardless of cap (defense in depth).
|
||||
// Previously returned f64::INFINITY.
|
||||
assert_eq!(filter.estimated_count(f64::INFINITY), None);
|
||||
assert_eq!(filter.estimated_count(0.05), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_estimated_count_fpr_cap_boundary() {
|
||||
// Cap boundary: FPR = fill^k = 0.05 at k=5 ⇒ fill ≈ 0.5493
|
||||
// 1KB filter (8192 bits). 560 bytes of 0xFF = 4480 bits set =
|
||||
// fill 0.5469, FPR ≈ 0.04877 — just below cap.
|
||||
// 564 bytes of 0xFF = 4512 bits set = fill 0.5508, FPR ≈ 0.05060 —
|
||||
// just above cap.
|
||||
|
||||
let mut below = vec![0x00u8; 1024];
|
||||
below[..560].fill(0xFF);
|
||||
let below_filter = BloomFilter::from_bytes(below, DEFAULT_HASH_COUNT).unwrap();
|
||||
assert!(
|
||||
below_filter.estimated_count(0.05).is_some(),
|
||||
"fill 0.5469 (FPR ≈ 0.049) must be accepted by cap 0.05"
|
||||
);
|
||||
|
||||
let mut above = vec![0x00u8; 1024];
|
||||
above[..564].fill(0xFF);
|
||||
let above_filter = BloomFilter::from_bytes(above, DEFAULT_HASH_COUNT).unwrap();
|
||||
assert_eq!(
|
||||
above_filter.estimated_count(0.05),
|
||||
None,
|
||||
"fill 0.5508 (FPR ≈ 0.051) must be rejected by cap 0.05"
|
||||
);
|
||||
|
||||
// Same above-cap filter with a looser cap is accepted.
|
||||
assert!(
|
||||
above_filter.estimated_count(0.10).is_some(),
|
||||
"fill 0.5508 (FPR ≈ 0.051) must be accepted by cap 0.10"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_default() {
|
||||
let default: BloomFilter = Default::default();
|
||||
let explicit = BloomFilter::new();
|
||||
assert_eq!(default, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_debug_format() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let debug = format!("{:?}", filter);
|
||||
assert!(debug.contains("BloomFilter"));
|
||||
assert!(debug.contains("8192"));
|
||||
assert!(debug.contains("hash_count"));
|
||||
|
||||
// With some entries
|
||||
for i in 0..10 {
|
||||
filter.insert(&make_node_addr(i));
|
||||
}
|
||||
let debug = format!("{:?}", filter);
|
||||
assert!(debug.contains("fill_ratio"));
|
||||
assert!(debug.contains("est_count"));
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Bloom subsystem unit tests, extracted from the co-located `#[cfg(test)]`
|
||||
//! blocks in the sibling source modules.
|
||||
|
||||
mod core;
|
||||
mod state;
|
||||
mod wire;
|
||||
@@ -1,302 +1,9 @@
|
||||
use super::*;
|
||||
use crate::NodeAddr;
|
||||
use std::collections::HashMap;
|
||||
//! Tests for `BloomState` (announcement state management).
|
||||
|
||||
fn make_node_addr(val: u8) -> NodeAddr {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[0] = val;
|
||||
NodeAddr::from_bytes(bytes)
|
||||
}
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// ===== BloomFilter Tests =====
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_new() {
|
||||
let filter = BloomFilter::new();
|
||||
assert_eq!(filter.num_bits(), DEFAULT_FILTER_SIZE_BITS);
|
||||
assert_eq!(filter.hash_count(), DEFAULT_HASH_COUNT);
|
||||
assert_eq!(filter.count_ones(), 0);
|
||||
assert!(filter.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_insert_contains() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
|
||||
assert!(!filter.contains(&node1));
|
||||
assert!(!filter.contains(&node2));
|
||||
|
||||
filter.insert(&node1);
|
||||
|
||||
assert!(filter.contains(&node1));
|
||||
// node2 might have false positive, but very unlikely with single insert
|
||||
assert!(!filter.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_multiple_inserts() {
|
||||
let mut filter = BloomFilter::new();
|
||||
|
||||
for i in 0..100 {
|
||||
let node = make_node_addr(i);
|
||||
filter.insert(&node);
|
||||
}
|
||||
|
||||
// All inserted items should be found
|
||||
for i in 0..100 {
|
||||
let node = make_node_addr(i);
|
||||
assert!(filter.contains(&node), "Node {} not found", i);
|
||||
}
|
||||
|
||||
// Fill ratio should be reasonable
|
||||
let fill = filter.fill_ratio();
|
||||
assert!(fill > 0.0 && fill < 0.5, "Unexpected fill ratio: {}", fill);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_merge() {
|
||||
let mut filter1 = BloomFilter::new();
|
||||
let mut filter2 = BloomFilter::new();
|
||||
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
|
||||
filter1.insert(&node1);
|
||||
filter2.insert(&node2);
|
||||
|
||||
filter1.merge(&filter2).unwrap();
|
||||
|
||||
assert!(filter1.contains(&node1));
|
||||
assert!(filter1.contains(&node2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_union() {
|
||||
let mut filter1 = BloomFilter::new();
|
||||
let mut filter2 = BloomFilter::new();
|
||||
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
|
||||
filter1.insert(&node1);
|
||||
filter2.insert(&node2);
|
||||
|
||||
let union = filter1.union(&filter2).unwrap();
|
||||
|
||||
assert!(union.contains(&node1));
|
||||
assert!(union.contains(&node2));
|
||||
// Original filters unchanged
|
||||
assert!(!filter1.contains(&node2));
|
||||
assert!(!filter2.contains(&node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_clear() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let node = make_node_addr(1);
|
||||
|
||||
filter.insert(&node);
|
||||
assert!(!filter.is_empty());
|
||||
|
||||
filter.clear();
|
||||
assert!(filter.is_empty());
|
||||
assert_eq!(filter.count_ones(), 0);
|
||||
assert!(!filter.contains(&node));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_merge_size_mismatch() {
|
||||
let mut filter1 = BloomFilter::with_params(1024, 7).unwrap();
|
||||
let filter2 = BloomFilter::with_params(2048, 7).unwrap();
|
||||
|
||||
let result = filter1.merge(&filter2);
|
||||
assert!(matches!(result, Err(BloomError::InvalidSize { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_custom_params() {
|
||||
let filter = BloomFilter::with_params(1024, 5).unwrap();
|
||||
assert_eq!(filter.num_bits(), 1024);
|
||||
assert_eq!(filter.num_bytes(), 128);
|
||||
assert_eq!(filter.hash_count(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_invalid_params() {
|
||||
// Not byte-aligned (1001 is not divisible by 8)
|
||||
assert!(matches!(
|
||||
BloomFilter::with_params(1001, 7),
|
||||
Err(BloomError::SizeNotByteAligned(1001))
|
||||
));
|
||||
|
||||
// Zero size
|
||||
assert!(matches!(
|
||||
BloomFilter::with_params(0, 7),
|
||||
Err(BloomError::SizeNotByteAligned(0))
|
||||
));
|
||||
|
||||
// Zero hash count
|
||||
assert!(matches!(
|
||||
BloomFilter::with_params(1024, 0),
|
||||
Err(BloomError::ZeroHashCount)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_bytes() {
|
||||
let original = BloomFilter::new();
|
||||
let bytes = original.as_bytes().to_vec();
|
||||
|
||||
let restored = BloomFilter::from_bytes(bytes, original.hash_count()).unwrap();
|
||||
|
||||
assert_eq!(original, restored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_estimated_count() {
|
||||
let mut filter = BloomFilter::new();
|
||||
|
||||
// Empty filter
|
||||
assert_eq!(filter.estimated_count(f64::INFINITY), Some(0.0));
|
||||
|
||||
// Insert some items
|
||||
for i in 0..50 {
|
||||
filter.insert(&make_node_addr(i));
|
||||
}
|
||||
|
||||
// Estimate should be reasonably close to 50
|
||||
let estimate = filter.estimated_count(f64::INFINITY).unwrap();
|
||||
assert!(
|
||||
estimate > 30.0 && estimate < 100.0,
|
||||
"Unexpected estimate: {}",
|
||||
estimate
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_equality() {
|
||||
let mut filter1 = BloomFilter::new();
|
||||
let mut filter2 = BloomFilter::new();
|
||||
|
||||
assert_eq!(filter1, filter2);
|
||||
|
||||
filter1.insert(&make_node_addr(1));
|
||||
assert_ne!(filter1, filter2);
|
||||
|
||||
filter2.insert(&make_node_addr(1));
|
||||
assert_eq!(filter1, filter2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_bytes_empty() {
|
||||
let result = BloomFilter::from_bytes(vec![], 5);
|
||||
assert!(matches!(result, Err(BloomError::SizeNotByteAligned(0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_bytes_zero_hash_count() {
|
||||
let result = BloomFilter::from_bytes(vec![0u8; 128], 0);
|
||||
assert!(matches!(result, Err(BloomError::ZeroHashCount)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_from_slice() {
|
||||
let mut original = BloomFilter::new();
|
||||
original.insert(&make_node_addr(42));
|
||||
let bytes = original.as_bytes();
|
||||
|
||||
let restored = BloomFilter::from_slice(bytes, original.hash_count()).unwrap();
|
||||
assert_eq!(original, restored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_insert_bytes_contains_bytes() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let data1 = b"hello world";
|
||||
let data2 = b"goodbye";
|
||||
|
||||
assert!(!filter.contains_bytes(data1));
|
||||
|
||||
filter.insert_bytes(data1);
|
||||
assert!(filter.contains_bytes(data1));
|
||||
assert!(!filter.contains_bytes(data2));
|
||||
|
||||
filter.insert_bytes(data2);
|
||||
assert!(filter.contains_bytes(data1));
|
||||
assert!(filter.contains_bytes(data2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_estimated_count_saturated() {
|
||||
// Create a small filter with all bits set
|
||||
let bytes = vec![0xFF; 8]; // all bits set
|
||||
let filter = BloomFilter::from_bytes(bytes, 3).unwrap();
|
||||
|
||||
// Saturated filter returns None regardless of cap (defense in depth).
|
||||
// Previously returned f64::INFINITY.
|
||||
assert_eq!(filter.estimated_count(f64::INFINITY), None);
|
||||
assert_eq!(filter.estimated_count(0.05), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_estimated_count_fpr_cap_boundary() {
|
||||
// Cap boundary: FPR = fill^k = 0.05 at k=5 ⇒ fill ≈ 0.5493
|
||||
// 1KB filter (8192 bits). 560 bytes of 0xFF = 4480 bits set =
|
||||
// fill 0.5469, FPR ≈ 0.04877 — just below cap.
|
||||
// 564 bytes of 0xFF = 4512 bits set = fill 0.5508, FPR ≈ 0.05060 —
|
||||
// just above cap.
|
||||
|
||||
let mut below = vec![0x00u8; 1024];
|
||||
below[..560].fill(0xFF);
|
||||
let below_filter = BloomFilter::from_bytes(below, DEFAULT_HASH_COUNT).unwrap();
|
||||
assert!(
|
||||
below_filter.estimated_count(0.05).is_some(),
|
||||
"fill 0.5469 (FPR ≈ 0.049) must be accepted by cap 0.05"
|
||||
);
|
||||
|
||||
let mut above = vec![0x00u8; 1024];
|
||||
above[..564].fill(0xFF);
|
||||
let above_filter = BloomFilter::from_bytes(above, DEFAULT_HASH_COUNT).unwrap();
|
||||
assert_eq!(
|
||||
above_filter.estimated_count(0.05),
|
||||
None,
|
||||
"fill 0.5508 (FPR ≈ 0.051) must be rejected by cap 0.05"
|
||||
);
|
||||
|
||||
// Same above-cap filter with a looser cap is accepted.
|
||||
assert!(
|
||||
above_filter.estimated_count(0.10).is_some(),
|
||||
"fill 0.5508 (FPR ≈ 0.051) must be accepted by cap 0.10"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_default() {
|
||||
let default: BloomFilter = Default::default();
|
||||
let explicit = BloomFilter::new();
|
||||
assert_eq!(default, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter_debug_format() {
|
||||
let mut filter = BloomFilter::new();
|
||||
let debug = format!("{:?}", filter);
|
||||
assert!(debug.contains("BloomFilter"));
|
||||
assert!(debug.contains("8192"));
|
||||
assert!(debug.contains("hash_count"));
|
||||
|
||||
// With some entries
|
||||
for i in 0..10 {
|
||||
filter.insert(&make_node_addr(i));
|
||||
}
|
||||
let debug = format!("{:?}", filter);
|
||||
assert!(debug.contains("fill_ratio"));
|
||||
assert!(debug.contains("est_count"));
|
||||
}
|
||||
|
||||
// ===== BloomState Tests =====
|
||||
use crate::proto::bloom::{BloomFilter, BloomState};
|
||||
use crate::testutil::make_node_addr;
|
||||
|
||||
#[test]
|
||||
fn test_bloom_state_new() {
|
||||
@@ -426,7 +133,7 @@ fn test_bloom_state_compute_outgoing_filter() {
|
||||
let mut filter2 = BloomFilter::new();
|
||||
filter2.insert(&make_node_addr(200));
|
||||
|
||||
let mut peer_filters = HashMap::new();
|
||||
let mut peer_filters = BTreeMap::new();
|
||||
peer_filters.insert(peer1, filter1);
|
||||
peer_filters.insert(peer2, filter2);
|
||||
|
||||
@@ -477,7 +184,7 @@ fn test_bloom_state_record_sent_filter() {
|
||||
state.record_sent_filter(peer, filter);
|
||||
|
||||
// Compute what would be sent to peer (just our own node, no peer filters)
|
||||
let peer_filters = HashMap::new();
|
||||
let peer_filters = BTreeMap::new();
|
||||
let peer_addrs = vec![peer];
|
||||
state.mark_changed_peers(&make_node_addr(99), &peer_addrs, &peer_filters);
|
||||
|
||||
@@ -514,7 +221,7 @@ fn test_bloom_state_remove_peer_state() {
|
||||
|
||||
// Sent filter cleared — mark_changed_peers should treat as "never sent"
|
||||
state.clear_pending_updates();
|
||||
let peer_filters = HashMap::new();
|
||||
let peer_filters = BTreeMap::new();
|
||||
let peer_addrs = vec![peer];
|
||||
state.mark_changed_peers(&make_node_addr(99), &peer_addrs, &peer_filters);
|
||||
assert!(state.needs_update(&peer)); // never sent → must send
|
||||
@@ -528,7 +235,7 @@ fn test_bloom_state_mark_changed_peers_never_sent() {
|
||||
let peer1 = make_node_addr(1);
|
||||
let peer2 = make_node_addr(2);
|
||||
|
||||
let peer_filters = HashMap::new();
|
||||
let peer_filters = BTreeMap::new();
|
||||
let peer_addrs = vec![peer1, peer2];
|
||||
|
||||
// No filters ever sent — all peers should be marked
|
||||
@@ -545,7 +252,7 @@ fn test_bloom_state_mark_changed_peers_unchanged() {
|
||||
|
||||
let peer1 = make_node_addr(1);
|
||||
let peer2 = make_node_addr(2);
|
||||
let peer_filters = HashMap::new();
|
||||
let peer_filters = BTreeMap::new();
|
||||
let peer_addrs = vec![peer1, peer2];
|
||||
|
||||
// Compute and record what would be sent to each peer
|
||||
@@ -568,7 +275,7 @@ fn test_bloom_state_mark_changed_peers_one_changed() {
|
||||
|
||||
let peer1 = make_node_addr(1);
|
||||
let peer2 = make_node_addr(2);
|
||||
let peer_filters = HashMap::new();
|
||||
let peer_filters = BTreeMap::new();
|
||||
let peer_addrs = vec![peer1, peer2];
|
||||
|
||||
// Record current outgoing filters for both peers
|
||||
@@ -580,7 +287,7 @@ fn test_bloom_state_mark_changed_peers_one_changed() {
|
||||
// Now peer1 sends us a filter with new entries
|
||||
let mut inbound_from_peer1 = BloomFilter::new();
|
||||
inbound_from_peer1.insert(&make_node_addr(100));
|
||||
let mut updated_peer_filters = HashMap::new();
|
||||
let mut updated_peer_filters = BTreeMap::new();
|
||||
updated_peer_filters.insert(peer1, inbound_from_peer1);
|
||||
|
||||
// mark_changed_peers triggered by receiving from peer1
|
||||
@@ -598,7 +305,7 @@ fn test_bloom_state_mark_changed_peers_excludes_source() {
|
||||
let mut state = BloomState::new(node);
|
||||
|
||||
let peer1 = make_node_addr(1);
|
||||
let peer_filters = HashMap::new();
|
||||
let peer_filters = BTreeMap::new();
|
||||
let peer_addrs = vec![peer1];
|
||||
|
||||
// peer1 is both the source and the only peer — should be skipped
|
||||
@@ -0,0 +1,99 @@
|
||||
//! Tests for the bloom wire codec (`FilterAnnounce`).
|
||||
|
||||
use crate::proto::bloom::BloomFilter;
|
||||
use crate::proto::bloom::FilterAnnounce;
|
||||
use crate::protocol::LinkMessageType;
|
||||
use crate::testutil::make_node_addr;
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_size_class() {
|
||||
let filter = BloomFilter::new();
|
||||
let announce = FilterAnnounce::new(filter.clone(), 100);
|
||||
|
||||
// v1 defaults
|
||||
assert_eq!(announce.size_class, 1);
|
||||
assert_eq!(announce.hash_count, 5);
|
||||
assert!(announce.is_v1_compliant());
|
||||
assert!(announce.is_valid());
|
||||
assert_eq!(announce.filter_size_bytes(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_with_size_class() {
|
||||
let filter = BloomFilter::with_params(2048 * 8, 7).unwrap();
|
||||
let announce = FilterAnnounce::with_size_class(filter, 100, 2);
|
||||
|
||||
assert_eq!(announce.size_class, 2);
|
||||
assert_eq!(announce.hash_count, 7);
|
||||
assert!(!announce.is_v1_compliant());
|
||||
assert!(announce.is_valid());
|
||||
assert_eq!(announce.filter_size_bytes(), 2048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_encode_decode_roundtrip() {
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&make_node_addr(42));
|
||||
filter.insert(&make_node_addr(99));
|
||||
let announce = FilterAnnounce::new(filter, 500);
|
||||
|
||||
let encoded = announce.encode().unwrap();
|
||||
// msg_type(1) + sequence(8) + hash_count(1) + size_class(1) + filter(1024)
|
||||
assert_eq!(encoded.len(), 1035);
|
||||
assert_eq!(encoded[0], LinkMessageType::FilterAnnounce.to_byte());
|
||||
|
||||
// Decode strips msg_type (as dispatcher does)
|
||||
let decoded = FilterAnnounce::decode(&encoded[1..]).unwrap();
|
||||
assert_eq!(decoded.sequence, 500);
|
||||
assert_eq!(decoded.hash_count, 5);
|
||||
assert_eq!(decoded.size_class, 1);
|
||||
assert!(decoded.is_valid());
|
||||
assert!(decoded.is_v1_compliant());
|
||||
|
||||
// Filter contents preserved
|
||||
assert!(decoded.filter.contains(&make_node_addr(42)));
|
||||
assert!(decoded.filter.contains(&make_node_addr(99)));
|
||||
assert!(!decoded.filter.contains(&make_node_addr(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_decode_rejects_bad_size_class() {
|
||||
let filter = BloomFilter::new();
|
||||
let announce = FilterAnnounce::new(filter, 100);
|
||||
let mut encoded = announce.encode().unwrap();
|
||||
|
||||
// Corrupt size_class byte (offset: 1 msg_type + 8 seq + 1 hash = 10)
|
||||
encoded[10] = 5; // invalid size_class > MAX_SIZE_CLASS
|
||||
|
||||
let result = FilterAnnounce::decode(&encoded[1..]);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid size_class")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_decode_rejects_non_v1_size_class() {
|
||||
// Build a size_class=0 payload manually (valid range but not v1)
|
||||
let filter = BloomFilter::with_params(512 * 8, 5).unwrap();
|
||||
let announce = FilterAnnounce::with_size_class(filter, 100, 0);
|
||||
let encoded = announce.encode().unwrap();
|
||||
|
||||
let result = FilterAnnounce::decode(&encoded[1..]);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("unsupported size_class")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_decode_rejects_truncated() {
|
||||
let result = FilterAnnounce::decode(&[0u8; 5]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//! FilterAnnounce message: bloom filter reachability propagation.
|
||||
|
||||
use super::error::ProtocolError;
|
||||
use super::link::LinkMessageType;
|
||||
use crate::bloom::BloomFilter;
|
||||
use super::BloomFilter;
|
||||
use crate::protocol::LinkMessageType;
|
||||
use crate::protocol::ProtocolError;
|
||||
|
||||
/// Bloom filter announcement for reachability propagation.
|
||||
///
|
||||
@@ -35,7 +35,7 @@ impl FilterAnnounce {
|
||||
pub fn new(filter: BloomFilter, sequence: u64) -> Self {
|
||||
Self {
|
||||
hash_count: filter.hash_count(),
|
||||
size_class: crate::bloom::V1_SIZE_CLASS,
|
||||
size_class: super::V1_SIZE_CLASS,
|
||||
filter,
|
||||
sequence,
|
||||
}
|
||||
@@ -64,7 +64,7 @@ impl FilterAnnounce {
|
||||
|
||||
/// Check if this is a v1-compliant filter (size_class=1).
|
||||
pub fn is_v1_compliant(&self) -> bool {
|
||||
self.size_class == crate::bloom::V1_SIZE_CLASS
|
||||
self.size_class == super::V1_SIZE_CLASS
|
||||
}
|
||||
|
||||
/// Minimum payload size after msg_type is stripped:
|
||||
@@ -142,10 +142,10 @@ impl FilterAnnounce {
|
||||
}
|
||||
|
||||
// v1 compliance check
|
||||
if size_class != crate::bloom::V1_SIZE_CLASS {
|
||||
if size_class != super::V1_SIZE_CLASS {
|
||||
return Err(ProtocolError::Malformed(format!(
|
||||
"unsupported size_class: {size_class} (v1 requires {})",
|
||||
crate::bloom::V1_SIZE_CLASS
|
||||
super::V1_SIZE_CLASS
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ impl FilterAnnounce {
|
||||
}
|
||||
|
||||
// Construct BloomFilter from bytes
|
||||
let filter = crate::bloom::BloomFilter::from_slice(&payload[pos..], hash_count)
|
||||
let filter = BloomFilter::from_slice(&payload[pos..], hash_count)
|
||||
.map_err(|e| ProtocolError::Malformed(format!("invalid bloom filter: {e}")))?;
|
||||
|
||||
let announce = Self {
|
||||
@@ -173,108 +173,3 @@ impl FilterAnnounce {
|
||||
Ok(announce)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::NodeAddr;
|
||||
|
||||
fn make_node_addr(val: u8) -> NodeAddr {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[0] = val;
|
||||
NodeAddr::from_bytes(bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_size_class() {
|
||||
let filter = BloomFilter::new();
|
||||
let announce = FilterAnnounce::new(filter.clone(), 100);
|
||||
|
||||
// v1 defaults
|
||||
assert_eq!(announce.size_class, 1);
|
||||
assert_eq!(announce.hash_count, 5);
|
||||
assert!(announce.is_v1_compliant());
|
||||
assert!(announce.is_valid());
|
||||
assert_eq!(announce.filter_size_bytes(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_with_size_class() {
|
||||
let filter = BloomFilter::with_params(2048 * 8, 7).unwrap();
|
||||
let announce = FilterAnnounce::with_size_class(filter, 100, 2);
|
||||
|
||||
assert_eq!(announce.size_class, 2);
|
||||
assert_eq!(announce.hash_count, 7);
|
||||
assert!(!announce.is_v1_compliant());
|
||||
assert!(announce.is_valid());
|
||||
assert_eq!(announce.filter_size_bytes(), 2048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_encode_decode_roundtrip() {
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&make_node_addr(42));
|
||||
filter.insert(&make_node_addr(99));
|
||||
let announce = FilterAnnounce::new(filter, 500);
|
||||
|
||||
let encoded = announce.encode().unwrap();
|
||||
// msg_type(1) + sequence(8) + hash_count(1) + size_class(1) + filter(1024)
|
||||
assert_eq!(encoded.len(), 1035);
|
||||
assert_eq!(encoded[0], LinkMessageType::FilterAnnounce.to_byte());
|
||||
|
||||
// Decode strips msg_type (as dispatcher does)
|
||||
let decoded = FilterAnnounce::decode(&encoded[1..]).unwrap();
|
||||
assert_eq!(decoded.sequence, 500);
|
||||
assert_eq!(decoded.hash_count, 5);
|
||||
assert_eq!(decoded.size_class, 1);
|
||||
assert!(decoded.is_valid());
|
||||
assert!(decoded.is_v1_compliant());
|
||||
|
||||
// Filter contents preserved
|
||||
assert!(decoded.filter.contains(&make_node_addr(42)));
|
||||
assert!(decoded.filter.contains(&make_node_addr(99)));
|
||||
assert!(!decoded.filter.contains(&make_node_addr(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_decode_rejects_bad_size_class() {
|
||||
let filter = BloomFilter::new();
|
||||
let announce = FilterAnnounce::new(filter, 100);
|
||||
let mut encoded = announce.encode().unwrap();
|
||||
|
||||
// Corrupt size_class byte (offset: 1 msg_type + 8 seq + 1 hash = 10)
|
||||
encoded[10] = 5; // invalid size_class > MAX_SIZE_CLASS
|
||||
|
||||
let result = FilterAnnounce::decode(&encoded[1..]);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid size_class")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_decode_rejects_non_v1_size_class() {
|
||||
// Build a size_class=0 payload manually (valid range but not v1)
|
||||
let filter = BloomFilter::with_params(512 * 8, 5).unwrap();
|
||||
let announce = FilterAnnounce::with_size_class(filter, 100, 0);
|
||||
let encoded = announce.encode().unwrap();
|
||||
|
||||
let result = FilterAnnounce::decode(&encoded[1..]);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("unsupported size_class")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_announce_decode_rejects_truncated() {
|
||||
let result = FilterAnnounce::decode(&[0u8; 5]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
//! A module here has been migrated out of the async node shell; the async
|
||||
//! I/O adapters remain in `node::handlers`.
|
||||
|
||||
pub(crate) mod bloom;
|
||||
pub(crate) mod discovery;
|
||||
pub(crate) mod fmp;
|
||||
pub(crate) mod mmp;
|
||||
|
||||
@@ -21,13 +21,11 @@
|
||||
//! layer, encrypted end-to-end independently of per-hop link encryption.
|
||||
|
||||
mod error;
|
||||
mod filter;
|
||||
mod link;
|
||||
pub(crate) mod session;
|
||||
|
||||
// Re-export all public types at protocol:: level
|
||||
pub use error::ProtocolError;
|
||||
pub use filter::FilterAnnounce;
|
||||
pub use link::{
|
||||
LinkMessageType, SESSION_DATAGRAM_HEADER_SIZE, SessionDatagram, SessionDatagramRef,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user