Marcin Pawlowski 9b03a68a84
Add linkability, messaging redundancy and churn percolation to pd; report
Extends the pd Blend simulator along two axes the deanonymization model
opened up, adds the reports/blend/pd report of record, and fixes three
correctness defects found while reviewing the result.

Linkability over time (pd.linkability):
- time to link an emitter ~ 30s*ln(1/(1-alpha))/(stake*q): inversely
  proportional to stake, so a 5% staker is linked in ~2 days and a 0.001%
  staker only after ~27 years;
- time to certify a node's stake >= theta from the count of attributable
  observations (relative precision ~1/sqrt(N)): sizing a node costs 100-400x
  more than identifying it, and sub-0.1% stake is practically unlearnable.
Both are closed forms over the exact deanonymization rates and a
stake-proportional 30 s emission cadence, checked against a Monte-Carlo of
the emission process in verify.

Messaging redundancy (R independent cascades per emission, R = 1..4):
- `redundancy` knob threaded through config/rng/propagation/engine/metrics/
  sweep; a node receives from whichever cascade reaches it first, so arrival
  times combine element-wise. Delivery and capture both follow 1-(1-x)^R, so
  redundancy trades reliability against anonymity and divides time-to-link
  by ~R. Measured: delivery 0.34 -> 0.81 at 30% churn for R = 1 -> 4, while a
  1%-staker's time to link falls 10 d -> 2.5 d.
- Redundancy buys NO coverage: a cascade only delivers if the sender could
  already route to its relay, so every delivered cascade floods the sender's
  own component. Coverage is flat in R to four decimals at every degree.
- Near the percolation threshold the cascades fail together rather than
  independently, so redundancy under-delivers against 1-(1-p1)^R there.

Churn percolation (configs/percolation.yaml, verify check 7):
- the flood only crosses responsive nodes, so it lives on the responsive
  sub-graph -- site percolation on a d-regular graph. A network survives churn
  only up to u_c = 1 - 1/(degree-1); measured collapse lands on the predicted
  threshold for every degree (3 -> 0.50, 6 -> 0.80, 16 -> 0.93), which inverts
  into the sizing rule degree > 1 + 1/(1-u).

Correctness fixes:
- redundancy delay used the fastest cascade's own full delay, which
  over-states it (min-max vs max-min); now the element-wise earliest arrival,
  reducing exactly to the single-cascade model at R = 1 (test);
- the "redundancy improves coverage" claim was false in both the report and
  the simulator README -- removed and replaced with the measured result;
- per-hop latency is degree-dependent (1.5 s at degree 16 to 2.7 s at degree
  3), not a flat 1.6 s; and the worst-case observation figure was averaged
  over degrees -- at degree 8 and f_adv = 0.2 it is 0.83 -> 1.000.

Statistics: round counts raised for resolution rather than speed -- 8000
rounds per cell in the main sweep, 9600 in the redundancy study, 6400 in the
percolation study, giving SEM <= 0.009 on every delivery rate and <= 0.04 s
on every delay mean. The previous redundancy grid (144 rounds/cell) produced a
non-monotonic delivery curve; it is now monotonic and within 0.015 of theory.
Adversary and deanonymization metrics remain closed-form and exact.

reports/blend/pd: the report of record -- peering-degree trade-offs across
speed, observation, eclipse, deanonymization and reliability, plus the
time-to-link, stake-inference, redundancy and churn-threshold sections, with
21 figures of record and an explicit sampling-error statement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:55 +02:00
..

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_adv of 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_frac of 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 the blend_hops relays; the final flood is plain.
  • Unresponsive nodes: a random unresponsive_frac of 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 (1unresponsive_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(N1, 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: redundancy R 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(1u)^blend_hops)^R) and captured if any cascade is whole-path-adversarial (deanon = 1(1f_adv^blend_hops)^R) — the same 1(1x)^R law, 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/(degree1). A network tolerates churn up to u_c = 1 1/(degree1) (degree 3 → 0.5, degree 6 → 0.8, degree 16 → 0.93) and shatters above it; configs/percolation.yaml walks u across the threshold and make verify checks it.
  • Linkability over time (pd.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. See configs/redundancy.yaml and the report.

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 redundancy              # configs/redundancy.yaml (R=1..4: delivery vs deanonymization)
make percolation             # configs/percolation.yaml (churn threshold u_c = 1-1/(degree-1))
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/.