Restrict bloom filter propagation to tree edges, update design docs

Gate peer_inbound_filters() to only collect from tree peers (parent
and children), so outgoing filter computation merges only tree-sourced
information. All peers still receive FilterAnnounce messages and store
filters locally for routing queries — the restriction is only on what
gets merged into outgoing filters.

This prevents bloom filter saturation where mesh shortcuts cause every
node's filter to converge toward the full network. With tree-only
merge, filters contain subtree (from children) + complement (from
parent) + single-hop mesh views.

Implementation:
- Add is_tree_peer() helper to determine tree parent/child relationship
- Gate peer_inbound_filters() to tree peers only (single control point)
- Trigger bloom filter exchange on tree relationship changes
- Add est_entries, set_bits, fill ratio, and tree_peer fields to
  FilterAnnounce send/receive debug logs
- Add test_bloom_filter_split_horizon test verifying directional
  asymmetry: upward filters contain only the child's subtree, downward
  filters contain only the complement
- Add print_filter_cardinality diagnostic helper for test inspection

Design docs:
- fips-bloom-filters.md: Add directional asymmetry and mesh peer filter
  subsections, update per-peer filter model, saturation mitigation,
  implementation status table
- fips-mesh-operation.md: Update filter propagation description, add
  directional asymmetry, tree relationship change trigger
- fips-intro.md: Rewrite bloom propagation paragraph for tree-only merge
This commit is contained in:
Johnathan Corgan
2026-02-23 14:00:00 +00:00
parent dc89edf60b
commit 717be3d960
8 changed files with 396 additions and 74 deletions

View File

