mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Redesign bloom filter parameters with mathematical justification
Analysis revealed original parameters (4KB, k=7) were oversized: - Expected filter occupancy ~250-800 entries, not ~4,096 - Original FPR estimates in docs were incorrect (5-7x optimistic) - d^(2K) formula overcounted by assuming mesh vs tree structure New v1 parameters: - Filter size: 1 KB (was 4 KB) - 75% bandwidth reduction - Hash functions: k=5 (was 7) - optimal for 800-1600 entries - K-hop scope: 2 (unchanged) Added forward compatibility via size_class field: - Power-of-2 sizes (512B, 1KB, 2KB, 4KB) enable folding - v1 requires size_class=1; future versions can negotiate larger - Receivers can fold larger filters down to preferred size Updated docs: - fips-routing.md: Part 1 rewritten with math foundation - fips-gossip-protocol.md: §3 and Appendix A.2 wire format - fips-architecture.md: Configuration parameters
This commit is contained in:
@@ -906,13 +906,16 @@ the limit but need not enforce it strictly.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `filter.size` | u32 | 32768 | Filter size in bits (4KB) |
|
||||
| `filter.hash_count` | u8 | 7 | Number of hash functions |
|
||||
| `filter.size_class` | u8 | 1 | Size class: 0=512B, 1=1KB, 2=2KB, 3=4KB |
|
||||
| `filter.hash_count` | u8 | 5 | Number of hash functions |
|
||||
| `filter.scope` | u8 | 2 | TTL for filter propagation (K) |
|
||||
| `filter.refresh.interval` | duration | 60s | Periodic FilterAnnounce refresh |
|
||||
| `filter.update.debounce` | duration | 500ms | Min interval between updates |
|
||||
| `filter.stale.threshold` | duration | 300s | Consider peer's filter stale |
|
||||
|
||||
v1 protocol requires `size_class=1` (1 KB filters). The size_class field is
|
||||
present in the wire format for forward compatibility with larger filters.
|
||||
|
||||
### Discovery Protocol
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|
||||
@@ -135,27 +135,57 @@ node's filter indicates which destinations are reachable through it.
|
||||
|
||||
```text
|
||||
FilterAnnounce {
|
||||
filter: BloomFilter, // 4096 bytes (32,768 bits)
|
||||
ttl: u8, // Remaining propagation hops
|
||||
sequence: u64, // For freshness/deduplication
|
||||
ttl: u8, // Remaining propagation hops
|
||||
filter: BloomFilter, // Variable size based on size_class
|
||||
}
|
||||
|
||||
BloomFilter {
|
||||
bits: [u8; 4096], // Bit array
|
||||
hash_count: u8, // Number of hash functions (typically 7)
|
||||
hash_count: u8, // Number of hash functions (5 for v1)
|
||||
size_class: u8, // Filter size: bytes = 512 << size_class
|
||||
bits: [u8; 512 << size_class], // Bit array (1024 bytes for v1)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Field Semantics
|
||||
### 3.2 Size Classes
|
||||
|
||||
**filter**: Contains node_ids reachable through this peer. Uses k=7 hash
|
||||
functions for near-optimal false positive rate at expected fill levels.
|
||||
Filter sizes are powers of 2 to enable folding (shrinking by ORing halves):
|
||||
|
||||
| size_class | Bits | Bytes | Status |
|
||||
|------------|--------|-------|---------------------|
|
||||
| 0 | 4,096 | 512 | Reserved (future) |
|
||||
| 1 | 8,192 | 1,024 | **v1 default** |
|
||||
| 2 | 16,384 | 2,048 | Reserved (future) |
|
||||
| 3 | 32,768 | 4,096 | Reserved (future) |
|
||||
|
||||
**v1 protocol**: All nodes MUST use size_class=1 (1 KB filters). Nodes MUST
|
||||
reject FilterAnnounce with size_class ≠ 1.
|
||||
|
||||
**Future versions**: Nodes may negotiate larger filters via capability exchange.
|
||||
Receivers can fold larger filters down to their preferred size.
|
||||
|
||||
### 3.3 Field Semantics
|
||||
|
||||
**sequence**: Monotonic counter for this node's filter. Allows receivers to
|
||||
detect stale or duplicate announcements.
|
||||
|
||||
**ttl**: Remaining propagation depth. Starts at K (typically 2), decremented
|
||||
each hop. At TTL=0, entries are not propagated further.
|
||||
|
||||
**sequence**: Monotonic counter for this node's filter. Allows receivers to
|
||||
detect stale or duplicate announcements.
|
||||
**hash_count**: Number of hash functions used. v1 uses k=5, which is optimal
|
||||
for 800-1,600 entries in a 1 KB filter.
|
||||
|
||||
**size_class**: Indicates filter size as `512 << size_class` bytes. Allows
|
||||
forward-compatible extension to larger filters.
|
||||
|
||||
**bits**: The Bloom filter bit array. To test membership:
|
||||
|
||||
```text
|
||||
for i in 0..hash_count:
|
||||
bit_index = hash(node_id, i) % (8 * bits.len())
|
||||
if !bits[bit_index]: return false
|
||||
return true // "maybe present"
|
||||
```
|
||||
|
||||
### 3.3 Filter Contents
|
||||
|
||||
@@ -486,43 +516,57 @@ Propagates Bloom filter reachability information.
|
||||
│ │ 0 │ msg_type │ 1 byte │ 0x11 │ │
|
||||
│ │ 1 │ sequence │ 8 bytes │ u64 LE, monotonic counter │ │
|
||||
│ │ 9 │ ttl │ 1 byte │ Remaining propagation hops │ │
|
||||
│ │ 10 │ hash_count │ 1 byte │ Number of hash functions (7) │ │
|
||||
│ │ 11 │ filter_bits │ 4096 bytes│ Bloom filter bit array │ │
|
||||
│ │ 10 │ hash_count │ 1 byte │ Number of hash functions (5) │ │
|
||||
│ │ 11 │ size_class │ 1 byte │ Filter size: 512 << class │ │
|
||||
│ │ 12 │ filter_bits │ variable │ 512 << size_class bytes │ │
|
||||
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
|
||||
│ │
|
||||
│ Total payload: 4107 bytes (fixed size) │
|
||||
│ With link overhead: 4136 bytes │
|
||||
│ Size classes (powers of 2 for foldability): │
|
||||
│ 0 = 512 bytes (4,096 bits) - Reserved for future │
|
||||
│ 1 = 1,024 bytes (8,192 bits) - v1 default │
|
||||
│ 2 = 2,048 bytes (16,384 bits) - Reserved for future │
|
||||
│ 3 = 4,096 bytes (32,768 bits) - Reserved for future │
|
||||
│ │
|
||||
│ v1 total payload: 1 + 8 + 1 + 1 + 1 + 1024 = 1036 bytes │
|
||||
│ With link overhead: 1065 bytes │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ BLOOM FILTER STRUCTURE │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ filter_bits[4096]: │
|
||||
│ filter_bits[1024] (v1, size_class=1): │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Byte 0 │ Byte 1 │ ... │ Byte 4095 │ │
|
||||
│ │ bits 0-7 │ bits 8-15 │ │ bits 32760-32767 │ │
|
||||
│ │ Byte 0 │ Byte 1 │ ... │ Byte 1023 │ │
|
||||
│ │ bits 0-7 │ bits 8-15 │ │ bits 8184-8191 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ To test membership of node_id: │
|
||||
│ filter_bits = 8 * (512 << size_class) // 8192 for v1 │
|
||||
│ for i in 0..hash_count: │
|
||||
│ bit_index = hash(node_id, i) % 32768 │
|
||||
│ if !filter_bits[bit_index]: return false │
|
||||
│ bit_index = hash(node_id, i) % filter_bits │
|
||||
│ if !bits[bit_index]: return false │
|
||||
│ return true // "maybe present" │
|
||||
│ │
|
||||
│ Folding (for future heterogeneous sizes): │
|
||||
│ To shrink a filter by half, OR its two halves: │
|
||||
│ small[i] = large[i] | large[i + small.len()] │
|
||||
│ This increases FPR but preserves correctness (no false negatives). │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Concrete example**:
|
||||
**Concrete example** (v1 with size_class=1):
|
||||
|
||||
```text
|
||||
PLAINTEXT BYTES:
|
||||
11 ← msg_type = FilterAnnounce
|
||||
2A 00 00 00 00 00 00 00 ← sequence = 42
|
||||
02 ← ttl = 2 (will propagate 2 more hops)
|
||||
07 ← hash_count = 7
|
||||
[4096 bytes of filter bits] ← Bloom filter
|
||||
05 ← hash_count = 5
|
||||
01 ← size_class = 1 (1 KB filter)
|
||||
[1024 bytes of filter bits] ← Bloom filter
|
||||
|
||||
Total: 4107 bytes
|
||||
Total: 1036 bytes
|
||||
```
|
||||
|
||||
### A.3 LookupRequest (0x12)
|
||||
|
||||
+97
-12
@@ -66,20 +66,105 @@ traffic tunnels through that peer.
|
||||
|
||||
| Parameter | Value | Rationale |
|
||||
|-----------|-------|-----------|
|
||||
| Filter size | 4 KB (32,768 bits) | Balances accuracy vs. memory |
|
||||
| Hash functions | 7 | Near-optimal for expected fill ratio |
|
||||
| Filter size | 1 KB (8,192 bits) | Sized for expected occupancy with margin |
|
||||
| Hash functions | 5 | Optimal for 800-1,600 entries at this size |
|
||||
| Scope (K) | 2 | Effective ~4-hop reach with TTL propagation |
|
||||
|
||||
### False Positive Rates
|
||||
### Mathematical Foundation
|
||||
|
||||
| Nodes in Filter | FPR |
|
||||
|-----------------|-----|
|
||||
| 1,000 | ~0.05% |
|
||||
| 2,000 | ~0.5% |
|
||||
| 5,000 | ~1.3% |
|
||||
| 10,000 | ~8% |
|
||||
**False Positive Rate (FPR):**
|
||||
|
||||
With K=2 and average degree d=8, expected nodes in scope ≈ d^(2K) ≈ 4,096.
|
||||
```text
|
||||
FPR = (1 - e^(-kn/m))^k
|
||||
```
|
||||
|
||||
Where m = bits, n = entries, k = hash functions.
|
||||
|
||||
**Optimal hash count:**
|
||||
|
||||
```text
|
||||
k_opt = (m/n) × ln(2) ≈ 0.693 × (m/n)
|
||||
```
|
||||
|
||||
For m=8,192 and expected n=800: k_opt ≈ 7. We use k=5 to accommodate higher
|
||||
occupancy scenarios (up to ~1,600 entries) while maintaining acceptable FPR.
|
||||
|
||||
**Required bits for target FPR:**
|
||||
|
||||
```text
|
||||
m = -1.44 × n × ln(p)
|
||||
```
|
||||
|
||||
For 1% FPR: m ≈ 9.6n bits. For 5% FPR: m ≈ 6.2n bits.
|
||||
|
||||
### Expected Filter Occupancy
|
||||
|
||||
Filter occupancy depends on K-hop scope and node degree, **not** total network
|
||||
size. The TTL mechanism bounds entries regardless of network scale.
|
||||
|
||||
**Nodes within h hops in a tree (branching factor b = d-1):**
|
||||
|
||||
```text
|
||||
nodes_within_h_hops = (b^(h+1) - 1) / (b - 1)
|
||||
```
|
||||
|
||||
For d=8 (b=7), K=2: each peer's 2-hop neighborhood ≈ 57 nodes.
|
||||
|
||||
**Outgoing filter to peer Q contains:**
|
||||
|
||||
- Self (1 entry)
|
||||
- Entries from (d-1) other peers' filters, with overlap
|
||||
|
||||
**Expected occupancy by node degree:**
|
||||
|
||||
| Degree (d) | Expected Entries | Notes |
|
||||
|------------|------------------|-------|
|
||||
| 5 | 100-200 | Constrained/IoT |
|
||||
| 8 | 250-400 | Typical node |
|
||||
| 12 | 500-800 | Well-connected |
|
||||
| 20+ | 1,200-1,800 | Hub node |
|
||||
|
||||
### False Positive Rates (1 KB filter, k=5)
|
||||
|
||||
| Entries | FPR | Scenario |
|
||||
|---------|-----|----------|
|
||||
| 200 | 0.02% | Low-degree node |
|
||||
| 400 | 0.3% | Typical node |
|
||||
| 800 | 2.4% | Well-connected |
|
||||
| 1,200 | 7.5% | Hub node |
|
||||
| 1,600 | 15% | Heavily loaded hub |
|
||||
|
||||
FPR above 5% triggers more LookupRequests but the discovery protocol handles
|
||||
this gracefully. Hub nodes may benefit from larger filters in future protocol
|
||||
versions (see §1.6).
|
||||
|
||||
### Size Classes (Forward Compatibility)
|
||||
|
||||
Filter sizes are powers of 2 to enable **folding** — a technique for shrinking
|
||||
filters by ORing halves:
|
||||
|
||||
```rust
|
||||
fn fold(filter: &[u8]) -> Vec<u8> {
|
||||
let half = filter.len() / 2;
|
||||
(0..half).map(|i| filter[i] | filter[i + half]).collect()
|
||||
}
|
||||
```
|
||||
|
||||
Folding preserves correctness (no false negatives) but increases FPR.
|
||||
|
||||
| size_class | Bits | Bytes | Status |
|
||||
|------------|------|-------|--------|
|
||||
| 0 | 4,096 | 512 | Reserved (future) |
|
||||
| 1 | 8,192 | 1,024 | **Current default** |
|
||||
| 2 | 16,384 | 2,048 | Reserved (future) |
|
||||
| 3 | 32,768 | 4,096 | Reserved (future) |
|
||||
|
||||
**v1 protocol**: All nodes MUST use size_class=1. The field is present in the
|
||||
wire format for forward compatibility.
|
||||
|
||||
**Future versions**: Nodes may negotiate larger filters. Receivers fold down
|
||||
to their preferred size if sender's filter is larger. This allows hub nodes
|
||||
to maintain higher precision while constrained nodes use smaller filters.
|
||||
|
||||
### Filter Contents
|
||||
|
||||
@@ -470,7 +555,7 @@ enum RouteState {
|
||||
|
||||
| Type | Purpose | Size | When Used |
|
||||
|------|---------|------|-----------|
|
||||
| FilterAnnounce | Bloom filter propagation | ~4.1 KB | Topology changes |
|
||||
| FilterAnnounce | Bloom filter propagation | ~1 KB | Topology changes |
|
||||
| LookupRequest | Discover coordinates | ~300 bytes | First contact with distant node |
|
||||
| LookupResponse | Return coordinates | ~400 bytes | Reply to discovery |
|
||||
| SessionSetup | Warm router caches + crypto init | ~400-700 bytes | Before data transfer |
|
||||
@@ -506,7 +591,7 @@ When nodes join/leave:
|
||||
|
||||
| Resource | Full Participant | Leaf-Only |
|
||||
|----------|------------------|-----------|
|
||||
| Bloom filter storage | d × 4 KB (d = peer count) | None |
|
||||
| Bloom filter storage | d × 1 KB (d = peer count) | None |
|
||||
| Coordinate cache | 10K-100K entries | None |
|
||||
| Route cache | 1K-10K entries | Minimal |
|
||||
| Bandwidth (idle) | < 1 KB/sec | Near zero |
|
||||
|
||||
Reference in New Issue
Block a user