Two gaps left by the previous review. Section 3.4 quoted the attribution bracket at N=20,000 while the only committed evidence carrying those columns was the timing run at N=2,000, so a reader diffing report against data saw different numbers for the same quantity. Added configs/attribution.yaml and a make target: it records both bounds and the graph hop distance at the reported scale, cheaply, since the adversary and deanonymization metrics are closed-form and the hop distance is a property of the topology. It reproduces the section exactly -- L = 2.58 and neighbourhood confidence 0.640 at degree 8, f_adv 0.2. It also surfaces a result the smaller run could not: degree cuts both ways. A sparser graph has longer routes, so it offers the adversary more upstream places to see the message -- L is 4.18 at degree 4 against 1.93 at degree 16, lifting neighbourhood confidence from 0.61 to 0.72. The low diameter that makes propagation fast also starves the adversary, one of the few places where raising the degree helps anonymity rather than hurting it. Section 3.11 was the only section without a figure. Fig 25 plots MAP success against the effective anonymity set for both release designs: the dashed sets separate far faster than the solid best-guess curves, which is the whole argument for not trusting perplexity alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
blend — a Monte-Carlo simulator for the Blend network
Measures the Blend network on a seeded peer graph: propagation, adversary exposure, deanonymization, reliability under churn, messaging redundancy and cover traffic. Peering degree is the primary study axis and the one that ties the rest together — it trades off, simultaneously:
- propagation speed — the full delay (ms) of a message: a random sender routes it along a
blend_hops-relay Blend path (each relay a free-running timed-release mix node) and the last relay floods the whole network; - adversary exposure — with a fraction
f_advof adversarial nodes, how many honest nodes are peered with ≥1 adversary (observed) and how many are fully surrounded (eclipsed); - deanonymization — tying propagation to the adversary: how often a message's whole blend path is adversarial (deanonymization — the adversary owns the cascade end-to-end) and how often the honest sender is additionally peered with an adversary (full deanonymization — the message is tied back to its originator); and
- reliability under churn — with a fraction
unresponsive_fracof nodes that relay nothing, the message success-delivery-rate (fraction of messages that survive the whole blend cascade to a responsive final relay) and the flood coverage of those that do.
The peer graph is a seeded random d-regular graph (exactly degree symmetric peers, identical
for everyone from one global seed). This is static-graph analysis — no consensus — so it is far
lighter than the TSI simulators and scales to 10⁶ nodes (sparse CSR + sampled Dijkstra; the
adversary metrics are exact at every N).
Model (all delays in ms)
- Link delay: geographic base (metro 15 → antipodal 200 ms) + exponential transport jitter.
- Processing lag: each node draws a fixed lag from a categorical distribution (default {10, 50, 100} ms at {0.5, 0.4, 0.1}), incurred every time it relays.
- Blend mixing: each relay releases on a free-running clock whose successive intervals are
Uniform{0…
max_blend_delay} whole seconds; a held message waits for the relay's next release (the renewal residual). Mixing happens only at theblend_hopsrelays; the final flood is plain. - Unresponsive nodes: a random
unresponsive_fracof the population relays nothing (its outgoing edges are removed). Relays are drawn from the whole node list blind to responsiveness, so a message dies if any relay on its path is unresponsive — the delivery-rate then tracks(1−unresponsive_frac)^blend_hops. Unresponsive nodes still receive, but they are routing holes, so a delivered flood can strand pockets; a higher peering degree supplies redundant paths that keep coverage high. This axis affects propagation only, not the adversary metrics. - Deanonymization: relays are picked blind to who is adversarial, so P(the whole blend path is
adversarial) is the exact hypergeometric
C(n_adv, blend_hops) / C(N−1, blend_hops)≈f_adv^blend_hops(deanon_rate) — placement-independent, driven by path length, not degree. Multiplying by the fraction of honest nodes with ≥1 adversary peer (observed_frac, which the worst-case-coverage placement maximizes) gives full_deanon_rate — the honest sender is also directly exposed, so the message is tied to its originator. Lengthening the blend path is the dominant defence; a higher degree speeds propagation but raises the chance a sender directly touches the adversary. Both are exact at every N (no Monte-Carlo), like the other adversary metrics. - Messaging redundancy:
redundancyR sends each emission over R independent blend cascades. A node receives the message from whichever cascade reaches it first (arrival times are combined element-wise), so it is delivered if any cascade delivers (delivery = 1−(1−(1−u)^blend_hops)^R) and captured if any cascade is whole-path-adversarial (deanon = 1−(1−f_adv^blend_hops)^R) — the same1−(1−x)^Rlaw, so redundancy trades reliability against anonymity. It buys no extra coverage: a cascade only delivers if the sender could route to its relay, so every delivered cascade floods the sender's own component. R = 1 is the plain single-cascade model (default), to which the whole aggregation reduces exactly. - Churn percolation: the flood only crosses responsive nodes, so it lives on the responsive
sub-graph — site percolation on a d-regular graph, whose giant component survives only while the
responsive fraction exceeds
1/(degree−1). A network tolerates churn up tou_c = 1 − 1/(degree−1)(degree 3 → 0.5, degree 6 → 0.8, degree 16 → 0.93) and shatters above it;configs/percolation.yamlwalks u across the threshold andmake verifychecks it. - Correlated outages:
n_regionssplits the network into equal-sized failure domains andregion_localityplaces that share of each node's peers inside its own domain (the locality matchings keep the graph exactly d-regular).churn_mode: regionalthen fails whole domains instead of scattered nodes, at an identical dead-node count. Locality is what makes this differ from uniform churn at all — with region-blind peering, dropping whole regions removes a uniformly random node set. Clustered failure leaves the survivors fully connected (frac_reached_livestays ~1) while stranding the dead domains (frac_reachedfalls); seeconfigs/correlated-churn.yaml. Regions are failure and peering domains only — link latency does not depend on them. - Linkability over time (
blend.linkability): given the rates above and an emission cadence (one node emits per 30 s slot, chosen ∝ stake), the module derives the time to link an emitter (≈ 30 s·ln(1/(1−α))/(stake·q), inverse in stake) and the time to learn its stake to a threshold from the count of attributable observations. Seeconfigs/redundancy.yamland the report.
Quick start
make install # or reuse a sibling venv: PYTHONPATH=src <python> -m blend.sweep ...
make smoke # fast end-to-end (every code path + 19 of 21 figure builders)
make verify # analytic checks (closed forms + graph invariants)
make test # unit tests
make sweep # configs/default.yaml (N up to 1e5, both adversary modes)
make sweep-fullscale # configs/fullscale.yaml (N up to 1e6, random-mode exact)
make redundancy # configs/redundancy.yaml (R=1..4: delivery vs deanonymization)
make percolation # configs/percolation.yaml (churn threshold u_c = 1-1/(degree-1))
make correlated-churn # configs/correlated-churn.yaml (AS/region outages vs uniform churn)
make cover-traffic # configs/cover-traffic.yaml (blending, mixing, the stake ceiling)
make timing # configs/timing.yaml (jitter vs clock-tick release under attack)
make figures RUN=runs/<dir>
Outputs
Three parquets per run: propagation.parquet (full_delay_ms_*, delivery_rate, coverN_ms, and
two coverage columns — frac_reached over all nodes and frac_reached_live over the responsive
ones, which diverge under correlated churn — vs degree / blend_hops / N / unresponsive_frac /
churn_mode / n_regions / region_locality / redundancy), adversary.parquet
(observed_frac / eclipsed_frac vs degree / f_adv / mode, random + worst-case envelope), and
deanon.parquet (deanon_rate / full_deanon_rate vs degree / blend_hops / redundancy / f_adv /
mode — propagation paths crossed with the adversary set). Figures render all three: delay vs degree /
path length / N, observation and eclipse vs f_adv and degree, delivery and coverage vs the
unresponsive fraction (with the u_c = 1-1/(degree-1) threshold), the deanonymization rates, and the
time-to-link / stake-inference / redundancy curves derived from them by linkability.
Layout
src/blend/: graph (matching-union CSR d-regular), propagation (Blend cascade), mixclock
(release-clock residual), adversary (exact observation/eclipse + deanonymization + placement),
config/engine/sweep/metrics, plotting. configs/ sweeps, tests/, scripts/ shims.
Reports of record live outside the sim at reports/blend/.