From e4d3d3a0f7c7fba8a77793af0dcf4c76b666263f Mon Sep 17 00:00:00 2001 From: Marcin Pawlowski Date: Thu, 6 Aug 2026 13:00:44 +0200 Subject: [PATCH] blend: close the attribution bracket with a neighbourhood-observability model The a/d estimator credits the adversary only with the sender own links, which understates it: to rule out that X was forwarding it need not hold X incoming link, only have seen the message anywhere upstream. That gives confidence = 1 / (1 + (1-f_adv)^L) for L upstream hops, with the local model as the L=1 case. L is not free -- the graph fixes it. A forwarder sits about halfway along a route and the peer graph is low-diameter: mean hop distance 5.15 at degree 8, so L ~ 2.6. Confidence rises from 0.56 to 0.64 at f_adv = 0.2, but 0.9 would need ~10 upstream hops. The low diameter that makes propagation fast is exactly what starves the adversary of observation points. So the bracket closes near the local model rather than near certainty, and the binary full_deanon treatment is NOT rescued by neighbourhood effects. Both ends are reported rather than one being chosen, since confident attribution is a threshold question: an adversary content with 0.64 attributes most senders, one demanding 0.9 attributes almost none. What remains unmodelled is an adversary combining this structural evidence with the timing evidence of 3.11. Co-Authored-By: Claude Opus 5 (1M context) --- reports/blend/README.md | 18 ++++++++- tools/simulators/blend/src/blend/adversary.py | 37 +++++++++++++++++++ tools/simulators/blend/tests/test_deanon.py | 26 +++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/reports/blend/README.md b/reports/blend/README.md index d74bca4..cf7ea7c 100644 --- a/reports/blend/README.md +++ b/reports/blend/README.md @@ -161,7 +161,21 @@ At degree 8, reaching 90 % confidence requires `a ≥ 8` — *every* peer advers So **the `full_deanon` figures above overstate confident origination by up to five orders of magnitude**: they count an adversary that has glimpsed one of the sender's eight links as having identified the sender. Mean confidence over honest nodes is 0.47 at `f_adv = 0.2` — nearer a coin flip than an identification. The honest statement is that whole-path capture is common and *confident attribution of the originator is rare*, and that the second requires eclipsing the sender, which §3.3 shows a degree of 6–8 already makes negligible. -Two caveats keep this from swinging too far the other way. This estimator uses **only the sender's own links**, so it is a *lower bound* on the adversary's capability: an honest peer that itself has adversarial peers leaks the message upstream too, and `observed_frac` is already 0.83 at `f_adv = 0.2` — most honest relays are themselves watched. The true value is bracketed by these two readings, and closing that gap is the open question flagged in §5. And confidence is a *threshold* choice: an adversary content with 0.53 attributes far more nodes than one demanding 0.9. +**That estimator credits the adversary only with the sender's own links, so it is a lower bound — and the upper end is worth pinning down.** To rule out that `X` was merely forwarding, the adversary does not need to hold `X`'s incoming link: it only needs to have seen the message *anywhere upstream*. A message that reached `X` travelled a route, and the adversary misses it only if no upstream node is adversarial, so + +**`confidence = 1 / (1 + (1 − f_adv)^L)`** for `L` upstream hops — the local model being exactly `L = 1`. + +`L` is not a free parameter; the graph fixes it. A forwarder sits about halfway along a route, and the peer graph is low-diameter: mean hop distance is **5.15 at degree 8** (3.87 at degree 16), so `L ≈ 2.6`. That gives: + +| `f_adv` | local (`L=1`) | neighbourhood (`L≈2.6`) | `L` needed for 0.9 | +|---|---|---|---| +| 0.10 | 0.53 | 0.57 | 20.9 | +| 0.20 | 0.56 | **0.64** | 9.8 | +| 0.33 | 0.60 | 0.74 | 5.5 | + +So the neighbourhood does help the adversary — 0.56 → 0.64 at `f_adv = 0.2` — but **reaching 0.9 would need ~10 upstream hops where the graph supplies 2.6**, and the low diameter that makes propagation fast (§3.1) is exactly what starves the adversary of upstream observation points. The bracket therefore closes **near the local model, not near certainty**: confident attribution of the originator stays out of reach at realistic parameters, and the binary `full_deanon` treatment is not rescued by neighbourhood effects. + +One caveat remains: confidence is a *threshold* choice. An adversary content with 0.64 attributes most senders; one demanding 0.9 attributes almost none. The report gives both ends rather than picking for the reader. ![Fig 12 — deanonymization vs path length](report-figures/12_deanon_vs_blendhops.png) *Fig 12 — Whole-path deanonymization rate vs blend-path length, one line per `f_adv` (log-y). Solid = simulated, dashed = the analytic `f_adv^blend_hops`; path length drives it down exponentially, independent of degree.* @@ -429,7 +443,7 @@ One objection needs disposing of: the clock figures above hand the observer the ## 5. Validity and caveats -- **Attribution confidence is bracketed, not settled.** §3.4 gives two readings of the same event: `full_deanon` (any adversarial peer counts as identification) and the confidence-weighted rate (only the sender's own links count as evidence). The first is an upper bound on adversary capability, the second a lower bound, and at `f_adv = 0.2`, degree 8 they differ by five orders of magnitude. The truth lies between, because an adversary also learns from the sender's *neighbourhood* — an honest peer with adversarial peers of its own leaks the message upstream, and with `observed_frac` at 0.83 most honest relays are themselves watched. Resolving that needs a k-hop observability model rather than the 1-hop one used here; until it exists, design against the upper bound and read the lower bound as the floor. +- **Attribution confidence is bracketed, not settled.** §3.4 gives two readings of the same event: `full_deanon` (any adversarial peer counts as identification) and the confidence-weighted rate (only the sender's own links count as evidence). The first is an upper bound on adversary capability, the second a lower bound, and at `f_adv = 0.2`, degree 8 they differ by five orders of magnitude. The truth lies between, because an adversary also learns from the sender's *neighbourhood* — an honest peer with adversarial peers of its own leaks the message upstream, and with `observed_frac` at 0.83 most honest relays are themselves watched. §3.4 now gives both ends: the 1-hop model and a k-hop one in which the adversary sees the message anywhere upstream. The graph's low diameter caps the second at ~2.6 upstream hops, so it lands at 0.64 rather than near 1 — the bracket closes nearer the floor than the ceiling. What is still unmodelled is an adversary combining that structural evidence with the timing evidence of §3.11, which is where the two lines of attack would reinforce each other. - **Structural adversary; timing correlation is the next study, and is blocked on cover traffic.** The adversary is modelled as controlling *nodes* and their peerings: it observes messages traversing relays it owns (deanonymization) and honest nodes it peers (observation). It does **not** perform timing or traffic-analysis correlation across honest relays. That is not an oversight but a sequencing constraint — a timing adversary is only meaningful against a network that emits **cover traffic**. With §3.10 that prerequisite in place, the timing study is now done and reported in §3.11: at the specified cover rate a relay's in-to-out matching is essentially forced under either release design, so the structural results above should be read as an upper bound on the anonymity a deployment actually gets. What remains outside the model is an adversary correlating timing *across* relays rather than at one, and one that combines timing with the structural evidence of §3.4. An adversary that adaptively targets the transport path of a *specific* known sender is likewise outside the current model. - **Churn is modelled both ways; adversarial churn is not.** §3.5 removes nodes independently and §3.9 removes whole AS/region failure domains, at matched churn — and the correlated case turns out to be *gentler* on the live network, so the uncorrelated threshold is the conservative one. What remains outside the model is **adversarially placed** churn: an attacker who chooses which nodes to silence (a cut set rather than a random or clustered set) would be worse than either, and the worst-case placement machinery used for the adversary in §3.3 has no counterpart here. Regional churn also assumes equal-sized domains; real AS sizes are heavy-tailed, so a single dominant provider failing would remove a larger, less uniform slice than modelled. - **Exactly d-regular topology — by design, not by simplification.** Every node has exactly the same number of peers because the protocol requires it: the peer graph is derived by every node from one global seed, so the degree is a protocol constant rather than an emergent property. This is the topology the deployed network will have, so the results are not an idealisation of some heavier-tailed reality — a degree *distribution* would be a different protocol, not a more realistic model of this one. diff --git a/tools/simulators/blend/src/blend/adversary.py b/tools/simulators/blend/src/blend/adversary.py index b1578e8..02932b1 100644 --- a/tools/simulators/blend/src/blend/adversary.py +++ b/tools/simulators/blend/src/blend/adversary.py @@ -100,6 +100,43 @@ def attribution_confidence(adv_peers: np.ndarray | int, degree: int) -> np.ndarr return degree / (2.0 * degree - a) +def neighbourhood_confidence(f_adv: float, upstream_hops: float) -> float: + """Attribution confidence when the adversary also learns from the sender's *neighbourhood*. + + :func:`attribution_confidence` counts only the sender's own links, which understates the + adversary: to rule out that ``X`` was merely forwarding, it does not need to hold ``X``'s + incoming link -- it only needs to have seen the message anywhere upstream. A message that + reached ``X`` travelled a route, and the adversary misses it only if **no** upstream node is + adversarial: + + P(miss | X forwarded) = (1 - f_adv) ** upstream_hops + confidence = 1 / (1 + (1 - f_adv) ** upstream_hops) + + The local model is exactly the ``upstream_hops = 1`` case. Confidence rises with route length + because a longer route offers more chances to be seen, and approaches 1 only for routes far + longer than a low-diameter peer graph actually has. + """ + if not (0.0 <= f_adv < 1.0): + raise ValueError("need 0 <= f_adv < 1") + return 1.0 / (1.0 + (1.0 - f_adv) ** max(upstream_hops, 0.0)) + + +def mean_upstream_hops(graph: Graph, rng: np.random.Generator, samples: int = 40) -> float: + """Mean number of nodes upstream of a forwarder: half the mean hop distance of the graph. + + A node that is forwarding sits somewhere along a route, uniformly on average, so it has about + half the route behind it. Hop distance is measured unweighted -- what matters is how many nodes + handled the message, not how long the links were. + """ + from scipy.sparse.csgraph import shortest_path + n = graph.n + src = rng.choice(n, size=min(samples, n), replace=False) + d = shortest_path(graph.weighted_csr(np.ones_like(graph.base)), method="D", + unweighted=True, indices=src) + finite = d[np.isfinite(d) & (d > 0)] + return float(finite.mean()) / 2.0 if finite.size else 1.0 + + def attribution_metrics(graph: Graph, adv_mask: np.ndarray, thresholds: tuple[float, ...] = (0.5, 0.9, 0.99)) -> dict: """Distribution of :func:`attribution_confidence` over the honest nodes of this placement. diff --git a/tools/simulators/blend/tests/test_deanon.py b/tools/simulators/blend/tests/test_deanon.py index 1b5c09d..0b5f841 100644 --- a/tools/simulators/blend/tests/test_deanon.py +++ b/tools/simulators/blend/tests/test_deanon.py @@ -146,3 +146,29 @@ def test_confident_attribution_is_far_rarer_than_observation(): assert am["observed_frac"] > 0.8 assert at["attributable_frac_90"] < 1e-4 assert at["attribution_conf_mean"] < 0.6 # one or two peers buys very little + + +def test_neighbourhood_confidence_reduces_to_the_local_model_at_one_hop(): + from blend.adversary import neighbourhood_confidence + for f in (0.1, 0.2, 0.33): + assert abs(neighbourhood_confidence(f, 1.0) - 1.0 / (1.0 + (1 - f))) < 1e-12 + + +def test_confidence_rises_with_route_length_but_needs_an_unrealistic_one_for_certainty(): + """Seeing the message anywhere upstream rules out forwarding, so a longer route helps the + adversary -- but reaching 0.9 needs ~10 upstream hops at f_adv=0.2, and a low-diameter peer + graph offers about 2.6.""" + from blend.adversary import neighbourhood_confidence + vals = [neighbourhood_confidence(0.2, L) for L in (1, 2, 5, 10, 20)] + assert all(b > a for a, b in zip(vals, vals[1:], strict=False)) + assert neighbourhood_confidence(0.2, 2.6) < 0.7 # realistic route: still not confident + assert neighbourhood_confidence(0.2, 10) > 0.9 # needs ~4x the real route length + + +def test_measured_route_length_leaves_attribution_uncertain(): + """The bracket closes near the local model, not near certainty.""" + from blend.adversary import mean_upstream_hops, neighbourhood_confidence + g = build_graph(SimConfig(n_nodes=20000, degree=8, graph_seed=0)) + L = mean_upstream_hops(g, np.random.default_rng(0), samples=12) + assert 1.5 < L < 4.0 # low-diameter graph, short routes + assert 0.55 < neighbourhood_confidence(0.2, L) < 0.75