@@ -64,8 +64,14 @@ distance calculation makes the actual forwarding decision.
A bloom filter with 8,192 bits and k = 5 saturates (FPR approaches 100%)
around 3,0004,000 entries. Beyond this, every query returns "maybe" and the
filter provides no candidate selection value. Hub nodes approaching this
threshold effectively fall back to tree-distance-only routing.
filter provides no candidate selection value.
Tree-only merge propagation mitigates saturation by limiting each filter's
content to tree-relevant entries. A node's outgoing filter to its parent
contains only its subtree; its outgoing filter to a child contains the
complement. Neither filter contains entries from mesh shortcuts' transitive
information, keeping filter occupancy proportional to the node's position
in the tree rather than the total mesh connectivity.
## Per-Peer Filter Model
@@ -79,7 +85,14 @@ The outbound filter for peer Q is computed by merging:
1. **This node's own identity** (node_addr)
2. **Leaf-only dependents** (if any — future direction)
3. **All other peers' inbound filters except Q's** (split-horizon exclusion)
3. **Tree peers' inbound filters except Q's** (tree-only merge with
split-horizon exclusion)
Only filters from **tree peers** (parent and children in the spanning tree)
are merged into outgoing filter computation. Filters from non-tree mesh
peers are stored locally for routing queries but are not propagated
transitively. This prevents bloom filter saturation where mesh shortcuts
cause every node's filter to converge toward the full network.
### Split-Horizon Exclusion
@@ -89,13 +102,38 @@ creating a routing loop where Q thinks it can reach a destination through
this node, and this node thinks it can reach the same destination through Q.
Split-horizon is computed per-peer: the outbound filter for peer Q merges
all inbound filters except Q's. For a node with peers A, B, C:
all tree peer inbound filters except Q's.
### Directional Asymmetry
Because merge is restricted to tree peers, outgoing filters exhibit
directional asymmetry along tree edges:
- **Upward (child → parent)**: Contains the child's subtree — the child's
own identity plus all entries merged from its children's filters
- **Downward (parent → child)**: Contains the complement — the parent's
own identity plus entries from all other tree peers (siblings' subtrees
and the parent's own parent direction)
Together, the upward and downward filters for a tree edge cover the entire
network with no overlap (excluding the node itself at the split point).
### Mesh Peer Filters
All peers — including non-tree mesh shortcuts — still **receive**
FilterAnnounce messages and **store** received filters locally. These
stored filters are consulted during routing (step 3 of `find_next_hop()`)
for single-hop shortcut discovery. However, mesh peer filters contain
only the mesh peer's own tree-propagated information, not transitive
entries from the broader network.
For a node with tree peers A, B and mesh peer C:
| Outbound to | Includes entries from |
| ----------- | -------------------- |
| A | Self + B's filter + C's filter |
| B | Self + A's filter + C's filter |
| C | Self + A's filter + B's filter |
| A | Self + B's filter (tree-only merge, excluding A) |
| B | Self + A's filter (tree-only merge, excluding B) |
| C | Self + A's filter + B's filter (tree-only merge, C excluded as non-tree) |
## Filter Propagation
@@ -121,10 +159,16 @@ cooldown period are coalesced into a single announcement.
### Propagation Scope
Filters propagate unboundedly — there is no TTL on filter propagation. At
steady state, every reachable destination appears in at least one peer's
filter. New nodes added to the network propagate through the entire mesh
within O(D × 500ms) where D is the network diameter.
Filters propagate transitively through tree edges only. Since the spanning
tree is a connected subgraph covering all nodes, every reachable
destination still appears in at least one tree peer's filter at steady
state. New nodes propagate through the tree within O(depth × 500ms) where
depth is the tree depth.
Mesh shortcuts provide single-hop filter visibility (the mesh peer's own
filter) but do not contribute to transitive propagation. This bounds the
information in each filter to tree-relevant entries rather than the full
network.
## Filter Expiration
@@ -232,10 +276,13 @@ negotiation not present in v1.
| 1 KB bloom filter (size_class 1) | **Implemented** |
| 5 hash functions | **Implemented** |
| Split-horizon filter computation | **Implemented** |
| Tree-only merge propagation | **Implemented** |
| Directional asymmetry (subtree/complement) | **Implemented** |
| Per-peer filter maintenance | **Implemented** |
| Event-driven updates | **Implemented** |
| 500ms rate limiting | **Implemented** |
| FilterAnnounce gossip | **Implemented** |
| FilterAnnounce gossip (all peers) | **Implemented** |
| Filter cardinality logging | **Implemented** |
| Size class negotiation | Future direction |
| Folding support | Future direction |
| Adaptive filter sizing | Future direction |

View File

@@ -350,22 +350,27 @@ routing — bloom filters narrow the set of peers worth considering, and the
actual forwarding decision ranks those candidates by tree distance and link
quality.
Filters propagate via gossip, with each node computing outbound filters by
merging the filters received from its other peers (a
Filters propagate transitively through tree edges, with each node computing
outbound filters by merging the filters received from its tree peers (parent
and children) using a
[split-horizon](https://en.wikipedia.org/wiki/Split_horizon_route_advertisement)
technique borrowed from distance-vector routing). At steady state, filters
represent the entire reachable network.
technique borrowed from distance-vector routing. All peers — including
non-tree mesh shortcuts — receive FilterAnnounce messages, but only tree
peers' filters are merged into outgoing computation. This prevents filter
saturation where mesh shortcuts would cause every filter to converge toward
the full network.
See [fips-bloom-filters.md](fips-bloom-filters.md) for filter parameters and
mathematical properties.
![Bloom filter propagation on a spanning tree](fips-bloom-propagation.svg)
Every filter is computed identically regardless of link type: the outbound
filter for peer Q merges this node's identity with all inbound filters
except Q's (split-horizon exclusion). The protocol makes no distinction
between tree parents, tree children, or mesh peers — the asymmetry visible
in the diagram is a consequence of tree topology, not a different operation.
The outbound filter for peer Q merges this node's identity with tree peer
inbound filters except Q's (split-horizon exclusion). This creates
directional asymmetry: upward filters (child → parent) contain the child's
subtree, while downward filters (parent → child) contain the complement.
Mesh peers receive filters but their inbound filters are not merged
transitively — they provide single-hop shortcut visibility only.
A node with multiple peers receives genuinely different filters from each.
In the diagram, R receives {B, D, E} from B and {C, F} from C — two disjoint

View File

@@ -100,25 +100,33 @@ decision uses tree coordinate distance to rank those candidates.
### How Filters Propagate
Nodes exchange **FilterAnnounce** messages with direct peers. Each
Nodes exchange **FilterAnnounce** messages with all direct peers. Each
FilterAnnounce replaces the previous filter for that peer — there is no
incremental update.
Filter computation uses **split-horizon exclusion**: the outbound filter
for peer Q is computed by merging the local node's own identity, its
leaf-only dependents (if any), and the filters received from all other
peers *except* Q. This prevents echo loops where a node advertises back
to Q the destinations it learned from Q.
Filter computation uses **tree-only merge with split-horizon exclusion**:
the outbound filter for peer Q is computed by merging the local node's own
identity, its leaf-only dependents (if any), and the inbound filters from
tree peers (parent and children) *except* Q. Filters from non-tree mesh
peers are stored locally for routing queries but are not merged into
outgoing filters. This prevents saturation where mesh shortcuts cause
filters to converge toward the full network.
Filters propagate unboundedly (no TTL). At steady state, every reachable
destination appears in at least one peer's filter.
The restriction creates **directional asymmetry**: upward filters
(child → parent) contain the child's subtree, while downward filters
(parent → child) contain the complement. Together they cover the entire
network.
Filters propagate transitively through tree edges. At steady state, every
reachable destination appears in at least one tree peer's filter.
### Update Triggers
Filter updates are event-driven, not periodic:
- Peer connects or disconnects
- A peer's incoming filter changes
- A peer's incoming filter changes (triggers recomputation for other peers)
- Tree relationship changes (new parent, new child, parent switch)
- Local state changes (new identity, leaf-only dependent changes)
Updates are rate-limited at 500ms to prevent storms during topology changes.
@@ -509,8 +517,8 @@ When traffic to a destination stops:
1. **Session idles out** (90s) — session torn down
2. **Coordinate caches expire** (300s) — transit nodes forget coordinates
3. **Bloom filters remain** — they have no TTL, so reachability information
persists
3. **Bloom filters remain** — they have no TTL, so tree-propagated
reachability information persists
When traffic resumes: