Make the fork-loss validation stand alone

The results document was written as a reply to a briefing note that will not be
circulated, so it depended on a file no reader would have -- and linked to one
that was never committed, so the link was dead on GitHub regardless.

Rewritten to be self-contained: it now states what the added spec section
claims, what was measured, and the verdict, without reference to the brief.
The six claims and six experiments are stated in its own terms rather than by
the brief's C-numbers and E-numbers, and it carries its own reproduction table
mapping each section to the config or script and run directory behind it.
Renamed fork-loss-validation.md accordingly.

The configs and scripts written for these experiments carried the same
dependency in their header comments ("Handoff E5", "Claim C2"); those now
describe what they measure directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcin Pawlowski 2026-08-07 11:52:58 +02:00
parent 18c15d329c
commit abf2c86285
No known key found for this signature in database
7 changed files with 188 additions and 143 deletions

View File

@ -0,0 +1,161 @@
# Validating the fork-loss claims in `analysis-total-stake-inference.md`
*A section quantifying what uncle references buy — § **Effect of Uncle References on Honest Slot Utilization** — was added to `docs/blockchain/raw/analysis-total-stake-inference.md` on the `analysis-fork-loss-in-tsi` branch of `logos-lips` (commit `d6fd7648`), produced by a quick standalone simulation. Several of its numbers conflict with [the TSI report](README.md) in this folder, which comes from a per-node network simulator. This document settles each claim at the **deployed spec's own operating point** — the one cell neither the section nor the report measured directly — and gives the numbers the section should carry.*
*Measurements: `tools/simulators/tsi/tsi-sim-pernode`, run 2026-08-06 against spec revision `d6fd7648`. Every figure below is reproducible from a committed script or config; sources are named per experiment and collected at the end.*
---
## Verdict
**The section's headline is wrong by ~17×, and it is concerned with the wrong bias.** The residual it attributes to deep forks is 0.08 pp, not 1.4 %. Meanwhile the deployed estimator carries a **+1.0 % bias of the opposite sign** that the section does not mention — from the on-chain rounding of `f`, not from forks. Rewriting the section around the second number rather than the first is the substantive change.
The no-uncle loss is also **understated, not overstated**: 33 % at the operating point, not 18.7 %, and it deepens with network size. The section undersells what uncle references buy by roughly half.
**But the section's number is not an arithmetic error, and experiment 5 identifies what it is.** It is what this model produces once *per-recipient* delay spread reaches ~8 slots — about 16× what Blend's cascade actually delivers. The cascade's variance is per **block** (every recipient moved together, harmless to the estimate); the standalone simulation's was per **recipient** (divergent, and the only kind that manufactures unrecoverable forks). That distinction is the whole disagreement, and it belongs in the rewritten section.
## Summary of measurements
```
1 the deployed operating point: δ_max = 4 D_vis = 8 s ρ = 0.27
2 at ρ = 0.27, N = 1000, 40 replicates, k = 2160, PAIRED
D̂/D U=0: 0.6677 ± 0.0044 U=1: 0.9985 U=2: 0.9986 U=4: 0.9997
countable ceiling, pooled U≥1: 0.00080 ± 0.00026 (t = 3.08)
U=0 negative control: 0.00000 ± 0.00000 (exact — shared random streams)
at N = 5000, 12 replicates, UNPAIRED
D̂/D U=0: 0.6537 ± 0.0251 U=1: 0.9997 U=2: 0.9979 U=4: 0.9996
U=0 negative-control noise floor: ±0.027 → no first-fork cost resolvable at this N
3 largest ρ with U=4 ≥ 0.98: > 1.87 (U=3 already suffices there) margin: ~7× the deployment
4 D̂/D vs W_abs {1,2,3,5,7,10,15,20}:
0.854 / 0.938 / 0.967 / 0.992 / 0.999 / 0.998 / 0.999 / 1.000
knee at W_abs ≈ 5; spec's W = 10 sits ~2× above it
5 per-recipient jitter {0,1,2,4,8} slots, δ_max = 4, U=1, exact oracle, 12 reps:
D̂/D 0.9983 / 0.9992 / 0.9991 / 0.9971 / 0.9871
depth≥2 orphans 0.25 % / 0.38 % / 0.53 % / 1.20 % / 3.30 %
consensus range_ratio = 0 and agreement = 1.000 in all 480 runs
→ the section's 0.986 is reproducible, at ~8 slots of per-recipient variance
6 f-precision, exact / 1e3 / 1e6: 0.99997 / 1.01026 / 0.99990 (closed form 1.000 / 1.0101 / 1.00001)
```
## Claim by claim
| # | Claim in the added section | Measured | Verdict |
|---|---|---|---|
| 1 | With uncles, the residual underestimate is **1.4 %** (`q` 0.813 → 0.986) | 0.030.15 % at every `U ≥ 1` cell | **Refuted.** Replace with "no residual resolvable in the design regime". |
| 2 | The residual is **deep-fork blocks** (the first-fork restriction) | Paired gap **0.08 pp ± 0.03** pooled (t = 3.08) | **Mechanism right, size ~17× overstated.** It is real and now resolved — but it is 0.0008, not 0.014. |
| 3 | Without uncles the loss is **18.7 %** (`q` = 0.813) | **33.2 %** at N = 1000, **34.6 %** at N = 5000 (42 % / 49.5 % at δ_max = 8) | **Refuted, in the unfavourable direction.** |
| 4 | `MAX_UNCLES` = 4 **never binds** (max 3 candidates observed) | `U = 3` still recovers at ρ = 1.87, the largest measured | **Corroborated,** with ~7× margin over the deployment's ρ = 0.27. |
| 5 | `w_u` = 300 **never binds** (median lag 34, max 197) | Knee at `W_abs ≈ 5`; spec's 10 is ~2× above it | **Right in effect, wrong in wording.** Say "≈ 2× above the measured knee at the deployment's load". |
| 6 | *(not mentioned)* | `PRECISION = 1e3`**D̂/D = 1.01026 ± 0.00056** | **The section's main omission.** Larger than everything else it discusses, and opposite in sign. |
---
## 1. The deployed operating point
Every number the section quotes is stated at an operating point nobody had pinned down, so this comes first — it decides which measurements are even relevant.
`analysis-block-times-blend-network.md` sets `blending_delay = 3` s as *"seconds spent in each Blend node"* — a **fixed per-hop dwell**, neither the mean of a distribution nor a bound. The max-delay arithmetic confirms it: `3δ + 5` gives 14 s at δ = 3 and 11 s at δ = 2, matching that document's prose exactly. The profile it selects is 2 s.
The simulator's per-hop delay is `Uniform(0, δ_max)` with mean `δ_max/2`, and the design laws reach latency only through the mean, so the matching value is:
> **`δ_max = 4`**, giving `D_vis = 3·2 + 4·0.5 = 8 s` and **`ρ = f·D_vis = 0.27`**.
(Charging the spec's 1 s Proof-of-Leadership time as well gives ρ ≈ 0.30; nothing below changes.) This lands inside the report's committed 40-replicate paired design-band grid, so claims 13 are answered from data of record rather than fresh runs.
## 2. Accuracy and the first-fork cost
At ρ = 0.27 every `U ≥ 1` cell sits at **0.99850.9997**. The section's 1.4 % residual is not there.
What *is* there, now that a paired design resolves it: a first-fork cost of **0.08 pp pooled** (95 % CI [0.03, 0.13], t = 3.08). The section's *mechanism* is correct — deep-fork blocks cannot be referenced, because only the first block of a fork has a parent on the referencing chain — but at the deployment's load the effect is two orders of magnitude below the claim.
The `U = 0` negative control is **exactly 0.00000 ± 0.00000**. With no uncle slots the restricted and unrestricted rules are identical by construction, and because the two arms share stake draws, peering graph and lottery outcomes, they agree bit-for-bit. That is the strongest available evidence that the 0.08 pp is signal and not seed noise — an unpaired comparison at this size cannot resolve anything below ~0.15 pp.
At N = 5000 the runs are unpaired and that same control reads ±0.027, so nothing below ~2.7 pp is resolvable there. The N = 5000 row is an exclusion bound, not a measurement.
## 3. Is `MAX_UNCLES = 4` slack?
Yes, by a wide margin. A dedicated load sweep takes `U = 3` to `ρ = 1.87` — roughly **7× the deployment's ρ = 0.27** — and it still recovers to within noise of 1.0 across the whole range. `U = 4` is bounded below by `U = 3`, so the spec's cap does not become the binding constraint anywhere near the deployment. Claim 4 stands as written.
## 4. Is `w_u = 300` adequate?
At the deployment's operating point, sweeping the window with a single uncle slot (so the window is the only thing that can bind):
| `W_abs` (block-intervals) | 1 | 2 | 3 | 5 | 7 | 10 | 15 | 20 |
|---|---|---|---|---|---|---|---|---|
| `D̂/D` | 0.854 | 0.938 | 0.967 | 0.992 | 0.999 | 0.998 | 0.999 | 1.000 |
The recovery knee is at `W_abs ≈ 5` and everything above ~7 is flat. The spec's `W = 10` (`w_u = 300` slots) therefore sits about **2× above the knee** — real margin, but a margin, not an absence of a constraint. "Never binds" overstates it; "≈ 2× above the measured knee at the deployment's load" is what the data supports. This matters because the window floor is set by block *spacing* rather than by network delay, so it does not shrink as the deployment's delay shrinks.
## 5. Per-recipient delay variance — where the section's number comes from
This is the experiment that could have invalidated the *report* rather than the section. It does not — but it does something more useful than refuting the section: it says exactly what the section's number assumes.
The two simulations differ in transport. The standalone drew an **independent propagation delay per (block, recipient)**; the per-node simulator runs a cascade of Blend relays and then floods network-wide **from the last relay**, so nodes receive a block at nearly the same time and their views stay synchronised. Independent per-recipient draws maximise view divergence, which is exactly what manufactures forks deeper than one block — the orphans no counting rule can recover.
Adding per-(block, node) arrival jitter on top of the cascade interpolates between the two models:
| jitter (slots) | 0 | 1 | 2 | 4 | 8 |
|---|---|---|---|---|---|
| `D̂/D` at `U = 1` | 0.9983 | 0.9992 | 0.9991 | 0.9971 | **0.9871** |
| orphans below their fork's first block | 0.25 % | 0.38 % | 0.53 % | 1.20 % | **3.30 %** |
| fork rate | 0.266 | 0.272 | 0.275 | 0.310 | 0.366 |
Accuracy is flat to ~2 slots, then bends. At 8 slots it lands on **0.9871** — essentially the section's 0.986 — with deep orphans at 3.3 %. So the standalone result is not an arithmetic error; it is what this model produces once per-recipient spread reaches roughly eight seconds.
That converts the disagreement into a question with a checkable answer: **does Blend deliver ~8 slots of per-recipient spread?** Under the spec's own model it cannot come close. The blending delay is a fixed per-hop dwell, and the cascade's final step is a network-wide gossip flood from the last relay, so what varies per *recipient* is only that flood — measured at `_mean ≈ 0.5` slot over a degree-6 graph. Eight slots is ~16× that.
The distinction is worth stating explicitly in the rewritten section, because the two are easy to conflate:
> **Variance in *when a block becomes public* is harmless to the estimate. Variance in *when each node sees it* is what manufactures unrecoverable forks.** Blend's cascade produces a lot of the first and very little of the second; the standalone model charged the second.
Consensus is untouched throughout — `range_ratio = 0` and `agreement_window = 1.000` in all 480 runs, at every jitter level — so this is an accuracy sensitivity only, never a safety one.
## 6. The bias the section missed
Three arms, identical but for how the estimator quantises its target rate:
| arm | `D̂/D` | closed form |
|---|---|---|
| exact `f` (the report's convention) | 0.99997 ± 0.00060 | 1.00000 |
| **the spec today, `PRECISION = 1e3`** | **1.01026 ± 0.00056** | 1.01010 |
| recommended, `PRECISION = 1e6` | 0.99990 ± 0.00061 | 1.00001 |
`cryptarchia-total-stake-inference.md` carries `const PRECISION: u64 = 1e3`, so the estimator drives density to `f_p = 0.033` rather than `1/30` and the chain reads **1.0 % high** — measured in the full per-node dynamics, matching the closed form to within one standard error.
This is **~13× the first-fork cost the section is concerned with**, opposite in sign, and removed entirely by a one-constant change. If any single number from this exercise belongs in the section, it is this one.
## Two things the simulator needed, and one the report did
Recorded because they change how a run must be configured, not just what it reports.
- **`f_precision` is now a config field.** It had been a module constant pinned at the *recommended* `1e6`, which is why nobody had measured what the deployed chain would read. The report's default remains exact `f` — the right choice for a design question, since it isolates the mechanism under test — but any run answering *"what will the deployed chain read"* must set `fixed_point: true, f_precision: 1000`. Both arms are run side by side by `scripts/spec_point.py`.
- **The derived window now floors rather than rounds**, matching the spec's `w_u := ⌊W·f⁻¹⌋`. No committed result moves — at `W = 10, f = 1/30` the quotient is exactly 300 either way — but it matters for the `W` and `f` sweeps, where the quotient is not an integer.
- **The unrestricted arm is a *ceiling*, not a candidate design.** The spec gates uncle validity: a block carrying an entry that fails the counting rules is rejected outright (*Block Header Validation*, step 10), so a chain in which deep-fork orphans are referenced cannot exist. Every "restricted vs unrestricted" comparison here and in the report should be read as "what the deployed rule recovers, against the most any rule could recover".
## The validity change does not disturb the incentive results
The same spec revision made uncle *content* a validity condition, which is worth checking against the report's incentive analysis rather than assuming. It holds, because **inclusion stayed soft**: a proposer *"may reference fewer uncles than it could, or pass over a candidate for another, and its block remains valid"*, and the selection procedure is *"a recommendation for filling the entries well, not a consensus rule."* Therefore:
- The report's recommendation of a **soft, reward-weighted** inclusion rule — never a validity rule — is still satisfied. What became validity-gated is the *content* of a reference, not whether one is made.
- The argument against a hard inclusion mandate still applies to a rule the spec did not adopt.
- The uncle-suppression adversary — produce blocks, reference nothing — remains a legal strategy, so those results stand unchanged.
- The restricted model's selection-time filter is now *exactly* what the protocol requires, rather than a faithful approximation of it.
One live consequence for the reward recommendation: the spec still argues that *"because uncle references carry no fork-choice weight and grant no reward, a proposer has no incentive to deviate"*. That clause survives in the current revision, and paying uncles — which the report recommends — removes its premise. The report already tracks this as an open spec-level item.
The one genuinely new consequence of the validity change: referencing an ineligible orphan now costs a proposer its whole block rather than merely failing to count. No modelled strategy does this, so no result moves — but it makes such a strategy self-defeating rather than merely ineffective, which is worth stating if junk-reference griefing is ever modelled.
## Reproducing these numbers
| § | source | data |
|---|---|---|
| 1 | `analysis-block-times-blend-network.md` (no simulation) | — |
| 2 | `configs/fine-delay-paired.yaml` run twice (default and `--old`); `configs/spec-point-n5000.yaml` likewise | `runs/*fine-paired-*`, `runs/*spec-n5000*` |
| 3 | `configs/rho-boundary.yaml` | `runs/*rho-boundary` |
| 4 | `configs/spec-point-window.yaml` | `runs/*spec-window` |
| 5 | `scripts/spec_jitter.py`, `configs/spec-point-jitter.yaml` | `runs/spec_jitter.parquet` |
| 6 | `scripts/spec_point.py` | `runs/spec_point.parquet` |
All under `tools/simulators/tsi/tsi-sim-pernode`. Run directories are dated; the ones cited here are the latest of each label.

View File

@ -1,118 +0,0 @@
# Results — fork-loss claims in `analysis-total-stake-inference.md`
*Answers to [`handoff-fork-loss-validation.md`](handoff-fork-loss-validation.md). Simulator: `tools/simulators/tsi/tsi-sim-pernode`. Run 2026-08-06 against spec revision `d6fd7648` (`analysis-fork-loss-in-tsi`).*
---
## Verdict
**The added section's headline is wrong by ~17×, and it is agonising over the wrong bias.** The residual it attributes to deep forks is 0.08 pp, not 1.4 %. Meanwhile the deployed estimator carries a **+1.0 % bias of the opposite sign** that the section does not mention — from the `f` rounding, not from forks. Rewriting the section around the second number rather than the first is the substantive change.
The no-uncle loss is also **understated, not overstated**: 33 % at the operating point, not 18.7 %, and it deepens with network size.
**But the section's number is not an arithmetic error, and E5 identifies what it is.** It is what this model produces once per-recipient delay spread reaches ~8 slots — which is ~16× the gossip spread Blend's cascade actually delivers, because the cascade's variance is per *block* (shared by all recipients, harmless to the estimate) rather than per *recipient* (divergent, and the only kind that manufactures unrecoverable forks). That distinction is the whole disagreement and belongs in the rewritten section.
## The requested table
```
E1 spec Blend profile: δ_max = 4 D_vis = 8 s ρ = 0.27
E2 at ρ = 0.27, N = 1000, 40 replicates, k = 2160, PAIRED
D̂/D U=0: 0.6677 ± 0.0044 U=1: 0.9985 U=2: 0.9986 U=4: 0.9997
countable ceiling, pooled U≥1: 0.00080 ± 0.00026 (t = 3.08)
U=0 negative control: 0.00000 ± 0.00000 (exact — paired streams)
at N = 5000, 12 replicates, UNPAIRED
D̂/D U=0: 0.6537 ± 0.0251 U=1: 0.9997 U=2: 0.9979 U=4: 0.9996
U=0 negative-control noise floor: ±0.027 → no first-fork cost resolvable at this N
E3 largest ρ with U=4 ≥ 0.98: > 1.87 (U=3 already suffices there) margin: ~7× the deployment
E4 D̂/D vs W_abs {1,2,3,5,7,10,15,20}:
0.854 / 0.938 / 0.967 / 0.992 / 0.999 / 0.998 / 0.999 / 1.000
knee at W_abs ≈ 5; spec's W = 10 sits ~2× above it
E5 jitter_mean {0,1,2,4,8} slots, δ_max = 4, U=1, exact oracle, 12 reps:
D̂/D 0.9983 / 0.9992 / 0.9991 / 0.9971 / 0.9871
depth≥2 orphans 0.25 % / 0.38 % / 0.53 % / 1.20 % / 3.30 %
consensus range_ratio = 0 and agreement = 1.000 in all 480 runs
→ the section's 0.986 is reproducible, at ~8 slots of per-recipient variance
E6 fixed_point off / 1e3 / 1e6: 0.99997 / 1.01026 / 0.99990 (predicted 1.000 / 1.0101 / 1.00001)
```
## Claim by claim
| # | Claim | Measured | Verdict |
|---|---|---|---|
| C1 | Residual underestimate **1.4 %** | 0.030.15 % at every `U ≥ 1` cell | **Refuted.** Replace with "no residual resolvable in the design regime". |
| C2 | The residual is **deep-fork blocks** | Paired gap **0.08 pp ± 0.03** pooled (t = 3.08) | **Mechanism right, size ~17× overstated.** It is real and now resolved — but it is 0.0008, not 0.014. |
| C3 | Without uncles the loss is **18.7 %** | **33.2 %** at N = 1000, **34.6 %** at N = 5000 (and 42 % / 49.5 % at δ_max = 8) | **Refuted, in the unfavourable direction.** The section understates the value of uncle references by roughly half. |
| C4 | `MAX_UNCLES = 4` never binds | `U = 3` still recovers at ρ = 1.87, the largest measured | **Corroborated,** with ~7× margin over the deployment's ρ = 0.27. |
| C5 | `w_u = 300` never binds | Knee at `W_abs ≈ 5`; spec's 10 is ~2× above it | **Right in effect, wrong in wording.** Say "≈ 2× above the measured knee at the deployment's load", not "never binds". |
| C6 | *(not mentioned)* | `PRECISION = 1e3`**D̂/D = 1.01026 ± 0.00056** | **The section's main omission.** Larger than everything else it discusses, and opposite in sign. |
### E1 — the operating point
`analysis-block-times-blend-network.md` sets `blending_delay = 3` s as *"seconds spent in each Blend node"* — a **fixed per-hop dwell**, neither a mean of a distribution nor a bound. The max-delay arithmetic confirms it: `3δ + 5` gives 14 s at δ = 3 and 11 s at δ = 2, matching the prose exactly. The chosen profile is 2 s.
The simulator's per-hop delay is `Uniform(0, δ_max)`, mean `δ_max/2`, and the design laws reach latency only through the mean — so the matching value is **`δ_max = 4`**, giving `D_vis = 3·2 + 4·0.5 = 8 s` and **`ρ = 0.27`**. (Charging the spec's 1 s PoL proof time as well would give ρ ≈ 0.30; nothing below changes.)
This lands inside the committed 40-replicate paired design-band grid, so C1C3 are answered from data of record rather than new runs.
### E2 — accuracy and the first-fork cost
At ρ = 0.27 every `U ≥ 1` cell sits at **0.99850.9997**. The section's 1.4 % residual is not there. What *is* there, now that the paired design resolves it: a first-fork cost of **0.08 pp pooled** (95 % CI [0.03, 0.13], t = 3.08). C2's mechanism is correct — it is the deep-fork blocks — but at the deployment's load it is two orders of magnitude below the claim.
The `U = 0` negative control is **exactly 0.00000 ± 0.00000**: with no uncle slots the two models are identical by construction, and under common random numbers they agree bit-for-bit. That is the strongest available check that the 0.08 pp is signal rather than seed noise.
At N = 5000 the runs are unpaired and the same negative control reads ±0.027, so nothing below ~2.7 pp is resolvable there — the N = 5000 row is an exclusion bound, not a measurement.
### E6 — the bias the section missed
Three arms, identical but for the estimator's target-rate quantisation:
| arm | `D̂/D` | closed form |
|---|---|---|
| exact `f` (report convention) | 0.99997 ± 0.00060 | 1.00000 |
| **spec today, `PRECISION = 1e3`** | **1.01026 ± 0.00056** | 1.01010 |
| recommended, `PRECISION = 1e6` | 0.99990 ± 0.00061 | 1.00001 |
The deployed estimator drives density to `f_p = 0.033` instead of `1/30`, so the chain reads **1.0 % high** — measured in the full per-node dynamics, matching `theory.fixed_point_bias` to within one standard error. It is ~13× the first-fork cost the section is concerned with, opposite in sign, and removed entirely by a one-constant change.
### E5 — the diagnostic: the section's number is reproducible, and that pins its hidden assumption
This was the experiment that could have invalidated the *report*. It does not — but it does something more useful than refuting the section: it says exactly what the section's number assumes.
Sweeping per-(block, node) arrival jitter on top of the cascade interpolates between the two delay models — 0 is the report's cascade, large values approach the standalone's independent-per-recipient draws:
| `jitter_mean` (slots) | 0 | 1 | 2 | 4 | 8 |
|---|---|---|---|---|---|
| `D̂/D` at `U = 1` | 0.9983 | 0.9992 | 0.9991 | 0.9971 | **0.9871** |
| depth-≥2 orphans | 0.25 % | 0.38 % | 0.53 % | 1.20 % | **3.30 %** |
| fork rate | 0.266 | 0.272 | 0.275 | 0.310 | 0.366 |
**Both branches of the handoff's pass/fail are true, at different jitter levels.** Up to ~2 slots of per-recipient variance the report is entirely robust: accuracy 0.999, deep orphans half a percent. But at 8 slots accuracy lands on **0.9871** — essentially the section's 0.986 — with deep orphans at 3.3 %. So the standalone result is not arithmetic error; it is what this model produces once per-recipient spread reaches roughly eight seconds.
That converts the disagreement into a question with a checkable answer: **does Blend deliver ~8 slots of per-recipient spread?** Under the spec's own model it cannot come close. The blending delay is a fixed per-hop dwell and the cascade's final step is a network-wide gossip flood from the last relay, so what varies per *recipient* is only that flood — and the report measures the gossip spread at `_mean ≈ 0.5` slot over a degree-6 graph. Eight slots is ~16× that. Per-*block* delay variance, which the cascade does have and which is large, moves every recipient together and so creates no deep forks at all.
The distinction is the whole disagreement, and it is worth stating in the section: **variance in *when a block becomes public* is harmless to the estimate; variance in *when each node sees it* is what manufactures unrecoverable forks.** The two are easy to conflate and the standalone model charged the second where Blend delivers the first.
Consensus is untouched throughout — `range_ratio = 0` and `agreement_window = 1.000` in all 480 runs, at every jitter level — reconfirming §6.1's structural argument at the deployment's own operating point.
**This also partly closes the report's open item 15** (correlated/heterogeneous latency untested): the report is robust to per-recipient variance up to ~24 slots and degrades measurably beyond, which is a bound it did not previously carry. What remains untested there is *spatially correlated* latency — nodes in a region straggling together — which jitter, being i.i.d. per (block, node), does not model.
## Notes on the guide (§4)
- **§4.1 item 1 — done.** `f_precision` is now a config field, so a spec-faithful arm can be run; it was a module constant pinned at the *recommended* 1e6, which is why nobody had measured what the chain would actually read. Default unchanged and appended to the RNG key only when non-default, so no committed run is reseeded.
- **§4.1 item 3 — done.** `uncle_window_slots` now floors rather than rounds, matching `w_u := ⌊W·f⁻¹⌋`. No committed result moves (W = 10, f = 1/30 is exactly 300 either way); it matters only for the `W` and `f` sweeps.
- **§4.1 item 2 — agreed, and the report needs it.** `--old` is no longer a candidate design: a block carrying a deep-fork reference is now *rejected*, so the unrestricted arm is an unreachable ceiling. This document labels it that way throughout; the report still calls it "the unrestricted model" in §2.1/§3.2/§6.6 and needs the same relabel.
### §6's review request — the incentive results survive
The handoff asks for §4.3's argument to be checked by someone who owns the incentive analysis rather than assumed. Checked, and it holds, for the reason given: **inclusion stayed soft.** The spec is explicit — a proposer *"may reference fewer uncles than it could, or pass over a candidate for another, and its block remains valid"*, and selection is *"a recommendation for filling the entries well, not a consensus rule."* So:
- §8.1 row 10 (*soft — never a validity rule*) is still satisfied: what became validity-gated is the **content** of a reference, not whether one is made.
- §6.8's argument against a hard inclusion mandate still applies to a rule the spec did not adopt.
- The `suppress` adversary — produce blocks, reference nothing — remains legal, so §6.3's results stand unchanged.
- The countable model's selection-time filter is now *exactly* what the protocol requires rather than a faithful approximation of it.
One correction to §4.3: it states the *"no incentive to deviate"* sentence "no longer exists in that form". It does — the clause survives verbatim inside a rewritten paragraph. **§8.5's implication (ii) is therefore still live:** the spec's no-deviation rationale rests on uncles granting no reward, and the report's recommendation to pay them removes it.
The one genuinely new consequence: referencing an ineligible orphan now costs a proposer its whole block rather than merely failing to count. No modelled strategy does this, so no result changes — but it makes such a strategy self-defeating rather than merely ineffective, which is worth stating if anyone later models junk-reference griefing.

View File

@ -1,7 +1,7 @@
# Handoff E5 — the diagnostic: does per-recipient delay variance reproduce the standalone result?
# The diagnostic: does per-recipient delay variance reproduce the standalone result?
#
# This is the only experiment in the handoff that could invalidate the REPORT rather than the spec
# section. The primary hypothesis for the original discrepancy is modelling, not measurement: the
# The one experiment here that could invalidate the REPORT rather than the spec section it is
# checking. The hypothesis for the discrepancy is modelling, not measurement: the
# standalone simulation drew an INDEPENDENT propagation delay per (block, recipient), while this
# simulator's blend topology runs a cascade of relays and then floods network-wide from the LAST
# relay, so nodes receive a block at nearly the same time and their views stay synchronised.
@ -12,7 +12,7 @@
# interpolates between the two models: 0 is the report's cascade, large values approach the
# standalone's independent-delay regime. The observable that decides it is deep_orphan_share --
# the fraction of in-window orphans sitting deeper than the first block of their fork, which is
# precisely the structural quantity behind claim C2.
# precisely the structural quantity behind the spec section's deep-fork claim.
#
# This is also the report's open item 15 (correlated/heterogeneous latency untested).
#
@ -31,7 +31,7 @@ degree: [6] # peering degree of the d-regular g
link_latency_mean: [0.5] # natural geographic transport (sub-slot)
link_latency_dist: [geo] # real-world geographic band mixture
blend_hops: [3] # the spec's Blend cascade length
blend_delay_max: [4.0] # E1: the spec's operating point (rho ~ 0.27)
blend_delay_max: [4.0] # the spec's operating point (rho ~ 0.27)
max_uncles: [0, 1, 2, 4] # 0 = negative control; 4 = the spec's MAX_UNCLES
uncle_strategy: [oldest] # spec Uncle Selection
init_dest: [common] # per-node initial D_est from agreement

View File

@ -1,12 +1,13 @@
# Handoff E2, the size arm: does the no-uncle loss deepen with N at the SPEC's operating point?
# Does the no-uncle loss deepen with N at the SPEC's operating point?
# Does the no-uncle loss deepen with N at the SPEC's operating point?
#
# The design-band study (fine-delay-paired.yaml) already settles the countable-vs-unrestricted
# question at delta_max = 4 with 40 replicates, but only at N = 1000. Claim C3 of the handoff
# ("without uncles the loss is 18.7 %") is the U = 0 column, and the report shows that column is
# question at delta_max = 4 with 40 replicates, but only at N = 1000. The spec section's claim
# that "without uncles the loss is 18.7 %" is the U = 0 column, and the report shows it is
# N-dependent (0.739 -> 0.635 over N = 1k -> 10k) at delta_max = 8. This measures it at the
# deployment's own delay instead, where nothing has been run at N > 1000.
#
# delta_max = 4 is the E1 answer: the spec's blending_delay is a FIXED 2 s per-hop dwell, and the
# delta_max = 4 is the deployed operating point: the spec's blending_delay is a FIXED 2 s per-hop dwell, and the
# simulator's Uniform(0, delta_max) matches it in the mean at 4. D_vis ~ 8 s, rho ~ 0.27.
#
# Run TWICE — default (countable) and with --old — so the first-fork gap is measured at this size
@ -21,7 +22,7 @@ degree: [6] # peering degree of the d-regular g
link_latency_mean: [0.5] # natural geographic transport (sub-slot)
link_latency_dist: [geo] # real-world geographic band mixture
blend_hops: [3] # the spec's Blend cascade length
blend_delay_max: [4.0] # E1: the spec's 2 s dwell, matched in the mean
blend_delay_max: [4.0] # the spec's 2 s dwell, matched in the mean
max_uncles: [0, 1, 2, 4] # 0 = the C3 column; 4 = the spec's MAX_UNCLES
uncle_strategy: [oldest] # spec Uncle Selection
init_dest: [common] # per-node initial D_est from agreement

View File

@ -1,12 +1,12 @@
# Handoff E4: is w_u = 300 adequate at the SPEC's operating point, and how much margin is there?
# Is w_u = 300 adequate at the SPEC's operating point, and how much margin is there?
#
# Claim C5 says the window "never binds" (observed median reference lag 34 slots, max 197). The
# report puts a hard recovery floor at ~7/f = 210 slots, set by block SPACING rather than by
# The spec section claims the window "never binds" (median reference lag 34 slots,
# max 197). The report puts a hard recovery floor at ~7/f = 210 slots, set by block SPACING rather than by
# delay, and advises widening to 450-600 near rho ~ 1. "Has margin at rho < 1" is defensible;
# "never binds" is not, and the difference is what this measures.
#
# absorption-window.yaml sweeps the same axis but starts at delta_max = 8; nothing has been run at
# the deployment's delta_max = 4 (E1). Since the floor is set by block spacing the knee should sit
# the deployment's delta_max = 4. Since the floor is set by block spacing the knee should sit
# in the same place, which is itself worth confirming rather than assuming — the report's
# delay-invariance claim is exactly what makes a single recommended W defensible.
#
@ -20,7 +20,7 @@ degree: [6] # peering degree of the d-regular g
link_latency_mean: [0.5] # natural geographic transport (sub-slot)
link_latency_dist: [geo] # real-world geographic band mixture
blend_hops: [3] # the spec's Blend cascade length
blend_delay_max: [4.0] # E1: the spec's operating point
blend_delay_max: [4.0] # the spec's operating point
window_absorption: [1, 2, 3, 5, 7, 10, 15, 20] # W in expected block-intervals; w_u = W/f slots
max_uncles: [1] # FIXED at one uncle: the question is about W
uncle_strategy: [oldest] # spec Uncle Selection

View File

@ -1,7 +1,7 @@
"""E5 — does per-recipient delay variance reproduce the standalone result? (the diagnostic).
"""does per-recipient delay variance reproduce the standalone result? (the diagnostic).
The only experiment in the fork-loss handoff that could invalidate the REPORT rather than the
spec section. The hypothesis for the original discrepancy is a modelling difference, not a
The one fork-loss experiment that could invalidate the REPORT rather than the spec section it
is checking. The hypothesis for the original discrepancy is a modelling difference, not a
measurement one: the standalone simulation drew an independent propagation delay per
(block, recipient), whereas this simulator's Blend cascade floods network-wide from the last
relay, so nodes receive a block at nearly the same time and their views stay synchronised.
@ -11,11 +11,12 @@ depth->=2 forks the first-fork rule cannot recover.
`jitter_mean` adds per-(block, node) arrival noise on top of the cascade, so sweeping it
interpolates between the two models. The deciding observable is `deep_orphan_share`: the fraction
of in-window orphans sitting deeper than the first block of their fork precisely the structural
quantity behind claim C2, and the thing `p_ref` conflates with "never picked up".
quantity behind the spec section's deep-fork claim, and the thing `p_ref` conflates with
"never picked up".
Pass / fail, as the handoff sets it:
Pass / fail:
* D-hat/D holds at ~1.000 and deep orphans stay negligible as jitter rises -> the standalone
model was simply wrong; C1/C2 are artefacts and the report is robust to this failure mode.
model was simply wrong; its numbers are artefacts and the report is robust to this failure mode.
* accuracy degrades toward 0.986 and deep orphans reach ~1 % of blocks at some jitter level
-> record that level and compare it to what Blend plausibly delivers; per-recipient variance
then becomes a parameter the report must carry, and the spec section's number is defensible
@ -97,7 +98,7 @@ def report(df: pd.DataFrame) -> None:
def main() -> None:
print(f"=== E5: jitter sweep, exact oracle, {len(JITTERS)*len(CAPS)*REPS*2} runs ===")
print(f"=== jitter sweep, exact oracle, {len(JITTERS)*len(CAPS)*REPS*2} runs ===")
report(sweep())
print(f"\nwrote {RUNS}/spec_jitter.parquet")

View File

@ -1,4 +1,4 @@
"""What the DEPLOYED chain would read at the spec's own operating point — handoff E1/E2/E6.
"""What the DEPLOYED chain would read at the spec's own operating point.
The report measures the mechanism: it drives the estimator to exact `f`, so its numbers isolate
fork loss from every other effect. That is the right default for design questions and the wrong
@ -6,7 +6,7 @@ one for "what will the deployed chain read", because the spec's estimator quanti
rate `cryptarchia-total-stake-inference.md` carries `const PRECISION: u64 = 1e3`, so
`f_p = 0.033` at `f = 1/30` and the recursion drives density to a target ~1 % below `f`.
E1 pins the operating point from `analysis-block-times-blend-network.md`: `blending_delay` is a
The operating point comes from `analysis-block-times-blend-network.md`: `blending_delay` is a
FIXED per-hop dwell of 2 s (the `3d+5` max-delay arithmetic gives 11 s at d=2 and 14 s at d=3,
matching the prose), so the simulator's `Uniform(0, delta_max)` matches it in the mean at
`delta_max = 4` -> `D_vis ~ 8 s`, `rho ~ 0.27`.
@ -43,7 +43,7 @@ EPOCHS = 20
REPS = 20
N_JOBS = 6
# The spec's operating point (E1), with the spec's own MAX_UNCLES rather than the report's U = 2.
# The spec's operating point , with the spec's own MAX_UNCLES rather than the report's U = 2.
SPEC_POINT = dict(n_nodes=1000, stake_dist="pareto", topology="blend", degree=6,
link_latency_mean=0.5, link_latency_dist="geo", blend_hops=3,
blend_delay_max=4.0, max_uncles=4, uncle_strategy="oldest",