From fb49a790fc7eda82d2a348581278714c1851b031 Mon Sep 17 00:00:00 2001 From: Marcin Pawlowski Date: Thu, 6 Aug 2026 14:12:44 +0200 Subject: [PATCH] blend: make sections 3.11 and the attribution bracket reproducible 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) --- reports/blend/README.md | 4 +- reports/blend/data/README.md | 1 + reports/blend/data/timing/adversary.parquet | Bin 0 -> 8910 bytes reports/blend/data/timing/deanon.parquet | Bin 0 -> 14130 bytes reports/blend/data/timing/propagation.parquet | Bin 0 -> 16681 bytes reports/blend/data/timing/traffic.parquet | Bin 0 -> 28341 bytes tools/simulators/blend/Makefile | 5 ++- tools/simulators/blend/configs/timing.yaml | 30 ++++++++++++++ tools/simulators/blend/src/blend/config.py | 4 ++ tools/simulators/blend/src/blend/engine.py | 25 ++++++++---- tools/simulators/blend/src/blend/metrics.py | 7 +++- .../simulators/blend/src/blend/propagation.py | 7 ++-- tools/simulators/blend/src/blend/sweep.py | 7 ++-- tools/simulators/blend/tests/test_timing.py | 37 ++++++++++++++++++ 14 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 reports/blend/data/timing/adversary.parquet create mode 100644 reports/blend/data/timing/deanon.parquet create mode 100644 reports/blend/data/timing/propagation.parquet create mode 100644 reports/blend/data/timing/traffic.parquet create mode 100644 tools/simulators/blend/configs/timing.yaml diff --git a/reports/blend/README.md b/reports/blend/README.md index cf7ea7c..04f1493 100644 --- a/reports/blend/README.md +++ b/reports/blend/README.md @@ -458,7 +458,7 @@ One objection needs disposing of: the clock figures above hand the observer the ## 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/_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)`; 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`, 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/_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`, 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 +466,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. 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-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. diff --git a/reports/blend/data/README.md b/reports/blend/data/README.md index eb52f24..b7230ac 100644 --- a/reports/blend/data/README.md +++ b/reports/blend/data/README.md @@ -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` | +| `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 tables rather than separate measurements, so they have no run of their own — `blend.linkability` diff --git a/reports/blend/data/timing/adversary.parquet b/reports/blend/data/timing/adversary.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ba024a25324c61eb6492655d88e398ca20f3e327 GIT binary patch literal 8910 zcmc&)-ESLN6(6UK>xQ=5mfCWpAfnJ6E!ynHcH(ZmYAIuGx` z&p6de2nlJ$!>UgRc;Er{FF*+K0Eh=30PO=>A@SCSeb@)uKLDyeaL&x!@p$go&TdP0 z)Q<0*bMO6~uY1n9Y7e-4GE#gsoqXy&}Q&J+LZTJj^f`eYbp;P0(og zz^;Jjv_9d4i8&aQoTeIROV>^7FPynfncoC3AY2Y6oJk0p>Y>m?2tBg2ahoO_Ae`|C zm7uBf5WbD}hlfTWoN$1Q5P^)xR1btmFk(9;kXU3EeYf6QG;#K=U)n8%6UG(BgLpL6 z%Po=MmJRs5+e`oYb^DM1zGKAV@{>CswSn@DCm6%7NFbbWfUH4Cn(75P;U`mGFDnwx zi$((3hxQZU*a$yFC1|RrpB!fU`j4L+{{EN$y<@I?b_cC^`zOyl%tkoj09k{GG;^LsD z2nJrIt$@%IXcsl%7)3EzOvt~yviy`~R)S}j)VaElP?!v4hxMMFWB+~*5UEsR)JvLrYDDvxIFSByp8fDVAkpOu zb)%`LMzRl~1MEj%VLw>{FuIZ<7)?DjnCplW-+k(1KfS>I^a4QA#S9^7>Rd}=KbAQ+0U*5GF|gfSDHFkS7P4D#O=`a`8D?AYk)|X zKt!aeGZ8}{1w(!|5c(hlnKkThL;Eds!@s%oaIK-s$}yBEwXf;jwRcy)`~CIRHS@D# z@Dvx-_8?}uk4ZU|`3m52ptv#a2n$i&)`z1mfeH0C8lV2*7*%o@CjX5t7(C#sfgiXWJ{Qn0y zrtTTogzbWM9F@uJ;9$&(W4m%p(VgbI1bC5nwl^orO=d)j?Sgiw_=R-$ydKalm^$rc zi{reJc!=RybulTNc-wZ!p*}&&!?LCg;4&g68 zFy7*lH7?C@H7->%XvyRRe~84pHa}C_F~8w$>|*e<95>`(9sJ@ZNiGRw_~y9nEX+^7 zlhxEdpDApWV!461Q|O8Me&b#vDVFXJYgx^rsfo6zSFN$?9VksHPIJic~u%^aj#*y z7(XhPa#eXp9a?*|Mg1_AOBwvWT#OEtLL#D+k}Ar8a?9K0h`E1Ml{0BrpRXfN)C*m4 z2V~rtkm-mo$|D}|_flMg*Hl%^T2+#G_*7II)*Pdyj3vtBE?lQ=TS|R*I#Mg}# zQ*C!5?_^E877G!Op)P0cVjP0skGoZfMP;;e^jmBb%P=mWw%6EEf85Ix*r)+%*3V#Z0=%gHEEJ zuS!~B_{wC|;#KtXjI(LS<*PUckPBL3EUrpce0ca0@=1LlYK0To%m=R`Uku864&s|v z`E;TwZh}8eY)vQ@xXbE^ESmSe|lX$Q{iTzeLpVn!W;O}_+tI*XEt0xof=6yQ_|P)M8h>oF-3d(Ylhx`o~wLOaac>)VS@HOLZe(l{S+q)~4xk z>+A`>KrSyBeNA27$&ZBMZdA7V9c9z(cOXYYT}q6w$AEg&g*i}%cH>4rq9@}p9*LWh zasD6|jHH-45F6MJWk2C}+HsCkK9W>|YhxSZ5&#;-}fqIkRWBU!y z{=qw@zLtL8@=$pgR;C17p3HaNoNaGI9%qiWLscj$H>+1#y53zH72EXB61ifiNg-GVC`5cGl zMj^mE;6Ior0T&)qI2_sbamOFt={WGWs6h{2+Q;>fFlu<$GpFKObN|~+c(pBbe$ZEX kZ%OU;TgD}J)w-mvn%CFmr|?ghr~Jcw>uU^i0RNNae?x`VrT_o{ literal 0 HcmV?d00001 diff --git a/reports/blend/data/timing/deanon.parquet b/reports/blend/data/timing/deanon.parquet new file mode 100644 index 0000000000000000000000000000000000000000..af0bd144f4408868e18ac39e8f02f9db98c97c47 GIT binary patch literal 14130 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 literal 0 HcmV?d00001 diff --git a/reports/blend/data/timing/propagation.parquet b/reports/blend/data/timing/propagation.parquet new file mode 100644 index 0000000000000000000000000000000000000000..dd4c439a92ecece9ae4d91b4dca930a6d6f0cbd4 GIT binary patch literal 16681 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 E3ytYfg#Z8m literal 0 HcmV?d00001 diff --git a/reports/blend/data/timing/traffic.parquet b/reports/blend/data/timing/traffic.parquet new file mode 100644 index 0000000000000000000000000000000000000000..cc9ab13f5f62172d1e7b5f1f1b39be9179c70521 GIT binary patch literal 28341 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 Y@Lvps|5WBT^^YKz!GHA){txT^4=~YeTL1t6 literal 0 HcmV?d00001 diff --git a/tools/simulators/blend/Makefile b/tools/simulators/blend/Makefile index f9fe822..2e597a3 100644 --- a/tools/simulators/blend/Makefile +++ b/tools/simulators/blend/Makefile @@ -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 figures verify test lint clean +.PHONY: install smoke sweep sweep-fullscale redundancy percolation correlated-churn timing 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 +timing: $(STAMP) ## release designs under a timing attack (jitter vs clock tick) + $(PY) -m blend.sweep --config configs/timing.yaml + correlated-churn: $(STAMP) ## correlated AS/region outages vs uniform churn, matched fractions $(PY) -m blend.sweep --config configs/correlated-churn.yaml diff --git a/tools/simulators/blend/configs/timing.yaml b/tools/simulators/blend/configs/timing.yaml new file mode 100644 index 0000000..1b16f01 --- /dev/null +++ b/tools/simulators/blend/configs/timing.yaml @@ -0,0 +1,30 @@ +# Release designs under a timing attack (report section 3.11). +# +# A blend relay holds a message and re-emits it; a node merely forwarding passes it straight +# through. If that delay is visible so is the relay's role, and matching an outgoing message to the +# incoming one links both ends of a hop. This sweeps the two ways of delaying at a MATCHED latency +# budget, so they differ only in HOW they delay: +# +# clock -- the free-running tick: everything held leaves together at the next tick. +# jitter -- each message waits its own independent draw, same mean as the clock's residual. +# +# min_blend_delay 0 vs 1 tests whether forbidding instant re-release buys anything. It should not: +# a zero-length gap is instantaneous, so it never covers an arrival and is never sampled. +# +# The rate is swept because section 3.10 predicts the answer is dominated by traffic, not delay -- +# a relay holding ~one message has nothing to be confused with under either design. +n_nodes: [2000] +degree: [8] +blend_hops: [3] +max_blend_delay: [30] +min_blend_delay: [0, 1] +release_mode: [clock, jitter] +cover_rate_mult: [1.0, 16.0, 64.0] +unresponsive_frac: [0.0] +f_adv: [0.2] +adversary_mode: [random] +seeds: 3 +base: + traffic_window_slots: 120 + n_rounds: 10 # propagation is not the subject here + n_placements: 1 diff --git a/tools/simulators/blend/src/blend/config.py b/tools/simulators/blend/src/blend/config.py index 5d77489..06d95d8 100644 --- a/tools/simulators/blend/src/blend/config.py +++ b/tools/simulators/blend/src/blend/config.py @@ -199,6 +199,10 @@ class SweepConfig: """(blend_hops, max_blend_delay) settings each topology is measured over.""" return [(bh, md) for bh in self.blend_hops for md in self.max_blend_delay] + def release_designs(self) -> list[tuple[int, str]]: + """(min_blend_delay, release_mode) pairs the cover-traffic timeline is measured over.""" + return [(lo, mode) for lo in self.min_blend_delay for mode in self.release_mode] + def adv_grid(self) -> list[tuple[float, str]]: """(f_adv, adversary_mode) settings; f_adv==0 keeps only one (mode-irrelevant) row.""" out: list[tuple[float, str]] = [] diff --git a/tools/simulators/blend/src/blend/engine.py b/tools/simulators/blend/src/blend/engine.py index b6c2466..9463930 100644 --- a/tools/simulators/blend/src/blend/engine.py +++ b/tools/simulators/blend/src/blend/engine.py @@ -15,6 +15,8 @@ from .adversary import ( adversary_metrics, attribution_metrics, deanon_metrics, + mean_upstream_hops, + neighbourhood_confidence, place_adversary, ) from .config import WORSTCASE_MODES, SimConfig @@ -29,7 +31,7 @@ from .rng import ( stake_seedseq, traffic_seedseq, ) -from .traffic import simulate_window, traffic_metrics +from .traffic import simulate_window, timing_linkability, traffic_metrics def run_graph_cell(base: SimConfig, prop_grid: list[tuple[int, int]], @@ -37,6 +39,7 @@ def run_graph_cell(base: SimConfig, prop_grid: list[tuple[int, int]], adv_grid: list[tuple[float, str]], churn_modes: list[str] | None = None, cover_rates: list[float] | None = None, + release_designs: list[tuple[int, str]] | None = None, ) -> tuple[list[dict], list[dict], list[dict], list[dict]]: """Build ``base``'s topology once; return (propagation, adversary, deanon, traffic rows). @@ -50,6 +53,7 @@ def run_graph_cell(base: SimConfig, prop_grid: list[tuple[int, int]], graph-free and therefore computed separately. """ graph = build_graph(base) + upstream = mean_upstream_hops(graph, np.random.default_rng(base.root_seed)) blend_hops_set = sorted({bh for bh, _ in prop_grid}) modes = churn_modes or [base.churn_mode] @@ -79,6 +83,9 @@ def run_graph_cell(base: SimConfig, prop_grid: list[tuple[int, int]], adv_mask = place_adversary(graph, f_adv, mode, rng, base.worstcase_max_n) adv = adversary_metrics(graph, adv_mask) att = attribution_metrics(graph, adv_mask) + # upper end of the attribution bracket: the adversary also sees the message upstream + att = dict(att, upstream_hops=upstream, + neighbourhood_conf=neighbourhood_confidence(f_adv, upstream)) adv_rows.append(adversary_row(base, f_adv, mode, rep, adv)) for bh in blend_hops_set: for R in redundancies: @@ -95,13 +102,15 @@ def run_graph_cell(base: SimConfig, prop_grid: list[tuple[int, int]], quota = quota_summary(stake, f, base.n_nodes, base.slots_per_epoch, srng, base.stake_inference_ratio, rate) for blend_hops, max_blend_delay in prop_grid: - trng = np.random.default_rng( - traffic_seedseq(base, blend_hops, max_blend_delay, rate)) - win = simulate_window(graph, cfg, trng, base.traffic_window_slots, - max_blend_delay, blend_hops) - tm = traffic_metrics(win, cfg, max_blend_delay) - traffic_rows.append( - traffic_row(base, blend_hops, max_blend_delay, rate, tm, quota)) + for lo, mode in (release_designs or [(base.min_blend_delay, base.release_mode)]): + trng = np.random.default_rng( + traffic_seedseq(base, blend_hops, max_blend_delay, rate)) + win = simulate_window(graph, cfg, trng, base.traffic_window_slots, + max_blend_delay, blend_hops, mode, lo) + tm = traffic_metrics(win, cfg, max_blend_delay) + tl = timing_linkability(win, cfg, max_blend_delay, lo, mode) + traffic_rows.append( + traffic_row(base, blend_hops, max_blend_delay, rate, tm, quota, lo, mode, tl)) return prop_rows, adv_rows, deanon_rows, traffic_rows diff --git a/tools/simulators/blend/src/blend/metrics.py b/tools/simulators/blend/src/blend/metrics.py index 0015e56..df4d5a8 100644 --- a/tools/simulators/blend/src/blend/metrics.py +++ b/tools/simulators/blend/src/blend/metrics.py @@ -28,7 +28,9 @@ def propagation_row(config: SimConfig, blend_hops: int, max_blend_delay: int, def traffic_row(config: SimConfig, blend_hops: int, max_blend_delay: int, - cover_rate_mult: float, traffic: dict, quota: dict) -> dict: + cover_rate_mult: float, traffic: dict, quota: dict, + min_blend_delay: int | None = None, release_mode: str | None = None, + timing: dict | None = None) -> dict: """One cover-traffic cell: what the timeline measured, plus the epoch emission budget. ``traffic`` comes from the windowed simulation (blending, mixing, counts) and ``quota`` from @@ -39,6 +41,8 @@ def traffic_row(config: SimConfig, blend_hops: int, max_blend_delay: int, "degree": config.degree, "blend_hops": blend_hops, "max_blend_delay": max_blend_delay, + "min_blend_delay": config.min_blend_delay if min_blend_delay is None else min_blend_delay, + "release_mode": release_mode or config.release_mode, "cover_rate_mult": cover_rate_mult, "block_interval_slots": config.block_interval_slots, "slots_per_epoch": config.slots_per_epoch, @@ -48,6 +52,7 @@ def traffic_row(config: SimConfig, blend_hops: int, max_blend_delay: int, "traffic_window_slots": config.traffic_window_slots, **traffic, **quota, + **(timing or {}), } diff --git a/tools/simulators/blend/src/blend/propagation.py b/tools/simulators/blend/src/blend/propagation.py index 65a9d1a..ef4cc68 100644 --- a/tools/simulators/blend/src/blend/propagation.py +++ b/tools/simulators/blend/src/blend/propagation.py @@ -71,7 +71,7 @@ def assign_responsive(n: int, unresponsive_frac: float, rng: np.random.Generator def blend_round(graph: Graph, sender: int, relays: np.ndarray, jitter_mean_ms: float, max_blend_delay: int, rng: np.random.Generator, coverage_pcts: tuple[float, ...], responsive: np.ndarray | None = None, - stats: bool = True) -> dict: + stats: bool = True, min_blend_delay: int = 0) -> dict: """One Blend cascade. ``delivered`` is True iff every relay forwards and the final relay (which must be responsive) floods; delay fields are NaN on a dropped message. @@ -101,7 +101,7 @@ def blend_round(graph: Graph, sender: int, relays: np.ndarray, jitter_mean_ms: f legs_ok = False break legs += float(d) - mix_total = float(mix_wait(rng, max_blend_delay, k).sum()) + mix_total = float(mix_wait(rng, max_blend_delay, k, min_blend_delay).sum()) final_relay = int(sources[k]) final_ok = responsive is None or bool(responsive[final_relay]) @@ -177,7 +177,8 @@ def propagation_metrics(graph: Graph, blend_hops: int, max_blend_delay: int, relays = rng.choice(n - 1, size=blend_hops, replace=False) relays[relays >= sender] += 1 # blend_hops distinct nodes, all != sender rc = blend_round(graph, sender, relays, config.transport_jitter_mean_ms, - max_blend_delay, rng, pcts, responsive, stats=False) + max_blend_delay, rng, pcts, responsive, stats=False, + min_blend_delay=config.min_blend_delay) if not rc["delivered"]: continue a = rc["arrival"] diff --git a/tools/simulators/blend/src/blend/sweep.py b/tools/simulators/blend/src/blend/sweep.py index 131d00c..943ad13 100644 --- a/tools/simulators/blend/src/blend/sweep.py +++ b/tools/simulators/blend/src/blend/sweep.py @@ -38,9 +38,9 @@ def new_run_dir(outdir: Path, label: str) -> Path: def _cell_worker(base: SimConfig, prop_grid, unresponsive_fracs, redundancies, adv_grid, - churn_modes, cover_rates): + churn_modes, cover_rates, release_designs): return run_graph_cell(base, prop_grid, unresponsive_fracs, redundancies, adv_grid, - churn_modes, cover_rates) + churn_modes, cover_rates, release_designs) def run_sweep(sweep: SweepConfig, n_jobs: int = -1) -> tuple[pd.DataFrame, ...]: @@ -50,11 +50,12 @@ def run_sweep(sweep: SweepConfig, n_jobs: int = -1) -> tuple[pd.DataFrame, ...]: redundancies = list(sweep.redundancy) churn_modes = list(sweep.churn_mode) cover_rates = list(sweep.cover_rate_mult) + release_designs = sweep.release_designs() adv_grid = sweep.adv_grid() bases = [sweep.base_config(n, d, g) for (n, d, g) in cells] results = Parallel(n_jobs=n_jobs, prefer="processes")( delayed(_cell_worker)(base, prop_grid, unresponsive_fracs, redundancies, adv_grid, - churn_modes, cover_rates) + churn_modes, cover_rates, release_designs) for base in tqdm(bases, desc="topologies") ) prop_rows = [r for pr, _, _, _ in results for r in pr] diff --git a/tools/simulators/blend/tests/test_timing.py b/tools/simulators/blend/tests/test_timing.py index 15ace32..1cb58e4 100644 --- a/tools/simulators/blend/tests/test_timing.py +++ b/tools/simulators/blend/tests/test_timing.py @@ -117,3 +117,40 @@ def test_jitter_beats_the_clock_at_a_matched_delay_budget(): _, j = _run("jitter", rate=64.0, slots=60) assert j["map_success"] < c["map_success"] assert j["timing_set_mean"] > c["timing_set_mean"] + + +def test_release_design_is_a_real_sweep_axis(): + """Regression: min_blend_delay and release_mode were declared on SweepConfig, validated and + keyed, but never read by the sweep -- a YAML setting them was silently ignored.""" + from blend.config import SweepConfig + sw = SweepConfig(min_blend_delay=[0, 1], release_mode=["clock", "jitter"]) + assert sorted(sw.release_designs()) == [(0, "clock"), (0, "jitter"), + (1, "clock"), (1, "jitter")] + + +def test_the_engine_emits_a_timing_row_per_release_design(): + """The timing measures must land in a result table, not only in ad-hoc analysis.""" + from blend.config import SimConfig + from blend.engine import run_graph_cell + base = SimConfig(n_nodes=600, degree=8, traffic_window_slots=40, n_rounds=5, n_placements=1) + _, _, deanon_rows, traffic_rows = run_graph_cell( + base, [(3, 30)], [0.0], [1], [(0.2, "random")], + cover_rates=[1.0], release_designs=[(0, "clock"), (0, "jitter")]) + assert len(traffic_rows) == 2 + for row in traffic_rows: + for col in ("release_mode", "min_blend_delay", "timing_set_mean", "map_success"): + assert col in row, col + assert {r["release_mode"] for r in traffic_rows} == {"clock", "jitter"} + # and the attribution bracket reaches the deanon table + for col in ("attribution_conf_mean", "upstream_hops", "neighbourhood_conf"): + assert col in deanon_rows[0], col + + +def test_propagation_honours_the_minimum_interval(): + """Regression: the non-timeline path called mix_wait without the minimum, so the knob was + silently inert on the delay tables of 3.1-3.2.""" + import inspect + + from blend import propagation + src = inspect.getsource(propagation.propagation_metrics) + assert "min_blend_delay=config.min_blend_delay" in src