mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-06 19:23:26 +00:00
blend: attribution evidence at the reported scale, and a figure for the timing study
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>
This commit is contained in:
parent
cad2bf51b2
commit
e9b79ce90a
@ -165,7 +165,7 @@ So **the `full_deanon` figures above overstate confident origination by up to fi
|
||||
|
||||
**`confidence = 1 / (1 + (1 − f_adv)^L)`** for `L` upstream hops — the local model being exactly `L = 1`.
|
||||
|
||||
`L` is not a free parameter; the graph fixes it. A forwarder sits about halfway along a route, and the peer graph is low-diameter: mean hop distance is **5.15 at degree 8** (3.87 at degree 16), so `L ≈ 2.6` — measured by `blend.adversary.mean_upstream_hops`, which the deanon table now records per run as `upstream_hops` alongside the resulting `neighbourhood_conf`. That gives:
|
||||
`L` is not a free parameter; the graph fixes it. A forwarder sits about halfway along a route, and the peer graph is low-diameter: mean hop distance is **5.15 at degree 8** (3.87 at degree 16), so `L ≈ 2.58` — recorded per run as `upstream_hops` alongside the resulting `neighbourhood_conf` (evidence in [`data/attribution/`](data/attribution)). That gives:
|
||||
|
||||
| `f_adv` | local (`L=1`) | neighbourhood (`L≈2.6`) | `L` needed for 0.9 |
|
||||
|---|---|---|---|
|
||||
@ -173,6 +173,8 @@ So **the `full_deanon` figures above overstate confident origination by up to fi
|
||||
| 0.20 | 0.56 | **0.64** | 9.8 |
|
||||
| 0.33 | 0.60 | 0.74 | 5.5 |
|
||||
|
||||
**Degree cuts both ways here.** 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 2.58 at degree 8 and 1.93 at degree 16, lifting neighbourhood confidence at `f_adv = 0.2` to 0.72, 0.64 and 0.61 respectively. The low diameter that makes propagation fast (§3.1) also starves the adversary of observation points — one of the few places in this report where raising the degree helps anonymity rather than hurting it.
|
||||
|
||||
So the neighbourhood does help the adversary — 0.56 → 0.64 at `f_adv = 0.2` — but **reaching 0.9 would need ~10 upstream hops where the graph supplies 2.6**, and the low diameter that makes propagation fast (§3.1) is exactly what starves the adversary of upstream observation points. The bracket therefore closes **near the local model, not near certainty**: confident attribution of the originator stays out of reach at realistic parameters, and the binary `full_deanon` treatment is not rescued by neighbourhood effects.
|
||||
|
||||
One caveat remains: confidence is a *threshold* choice. An adversary content with 0.64 attributes most senders; one demanding 0.9 attributes almost none. The report gives both ends rather than picking for the reader.
|
||||
@ -417,10 +419,13 @@ The measure is the **effective anonymity set** of a release: the perplexity of t
|
||||
|
||||
**At the specified cover rate, timing linkage is essentially total under either design** — the adversary's best guess is right 98–99 % of the time. That is the direct consequence of §3.10's result that a relay holds 0.0014 messages: with one message per relay there is nothing to confuse it with, and no amount of delay manufactures ambiguity out of a single message. **Traffic buys timing protection; delay does not.**
|
||||
|
||||
Between the two designs, **the independent draw wins at equal cost**, contrary to the intuition that batching at a tick should be safer. The clock's tick is a *hard boundary*: the observer can exclude everything before the previous tick, so the candidate set is exactly one inter-tick batch. An exponential has unbounded support and never excludes anything outright. But the margin is much smaller than the set size suggests — at 64 msg/s jitter's effective set is **67 %** larger while its advantage in defeating the best guess is only **19 %**. The heavy tail keeps old arrivals nominally possible while contributing almost nothing, so **perplexity alone would have overstated the case for Poisson-style mixing by roughly a factor of three.**
|
||||
Between the two designs, **the independent draw wins at equal cost**, contrary to the intuition that batching at a tick should be safer. The clock's tick is a *hard boundary*: the observer can exclude everything before the previous tick, so the candidate set is exactly one inter-tick batch. An exponential has unbounded support and never excludes anything outright. But the margin is much smaller than the set size suggests — at 64 msg/s jitter's effective set is **67 %** larger while its advantage in defeating the best guess is only **19 %**. The heavy tail keeps old arrivals nominally possible while contributing almost nothing, so **perplexity alone would have overstated the case for Poisson-style mixing by roughly a factor of three** (**Fig 25**).
|
||||
|
||||
One objection needs disposing of: the clock figures above hand the observer the *tick schedule*, which is the conservative assumption but also the harsher one, since a real observer sees only ticks that released something. Measured both ways, it makes **no difference at all** — and provably so. A silent tick implies nothing was pending at it, and any arrival older than the previous release has already left, so the window bounded by the true previous tick and the window bounded by the last observed release contain the same arrivals. The clock design is not being handicapped by a generous adversary.
|
||||
|
||||

