mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 03:33:33 +00:00
pd review: document the new columns, pin the coverage notion, drop dead code
Third review pass over the blend material. Completeness: - the simulator README documented neither frac_reached_live nor the three correlated-churn columns (churn_mode, n_regions, region_locality) that every run now writes, and its model section never described correlated outages at all; - the knowledge graph had no pd nodes -- graphify update had never been run since the simulator was added (2643 -> 2968 nodes). Correctness/coherence: - section 3.5 quotes coverage without saying which coverage, now that 3.9 distinguishes all-node from live-network. It is all-node; under uniform churn the two agree to 0.001, so nothing in 3.5 turns on it. Said so explicitly; - 3.9 named its groups AS/region without noting that link latency ignores them. Regions are failure and peering domains, not latency domains -- real co-located nodes would also be faster, so the clustered delays are if anything pessimistic. Redundancy: - style.band_plot was dead: never called by any figure. Removed, with the two imports it alone needed; - the units sentence appeared verbatim in the header note and again opening the model section. Dropped the second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
25d6463c23
commit
c42d030f0d
@ -49,8 +49,6 @@ A node's exposure is not only *whether* it is deanonymized but *how soon*: if ea
|
||||
<a id="s2"></a>
|
||||
## 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 |
|
||||
|
||||
@ -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/<dir>
|
||||
```
|
||||
|
||||
## 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 /
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user