Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation,
adversary exposure, deanonymization and time-to-link, reliability under uniform
and correlated churn, messaging redundancy, and cover traffic. The pd name no
longer describes it.
tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend,
and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows.
The text substitutions are deliberately narrow. pd is also the conventional pandas
alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend.
rewrite would have corrupted four files. Only package-unambiguous forms were
changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the
pyproject name. All four import pandas as pd lines are untouched and verified.
Both READMEs reframed: peering degree is now presented as the primary axis that
ties the others together rather than as the subject, and the relative links, which
lost a directory level in the move, are corrected.
Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets,
the script shims, an end-to-end smoke run, and data/report_numbers.py still
reproducing the report tables from the checked-in evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:20:07 +02:00
|
|
|
|
# blend — a Monte-Carlo simulator for the Blend network
|
Add pd: peering-degree Blend Monte-Carlo graph simulator
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>
2026-08-03 16:47:46 +02:00
|
|
|
|
|
Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation,
adversary exposure, deanonymization and time-to-link, reliability under uniform
and correlated churn, messaging redundancy, and cover traffic. The pd name no
longer describes it.
tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend,
and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows.
The text substitutions are deliberately narrow. pd is also the conventional pandas
alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend.
rewrite would have corrupted four files. Only package-unambiguous forms were
changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the
pyproject name. All four import pandas as pd lines are untouched and verified.
Both READMEs reframed: peering degree is now presented as the primary axis that
ties the others together rather than as the subject, and the relative links, which
lost a directory level in the move, are corrected.
Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets,
the script shims, an end-to-end smoke run, and data/report_numbers.py still
reproducing the report tables from the checked-in evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:20:07 +02:00
|
|
|
|
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:
|
Add pd: peering-degree Blend Monte-Carlo graph simulator
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>
2026-08-03 16:47:46 +02:00
|
|
|
|
|
|
|
|
|
|
- **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
|
|
|
|
|
|
`(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.
|
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-04 22:37:22 +02:00
|
|
|
|
- **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−(1−u)^blend_hops)^R`)
|
|
|
|
|
|
and captured if any cascade is whole-path-adversarial (`deanon = 1−(1−f_adv^blend_hops)^R`) — the
|
|
|
|
|
|
same `1−(1−x)^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/(degree−1)`. A network tolerates churn up to
|
|
|
|
|
|
`u_c = 1 − 1/(degree−1)` (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.
|
pd review: document the new columns, pin the coverage notion, drop dead code
Third review pass over the blend material.
Completeness:
- the simulator README documented neither frac_reached_live nor the three
correlated-churn columns (churn_mode, n_regions, region_locality) that every
run now writes, and its model section never described correlated outages at all;
- the knowledge graph had no pd nodes -- graphify update had never been run since
the simulator was added (2643 -> 2968 nodes).
Correctness/coherence:
- section 3.5 quotes coverage without saying which coverage, now that 3.9
distinguishes all-node from live-network. It is all-node; under uniform churn
the two agree to 0.001, so nothing in 3.5 turns on it. Said so explicitly;
- 3.9 named its groups AS/region without noting that link latency ignores them.
Regions are failure and peering domains, not latency domains -- real co-located
nodes would also be faster, so the clustered delays are if anything pessimistic.
Redundancy:
- style.band_plot was dead: never called by any figure. Removed, with the two
imports it alone needed;
- the units sentence appeared verbatim in the header note and again opening the
model section. Dropped the second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:03:22 +02:00
|
|
|
|
- **Correlated outages:** `n_regions` splits the network into equal-sized **failure domains** and
|
|
|
|
|
|
`region_locality` places that share of each node's peers inside its own domain (the locality
|
|
|
|
|
|
matchings keep the graph exactly d-regular). `churn_mode: regional` then 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_live`
|
|
|
|
|
|
stays ~1) while stranding the dead domains (`frac_reached` falls); see `configs/correlated-churn.yaml`.
|
|
|
|
|
|
Regions are failure and peering domains only — link latency does not depend on them.
|
Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation,
adversary exposure, deanonymization and time-to-link, reliability under uniform
and correlated churn, messaging redundancy, and cover traffic. The pd name no
longer describes it.
tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend,
and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows.
The text substitutions are deliberately narrow. pd is also the conventional pandas
alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend.
rewrite would have corrupted four files. Only package-unambiguous forms were
changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the
pyproject name. All four import pandas as pd lines are untouched and verified.
Both READMEs reframed: peering degree is now presented as the primary axis that
ties the others together rather than as the subject, and the relative links, which
lost a directory level in the move, are corrected.
Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets,
the script shims, an end-to-end smoke run, and data/report_numbers.py still
reproducing the report tables from the checked-in evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:20:07 +02:00
|
|
|
|
- **Linkability over time** (`blend.linkability`): given the rates above and an emission cadence (one
|
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-04 22:37:22 +02:00
|
|
|
|
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.
|
Add pd: peering-degree Blend Monte-Carlo graph simulator
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>
2026-08-03 16:47:46 +02:00
|
|
|
|
|
|
|
|
|
|
## Quick start
|
|
|
|
|
|
```
|
Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation,
adversary exposure, deanonymization and time-to-link, reliability under uniform
and correlated churn, messaging redundancy, and cover traffic. The pd name no
longer describes it.
tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend,
and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows.
The text substitutions are deliberately narrow. pd is also the conventional pandas
alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend.
rewrite would have corrupted four files. Only package-unambiguous forms were
changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the
pyproject name. All four import pandas as pd lines are untouched and verified.
Both READMEs reframed: peering degree is now presented as the primary axis that
ties the others together rather than as the subject, and the relative links, which
lost a directory level in the move, are corrected.
Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets,
the script shims, an end-to-end smoke run, and data/report_numbers.py still
reproducing the report tables from the checked-in evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:20:07 +02:00
|
|
|
|
make install # or reuse a sibling venv: PYTHONPATH=src <python> -m blend.sweep ...
|
2026-08-05 10:32:06 +02:00
|
|
|
|
make smoke # fast end-to-end (every code path + 19 of 21 figure builders)
|
Add pd: peering-degree Blend Monte-Carlo graph simulator
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>
2026-08-03 16:47:46 +02:00
|
|
|
|
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)
|
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-04 22:37:22 +02:00
|
|
|
|
make redundancy # configs/redundancy.yaml (R=1..4: delivery vs deanonymization)
|
|
|
|
|
|
make percolation # configs/percolation.yaml (churn threshold u_c = 1-1/(degree-1))
|
Add pd: peering-degree Blend Monte-Carlo graph simulator
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>
2026-08-03 16:47:46 +02:00
|
|
|
|
make figures RUN=runs/<dir>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Outputs
|
pd review: document the new columns, pin the coverage notion, drop dead code
Third review pass over the blend material.
Completeness:
- the simulator README documented neither frac_reached_live nor the three
correlated-churn columns (churn_mode, n_regions, region_locality) that every
run now writes, and its model section never described correlated outages at all;
- the knowledge graph had no pd nodes -- graphify update had never been run since
the simulator was added (2643 -> 2968 nodes).
Correctness/coherence:
- section 3.5 quotes coverage without saying which coverage, now that 3.9
distinguishes all-node from live-network. It is all-node; under uniform churn
the two agree to 0.001, so nothing in 3.5 turns on it. Said so explicitly;
- 3.9 named its groups AS/region without noting that link latency ignores them.
Regions are failure and peering domains, not latency domains -- real co-located
nodes would also be faster, so the clustered delays are if anything pessimistic.
Redundancy:
- style.band_plot was dead: never called by any figure. Removed, with the two
imports it alone needed;
- the units sentence appeared verbatim in the header note and again opening the
model section. Dropped the second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:03:22 +02:00
|
|
|
|
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`
|
2026-08-05 10:32:06 +02:00
|
|
|
|
(`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`.
|
Add pd: peering-degree Blend Monte-Carlo graph simulator
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>
2026-08-03 16:47:46 +02:00
|
|
|
|
|
|
|
|
|
|
## Layout
|
Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation,
adversary exposure, deanonymization and time-to-link, reliability under uniform
and correlated churn, messaging redundancy, and cover traffic. The pd name no
longer describes it.
tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend,
and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows.
The text substitutions are deliberately narrow. pd is also the conventional pandas
alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend.
rewrite would have corrupted four files. Only package-unambiguous forms were
changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the
pyproject name. All four import pandas as pd lines are untouched and verified.
Both READMEs reframed: peering degree is now presented as the primary axis that
ties the others together rather than as the subject, and the relative links, which
lost a directory level in the move, are corrected.
Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets,
the script shims, an end-to-end smoke run, and data/report_numbers.py still
reproducing the report tables from the checked-in evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:20:07 +02:00
|
|
|
|
`src/blend/`: `graph` (matching-union CSR d-regular), `propagation` (Blend cascade), `mixclock`
|
Add pd: peering-degree Blend Monte-Carlo graph simulator
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>
2026-08-03 16:47:46 +02:00
|
|
|
|
(release-clock residual), `adversary` (exact observation/eclipse + deanonymization + placement),
|
|
|
|
|
|
`config`/`engine`/`sweep`/`metrics`, `plotting`. `configs/` sweeps, `tests/`, `scripts/` shims.
|
Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation,
adversary exposure, deanonymization and time-to-link, reliability under uniform
and correlated churn, messaging redundancy, and cover traffic. The pd name no
longer describes it.
tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend,
and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows.
The text substitutions are deliberately narrow. pd is also the conventional pandas
alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend.
rewrite would have corrupted four files. Only package-unambiguous forms were
changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the
pyproject name. All four import pandas as pd lines are untouched and verified.
Both READMEs reframed: peering degree is now presented as the primary axis that
ties the others together rather than as the subject, and the relative links, which
lost a directory level in the move, are corrected.
Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets,
the script shims, an end-to-end smoke run, and data/report_numbers.py still
reproducing the report tables from the checked-in evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:20:07 +02:00
|
|
|
|
Reports of record live outside the sim at `reports/blend/`.
|