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>
Written from configs/cover-traffic.yaml: the rate swept over three decades against
three release delays, each timeline paired with the epoch emission budget.
The finding that shapes the section is that mixing never happens. At one message
per second a relay holds 0.0014 messages and never more than two; even 256x the
rate reaches only 0.39, matching Little law exactly. So the anonymity set is
entirely blending -- the broadcasts a relay saw between releases -- which follows
rate*(2M+1)/3, twice the mean hold. Measured within ~1% over most of the range.
That makes delay the cheap lever: both knobs enter linearly, but bandwidth is paid
on every link while delay is paid once per hop. An anonymity set of 100 costs 42.9
msg/s at a 3s delay and 4.9 msg/s at 30s.
The quota ceiling is the hard edge. A node proposals must fit its emission budget,
capping stake at ln(1-q)/ln(1-f) of INFERRED stake -- about 0.1% at the baseline
rate once Poisson fluctuation is allowed for. A 9.5% holder overruns by ~65x and
is distinguishable by emission count alone, before any path is captured.
Two existing sections needed correcting as a result. 3.6-3.7: with cover traffic
running, catching an emission is not catching a block, and the large stakers those
sections analyse sit one to two orders of magnitude above the quota ceiling, so
their binding exposure is the quota rather than the cascade. 5: the timing-
correlation adversary was deferred for want of cover traffic and is now unblocked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The simulator gitignores its runs/ directory, so every table and figure in
reports/blend/pd rested on data that existed only on one machine. This adds the
sweep outputs of record under reports/blend/pd/data -- one directory per study,
1 MB total -- so any number can be checked against its source, or challenged,
without re-running hours of compute.
report_numbers.py comes with them: run it and it prints every value the report
quotes together with its across-topology standard error, straight from these
parquets. It reproduces the report tables exactly.
Kept: default (8000 rounds/cell), redundancy (9600), percolation (6400),
correlated-churn (6400), fullscale (192, the deliberately lighter 1e6 check).
Omitted: the smoke runs, and an earlier 144-rounds/cell redundancy grid whose
sampling error produced a non-monotonic delivery curve -- superseded, and the
reason the kept grid samples 9600.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the two open items from the review.
3.9 correlated (AS/region) outages, from configs/correlated-churn.yaml at 6400
rounds/cell: clustered failure does NOT fragment the network -- the live coverage
holds at 1.000 at every degree through 80% churn, and the percolation threshold
of 3.5 never appears, because removing a failure domain removes a whole
neighbourhood and leaves every surviving one intact. At degree 4 and 70% churn,
scattered failure annihilates the network (live coverage 0.001, delivery 0.000)
while the same number of clustered failures still delivers 30% of messages. The
cost is that dead domains become unreachable islands, so all-node coverage falls
instead. This reverses the caveat previously written, which asserted correlated
outages would degrade coverage faster; 3.5 uncorrelated threshold is in fact the
conservative case for the operating network.
10^6: the fullscale run confirms the results carry -- adversary closed forms
reproduced to 1.6e-4 at a million nodes, coverage under churn indistinguishable
from 10^5, delivery still tracking (1-u)^hops, and full delay up only 5.8% from
10^5. It samples 192 rounds/cell against 8000, so it is recorded as a scaling
check rather than a source of headline numbers, and the tables are not restated
from it.
Remaining churn caveat is now the honest one: adversarially placed churn (a cut
set rather than a random or clustered set) is still unmodelled, as are
heavy-tailed AS sizes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uncorrelated churn alone was incomplete: real outages take out a datacentre, AS
or region as a unit. Adds failure domains and a correlated churn mode, plus the
metric needed to tell the two apart.
- n_regions / region_locality: nodes belong to equal-sized failure domains, and
a configurable share of each node peers inside its own domain. Locality is what
makes a failure domain a connectivity domain -- with region-blind peering,
dropping whole regions removes a uniformly random set of nodes and is
indistinguishable from uniform churn. The locality matchings keep the graph
exactly d-regular (they change where peers are, never how many).
- churn_mode = uniform | regional, swept per topology so both modes are compared
on the same graph at an identical dead-node count.
- frac_reached_live: coverage of the *responsive* network, alongside coverage of
all nodes. The two move in opposite directions under correlated failure, so one
number could not express the result.
Measured (degree 4, 20 domains, 75% locality, half the network dead): clustered
failure leaves the survivors fully connected -- live coverage 1.000 and delivery
equal to the live-relay rate, i.e. nothing lost to routing -- where the same
number of scattered failures gives 0.857 live coverage and loses delivery to
broken routes. Correlated outages are gentler on the survivors than uniform
churn, while stranding the dead domains. Verify check 8 anchors this.
Also, per review of the caveats: exact d-regularity is a protocol requirement
rather than a modelling simplification, and the timing-correlation adversary is
deferred because it is only meaningful once the network emits cover traffic,
which this simulator does not yet do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The headline claimed the findings covered "the sizes tested (10^3-10^6)" and the
method note claimed exactness "including 10^6", but no run behind this report
exceeds N = 100000 -- 10^6 is a capability of the simulator (make sweep-fullscale)
that was never exercised for these numbers. Claims now match the measurements,
with an explicit caveat in section 5 distinguishing the three decades actually
measured from the extrapolation to 10^6.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correctness/completeness pass over the blend material only (TSI untouched).
- report Model section (2) was missing two of the six axes: messaging
redundancy (R cascades, first-arrival combination) and the emission/linking
model (30 s stake-proportional cadence, what counts as linked) were defined
only inline in the findings;
- method note still claimed 200 rounds x 8 topologies, contradicting the 1000
x 8 the tables now come from;
- design guidance carried two superseded numbers: worst-case observation as
"+0.15 absolute" (it saturates at 1.000 at degree 8, f_adv 0.2) and the
redundancy example (0.34 -> 0.72, measured 0.342 -> 0.713);
- figure references were incoherent: Figs 2 and 14 were cited in the text but
never shown, and Fig 8 was shown but never cited. All 15 embedded figures are
now cited and all citations resolve;
- simulator README listed two parquets for smoke (there are three) and omitted
redundancy from the propagation/deanon column lists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>