From 0c72a39e382e1beeb7d63ddc4d5f8c3f2f3a7dd5 Mon Sep 17 00:00:00 2001 From: Marcin Pawlowski Date: Thu, 6 Aug 2026 12:46:31 +0200 Subject: [PATCH] blend: show the clock design is not handicapped by the adversary assumption The jitter-vs-clock verdict rested on handing the clock adversary the tick schedule, which is the conservative choice but also the harsher one -- a real observer sees only ticks that released something. Rather than assume, both are now measurable, and they are provably equivalent. A silent tick implies nothing was pending at it, and any arrival older than the previous release has demonstrably already left. So the candidate window bounded by the true previous tick and the one bounded by the previous observed release hold the same arrivals. Confirmed identical to 1e-9 in both the sparse regime (one hold per relay) and the dense one (56 holds per relay), where the two coincide for different reasons. That removes the objection to the comparison: jitter beats the clock at a matched delay budget on merit, not because the clock was given a stronger opponent. Co-Authored-By: Claude Opus 5 (1M context) --- tools/simulators/blend/src/blend/traffic.py | 28 +++++++++++++++----- tools/simulators/blend/tests/test_timing.py | 29 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/tools/simulators/blend/src/blend/traffic.py b/tools/simulators/blend/src/blend/traffic.py index 2eacd8d..fc81f46 100644 --- a/tools/simulators/blend/src/blend/traffic.py +++ b/tools/simulators/blend/src/blend/traffic.py @@ -261,7 +261,8 @@ def traffic_metrics(win: TrafficWindow, config: SimConfig, def timing_linkability(win: TrafficWindow, config: SimConfig, max_blend_delay: int | None = None, min_blend_delay: int | None = None, - release_mode: str | None = None) -> dict: + release_mode: str | None = None, + adversary_knows_schedule: bool = True) -> dict: """Can an observer match a relay's outgoing message to the incoming one, from timing alone? This is the attack that distinguishes a *blended* message from a merely *relayed* one: a relay @@ -277,6 +278,13 @@ def timing_linkability(win: TrafficWindow, config: SimConfig, weighted by the delay density (exponential here). Nothing is quantised, so the weights decay smoothly and the posterior concentrates on whichever arrival is closest to the expected lag. + ``adversary_knows_schedule`` selects how much the clock design concedes. A free-running clock + ticks whether or not anything is held, but only ticks that *release* something are visible. The + strong adversary (default) is handed the true schedule and can exclude everything before the + previous tick; the weak one sees only the node's previous release, so silent ticks widen its + candidate window. Jitter has no schedule to know, so the switch does not affect it -- which is + exactly why the comparison has to be run both ways. + ``linked_frac`` is the share of releases whose set collapses to one candidate: the message is then linked with certainty, whatever the nominal delay was. @@ -301,14 +309,20 @@ def timing_linkability(win: TrafficWindow, config: SimConfig, for node, holds in by_node.items(): arrivals = np.sort(np.array([h.arrived for h in holds])) true_src = {h.released: h.arrived for h in holds} # the arrival that really produced it - for r in sorted({h.released for h in holds}): + rel_times = sorted({h.released for h in holds}) + for r in rel_times: if mode == "clock": - clock = win.clocks.get(node) prev = 0.0 - if clock is not None: - earlier = [t for t in clock.ticks_in(-1e18, r) if t < r] - if earlier: - prev = earlier[-1] + if adversary_knows_schedule: + clock = win.clocks.get(node) + if clock is not None: + earlier = [t for t in clock.ticks_in(-1e18, r) if t < r] + if earlier: + prev = earlier[-1] + else: + seen = [t for t in rel_times if t < r] # only releases are observable + if seen: + prev = seen[-1] cand = arrivals[(arrivals > prev) & (arrivals <= r + 1e-12)] n_c = max(len(cand), 1) sets.append(float(n_c)) # uniform posterior -> perplexity = n diff --git a/tools/simulators/blend/tests/test_timing.py b/tools/simulators/blend/tests/test_timing.py index 2a20244..15ace32 100644 --- a/tools/simulators/blend/tests/test_timing.py +++ b/tools/simulators/blend/tests/test_timing.py @@ -88,3 +88,32 @@ def test_perplexity_flatters_jitter_more_than_the_best_guess_does(): map_gain = (1 - j["map_success"]) / (1 - c["map_success"]) assert set_gain > 1.0 and map_gain > 1.0 # jitter wins on both assert set_gain > map_gain # but the set measure overstates by how much + + +def test_knowing_the_tick_schedule_gains_the_adversary_nothing(): + """The clock design concedes the same whether or not the observer knows the tick times. + + A silent tick implies nothing was pending at it, and any arrival older than the previous + release has demonstrably already left. So the candidate window bounded by the true previous + tick and the one bounded by the previous observed release contain the same arrivals. This + matters because it removes the obvious objection to the jitter-vs-clock comparison: the clock + design is not being handicapped by a generous adversary assumption. + """ + for n, rate, slots in ((2000, 1.0, 120), (200, 32.0, 90)): # sparse and dense + cfg = SimConfig(n_nodes=n, degree=8, blend_hops=3, max_blend_delay=30, + release_mode="clock", cover_rate_mult=rate) + g = build_graph(cfg) + w = simulate_window(g, cfg, np.random.default_rng(5), slots) + strong = timing_linkability(w, cfg, adversary_knows_schedule=True) + weak = timing_linkability(w, cfg, adversary_knows_schedule=False) + assert abs(strong["timing_set_mean"] - weak["timing_set_mean"]) < 1e-9 + assert abs(strong["map_success"] - weak["map_success"]) < 1e-9 + + +def test_jitter_beats_the_clock_at_a_matched_delay_budget(): + """The verdict, on the measure a heavy tail cannot flatter: at equal mean delay the + independent-draw design leaves the adversary's best guess wrong more often.""" + _, c = _run("clock", rate=64.0, slots=60) + _, j = _run("jitter", rate=64.0, slots=60) + assert j["map_success"] < c["map_success"] + assert j["timing_set_mean"] > c["timing_set_mean"]