Documentation sweep: make every finding reachable from the docs, not just the commits

Audited the report against this session's artifacts. All 37 figures are
referenced and none is orphaned, sec 8.3 items 1-19 are contiguous, and no
internal anchor is dead. Two knobs were measured but undocumented, and the
simulator README had drifted behind the code:

- sec 7 now documents f_precision alongside fixed_point, with the three measured
  arms (0.99997 exact / 1.01026 at the spec's 1e3 / 0.99990 at 1e6) rather than
  just the closed form, and says plainly that the default is the DESIGN choice
  while a spec-faithful arm needs both flipped.
- sec 7 gains deep_orphan_share, with its three regimes: 0.25% honestly,
  3.3% at 8 slots of per-recipient jitter, 19-41% under a private chain. It is
  the quantity p_ref conflates with "eligible but never picked up", which is the
  distinction that answered item 5.
- The simulator README lists the four new scripts and the new configs, describes
  all three adversary strategies (the selfish one was undocumented there), adds
  the new fork-structure metrics, and carries a short "modelling the DEPLOYED
  chain rather than the mechanism" table -- the two defaults that are
  deliberately not spec-faithful, and when to flip them.

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

View File

@ -793,7 +793,7 @@ Every parameter a protocol designer or operator can set, in three groups: **prot
| `w_u`, `w_n` — uncle / nephew rewards | compensation for referenced orphans / for referencing them | fairness: honest recovery 0.61 → 1.00 as `w_u` 0 → 1 (at `p_ref` = 1); hiding forfeits both rewards; safety invariant **`w_u + w_n < 1`** (else uncle farming pays); raising `w_n` alone marginally *helps* the attacker | [§6.7](#s6-7)/6.8, figs 14, 15 |
| inclusion rule | whether references are incentivised or mandated | **soft** (reward-weighted) only — a hard validity mandate cannot be encoded fork-safely (censorship or consensus fork) | [§6.8](#s6-8) |
| `genesis_d_factor`, `init_dest` | the initial `D̂` | a *common* wrong guess self-corrects in ~2 epochs (fig1); a *heterogeneous* init is preserved forever (fig21) — genesis `D̂` must be a shared constant | [§3.2](#s3-2), [Appendix C](#sC) |
| `fixed_point` | on-chain integer-f truncation `f_p = ⌊f·1000⌋/1000` | the **only** residual bias after slot-counting: ≈ +1 % (`f/f_p`), removed by higher-precision `f` ([Appendix A](#sA)) | [§2.2](#s2-2), [§5](#s5) |
| `fixed_point`, `f_precision` | whether the estimator quantises its target rate as the on-chain integer algorithm does, and at what scale (`f_p = ⌊f·f_precision⌋/f_precision`) | the **only** residual bias after slot-counting. Measured at the deployment's operating point: `D̂/D` = 0.99997 with exact `f`, **1.01026** at the spec's `f_precision` = 10³, 0.99990 at the recommended 10⁶. Default off, because a design question wants the mechanism isolated — but a *spec-faithful* arm must set `fixed_point: true, f_precision: 1000`, and that arm is the one that says what a deployed chain would read | [§2.2](#s2-2), [§5](#s5), [Appendix A](#sA) |
<a id="s7-2"></a>
### 7.2 Network / environment parameters
@ -814,6 +814,7 @@ Every parameter a protocol designer or operator can set, in three groups: **prot
| knob | probes | result | where |
|---|---|---|---|
| `adversary_strategy: selfish` | private-chain (SM1) override in the per-node engine | the profitable lever, now measurable end-to-end: revenue tracks EyalSirer at sub-slot latency (0.0356 vs 0.0356 at α = 0.1) and exceeds it as natural forking lifts `α_eff`; deflates `D̂` to 0.76 at α = 0.3 (δ = 8) with **no** cap able to recover it ([§6.8](#s6-8)) | [§6.6](#s6-6), [§6.8](#s6-8) |
| `deep_orphan_share` (engine output) | share of in-window orphans sitting *below* the first block of their fork | the structural observable behind the first-fork cost, and the one `p_ref` conflates with "eligible but never picked up": these blocks are unreferenceable by construction ([§2.1](#s2-1)). 0.25 % honestly at the deployment's point, 3.3 % at 8 slots of per-recipient jitter ([§6.1](#s6-1)), 1941 % under a private-chain attack ([§6.8](#s6-8)) | [§6.1](#s6-1), [§6.8](#s6-8) |
| `p_ref_honest` (engine output) | reference rate over orphans produced *outside* the coalition | equals `p_ref` under suppression; diverges sharply under a private chain (0.450.69 vs ≈ 0.99), which is the quantity the estimator repair actually depends on | [§6.8](#s6-8) |
| `adversary_frac` (β_adv), `adversary_strategy` | uncle suppression / abstention withholding | suppression weak at ρ < 1, a cheap lever only at ρ > 1 (1.43× at ρ = 1.36, β_adv = 0.5); withholding → `D̂ ≈ (1β_adv)` — the *correct* active-stake answer, topology-independent | [§6.3](#s6-3)/6.4, figs 8, 9 |
| `adversary_period`, `adversary_withhold_epochs` | dynamic withhold-rejoin schedules | unprofitable at every duty ψ > 0; griefing bounded (`deflation = β_adv·ψ`); no persistence | [§6.5](#s6-5), figs 1012 |

View File

@ -48,8 +48,18 @@
the pre-redesign model unchanged — window = `uncle_window` slots, any-depth orphans
referenceable, every baked reference counted — and **bit-reproduces historical runs** (the
old model's RNG key is byte-identical to the pre-`uncle_model` key).
- **Adversaries** (`adversary_strategy`, over a coalition holding `adversary_frac` of stake,
selected at random or `adversary_selection: whale` for the largest holders at matched stake):
- `suppress` — produces normally but references no uncles, starving the recovered density.
- `withhold` — never gossips its blocks; abstention, a dead loss to the attacker.
- `selfish` — mines a **private chain** and releases it under EyalSirer SM1 rules, orphaning
honest work. Only visibility is modelled: a coalition member's fork choice builds on the
private tip whenever it leads, so the chain forms and is abandoned emergently. Forces the
exact full scan, since a hidden block breaks the windowed horizon's premise.
- **Metrics:** per-node `D_est` spread (`range`, `IQR`), canonical-chain **agreement**
(window prefix vs current tip), mean accuracy, and — with `init_dest=heterogeneous`
(window prefix vs current tip), mean accuracy, fork structure (`fork_rate`, reorg depth,
`p_ref` and `p_ref_honest`, and `deep_orphan_share` — the share of orphans below their fork's
first block, i.e. unreferenceable by construction), and — with `init_dest=heterogeneous`
transient re-convergence.
## Headline result
@ -157,6 +167,9 @@ src/tsi_sim/ constants config rng stake lottery topology blocktree(+build_tree
configs/ smoke.yaml default.yaml fullscale.yaml
countable-vs-old.yaml absorption-window.yaml (countable-model studies)
fine-delay.yaml (delay 1-5 at 40 replicates: the design band, high precision)
uncle-selection.yaml (the spec's oldest-first rule vs a deviating proposer)
spec-point-{n5000,window,jitter}.yaml (the DEPLOYED operating point: delta_max=4
from the spec's Blend profile — size, window and per-recipient-variance arms)
tests/ test_{pernode,config,rng,lottery,blocktree,uncles,tsi_counting,stake,
theory,latency,theory_convergence,countable_counting,
countable_selfish,...}.py
@ -164,4 +177,20 @@ scripts/ plot_countable_vs_old.py (countable-vs-unrestricted comparison f
plot_fine_delay.py (design-band accuracy + model gap with 95% CIs)
countable_selfish.py (first-fork ceiling under the selfish MDP; fig36)
adversary_variants.py (whale/jitter/slow-beta variants + the withhold-load sweep)
deflation_frontier.py (how far a PAID adversary can deflate D_est; fig37)
selfish_uncle_margin.py (does the uncle cap need margin under a private chain?)
spec_point.py (the deployed operating point; the three f-precision arms)
spec_jitter.py (per-recipient delay variance — the transport diagnostic)
```
### Modelling the *deployed* chain rather than the mechanism
Two defaults are deliberately **not** spec-faithful, because the report's job is to isolate
mechanisms. Flip both for any run meant to answer "what would the deployed chain read":
| knob | default (design) | spec-faithful | why it matters |
|---|---|---|---|
| `fixed_point` / `f_precision` | `False` / `1e6` — exact `f` | `True` / `1000` | the spec quantises the target rate at `1e3`, which reads **+1.0 %** high; this is the largest error in the deployed estimator |
| `uncle_model` | `countable` — the spec's rules | (same) | `--old` is an unreachable **ceiling**, not an alternative: the spec *rejects* a block carrying a reference that fails the counting rules |
`scripts/spec_point.py` runs both arms side by side.