Static-graph simulator quantifying how a node's peering degree trades off propagation speed, adversary exposure, deanonymization, and reliability in the Blend network. Scales to 1e6 nodes (sparse CSR + sampled Dijkstra); the adversary and deanonymization metrics are exact at every N. Model (ms): seeded d-regular peer graph (matching-union), Blend cascade (sender -> blend_hops timed-release mix relays -> final flood), geographic link base + exponential transport jitter, per-node processing lag, free-running release-clock mixing. Metrics: - propagation: full-delay mean/p50/p90/p99, path/broadcast split, coverage times - reliability: message success-delivery-rate ~ (1-unresponsive_frac)^blend_hops and flood coverage, with unresponsive nodes modelled as routing holes - adversary (exact): observed/eclipsed fractions, random + worst-case placement - deanonymization (exact): P(whole blend path adversarial) ~ f_adv^blend_hops, and full deanonymization (path adversarial AND honest sender peered with an adversary) = deanon_rate * observed_frac Deterministic blake2b seed streams, three parquet tables, joblib parallelism, memguard, an analytic verify harness, 50 unit tests, and an auto-installing Makefile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pd — peering-degree Monte-Carlo graph simulator
Quantifies how a node's peering degree trades off, in the Blend network:
- 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.
Quick start
make install # or reuse a sibling venv: PYTHONPATH=src <python> -m pd.sweep ...
make smoke # fast end-to-end -> runs/<ts>_smoke/{propagation,adversary}.parquet + figures/
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 figures RUN=runs/<dir>
Outputs
Three parquets per run: propagation.parquet (full_delay_ms_*, delivery_rate, frac_reached,
coverN_ms vs degree / blend_hops / N / unresponsive_frac), 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 / f_adv / mode — propagation paths crossed
with the adversary set). Figures render all three, including delivery-rate and flood-coverage vs the
unresponsive fraction and the deanonymization rates vs blend-path length, f_adv, and degree.
Layout
src/pd/: 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/pd/.