diff --git a/reports/blend/pd/README.md b/reports/blend/pd/README.md index b2204ca..97c4cee 100644 --- a/reports/blend/pd/README.md +++ b/reports/blend/pd/README.md @@ -49,8 +49,6 @@ A node's exposure is not only *whether* it is deanonymized but *how soon*: if ea ## 2. Model -All delays are in milliseconds; the mix clock's maximum interval is in whole seconds. - **Peer graph.** A seeded random **d-regular** graph over a globally known node list: every node has exactly `degree` symmetric peers, and the whole graph is reconstructed identically by everyone from one global seed. It is realized as a vectorized union of `degree` random perfect matchings (simple, low-diameter, scalable to 10⁶). The graph is a pure function of the topology seed and is identical across every adversary and propagation setting measured on it. **Blend cascade (propagation).** Each round, a sender routes a message along a path of `blend_hops` relays — each a free-running timed-release mix — and the last relay floods it to the whole network. A transport leg between two nodes is the **directed shortest path** with edge weight `base(u,v) + Exp(jitter) + processing(u)`: a geographic base latency (metro 15 → antipodal 200 ms), an exponential transport jitter (mean 5 ms), and the relaying node's fixed processing lag (drawn once per node from {10, 50, 100} ms at {0.5, 0.4, 0.1}). At each relay the message waits the **residual** to that relay's next release on a free-running clock whose intervals are Uniform{0…`max_blend_delay`} seconds; the final flood is plain transport. The **full delay** is the sum of the transport legs, the per-hop mixing waits, and the broadcast to the last node. @@ -159,7 +157,7 @@ With a fraction `u` of unresponsive nodes, a message survives only if every rela Every entry sits within 0.007 of `(1 − u)^blend_hops` (SEM ≤ 0.009), so the law is exact to measurement precision in this regime. This compounds with the anonymity lever of §3.4: a 5-hop path that holds deanonymization to a few per mille also loses 41 % of messages at 10 % churn and 97 % at 50 % churn (**Fig 10**). The two are directly opposed — every hop bought for anonymity is paid for in delivery — which caps how long a usable path can be at a given churn level. -The second churn effect is on **coverage**: unresponsive nodes still receive but do not relay, so they are routing holes that can strand pockets during the final flood. Here a higher degree is decisive, and not gradually — **the collapse has a threshold, and it is exactly predictable.** Because unresponsive nodes relay nothing, the network that actually carries a flood is the sub-graph induced on the responsive nodes, and that is textbook *site percolation* on a random `d`-regular graph, whose giant component survives only while the responsive fraction exceeds `1/(degree − 1)`. The network therefore tolerates churn up to +The second churn effect is on **coverage**: unresponsive nodes still receive but do not relay, so they are routing holes that can strand pockets during the final flood. Coverage here — and everywhere in this section — is the fraction of **all** nodes reached, counting the unresponsive ones, which under uniform churn still receive because they sit among live peers. (§3.9 introduces a second notion, coverage of the *live* network only, because correlated failure separates the two. Under the uniform churn of this section they agree to within 0.001, so nothing below depends on which is meant.) Here a higher degree is decisive, and not gradually — **the collapse has a threshold, and it is exactly predictable.** Because unresponsive nodes relay nothing, the network that actually carries a flood is the sub-graph induced on the responsive nodes, and that is textbook *site percolation* on a random `d`-regular graph, whose giant component survives only while the responsive fraction exceeds `1/(degree − 1)`. The network therefore tolerates churn up to **`u_c = 1 − 1/(degree − 1)`** @@ -278,6 +276,8 @@ Expressing the cost as *time* rather than as a per-emission probability is what One modelling point decides whether this is interesting at all. If peering ignores regions, then dropping whole regions still removes a *uniformly random* set of nodes, and correlated churn is statistically indistinguishable from uniform churn — the clustering exists in the operator's world but not in the graph's. Correlation only matters when a failure domain is also a **connectivity** domain, so these runs place 75 % of every node's peers inside its own region (40 domains, N = 20 000, 6 400 rounds/cell). +A region here is a **failure domain and a peering domain, but not a latency domain**: link delays are still drawn from the same geographic mixture regardless of whether a link is intra- or inter-region. Real co-located nodes would also talk to each other faster, which would make intra-region flooding quicker than modelled — so the delay figures in this section are, if anything, pessimistic for the clustered case. Nothing in the coverage or delivery results depends on it, since those turn on reachability rather than speed. + The result runs opposite to the intuition that correlated failure is the harsher case: | degree | u | live coverage (uniform → regional) | all-node coverage | delivery | diff --git a/tools/simulators/blend/pd/README.md b/tools/simulators/blend/pd/README.md index a157953..0fcbec1 100644 --- a/tools/simulators/blend/pd/README.md +++ b/tools/simulators/blend/pd/README.md @@ -54,6 +54,14 @@ adversary metrics are exact at every N). responsive fraction exceeds `1/(degree−1)`. A network tolerates churn up to `u_c = 1 − 1/(degree−1)` (degree 3 → 0.5, degree 6 → 0.8, degree 16 → 0.93) and shatters above it; `configs/percolation.yaml` walks u across the threshold and `make verify` checks it. +- **Correlated outages:** `n_regions` splits the network into equal-sized **failure domains** and + `region_locality` places that share of each node's peers inside its own domain (the locality + matchings keep the graph exactly d-regular). `churn_mode: regional` then fails whole domains + instead of scattered nodes, at an identical dead-node count. Locality is what makes this differ + from uniform churn at all — with region-blind peering, dropping whole regions removes a uniformly + random node set. Clustered failure leaves the survivors fully connected (`frac_reached_live` + stays ~1) while stranding the dead domains (`frac_reached` falls); see `configs/correlated-churn.yaml`. + Regions are failure and peering domains only — link latency does not depend on them. - **Linkability over time** (`pd.linkability`): given the rates above and an emission cadence (one node emits per 30 s slot, chosen ∝ stake), the module derives the *time to link* an emitter (`≈ 30 s·ln(1/(1−α))/(stake·q)`, inverse in stake) and the *time to learn its stake* to a threshold @@ -73,8 +81,10 @@ make figures RUN=runs/ ``` ## Outputs -Three parquets per run: `propagation.parquet` (`full_delay_ms_*`, `delivery_rate`, `frac_reached`, -`coverN_ms` vs degree / blend_hops / N / unresponsive_frac / redundancy), `adversary.parquet` +Three parquets per run: `propagation.parquet` (`full_delay_ms_*`, `delivery_rate`, `coverN_ms`, and +two coverage columns — `frac_reached` over all nodes and `frac_reached_live` over the responsive +ones, which diverge under correlated churn — vs degree / blend_hops / N / unresponsive_frac / +`churn_mode` / `n_regions` / `region_locality` / redundancy), `adversary.parquet` (`observed_frac` / `eclipsed_frac` vs degree / f_adv / mode, random + worst-case envelope), and `deanon.parquet` (`deanon_rate` / `full_deanon_rate` vs degree / blend_hops / redundancy / f_adv / mode — propagation paths crossed with the adversary set). Figures render all three: delay vs degree / diff --git a/tools/simulators/blend/pd/src/pd/plotting/style.py b/tools/simulators/blend/pd/src/pd/plotting/style.py index bb30d53..3e9f173 100644 --- a/tools/simulators/blend/pd/src/pd/plotting/style.py +++ b/tools/simulators/blend/pd/src/pd/plotting/style.py @@ -7,12 +7,10 @@ series; ``cividis`` (perceptually uniform, CVD-safe) for heatmaps. Figures are s from __future__ import annotations -from collections.abc import Sequence from pathlib import Path import matplotlib as mpl import matplotlib.pyplot as plt -import numpy as np # Okabe-Ito colorblind-safe qualitative palette OKABE_ITO = [ @@ -59,24 +57,6 @@ def color_for(index: int) -> str: return OKABE_ITO[index % len(OKABE_ITO)] -def band_plot( - ax: plt.Axes, - x: Sequence[float], - series: np.ndarray, - *, - color: str, - label: str | None = None, - percentiles: tuple[float, float] = (10, 90), -) -> None: - """Plot the mean of ``series`` (shape ``(n_replicates, len(x))``) with a percentile band.""" - x = np.asarray(x, dtype=float) - mean = np.nanmean(series, axis=0) - lo = np.nanpercentile(series, percentiles[0], axis=0) - hi = np.nanpercentile(series, percentiles[1], axis=0) - ax.plot(x, mean, color=color, label=label) - ax.fill_between(x, lo, hi, color=color, alpha=0.18, linewidth=0) - - def save(fig: plt.Figure, out_stem: str | Path, provenance: str | None = None) -> list[Path]: """Save ``fig`` as a 300-dpi PNG. ``out_stem`` has no suffix. Returns written paths.""" out_stem = Path(out_stem)