Bloom module test coverage, benchmarks, and design doc corrections

Testing:
- Add 14 bloom module tests (39 total): from_bytes error paths,
  from_slice round-trip, insert_bytes/contains_bytes, estimated_count
  saturation, Default/Debug traits, mark_changed_peers cascade
  prevention (4 scenarios), remove_peer_state, record_sent_filter,
  leaf_dependents accessor.

Benchmarks:
- Add criterion benchmark suite for bloom filter hot-path operations:
  insert, contains, merge, from_bytes, fill_ratio, estimated_count,
  equality, compute_outgoing_filter, mark_changed_peers, base_filter.
  Parameterized over realistic occupancy levels and peer counts.

Design doc corrections:
- Fix visited bloom filter hash_count in gossip protocol doc (7→5,
  matching code for 256-byte filter occupancy).
- Correct LookupResponse proof signature scope in fips-routing.md
  and fips-gossip-protocol.md: proof covers (request_id || target)
  only — coords excluded to survive tree reconvergence during lookup
  RTT.
This commit is contained in:
Johnathan Corgan
2026-02-15 16:05:59 +00:00
parent b8a1f322c2
commit af4583d989
6 changed files with 882 additions and 8 deletions

359
Cargo.lock generated
View File

@@ -21,6 +21,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.6.21"
@@ -107,6 +113,12 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bech32"
version = "0.11.1"
@@ -157,6 +169,12 @@ dependencies = [
"piper",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -189,6 +207,12 @@ dependencies = [
"syn",
]
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.54"
@@ -235,6 +259,33 @@ dependencies = [
"zeroize",
]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -310,12 +361,73 @@ dependencies = [
"libc",
]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -359,6 +471,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -415,6 +533,7 @@ dependencies = [
"bech32",
"chacha20poly1305",
"clap",
"criterion",
"dirs",
"futures",
"hex",
@@ -567,6 +686,17 @@ dependencies = [
"wasip2",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -579,6 +709,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@@ -637,18 +773,48 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -815,6 +981,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -827,6 +1002,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "opaque-debug"
version = "0.3.1"
@@ -874,6 +1055,34 @@ dependencies = [
"futures-io",
]
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "poly1305"
version = "0.8.0"
@@ -948,6 +1157,26 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_users"
version = "0.5.2"
@@ -959,6 +1188,18 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
@@ -1007,12 +1248,27 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "secp256k1"
version = "0.30.0"
@@ -1063,6 +1319,19 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
@@ -1228,6 +1497,16 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tokio"
version = "1.49.0"
@@ -1396,6 +1675,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -1411,6 +1700,70 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows-link"
version = "0.2.1"
@@ -1630,3 +1983,9 @@ name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -27,3 +27,8 @@ simple-dns = "0.9"
[dev-dependencies]
tempfile = "3.15"
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "bloom"
harness = false

266
benches/bloom.rs Normal file
View File

@@ -0,0 +1,266 @@
//! Benchmarks for the bloom filter module.
//!
//! Run with: cargo bench --bench bloom
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use fips::bloom::{BloomFilter, BloomState};
use fips::NodeAddr;
use std::collections::HashMap;
fn make_node_addr(val: u16) -> NodeAddr {
let mut bytes = [0u8; 16];
bytes[0..2].copy_from_slice(&val.to_le_bytes());
NodeAddr::from_bytes(bytes)
}
/// Pre-populate a filter with `n` entries for realistic benchmarks.
fn populated_filter(n: u16) -> BloomFilter {
let mut filter = BloomFilter::new();
for i in 0..n {
filter.insert(&make_node_addr(i));
}
filter
}
// ===== BloomFilter Benchmarks =====
fn bench_insert(c: &mut Criterion) {
let mut group = c.benchmark_group("bloom_insert");
let node = make_node_addr(9999);
// Insert into empty filter
group.bench_function("empty", |b| {
b.iter(|| {
let mut filter = BloomFilter::new();
filter.insert(black_box(&node));
})
});
// Insert into a filter with 400 entries (typical occupancy)
let base = populated_filter(400);
group.bench_function("400_entries", |b| {
b.iter(|| {
let mut filter = base.clone();
filter.insert(black_box(&node));
})
});
group.finish();
}
fn bench_contains(c: &mut Criterion) {
let mut group = c.benchmark_group("bloom_contains");
let present = make_node_addr(200);
let absent = make_node_addr(9999);
for &count in &[100, 400, 800] {
let filter = populated_filter(count);
group.bench_with_input(
BenchmarkId::new("hit", count),
&count,
|b, _| {
b.iter(|| filter.contains(black_box(&present)))
},
);
group.bench_with_input(
BenchmarkId::new("miss", count),
&count,
|b, _| {
b.iter(|| filter.contains(black_box(&absent)))
},
);
}
group.finish();
}
fn bench_merge(c: &mut Criterion) {
let mut group = c.benchmark_group("bloom_merge");
for &count in &[100, 400, 800] {
let filter_a = populated_filter(count);
let filter_b = populated_filter(count + 500); // different entries
group.bench_with_input(
BenchmarkId::from_parameter(count),
&count,
|b, _| {
b.iter(|| {
let mut target = filter_a.clone();
target.merge(black_box(&filter_b)).unwrap();
})
},
);
}
group.finish();
}
fn bench_from_bytes(c: &mut Criterion) {
let filter = populated_filter(400);
let bytes = filter.as_bytes().to_vec();
let hash_count = filter.hash_count();
c.bench_function("bloom_from_bytes", |b| {
b.iter(|| {
BloomFilter::from_bytes(black_box(bytes.clone()), black_box(hash_count)).unwrap()
})
});
}
fn bench_fill_ratio(c: &mut Criterion) {
let filter = populated_filter(400);
c.bench_function("bloom_fill_ratio", |b| {
b.iter(|| filter.fill_ratio())
});
}
fn bench_estimated_count(c: &mut Criterion) {
let filter = populated_filter(400);
c.bench_function("bloom_estimated_count", |b| {
b.iter(|| filter.estimated_count())
});
}
fn bench_equality(c: &mut Criterion) {
let mut group = c.benchmark_group("bloom_equality");
let filter_a = populated_filter(400);
let filter_b = filter_a.clone();
let mut filter_c = filter_a.clone();
filter_c.insert(&make_node_addr(9999));
group.bench_function("equal", |b| {
b.iter(|| black_box(&filter_a) == black_box(&filter_b))
});
group.bench_function("not_equal", |b| {
b.iter(|| black_box(&filter_a) == black_box(&filter_c))
});
group.finish();
}
// ===== BloomState Benchmarks =====
fn bench_compute_outgoing_filter(c: &mut Criterion) {
let mut group = c.benchmark_group("bloom_compute_outgoing");
for &peer_count in &[2, 5, 10, 20] {
let my_node = make_node_addr(0);
let mut state = BloomState::new(my_node);
state.add_leaf_dependent(make_node_addr(1));
// Create peer filters with realistic content
let mut peer_filters = HashMap::new();
let mut peer_addrs = Vec::new();
for i in 0..peer_count {
let peer = make_node_addr(100 + i);
peer_addrs.push(peer);
let mut pf = BloomFilter::new();
// Each peer knows about ~50 nodes
for j in 0..50 {
pf.insert(&make_node_addr(1000 + i * 50 + j));
}
peer_filters.insert(peer, pf);
}
let exclude = peer_addrs[0];
group.bench_with_input(
BenchmarkId::from_parameter(peer_count),
&peer_count,
|b, _| {
b.iter(|| {
state.compute_outgoing_filter(
black_box(&exclude),
black_box(&peer_filters),
)
})
},
);
}
group.finish();
}
fn bench_mark_changed_peers(c: &mut Criterion) {
let mut group = c.benchmark_group("bloom_mark_changed_peers");
for &peer_count in &[2, 5, 10, 20] {
let my_node = make_node_addr(0);
let mut state = BloomState::new(my_node);
let mut peer_filters = HashMap::new();
let mut peer_addrs = Vec::new();
for i in 0..peer_count {
let peer = make_node_addr(100 + i);
peer_addrs.push(peer);
let mut pf = BloomFilter::new();
for j in 0..50 {
pf.insert(&make_node_addr(1000 + i * 50 + j));
}
peer_filters.insert(peer, pf);
}
// Record initial outgoing filters so the benchmark measures
// the change-detection comparison, not just "never sent"
for &peer in &peer_addrs {
let outgoing = state.compute_outgoing_filter(&peer, &peer_filters);
state.record_sent_filter(peer, outgoing);
}
let source = peer_addrs[0];
group.bench_with_input(
BenchmarkId::new("steady_state", peer_count),
&peer_count,
|b, _| {
b.iter(|| {
// Clone state so mark_changed_peers doesn't accumulate
let mut s = state.clone();
s.mark_changed_peers(
black_box(&source),
black_box(&peer_addrs),
black_box(&peer_filters),
);
})
},
);
}
group.finish();
}
fn bench_base_filter(c: &mut Criterion) {
let my_node = make_node_addr(0);
let mut state = BloomState::new(my_node);
for i in 1..=10 {
state.add_leaf_dependent(make_node_addr(i));
}
c.bench_function("bloom_base_filter_10_deps", |b| {
b.iter(|| state.base_filter())
});
}
criterion_group!(
benches,
bench_insert,
bench_contains,
bench_merge,
bench_from_bytes,
bench_fill_ratio,
bench_estimated_count,
bench_equality,
bench_compute_outgoing_filter,
bench_mark_changed_peers,
bench_base_filter,
);
criterion_main!(benches);

View File

@@ -386,8 +386,10 @@ LookupResponse {
**target_coords**: The target's current tree coordinates. This is the primary
payload — cached by the originator to enable routing to the target.
**proof**: Target's signature over `(request_id || target || target_coords)`.
Prevents malicious nodes from claiming reachability and blackholing traffic.
**proof**: Target's signature over `(request_id || target)`. Prevents
malicious nodes from claiming reachability and blackholing traffic.
Coordinates are excluded from the proof to avoid invalidation during
tree reconvergence (see [fips-routing.md](fips-routing.md) §2.4).
### 5.3 Routing
@@ -680,7 +682,7 @@ PLAINTEXT BYTES:
40 ← ttl = 64
04 00 ← origin_coords_count = 4
[16 bytes] × 4 ← origin's ancestry (64 bytes)
07 ← visited hash_count = 7
05 ← visited hash_count = 5
[256 bytes visited bloom] ← nodes that have seen this request
Total: 1 + 8 + 16 + 16 + 1 + 2 + 64 + 1 + 256 = 365 bytes
@@ -708,8 +710,8 @@ Returns target's coordinates to the requester.
│ │ ... │ proof │ 64 bytes │ Target's signature │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Proof signature covers: (request_id || target || target_coords)
Prevents malicious nodes from claiming reachability for any target.
│ Proof signature covers: (request_id || target)
Coords excluded to survive tree reconvergence during lookup RTT.
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```

View File

@@ -301,9 +301,17 @@ Each router forwards toward the origin using tree distance.
### Security
The target signs the LookupResponse with a proof covering
`(request_id || target || target_coords)`. Without this signature, a malicious
node could claim reachability for any target and blackhole traffic. The
signature proves the target authorized the route.
`(request_id || target)`. Without this signature, a malicious node could
claim reachability for any target and blackhole traffic. The signature
proves the target authorized the route.
Note: `target_coords` are intentionally excluded from the proof. Binding
coordinates would invalidate the signature whenever the spanning tree
reconverges (parent switch, root change), causing valid responses to be
rejected if tree topology shifts during the lookup RTT. Since coordinates
are ephemeral routing hints and data integrity is protected by the
session layer, coordinate tampering by a transit node only causes routing
inefficiency, not a security breach.
### Caching

View File

@@ -190,6 +190,78 @@ fn test_bloom_filter_equality() {
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();
assert!(filter.estimated_count().is_infinite());
}
#[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 =====
#[test]
@@ -338,3 +410,165 @@ fn test_bloom_state_compute_outgoing_filter() {
assert!(outgoing2.contains(&make_node_addr(100))); // from peer1
assert!(outgoing2.contains(&make_node_addr(101))); // from peer1
}
#[test]
fn test_bloom_state_leaf_dependents_accessor() {
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let leaf1 = make_node_addr(1);
let leaf2 = make_node_addr(2);
state.add_leaf_dependent(leaf1);
state.add_leaf_dependent(leaf2);
let deps = state.leaf_dependents();
assert!(deps.contains(&leaf1));
assert!(deps.contains(&leaf2));
assert!(!deps.contains(&make_node_addr(99)));
assert_eq!(deps.len(), 2);
}
#[test]
fn test_bloom_state_record_sent_filter() {
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let peer = make_node_addr(1);
let mut filter = BloomFilter::new();
filter.insert(&make_node_addr(42));
// Record a sent filter, then mark_changed_peers should detect no change
// when the outgoing filter matches what was recorded
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_addrs = vec![peer];
state.mark_changed_peers(&make_node_addr(99), &peer_addrs, &peer_filters);
// Outgoing filter (just self) differs from recorded (self + node 42),
// so peer should be marked for update
assert!(state.needs_update(&peer));
}
#[test]
fn test_bloom_state_remove_peer_state() {
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let peer = make_node_addr(1);
// Populate all three internal maps for this peer
state.mark_update_needed(peer);
state.record_update_sent(peer, 1000);
state.mark_update_needed(peer); // re-mark after send
let filter = BloomFilter::new();
state.record_sent_filter(peer, filter);
assert!(state.needs_update(&peer));
// Remove all peer state
state.remove_peer_state(&peer);
// Pending updates cleared
assert!(!state.needs_update(&peer));
// Debounce state cleared — should be able to send immediately
state.mark_update_needed(peer);
assert!(state.should_send_update(&peer, 0));
// Sent filter cleared — mark_changed_peers should treat as "never sent"
state.clear_pending_updates();
let peer_filters = HashMap::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
}
#[test]
fn test_bloom_state_mark_changed_peers_never_sent() {
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let peer1 = make_node_addr(1);
let peer2 = make_node_addr(2);
let peer_filters = HashMap::new();
let peer_addrs = vec![peer1, peer2];
// No filters ever sent — all peers should be marked
state.mark_changed_peers(&make_node_addr(99), &peer_addrs, &peer_filters);
assert!(state.needs_update(&peer1));
assert!(state.needs_update(&peer2));
}
#[test]
fn test_bloom_state_mark_changed_peers_unchanged() {
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let peer1 = make_node_addr(1);
let peer2 = make_node_addr(2);
let peer_filters = HashMap::new();
let peer_addrs = vec![peer1, peer2];
// Compute and record what would be sent to each peer
let outgoing1 = state.compute_outgoing_filter(&peer1, &peer_filters);
let outgoing2 = state.compute_outgoing_filter(&peer2, &peer_filters);
state.record_sent_filter(peer1, outgoing1);
state.record_sent_filter(peer2, outgoing2);
// Nothing changed — no peers should be marked
state.mark_changed_peers(&make_node_addr(99), &peer_addrs, &peer_filters);
assert!(!state.needs_update(&peer1));
assert!(!state.needs_update(&peer2));
}
#[test]
fn test_bloom_state_mark_changed_peers_one_changed() {
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let peer1 = make_node_addr(1);
let peer2 = make_node_addr(2);
let peer_filters = HashMap::new();
let peer_addrs = vec![peer1, peer2];
// Record current outgoing filters for both peers
let outgoing1 = state.compute_outgoing_filter(&peer1, &peer_filters);
let outgoing2 = state.compute_outgoing_filter(&peer2, &peer_filters);
state.record_sent_filter(peer1, outgoing1);
state.record_sent_filter(peer2, outgoing2);
// 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();
updated_peer_filters.insert(peer1, inbound_from_peer1);
// mark_changed_peers triggered by receiving from peer1
state.mark_changed_peers(&peer1, &peer_addrs, &updated_peer_filters);
// peer1 is excluded (it's the source), peer2's outgoing changed
// (now includes peer1's entries via split-horizon)
assert!(!state.needs_update(&peer1));
assert!(state.needs_update(&peer2));
}
#[test]
fn test_bloom_state_mark_changed_peers_excludes_source() {
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let peer1 = make_node_addr(1);
let peer_filters = HashMap::new();
let peer_addrs = vec![peer1];
// peer1 is both the source and the only peer — should be skipped
state.mark_changed_peers(&peer1, &peer_addrs, &peer_filters);
assert!(!state.needs_update(&peer1));
}