Author: Lucas Luhur Last updated: 2026-07-07
Open question:
- Cover composition — traffic-level only vs traffic-level + internal cover
- traffic-level cover → other validators sending decoys into the anonymity system
- internal cover → the anonymity system generating its own decoys
- the open question is whether a layer also adds its own layer-internal cover on top — Chaum uniform / Loopix Poisson emitted by the layer's own infrastructure. This is only for mixnet: tor_fifo / dandelion++ have no internal cover by protocol
The experiments framework is the composition layer that turns the validated modules (consensus → broadcast network → anonymity → adversary → measures) into swept, reproducible runs. This also delivers the first real anonymisation layer, tor_fifo .
It treats every pipeline stage as a component plugged by name from a registry: change one string in a config and the layer / attack / measure swaps out, with the framework core untouched.
| Parameter | Symbol | Meaning | Value |
|---|---|---|---|
| Mix hops | $k$ | relay hops on the shared path | 3 |
| Mixing delay | $μ$ | mean per-hop hold, slots | 1 |
| Cover per slot | $ | S | $ |
| Cover policy | — | peer selection: flat / stake_weighted |
flat |
| Seed / reps | — | master seed + repetitions per cell (reproducibility) | 0 / 3 |
Inherited from the consensus modules: $N = 1000, T = 388,800, f = 1/30, Pareto\ k = 1.33$
run_once composes one experiment cell end to end, each stage selected by name from its registry.
def run_once(cfg, rng=None):
rng = np.random.default_rng(rng)
validate_pairing(cfg.attack, cfg.measures, ATTACKS, MEASURES) # guess-type wall
# consensus (the system) -> real winner events
alpha = sample_relative_stakes(cfg.N, cfg.shape, rng=rng)
slots, nodes = simulate_events(alpha, f=cfg.f, T=cfg.T, rng=rng)
# traffic-level cover -> augmented, tagged, batched stream
slots, nodes, is_dummy, group = inject_dummies(
slots, nodes, cfg.N, params=cfg.dummy, alpha=alpha, T=cfg.T, rng=rng)
# defence (plug by name) -> the Trace
trace = LAYERS[cfg.layer](slots, nodes, is_dummy, group,
params=cfg.layer_params, latency_oracle=None, rng=rng)
# attacker + scorer (plug by name, type-matched)
guess = ATTACKS[cfg.attack].run(trace, params=cfg.attack_params, rng=rng)
return {m: MEASURES[m].score(guess, ...) for m in cfg.measures}
The fixed data contract, left to right:
$$ \text{consensus} \to \texttt{inject\_dummies} \to \texttt{LAYERS}[\text{layer}] \to \texttt{ATTACKS}[\text{attack}] \to \{\texttt{MEASURES}[m]\} $$
Each run has an independent, decorrelated RNG (SeedSequence(seed).spawn) so every run is bit-reproducible.
Trace — the one load-bearing contractEvery layer emits the same schema, and the downstream attack consumes only it (never the ground truth). This makes layers interchangeable: tor-like/mixnet/dandelion++ will follow the same pipeline.