|
||||
*Fig 25 — Matching a relay's output to its input, at a matched delay budget. Solid = MAP success (↓ good), dashed = effective anonymity set on the right axis (↑ good). The dashed lines separate far faster than the solid ones: the set size credits jitter with an advantage the best-guess attack largely takes back.*
|
||||
|
||||
**A minimum release interval changes nothing.** Forbidding zero-length intervals (`min_blend_delay = 1`) leaves the mean hold at 10.14 s against 10.22 s, and MAP success at 0.858 against 0.860 — identical within noise. This is structural rather than a null measurement: a zero-length gap is instantaneous, so it can never cover a message arrival and is never drawn by the residual or by the size-biased interval. Excluding it removes probability mass that was never sampled. What it *does* change is `E[S]`, the spacing of release opportunities — so a minimum would matter under per-message jitter, where a zero draw is a genuinely unmixed forward, but under a free-running clock it buys nothing.
|
||||
|
||||
---
|
||||
@ -458,7 +463,7 @@ One objection needs disposing of: the clock figures above hand the observer the
|
||||
<a id="s6"></a>
|
||||
## 6. Reproducibility
|
||||
|
||||
The simulator, configs, and analytic checks live in [`tools/simulators/blend`](../../tools/simulators/blend). From that directory: `make install`, then `make sweep` runs the main grid (`configs/default.yaml`: N up to 10⁵, degree 3–16, 1–5 blend hops, `f_adv` up to 0.5, unresponsive fractions to 0.5, all three placement modes, 8 topology seeds) into `runs/<timestamp>_default/`, writing three tables — `propagation.parquet`, `adversary.parquet`, and `deanon.parquet` — and rendering the figures. `make sweep-fullscale` extends the exact metrics to 10⁶ nodes. The messaging-redundancy study (§3.8) and the linkability figures come from `configs/redundancy.yaml` (`python -m blend.sweep --config configs/redundancy.yaml`), which sweeps `redundancy` ∈ {1, 2, 3, 4} alongside the churn and adversary grids; the churn-threshold study (§3.5, Fig 20) comes from `configs/percolation.yaml`, which walks the unresponsive fraction to 0.9 so each degree's collapse can be located against `u_c = 1 − 1/(degree − 1)`; the release-design study (§3.11) from `configs/timing.yaml` (`make timing`), which sweeps the two designs and the minimum interval against the cover rate; and the correlated-outage study (§3.9, Fig 22) from `configs/correlated-churn.yaml` (`make correlated-churn`), which partitions the network into failure domains and runs both churn modes on the same topologies. `make sweep-fullscale` produces the 10⁶ scaling check described in §5; and the cover-traffic study (§3.10, Figs 23–24) comes from `configs/cover-traffic.yaml` (`make cover-traffic`), which sweeps the emission rate over three decades against three release delays and pairs each timeline with the epoch-scale emission budget. Round counts in all three configs are set for statistical resolution, not speed — see the sampling-error note in §5. `make verify` runs the analytic anchors (d-regularity; `observed ≈ 1 − (1 − f)^degree`; `eclipsed ≈ f^degree`; delivery `≈ (1 − u)^blend_hops`; both deanonymization rates against a direct Monte-Carlo of the same draw; and — check 6 — `deanon_R` / `delivery_R = 1 − (1 − x)^R` for R independent cascades and the time-to-link geometric law), and `make test` the unit suite (`test_linkability.py` covers the time-to-link and stake formulae). The time-to-link and stake-inference curves are computed by `blend.linkability` from these exact rates.
|
||||
The simulator, configs, and analytic checks live in [`tools/simulators/blend`](../../tools/simulators/blend). From that directory: `make install`, then `make sweep` runs the main grid (`configs/default.yaml`: N up to 10⁵, degree 3–16, 1–5 blend hops, `f_adv` up to 0.5, unresponsive fractions to 0.5, all three placement modes, 8 topology seeds) into `runs/<timestamp>_default/`, writing three tables — `propagation.parquet`, `adversary.parquet`, and `deanon.parquet` — and rendering the figures. `make sweep-fullscale` extends the exact metrics to 10⁶ nodes. The messaging-redundancy study (§3.8) and the linkability figures come from `configs/redundancy.yaml` (`python -m blend.sweep --config configs/redundancy.yaml`), which sweeps `redundancy` ∈ {1, 2, 3, 4} alongside the churn and adversary grids; the churn-threshold study (§3.5, Fig 20) comes from `configs/percolation.yaml`, which walks the unresponsive fraction to 0.9 so each degree's collapse can be located against `u_c = 1 − 1/(degree − 1)`; the release-design study (§3.11) from `configs/timing.yaml` (`make timing`), which sweeps the two designs and the minimum interval against the cover rate; the attribution bracket (§3.4) from `configs/attribution.yaml` (`make attribution`), which records both bounds and the graph's hop distance at the reported scale; and the correlated-outage study (§3.9, Fig 22) from `configs/correlated-churn.yaml` (`make correlated-churn`), which partitions the network into failure domains and runs both churn modes on the same topologies. `make sweep-fullscale` produces the 10⁶ scaling check described in §5; and the cover-traffic study (§3.10, Figs 23–24) comes from `configs/cover-traffic.yaml` (`make cover-traffic`), which sweeps the emission rate over three decades against three release delays and pairs each timeline with the epoch-scale emission budget. Round counts in all three configs are set for statistical resolution, not speed — see the sampling-error note in §5. `make verify` runs the analytic anchors (d-regularity; `observed ≈ 1 − (1 − f)^degree`; `eclipsed ≈ f^degree`; delivery `≈ (1 − u)^blend_hops`; both deanonymization rates against a direct Monte-Carlo of the same draw; and — check 6 — `deanon_R` / `delivery_R = 1 − (1 − x)^R` for R independent cascades and the time-to-link geometric law), and `make test` the unit suite (`test_linkability.py` covers the time-to-link and stake formulae). The time-to-link and stake-inference curves are computed by `blend.linkability` from these exact rates.
|
||||
|
||||
The figures of record for this report are the copies checked in under [`report-figures/`](report-figures); the simulator does not commit its own generated figures. To regenerate: run the sweeps above, then copy `runs/<…>/figures/*.png` into `report-figures/`.
|
||||
|
||||
@ -466,4 +471,4 @@ The **evidence** is checked in too: [`data/`](data) holds the sweep outputs behi
|
||||
|
||||
## Figures
|
||||
|
||||
All twenty-four rendered figures are versioned in [`report-figures/`](report-figures): `01`–`03` propagation delay (vs degree, vs path length, vs N); `04`–`09` adversary observation and eclipse (vs `f_adv`, vs degree, and heatmaps); `10`–`11` reliability under churn (delivery and coverage); `12`–`15` deanonymization (whole-path and full, vs path length, `f_adv`, and degree); `16`–`18` linkability over time (time to link vs stake, with redundancy, and time to learn stake vs threshold); `19` the redundancy reliability-vs-anonymity trade-off in probability and `21` the same trade in delivery-vs-time-to-link; `20` the churn-percolation threshold; `22` correlated versus uniform outages; `23`–`24` cover traffic (the anonymity set against rate and delay, and the emission-quota stake ceiling). §3.11's timing results are tabulated rather than plotted; its evidence is in [`data/timing/`](data/timing). Eighteen of the twenty-four are embedded above; the other six (`04`–`06`, `09`, `11`, `13`) are alternative cuts of data already shown — for instance 11 and 20 both plot coverage against churn, and 20 supersedes 11 by walking the churn past every degree's threshold.
|
||||
All twenty-five rendered figures are versioned in [`report-figures/`](report-figures): `01`–`03` propagation delay (vs degree, vs path length, vs N); `04`–`09` adversary observation and eclipse (vs `f_adv`, vs degree, and heatmaps); `10`–`11` reliability under churn (delivery and coverage); `12`–`15` deanonymization (whole-path and full, vs path length, `f_adv`, and degree); `16`–`18` linkability over time (time to link vs stake, with redundancy, and time to learn stake vs threshold); `19` the redundancy reliability-vs-anonymity trade-off in probability and `21` the same trade in delivery-vs-time-to-link; `20` the churn-percolation threshold; `22` correlated versus uniform outages; `23`–`24` cover traffic (the anonymity set against rate and delay, and the emission-quota stake ceiling). `25` the timing attack (§3.11). Evidence for each study is in [`data/`](data). Nineteen of the twenty-five are embedded above; the other six (`04`–`06`, `09`, `11`, `13`) are alternative cuts of data already shown — for instance 11 and 20 both plot coverage against churn, and 20 supersedes 11 by walking the churn past every degree's threshold.
|
||||
|
||||
@ -15,6 +15,7 @@ Each run directory holds the three tables the simulator writes: `propagation.par
|
||||
| `correlated-churn/` | `configs/correlated-churn.yaml` | 800 × 8 = **6 400/cell** | §3.9 — correlated AS/region outages vs uniform churn |
|
||||
| `fullscale/` | `configs/fullscale.yaml` | 64 × 3 = **192/cell** | §5 — the 10⁶ scaling check (deliberately lighter; not a source of headline numbers) |
|
||||
| `cover-traffic/` | `configs/cover-traffic.yaml` | 900 s timeline × 4 seeds | §3.10 — blending, mixing, and the emission-quota stake ceiling. Carries a fourth table, `traffic.parquet` |
|
||||
| `attribution/` | `configs/attribution.yaml` | closed-form + 4 seeds | §3.4 — the attribution bracket at the report's scale: local confidence, attributable fractions, upstream hops, neighbourhood confidence |
|
||||
| `timing/` | `configs/timing.yaml` | 120 s timeline × 3 seeds | §3.11 — the two release designs under a timing attack, and the minimum-interval control |
|
||||
|
||||
The linkability results (§3.6–§3.7) and both deanonymization rates are closed forms over these
|
||||
|
||||
BIN
reports/blend/data/attribution/adversary.parquet
Normal file
BIN
reports/blend/data/attribution/adversary.parquet
Normal file
Binary file not shown.
BIN
reports/blend/data/attribution/deanon.parquet
Normal file
BIN
reports/blend/data/attribution/deanon.parquet
Normal file
Binary file not shown.
BIN
reports/blend/data/attribution/propagation.parquet
Normal file
BIN
reports/blend/data/attribution/propagation.parquet
Normal file
Binary file not shown.
@ -178,8 +178,11 @@ m = TMT[TMT.release_mode == "clock"].groupby("min_blend_delay").agg(
|
||||
print(m.to_string(index=False, float_format=lambda v: f"{v:.3f}"))
|
||||
|
||||
# --- 3.4 attribution bracket ---------------------------------------------------------------------
|
||||
print("\n### 3.4 attribution: local confidence vs the neighbourhood bound")
|
||||
TD = pd.read_parquet(TM + "/deanon.parquet")
|
||||
cols = ["attribution_conf_mean", "attributable_frac_50", "attributable_frac_90",
|
||||
"upstream_hops", "neighbourhood_conf"]
|
||||
print(TD[cols].mean().round(4).to_string())
|
||||
AT = os.path.join(_here, "attribution")
|
||||
print("\n### 3.4 attribution bracket (N=20k, random placement)")
|
||||
AD = pd.read_parquet(AT + "/deanon.parquet")
|
||||
a = AD[AD.adversary_mode == "random"].groupby(["degree", "f_adv"]).agg(
|
||||
observed=("observed_frac", "mean"), local_mean=("attribution_conf_mean", "mean"),
|
||||
attributable_90=("attributable_frac_90", "mean"), upstream=("upstream_hops", "mean"),
|
||||
neighbourhood=("neighbourhood_conf", "mean")).reset_index()
|
||||
print(a.to_string(index=False, float_format=lambda v: f"{v:.5f}"))
|
||||
|
||||
BIN
reports/blend/report-figures/25_timing_linkability.png
Normal file
BIN
reports/blend/report-figures/25_timing_linkability.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
@ -8,7 +8,7 @@ export OPENBLAS_NUM_THREADS := 1
|
||||
export MKL_NUM_THREADS := 1
|
||||
export NUMEXPR_NUM_THREADS := 1
|
||||
|
||||
.PHONY: install smoke sweep sweep-fullscale redundancy percolation correlated-churn cover-traffic timing figures verify test lint clean
|
||||
.PHONY: install smoke sweep sweep-fullscale redundancy percolation correlated-churn cover-traffic timing attribution figures verify test lint clean
|
||||
|
||||
# The stamp is the real install; targets below depend on it so `make sweep` (etc.) auto-installs
|
||||
# on a fresh checkout and re-installs whenever pyproject.toml changes.
|
||||
@ -35,6 +35,9 @@ redundancy: $(STAMP) ## messaging redundancy R=1..4 (delivery vs deanonymization
|
||||
percolation: $(STAMP) ## churn threshold: coverage collapse at u_c = 1 - 1/(degree-1)
|
||||
$(PY) -m blend.sweep --config configs/percolation.yaml
|
||||
|
||||
attribution: $(STAMP) ## attribution confidence: the local bound and the neighbourhood bound
|
||||
$(PY) -m blend.sweep --config configs/attribution.yaml
|
||||
|
||||
cover-traffic: $(STAMP) ## cover traffic: blending, mixing, and the emission-quota ceiling
|
||||
$(PY) -m blend.sweep --config configs/cover-traffic.yaml
|
||||
|
||||
|
||||
23
tools/simulators/blend/configs/attribution.yaml
Normal file
23
tools/simulators/blend/configs/attribution.yaml
Normal file
@ -0,0 +1,23 @@
|
||||
# Attribution confidence at the scale the report quotes (section 3.4).
|
||||
#
|
||||
# Capturing a cascade identifies the MESSAGE, not the originator: an honest node seen transmitting
|
||||
# may simply have been passing one along. This run measures both ends of the resulting bracket.
|
||||
#
|
||||
# local -- confidence d/(2d-a) from the sender's own a adversarial peers, and the share of
|
||||
# honest nodes attributable at 0.5 / 0.9 / 0.99.
|
||||
# neighbourhood -- 1/(1+(1-f)^L), where the adversary also sees the message anywhere upstream.
|
||||
# L is fixed by the graph, so upstream_hops is recorded per run.
|
||||
#
|
||||
# Propagation is not the subject, so n_rounds is minimal: the adversary and deanonymization metrics
|
||||
# are closed-form and exact, and the hop distance is a property of the topology.
|
||||
n_nodes: [20000]
|
||||
degree: [4, 8, 16]
|
||||
blend_hops: [3]
|
||||
max_blend_delay: [3]
|
||||
unresponsive_frac: [0.0]
|
||||
f_adv: [0.1, 0.2, 0.33, 0.5]
|
||||
adversary_mode: [random, worstcase_coverage]
|
||||
seeds: 4
|
||||
base:
|
||||
n_rounds: 10
|
||||
n_placements: 4
|
||||
@ -709,3 +709,42 @@ def quota_stake_ceiling(traffic: pd.DataFrame):
|
||||
ax.set_title("Emission quota: the most stake a node can hold and stay uniform")
|
||||
ax.legend(fontsize=7)
|
||||
return fig
|
||||
|
||||
|
||||
def timing_linkability_vs_rate(traffic: pd.DataFrame):
|
||||
"""Can an observer match a relay's outgoing message to the incoming one? (§3.11)
|
||||
|
||||
Solid = MAP success, how often the observer's single best guess is right; dashed = the
|
||||
effective anonymity set on the right axis. Both release designs cost the same mean delay, so
|
||||
they differ only in how they delay. The two lines together make the point that the set size
|
||||
alone would miss: a heavy-tailed delay inflates the set while leaving the best guess nearly as
|
||||
good, so the jitter design's real advantage is a fraction of its apparent one.
|
||||
"""
|
||||
if (traffic is None or not len(traffic) or "map_success" not in traffic
|
||||
or traffic["release_mode"].nunique() < 2):
|
||||
return None
|
||||
import matplotlib.pyplot as plt
|
||||
style.apply_style()
|
||||
d = traffic[traffic.min_blend_delay == traffic.min_blend_delay.min()]
|
||||
fig, ax = plt.subplots()
|
||||
ax2 = ax.twinx()
|
||||
ax2.grid(False)
|
||||
for i, mode in enumerate(sorted(d.release_mode.unique())):
|
||||
c = style.color_for(i)
|
||||
s = d[d.release_mode == mode].groupby("cover_rate_mult").agg(
|
||||
mp=("map_success", "mean"), es=("timing_set_mean", "mean")).reset_index()
|
||||
ax.plot(s.cover_rate_mult, s.mp, "-o", ms=5, color=c, label=f"{mode}: MAP success ↓ good")
|
||||
ax2.plot(s.cover_rate_mult, s.es, "--s", ms=4, color=c, alpha=0.65,
|
||||
label=f"{mode}: effective set ↑ good")
|
||||
ax.axhline(1.0, ls=":", lw=0.8, color="0.6")
|
||||
ax.set_xscale("log")
|
||||
ax.set_xlabel("cover-traffic rate (messages/second, network-wide)")
|
||||
ax.set_ylabel("MAP success: best guess is correct")
|
||||
ax.set_ylim(0.0, 1.05)
|
||||
ax2.set_ylabel("effective anonymity set of a release")
|
||||
h1, l1 = ax.get_legend_handles_labels()
|
||||
h2, l2 = ax2.get_legend_handles_labels()
|
||||
ax.legend(h1 + h2, l1 + l2, fontsize=7, loc="center left")
|
||||
ax.set_title("Timing: matching a relay's output to its input\n"
|
||||
"(matched delay budget; at the baseline rate both designs fail)")
|
||||
return fig
|
||||
|
||||
@ -44,6 +44,7 @@ _DEANON_BUILDERS = [
|
||||
_TRAFFIC_BUILDERS = [
|
||||
("23_blending_vs_rate_and_delay", figures.blending_vs_rate_and_delay),
|
||||
("24_quota_stake_ceiling", figures.quota_stake_ceiling),
|
||||
("25_timing_linkability", figures.timing_linkability_vs_rate),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user