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>
The 3.11 table carried numbers from the ad-hoc analysis that preceded the sweep.
Replaced with the values the checked-in run actually produces (MAP success
0.993/0.905/0.683 clock, 0.989/0.832/0.550 jitter), so every figure in the report
is traceable to data/. The minimum-interval control likewise now quotes the
committed 10.14s vs 10.22s and 0.858 vs 0.860.
cover-traffic was the only study without a make target, and correlated-churn,
cover-traffic and timing were missing from the simulator quick-start. Added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding: the timing study and the neighbourhood-confidence numbers were
produced by ad-hoc analysis, not by the simulator. timing_linkability,
neighbourhood_confidence and mean_upstream_hops had no callers outside their own
modules; min_blend_delay and release_mode were declared on SweepConfig, validated
and keyed, but never read by sweep.py, so a YAML setting them was silently
ignored; and propagation.py called mix_wait without the minimum, leaving the knob
inert on the delay tables of 3.1-3.2. Section 6 promised every number was
reproducible and data/README claimed to hold the evidence behind every number --
both were false for 3.11.
Now wired end to end: release_designs() is a real sweep axis, the engine measures
the timing attack per design and records it in traffic.parquet, and the deanon
table carries the full attribution bracket (local confidence, attributable
fractions, upstream hops, neighbourhood confidence). Added configs/timing.yaml
and a make target.
The committed sweep reproduces 3.11: MAP success 0.993/0.905/0.683 for clock and
0.989/0.832/0.550 for jitter across the swept rates, and the minimum interval
changes nothing (0.993 vs 0.993). Evidence checked in under data/timing.
Three regression tests pin the wiring so a measure cannot go back to living only
in analysis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The a/d estimator credits the adversary only with the sender own links, which
understates it: to rule out that X was forwarding it need not hold X incoming
link, only have seen the message anywhere upstream. That gives
confidence = 1 / (1 + (1-f_adv)^L)
for L upstream hops, with the local model as the L=1 case.
L is not free -- the graph fixes it. A forwarder sits about halfway along a route
and the peer graph is low-diameter: mean hop distance 5.15 at degree 8, so L ~ 2.6.
Confidence rises from 0.56 to 0.64 at f_adv = 0.2, but 0.9 would need ~10 upstream
hops. The low diameter that makes propagation fast is exactly what starves the
adversary of observation points.
So the bracket closes near the local model rather than near certainty, and the
binary full_deanon treatment is NOT rescued by neighbourhood effects. Both ends
are reported rather than one being chosen, since confident attribution is a
threshold question: an adversary content with 0.64 attributes most senders, one
demanding 0.9 attributes almost none.
What remains unmodelled is an adversary combining this structural evidence with
the timing evidence of 3.11.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prices two ways of delaying at a matched latency budget: the free-running clock
(batch at the tick) against an independent per-message draw. Measured by the
effective anonymity set of a release, and by MAP success -- how often the
observer single best guess is right -- because perplexity flatters a heavy tail.
The dominant result is that at the specified cover rate timing linkage is
essentially total under BOTH designs: the best guess is right 98-99% of the time.
That follows directly from the mixing~0 result of 3.10 -- a relay holds about one
message, and delay cannot manufacture ambiguity out of one message. Traffic buys
timing protection; delay does not.
Between designs the independent draw wins at equal cost, against the intuition
that batching should be safer: a tick is a hard boundary the observer can exclude
behind, whereas an exponential never excludes anything outright. But the margin is
a third of what the set size suggests (68% larger set, 19% better against the best
guess), so perplexity alone would have overstated the case for Poisson-style
mixing threefold.
A minimum release interval changes nothing, structurally: a zero-length gap is
instantaneous, so it never covers an arrival and is never sampled.
Section 5 updated -- the timing caveat is discharged, and the structural results
are now flagged as an upper bound on the anonymity a deployment actually gets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
full_deanon counted any sender with an adversarial peer as identified. Capturing a
cascade tells the adversary WHICH message it is following, not WHO started it:
seeing an honest X transmit is equally consistent with X having received it from a
peer the adversary cannot watch. Separating the two gives
confidence = 1/(2 - a/d) = d/(2d - a)
for a adversarial peers of degree d. The path length does not enter -- the
conditioning event already fixes the relays as adversarial, so an honest X is not
one of them for this message.
The consequence is large. One peer of eight is worth 0.53, barely above the 0.5
prior, and 90% confidence needs a >= 8: every peer, which is the ECLIPSE condition
rather than the observation condition. Measured, attributable_frac_90 equals
eclipsed_frac exactly. At f_adv = 0.2, degree 8 that is 2.6e-6 against an
observed_frac of 0.83 -- the published figure overstates confident origination by
five orders of magnitude.
Stated in the report as a bracket rather than a replacement: full_deanon is the
upper bound on adversary capability, this is the lower bound, and the truth lies
between because the adversary also learns from the sender neighbourhood. Closing
that gap needs a k-hop observability model and is recorded as open in section 5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>