mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 03:33:33 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
4915444c3e
commit
0c72a39e38
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user