diff --git a/Cargo.lock b/Cargo.lock index 421c8c2..5c63565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index a16a7fd..b136847 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/benches/bloom.rs b/benches/bloom.rs new file mode 100644 index 0000000..b63d4a2 --- /dev/null +++ b/benches/bloom.rs @@ -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); diff --git a/docs/design/fips-gossip-protocol.md b/docs/design/fips-gossip-protocol.md index ed07f17..9909a5e 100644 --- a/docs/design/fips-gossip-protocol.md +++ b/docs/design/fips-gossip-protocol.md @@ -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. │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` diff --git a/docs/design/fips-routing.md b/docs/design/fips-routing.md index 18271e8..ee6de29 100644 --- a/docs/design/fips-routing.md +++ b/docs/design/fips-routing.md @@ -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 diff --git a/src/bloom/tests.rs b/src/bloom/tests.rs index 6bb5a45..ace4db6 100644 --- a/src/bloom/tests.rs +++ b/src/bloom/tests.rs @@ -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)); +}