diff --git a/reports/tsi/tsi-report-1-overview-and-recommendations.md b/reports/tsi/tsi-report-1-overview-and-recommendations.md index 4491dfb..c5df0eb 100644 --- a/reports/tsi/tsi-report-1-overview-and-recommendations.md +++ b/reports/tsi/tsi-report-1-overview-and-recommendations.md @@ -8,7 +8,7 @@ --- -> **The uncle-reference model analysed here.** Uncle references are *counting-only* — they never affect a block's validity ([§6.7](tsi-report-3-robustness-and-incentives.md#s6-7)–[§6.8](tsi-report-3-robustness-and-incentives.md#s6-8)) — and are deduplicated by **slot**, not by block ([§8.5](#s8-5)). The reference window is derived from a **window absorption parameter**, `w_u = W_abs/f` with `W_abs = 10` expected block-intervals and bound `W_abs ≤ 0.6·k`, which is the `W = 300 = 10/f` sizing rule of [§3.4](tsi-report-2-accuracy-and-design.md#s3-4). One structural constraint shapes the results: only the **first block of a fork** — one whose parent lies on the referencing chain — is countable, which is the price of verifying every reference from chain data alone. This report calls that the **countable** model, and measures it against an **unrestricted** baseline in which any orphan in the window is referenceable at any fork depth ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1), [§3.2](tsi-report-2-accuracy-and-design.md#s3-2), [§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The result: **in the design regime `ρ < 1` the two are practically identical.** A dedicated 40-replicate sweep of the operating band (`δ_max` 1–5, `ρ ≈ 0.25–0.45`) puts every `U ≥ 1` cell of both models in 0.998–1.001, resolves no difference in any individual cell (widest 95 % CI ±0.15 pp), and bounds the pooled first-fork cost below **0.15 %** — rising monotonically with delay and separating from zero only at the top of the band ([§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a)). Differences appear only at `ρ ≥ 1`: the restriction trims ~1.4 % at `ρ ≈ 1`, and under overload (`ρ ≈ 1.8`) recovery saturates near 0.95 where the unrestricted model reaches ~1.00. Deep forks are unrecoverable by construction, so past `ρ ≈ 1` the ceiling is set by the fork *depth* profile and **no uncle cap buys it back** — a second, independent reason for the [§8](#s8) "keep `ρ < 1`" rule. The closed-form accuracy with the measured effective utilization, `log(1−f)/log(1−f/q_u)`, predicts every countable cell to within 0.2 %. +> **The uncle-reference model analysed here.** Uncle references are *counting-only* — they never affect a block's validity ([§6.7](tsi-report-3-robustness-and-incentives.md#s6-7)–[§6.8](tsi-report-3-robustness-and-incentives.md#s6-8)) — and are deduplicated by **slot**, not by block ([§8.5](#s8-5)). The reference window is derived from a **window absorption parameter**, `w_u = W_abs/f` with `W_abs = 10` expected block-intervals and bound `W_abs ≤ 0.6·k`, which is the `W = 300 = 10/f` sizing rule of [§3.4](tsi-report-2-accuracy-and-design.md#s3-4). One structural constraint shapes the results: only the **first block of a fork** — one whose parent lies on the referencing chain — is countable, which is the price of verifying every reference from chain data alone. This report calls that the **countable** model, and measures it against an **unrestricted** baseline in which any orphan in the window is referenceable at any fork depth ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1), [§3.2](tsi-report-2-accuracy-and-design.md#s3-2), [§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The result: **in the design regime `ρ < 1` the two are practically identical.** A dedicated 40-replicate sweep of the operating band (`δ_max` 1–5, `ρ ≈ 0.21–0.41`) puts every `U ≥ 1` cell of both models in 0.998–1.001, resolves no difference in any individual cell (widest 95 % CI ±0.15 pp), and bounds the first-fork cost at **≤ 0.2 %** — indistinguishable from zero through `δ_max` ≤ 3 and rising monotonically with delay thereafter ([§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a)). Differences appear only at `ρ ≥ 1`: the restriction trims ~1.4 % at `ρ ≈ 1`, and under overload (`ρ ≈ 1.8`) recovery saturates near 0.95 where the unrestricted model reaches ~1.00. Deep forks are unrecoverable by construction, so past `ρ ≈ 1` the ceiling is set by the fork *depth* profile and **no uncle cap buys it back** — a second, independent reason for the [§8](#s8) "keep `ρ < 1`" rule. The closed-form accuracy with the measured effective utilization, `log(1−f)/log(1−f/q_u)`, predicts every countable cell to within 0.2 %. ## 1. Executive summary diff --git a/reports/tsi/tsi-report-2-accuracy-and-design.md b/reports/tsi/tsi-report-2-accuracy-and-design.md index 577038e..00d9f2b 100644 --- a/reports/tsi/tsi-report-2-accuracy-and-design.md +++ b/reports/tsi/tsi-report-2-accuracy-and-design.md @@ -116,12 +116,12 @@ The U = 0 under-count **deepens with N** (more nodes → more concurrent proposa | `δ_max` (s) | ρ | U | countable | unrestricted | gap (t) | |---|---|---|---|---|---| -| 4 | ≈ 0.4 | 1 / 2 / 4 | 0.9979 ± 0.0015 / 0.9982 ± 0.0014 / 0.9995 ± 0.0012 | 0.9981 ± 0.0006 / 0.9985 ± 0.0012 / 0.9992 ± 0.0013 | *none resolved* (0.1–0.2) | -| 8 | ≈ 0.6 | 1 / 2 / 4 | 0.9965 ± 0.0015 / 0.9955 ± 0.0017 / 0.9963 ± 0.0015 | 0.9986 ± 0.0017 / 0.9993 ± 0.0016 / 0.9989 ± 0.0017 | *none resolved* (1.0–1.6) | -| 16 | ≈ 1.0 | 1 | 0.9647 ± 0.0065 | 0.9610 ± 0.0043 | *not resolved* (0.5) | -| 16 | ≈ 1.0 | 2 / 4 | 0.9862 ± 0.0010 / 0.9911 ± 0.0032 | 0.9999 ± 0.0005 / 1.0013 ± 0.0011 | −0.014 (12.1) / −0.010 (3.0) | -| 32 | ≈ 1.8 | 1 | 0.608 ± 0.069 | 0.570 ± 0.042 | *not resolved* (0.5) | -| 32 | ≈ 1.8 | 2 / 4 | 0.9343 ± 0.0015 / 0.9517 ± 0.0007 | 0.9774 ± 0.0064 / 1.0020 ± 0.0010 | −0.043 (6.5) / −0.050 (41.0) | +| 4 | ≈ 0.36 | 1 / 2 / 4 | 0.9979 ± 0.0015 / 0.9982 ± 0.0014 / 0.9995 ± 0.0012 | 0.9981 ± 0.0006 / 0.9985 ± 0.0012 / 0.9992 ± 0.0013 | *none resolved* (0.1–0.2) | +| 8 | ≈ 0.56 | 1 / 2 / 4 | 0.9965 ± 0.0015 / 0.9955 ± 0.0017 / 0.9963 ± 0.0015 | 0.9986 ± 0.0017 / 0.9993 ± 0.0016 / 0.9989 ± 0.0017 | *none resolved* (1.0–1.6) | +| 16 | ≈ 0.96 | 1 | 0.9647 ± 0.0065 | 0.9610 ± 0.0043 | *not resolved* (0.5) | +| 16 | ≈ 0.96 | 2 / 4 | 0.9862 ± 0.0010 / 0.9911 ± 0.0032 | 0.9999 ± 0.0005 / 1.0013 ± 0.0011 | −0.014 (12.1) / −0.010 (3.0) | +| 32 | ≈ 1.76 | 1 | 0.608 ± 0.069 | 0.570 ± 0.042 | *not resolved* (0.5) | +| 32 | ≈ 1.76 | 2 / 4 | 0.9343 ± 0.0015 / 0.9517 ± 0.0007 | 0.9774 ± 0.0064 / 1.0020 ± 0.0010 | −0.043 (6.5) / −0.050 (41.0) | **The `U = 0` rows are a negative control.** With no uncles the two models are identical by construction — no reference is ever taken, so any measured gap is pure between-run noise (the two models draw independent RNG streams by design, so the comparison is unpaired). That control reads **+0.010 (t = 0.8) at `δ_max = 4`** but **−0.230 (t = 2.1) at `δ_max = 32`**, where single replicates range from 0.05 to 0.60. Read the table against that floor: at `δ_max = 32` a gap must clear ~0.2 to mean anything on a single-replicate basis, which is exactly why the `U = 1` cells there are reported as unresolved while the tightly-clustered `U = 2` and `U = 4` cells are not. @@ -144,29 +144,31 @@ So: **at the operating loads (`ρ < 1`) no difference between the models is dete **The question.** [§3.2](#s3-2) samples the mixing delay at 4/8/16/32. That resolves the overload regime, but it leaves the band the parameters are actually chosen in — the low-delay end, where every operating point sits — measured at four-fold spacing and five replicates, which is enough to say "no difference detected" and nothing more. How accurate is TSI across `δ_max` = 1–5, and how large a first-fork cost can be *excluded* there? -**The finding.** With one uncle slot the estimate is exact across the whole band: every `U ≥ 1` cell under both referencing models lands in **0.998–1.001**, flat in delay. Without uncles the same band decays steeply, 0.810 → 0.640. Through `δ_max ≤ 4` the two referencing models are **indistinguishable**, and the first-fork restriction begins to cost something measurable only at the top of the band. +**The finding.** One uncle slot holds the estimate at the true stake across the whole band: every `U ≥ 1` cell under both referencing models lands in **0.998–1.001**, flat in delay, while the uncle-free baseline decays 0.810 → 0.640. The two referencing models are **indistinguishable through `δ_max` ≤ 3**; at the top of the band the first-fork restriction becomes measurable and costs **0.1–0.2 %**. Two independent tests — the model-vs-model gap, and each model against the exact target 1.0 — agree on that onset. -`configs/fine-delay.yaml` spends replicates instead of range — `δ_max` ∈ {1,2,3,4,5}, `U` ∈ {0,1,2,4}, **40 replicates** per cell (8× [§3.2](#s3-2)), N = 1 000, blend, 3 hops — run under both models ([§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The band spans `ρ ≈ 0.25` to `0.45`, entirely inside the design regime. +`configs/fine-delay.yaml` spends replicates instead of range — `δ_max` ∈ {1,2,3,4,5}, `U` ∈ {0,1,2,4}, **40 replicates** per cell (8× [§3.2](#s3-2)), N = 1 000, blend, 3 hops — run under both models ([§9](tsi-report-4-reproducibility-and-appendices.md#s9)). The band spans `ρ ≈ 0.21` to `0.41`, entirely inside the design regime. | `δ_max` (s) | ρ | `U=0` countable / unrestricted | `U≥1` countable | `U≥1` unrestricted | pooled gap (t) | |---|---|---|---|---|---| -| 1 | ≈ 0.25 | 0.810 / 0.806 | 0.9994 – 0.9999 | 0.9990 – 1.0010 | −0.0004 ± 0.0007 (1.1) | -| 2 | ≈ 0.30 | 0.762 / 0.763 | 0.9996 – 1.0002 | 0.9996 – 1.0003 | +0.0000 ± 0.0008 (0.0) | -| 3 | ≈ 0.35 | 0.716 / 0.723 | 0.9995 – 1.0002 | 0.9992 – 1.0004 | −0.0001 ± 0.0008 (0.2) | -| 4 | ≈ 0.40 | 0.664 / 0.668 | 0.9988 – 0.9996 | 0.9995 – 0.9999 | −0.0005 ± 0.0008 (1.3) | -| 5 | ≈ 0.45 | 0.640 / 0.623 | 0.9982 – 0.9988 | 0.9995 – 1.0003 | **−0.0014 ± 0.0007 (3.7)** | +| 1 | ≈ 0.21 | 0.810 / 0.806 | 0.9994 – 0.9999 | 0.9990 – 1.0010 | −0.0004 ± 0.0007 (1.1) | +| 2 | ≈ 0.26 | 0.762 / 0.763 | 0.9996 – 1.0002 | 0.9996 – 1.0003 | +0.0000 ± 0.0008 (0.0) | +| 3 | ≈ 0.31 | 0.716 / 0.723 | 0.9995 – 1.0002 | 0.9992 – 1.0004 | −0.0001 ± 0.0008 (0.2) | +| 4 | ≈ 0.36 | 0.664 / 0.668 | 0.9988 – 0.9996 | 0.9995 – 0.9999 | −0.0005 ± 0.0008 (1.3) | +| 5 | ≈ 0.41 | 0.640 / 0.623 | 0.9982 – 0.9988 | 0.9995 – 1.0003 | **−0.0014 ± 0.0007 (3.7)** | **No individual cell resolves a model difference.** Across the 15 `U ≥ 1` cells the widest 95 % CI half-width is ±0.0015, one cell clears `t = 2` (0.75 are expected to by chance), and its `t = 2.59` does not survive the Bonferroni threshold of 2.94 for 15 tests. Read cell by cell, the honest statement is that any difference is **smaller than ±0.15 pp**. **Pooled, a small delay-dependent cost appears.** The three uncle caps are independent measurements of the same underlying difference, so inverse-variance pooling across them buys ~√3 in precision. Pooled per delay, `δ_max` 1–4 stay unresolved (|t| ≤ 1.3) while **`δ_max = 5` resolves at −0.0014 ± 0.0007 (t = 3.7)** — surviving correction for the five delays tested. Over the whole band the pooled gap is −0.00048 ± 0.00033 (t = 2.8), and 11 of 15 cells are negative. So the first-fork restriction does cost something, in the direction theory predicts, and the cost is **monotone in delay and below 0.15 % everywhere in this band** — negligible against the ±0.9 % per-epoch sampling noise of [Appendix B](tsi-report-4-reproducibility-and-appendices.md#sB). (The pooling was chosen after inspecting the per-cell results; the per-delay trend, not the whole-band figure, is the defensible claim.) +**The absolute test agrees.** Asking the same question without reference to the other model — is each cell's equilibrium exactly 1.0? — reproduces the onset independently. Under the **unrestricted** model 1 of 15 cells sits significantly below 1 (`t` = −2.09, consistent with chance). Under the **countable** model 4 of 15 do, and they are not scattered: `δ_max = 4` at `U = 1` (−0.0012, `t` = −2.6) and **all three uncle caps at `δ_max = 5`** (−0.0012 to −0.0019, `t` = −2.5 to −3.7). A shortfall that appears at every cap simultaneously, only at the top of the band, and only under the restricted model, is the first-fork cost — the same effect the gap test resolves at `δ_max = 5`, seen from the absolute side. + **The negative control passes.** The `U = 0` arms — identical models by construction — show |gap| ≤ 0.016 with max `t` = 1.26, i.e. no spurious signal, but a 95 % CI of ±0.025: **17× wider than the entire `U ≥ 1` range.** The unrecovered regime is intrinsically noisy, which is precisely why the model comparison has to be made where uncles are active. -![Fig 34 — design-regime accuracy, δ_max 1–5, countable (solid) vs unrestricted (dashed) per uncle cap, error bars = replicate SEM over 40 replicates: every U ≥ 1 curve is pinned at 1.000 across the band under both models, while U = 0 (the negative control) decays 0.81 → 0.64.](report-figures/fig34_fine_delay_accuracy.png) +![Fig 34 — design-regime accuracy, δ_max 1–5, countable (solid) vs unrestricted (dashed) per uncle cap, error bars = replicate SEM over 40 replicates: every U ≥ 1 curve sits at 1.000 across the band under both models (to within 0.2 %), while U = 0 — the negative control — decays 0.81 → 0.64.](report-figures/fig34_fine_delay_accuracy.png) ![Fig 35 — the countable − unrestricted gap with 95% CIs, zoomed to the U ≥ 1 scale, with the inverse-variance pooled estimate in black: no single cell resolves, the pooled trend is monotone in delay, and only δ_max = 5 separates from zero (−0.0014 ± 0.0007).](report-figures/fig35_fine_delay_gap.png) -**What this settles.** One uncle slot is sufficient — not approximately, exactly — everywhere in the operating band, under either referencing model; the [§3.3](#s3-3) `U ≥ ⌈ρ⌉` rule has margin to spare at `ρ ≤ 0.45`. And the first-fork restriction, which [§3.2](#s3-2) shows costing 1.4 % at `ρ ≈ 1` and 5 % under overload, costs **less than 0.15 %** anywhere a deployment should be operating. +**What this settles.** One uncle slot is sufficient everywhere in the operating band under either referencing model — the [§3.3](#s3-3) `U ≥ ⌈ρ⌉` rule has margin to spare at `ρ ≤ 0.41`, and raising `U` past 1 buys nothing here (at `δ_max = 5` all three caps sit at the same 0.1–0.2 % shortfall, so the residual is *not* a capacity limit). And the first-fork restriction, which [§3.2](#s3-2) shows costing 1.4 % at `ρ ≈ 1` and 5 % under overload, costs **at most 0.2 %** anywhere a deployment should be operating — an order of magnitude below the ±0.9 % per-epoch sampling noise of [Appendix B](tsi-report-4-reproducibility-and-appendices.md#sB), and below the ~1 % fixed-point rounding bias of [Appendix A](tsi-report-4-reproducibility-and-appendices.md#sA). The [§1](tsi-report-1-overview-and-recommendations.md#s1) statement that one uncle restores the estimate to the true stake holds at the precision that matters; this section puts the residual at 0.1–0.2 % at the top of the band rather than zero. ### 3.3 One uncle is not always enough — the load `ρ` diff --git a/reports/tsi/tsi-report-4-reproducibility-and-appendices.md b/reports/tsi/tsi-report-4-reproducibility-and-appendices.md index 473701b..61dc7d6 100644 --- a/reports/tsi/tsi-report-4-reproducibility-and-appendices.md +++ b/reports/tsi/tsi-report-4-reproducibility-and-appendices.md @@ -46,14 +46,14 @@ Sweep studies are committed configs, run with `make ` (writes a dated `run | U = 0 fluctuation series (zero delay, k ∈ {256, 1024, 2160}) | `scripts/appendix_fluct.py --run` (`runs/fluctuation_u0.parquet`) | [Appendix B](#sB) | | CI smoke grid + analytic sanity checks | `configs/smoke.yaml`; `scripts/verify.py` (`make verify`) — validation only, no figures | — | | **countable vs unrestricted referencing** (accuracy over delay × U; measured `q_u`/recovery `r`) | `configs/countable-vs-old.yaml` run twice — default and with `--old`; figures + significance table by `scripts/plot_countable_vs_old.py` | [§2.1](tsi-report-2-accuracy-and-design.md#s2-1), [§3.2](tsi-report-2-accuracy-and-design.md#s3-2) | -| **fine delay band** (δ_max 1–5 at 40 replicates; tight CI on the model gap in the design regime) | `configs/fine-delay.yaml` run twice — default and with `--old`; figure + table by `scripts/plot_fine_delay.py` | [§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a) | +| **fine delay band** (δ_max 1–5 at 40 replicates; tight CI on the model gap in the design regime) | `configs/fine-delay.yaml` run twice — default and with `--old`; figures + significance tables (model gap, and each model vs the exact 1.0 target) by `scripts/plot_fine_delay.py` | [§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a) | | **window absorption sweep** (`W` in expected block-intervals, `w_u = W/f` derived) | `configs/absorption-window.yaml`; figure by `scripts/plot_countable_vs_old.py` | [§3.4](tsi-report-2-accuracy-and-design.md#s3-4) | **Uncle-model convention.** The simulator's default is the **countable** model — first-fork candidates only, derived window `w_u = W/f`, occupied-slot exclusion, per-reference counting rules ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1)). The **unrestricted** baseline is preserved in the code and selected with `--old` on `tsi-sweep`/`tsi-verify`. Its RNG key is byte-identical to the pre-restriction key, so `--old` **bit-reproduces the earlier runs**: a `rho-boundary` cell (δ_max = 8, U = 2, k = 256, N = 1 000) re-run under `--old` matches the committed `2026-07-27_195627_rho-boundary` parquet with `max |Δ| = 0` on every epoch and every metric. Studies in the table above that predate the countable default were produced under the unrestricted model and reproduce exactly under `--old`; the comparison rows quantify where the two models differ, and in the design regime (`ρ < 1`) no difference is resolvable, so those findings carry over unchanged. Because the two models draw independent RNG streams, every countable-vs-unrestricted comparison is **unpaired**, and its resolution is set by the replicate spread rather than by the effect size. Each comparison sweep therefore includes a `U = 0` arm as a **negative control**: with no uncles the models are identical by construction, so the measured `U = 0` gap is a direct reading of the noise floor at that delay and replicate count. At `δ_max = 32` with 5 replicates that floor is ≈ 0.23 in `D̂/D` — larger than several real effects elsewhere in the grid — which is why [§3.2](tsi-report-2-accuracy-and-design.md#s3-2) reports a `t` statistic per cell and why the design regime is measured separately at 40 replicates ([§3.2a](tsi-report-2-accuracy-and-design.md#s3-2a)). -All studies were **re-run on 2026-07-23/24 with the corrected slot-counting mechanism** ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1)) and the early-stop optimisation; the resilient batch is `scripts/run_all_reruns.sh` (per-step log in `runs/rerun_status.log`). Canonical run directories (latest): fullscale N=5000/10000 = `2026-07-24_094519_fullscale`; fullscale N=1000/2000 = `2026-07-23_171803_fullscale-small`; uncle-window = `2026-07-24_001456`; window-uncles = `2026-07-24_014240`; block-rate = `2026-07-24_043943`; blend-hops-delay = `2026-07-24_064052`; window-scale = `2026-07-24_085234`; latency-shape = `2026-07-24_090014_expdist`; stake-tail = `2026-07-24_090044_pareto133`; heterogeneous-start = `2026-07-24_090114_default`; N-scaling = the `nscaling-{a,b}` + `nscaling32-{a,b}` runs; adversary grids = `runs/adversary_grid/`; jitter = `runs/jitter_grid/`; bootstrap = `runs/bootstrap_fullscale/`; fluctuation = `runs/fluctuation_u0.parquet`; fork-rate = `runs/fork_rate_vs_delay.parquet`; ρ-boundary = `2026-07-27_195627_rho-boundary`. +All studies were **re-run on 2026-07-23/24 with the corrected slot-counting mechanism** ([§2.1](tsi-report-2-accuracy-and-design.md#s2-1)) and the early-stop optimisation; the resilient batch is `scripts/run_all_reruns.sh` (per-step log in `runs/rerun_status.log`). Canonical run directories (latest): fullscale N=5000/10000 = `2026-07-24_094519_fullscale`; fullscale N=1000/2000 = `2026-07-23_171803_fullscale-small`; uncle-window = `2026-07-24_001456`; window-uncles = `2026-07-24_014240`; block-rate = `2026-07-24_043943`; blend-hops-delay = `2026-07-24_064052`; window-scale = `2026-07-24_085234`; latency-shape = `2026-07-24_090014_expdist`; stake-tail = `2026-07-24_090044_pareto133`; heterogeneous-start = `2026-07-24_090114_default`; N-scaling = the `nscaling-{a,b}` + `nscaling32-{a,b}` runs; adversary grids = `runs/adversary_grid/`; jitter = `runs/jitter_grid/`; bootstrap = `runs/bootstrap_fullscale/`; fluctuation = `runs/fluctuation_u0.parquet`; fork-rate = `runs/fork_rate_vs_delay.parquet`; ρ-boundary = `2026-07-27_195627_rho-boundary`. Referencing-model studies: countable-vs-unrestricted = `2026-08-04_103536_cvo-countable` / `2026-08-04_104010_cvo-old`; window absorption = `2026-08-04_104633_absorption-window`; fine delay band = `2026-08-04_191441_fine-countable` / `2026-08-04_195353_fine-old`. Figures are in `report-figures/` (`fig1`–`fig29`, plus [Appendix B](#sB)'s `figB1`–`figB2`; numbering is generation order, not order of appearance). Committed generators: `fig1` (bootstrap, k=2160) by `scripts/bootstrap_dynamics.py`; `fig2`,`fig4`,`fig5`,`fig17`–`fig22` by `scripts/regenerate_extra_figs.py` from the latest sweeps (`fig3` hops×delay×U grid by `scripts/hops_delay_grid.py`, `fig6` (block-rate `U_min` grid + ρ-collapse) rendered ad hoc from `runs/2026-07-24_043943_block-rate` with no committed generator; `fig26` deficit-vs-ρ by `scripts/rho_boundary_analysis.py`) (fullscale-derived `fig17`–`fig20` pool both sizes in that run, N = 5 000 and N = 10 000 — the generators filter on stake_dist/topology/degree/init_dest only, never on `n_nodes`); `fig8`,`fig9` by `scripts/adversary_figs.py` from `runs/adversary_grid/`; `fig10`–`fig12` by `scripts/dynamic_withhold.py`; `fig13`–`fig15` by `scripts/selfish_mining.py`/`selfish_rewards.py`/`reward_mandate.py`; `fig16` by `scripts/stake_vs_delay.py`; `fig23`–`fig24` by `scripts/nscaling_analysis.py`; `fig25` by `scripts/window_scale_analysis.py`; `fig27`–`fig28` by `scripts/reorg_depth.py` (fork rates via `--measure`; private-chain model `src/tsi_sim/reorg.py`); `fig29` by `scripts/churn.py`; `figB1`–`figB2` by `scripts/appendix_fluct.py`. `fig7` (feedback fixed-point) is an analytic overlay; `fig30`–`fig33` (countable-vs-unrestricted accuracy, `q_u`-prediction check, recovery rate, absorption-window sweep) by `scripts/plot_countable_vs_old.py` from the `cvo-countable`/`cvo-old`/`absorption-window` runs; `fig34`–`fig35` (design-regime accuracy and the model gap with 95 % CIs) by `scripts/plot_fine_delay.py` from the `fine-countable`/`fine-old` runs. Every figure type the per-node simulator generates appears in this report, and the fork-rate/reorg-depth study closes the previous reproducibility gap for the adversarial figures (`fig8`,`fig9` now have committed generators from `runs/adversary_grid/`). diff --git a/tools/simulators/tsi/tsi-sim-pernode/scripts/plot_fine_delay.py b/tools/simulators/tsi/tsi-sim-pernode/scripts/plot_fine_delay.py index 5bfd32c..a2a10e9 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/scripts/plot_fine_delay.py +++ b/tools/simulators/tsi/tsi-sim-pernode/scripts/plot_fine_delay.py @@ -34,7 +34,7 @@ import numpy as np import pandas as pd from tsi_sim.plotting import style -from tsi_sim.plotting.figures_pernode import equilibrium, sem +from tsi_sim.plotting.figures_pernode import equilibrium, rho_for, sem DELAY = "blend_delay_max" # Normal approximation: with 40 replicates per arm the t-quantile is within ~2% of 1.96, @@ -56,6 +56,20 @@ def _cells(df: pd.DataFrame) -> pd.DataFrame: mean_q_eff=("mean_q_eff", "mean")) +def vs_one(cells: pd.DataFrame) -> pd.DataFrame: + """Test each U >= 1 cell against the exact target 1.0. + + An independent read on the same question the gap test asks: the report's claim is that + uncle recovery restores the equilibrium to EXACTLY the true stake, so a systematic + shortfall across uncle caps is the first-fork cost seen from the absolute side rather + than differentially. 40 replicates give ~0.0005 resolution, enough to see 0.1%. + """ + u = cells[cells.max_uncles > 0].copy() + u["dev"] = u.mean_ratio - 1.0 + u["t"] = u.dev / u.sem_ratio.replace(0, np.nan) + return u.sort_values(["max_uncles", DELAY]) + + def gaps(cnt: pd.DataFrame, old: pd.DataFrame) -> pd.DataFrame: """countable - unrestricted per cell, with the unpaired SE and 95% CI half-width.""" m = cnt.merge(old, on=[DELAY, "max_uncles"], suffixes=("_c", "_o")) @@ -150,10 +164,18 @@ def main() -> None: out = Path(args.out) out.mkdir(parents=True, exist_ok=True) - cnt, old = _cells(_load(args.countable)), _cells(_load(args.old)) + cnt_raw = _load(args.countable) + cnt, old = _cells(cnt_raw), _cells(_load(args.old)) g = gaps(cnt, old) prov = "tsi-sim-pernode fine-delay.yaml (+--old)" + # rho per delay, derived (never hand-substituted) — these are the report's axis labels. + delays = sorted(cnt[DELAY].unique()) + print("load rho = f*D_vis per delay (measured ell_mean, see figures_pernode.rho_for):") + print(" " + " ".join(f"delay={d:g}: rho={r:.3f}" + for d, r in zip(delays, rho_for(cnt_raw, delays), strict=True))) + print() + written = [] written += style.save(fig_accuracy(cnt, old), out / "fine_accuracy_vs_delay", prov) written += style.save(fig_gap(g), out / "fine_gap_vs_delay", prov) @@ -193,6 +215,18 @@ def main() -> None: print(f"\nU=0 negative control (true gap = 0): |gap| up to {ctl.gap.abs().max():.4f}, " f"max t = {ctl.t.max():.2f}, 95% CI +-{ctl.ci95.max():.4f} " f"-> control {'PASSES' if ctl.t.max() < 2 else 'FAILS'}") + + # Absolute test: does uncle recovery actually land on 1.0? Same question as the gap + # test, asked without reference to the other model. + for lbl, cells in (("countable", cnt), ("unrestricted", old)): + v = vs_one(cells) + lo = v[v.t <= -2] + print(f"\nvs exact 1.0, {lbl}: {len(lo)}/{len(v)} cells significantly BELOW 1") + if len(lo): + for d, s in lo.groupby(DELAY): + caps = "/".join(f"U={int(x)}" for x in sorted(s.max_uncles)) + print(f" delay={d:>4g}: {caps} dev {s.dev.min():+.5f}..{s.dev.max():+.5f} " + f"t {s.t.min():.2f}..{s.t.max():.2f}") print(f"wrote {len(written)} files -> {out}") diff --git a/tools/simulators/tsi/tsi-sim-pernode/scripts/rho_boundary_analysis.py b/tools/simulators/tsi/tsi-sim-pernode/scripts/rho_boundary_analysis.py index c293eb1..66319b8 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/scripts/rho_boundary_analysis.py +++ b/tools/simulators/tsi/tsi-sim-pernode/scripts/rho_boundary_analysis.py @@ -21,33 +21,14 @@ import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) -from tsi_sim.config import SimConfig # noqa: E402 from tsi_sim.plotting import style # noqa: E402 -from tsi_sim.topology import build_path_latency # noqa: E402 +from tsi_sim.plotting.figures_pernode import graph_ell_mean # noqa: E402 HERE = Path(__file__).resolve().parent.parent RUNS = HERE / "runs" FIGS = HERE / "report-figures" -def graph_ell_mean(df: pd.DataFrame) -> float: - """``ell_mean`` — the mean shortest-path (gossip) latency of the run's OWN peering graph. - - Measured from the run's recorded ``(n_nodes, degree, link_latency_mean, link_latency_dist)`` - rather than hardcoded, so the rho axis stays correct if any of those change. It is a statistical - property of the random d-regular geo graph (seed-invariant to <1% at this N), so one - representative draw suffices. Post-processing only — this rebuilds the latency *graph* to read - off its mean, and never touches or re-runs the simulation. - """ - row = df.iloc[0] - cfg = SimConfig(n_nodes=int(row.n_nodes), degree=int(row.degree), topology="blend", - link_latency_mean=float(row.link_latency_mean), - link_latency_dist=str(row.link_latency_dist), k=int(row.k)) - pl = build_path_latency(cfg, np.random.default_rng(0)) - n = pl.shape[0] - return float(pl[~np.eye(n, dtype=bool)].mean()) - - def load() -> pd.DataFrame: src = sorted(RUNS.glob("*_rho-boundary/results.parquet"))[-1] df = pd.read_parquet(src) diff --git a/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/plotting/figures_pernode.py b/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/plotting/figures_pernode.py index dfad269..7843d86 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/plotting/figures_pernode.py +++ b/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/plotting/figures_pernode.py @@ -48,6 +48,41 @@ def recovery_rate(q, q_u): return (q_u - q) / np.maximum(1.0 - q, 1e-12) +def graph_ell_mean(df: pd.DataFrame) -> float: + """``ell_mean`` — the mean shortest-path (gossip) latency of the run's OWN peering graph. + + Measured from the run's recorded ``(n_nodes, degree, link_latency_mean, + link_latency_dist)`` rather than hardcoded, so any derived quantity stays correct if + those change. A statistical property of the random d-regular geo graph (seed-invariant + to <1% at these N), so one representative draw suffices. Post-processing only: this + rebuilds the latency *graph* to read off its mean and never re-runs the simulation. + """ + from ..config import SimConfig + from ..topology import build_path_latency + + row = df.iloc[0] + cfg = SimConfig(n_nodes=int(row.n_nodes), degree=int(row.degree), topology="blend", + link_latency_mean=float(row.link_latency_mean), + link_latency_dist=str(row.link_latency_dist), k=int(row.k)) + pl = build_path_latency(cfg, np.random.default_rng(0)) + n = pl.shape[0] + return float(pl[~np.eye(n, dtype=bool)].mean()) + + +def rho_for(df: pd.DataFrame, delay) -> np.ndarray: + """Load ``rho = f * D_vis`` with ``D_vis = hops*delta_max/2 + (hops+1)*ell_mean``. + + Every report quotation of ``rho`` must come through here. ``ell_mean`` is the MEASURED + mean gossip latency (1.21 slots at N=1000/degree=6), not the per-link + ``link_latency_mean`` parameter (0.5) — using the latter understates ``rho`` by ~0.1, + and hand-substituting a guessed value is how wrong axis labels get into a report. + """ + f = float(df.f.iloc[0]) + hops = int(df.blend_hops.iloc[0]) + ell = graph_ell_mean(df) + return f * (hops * np.asarray(delay, dtype=float) / 2.0 + (hops + 1) * ell) + + def _lat_axis(topo: str) -> tuple[str, str]: """(dataframe column, axis label) for the dominant latency knob of a graph topology.""" if topo == "blend":