2026-07-30 18:57:10 +02:00
# tsi-sim-pernode — Cryptarchia TSI **per-node** network simulator (Phase 2)
> The reduced-model simulators (`../tsi-sim/`, `../tsi-sim-mc/`) collapse the network to one
> global canonical chain and one scalar `D_est` per epoch. **This package removes that
> collapse:** every one of the `N` nodes runs TSI individually with its **own** `D_est`, from
> its **own** partial view of the block tree under explicit message propagation over a peering
> graph. Its job is to *test* the reduced model's assumption that all honest nodes agree.
## What it models
- **Per-node lottery:** node `i` wins a slot with `φ_f(w_i / D_est_i)` — `D_est` is a length-`N`
**vector** , each node self-updating from its own view (the reduced model's key reuse: the
sparse sampler already takes a per-node probability vector).
- **Topology** (`topology` ): three propagation models over the network.
- `full_mesh` : every node one hop away with uniform latency `L` — reproduces the reduced
model exactly (validation baseline).
- `regular` : a random **d-regular** peering graph (configurable `degree` ) with per-link
latency (`link_latency_dist ∈ {fixed, uniform, exp, geo}` , all with mean
`link_latency_mean` ). A block reaches a node after the shortest **weighted** path from its
producer (gossip flooding). Models **direct block gossip** .
- `blend` : the **same** d-regular graph, but a block is first routed through the **Blend
mixnet** before it is public — the producer picks `blend_hops` distinct relay nodes
uniformly at random, the block hops `producer → r₁ → … → r_hops` over the graph, each relay
waiting a `Uniform(0, blend_delay_max)` **mixing delay** before forwarding, and the last
relay's forward is the final network-wide gossip that makes the block visible. Relays are
blind forwarders (they learn the block only from that final gossip). The dominant latency is
the per-hop mixing, not the graph transport — this is the multi-slot regime where forks and
the stake underestimate appear and uncle references matter. Because the mixing delays are
`Uniform` -bounded, the windowed fork choice stays **exact** (horizon
`(blend_hops+1)·max_path_latency + blend_hops·blend_delay_max` ).
- **Real-world latency (units).** Latency is in **slots** and a slot is **1 s** , so measured
internet latencies (tens– hundreds of ms) are *fractions* of a slot; arrivals are therefore
kept **sub-slot (float)** , not rounded to whole slots. `link_latency_dist=geo` draws each
link from a geographic band mixture (`~15 ms` metro → `~200 ms` antipodal, EU↔EU ≪ EU↔AU),
rescaled so `link_latency_mean` stays the mean-latency knob. So `regular` runs the realistic
sub-slot direct-gossip regime (`~0.05– 0.2` slot), where forks are rare, and `blend` runs the
multi-slot Blend-mixnet regime, where per-hop mixing delays dominate.
- **Per-node views:** one global block tree plus an `(N × n_blocks)` **arrival matrix** `A` ;
each node builds on / measures density over the blocks that have arrived at it. Uncle refs
are **baked at production** from the producer's view (faithful — immutable once adopted).
Countable uncle model: spec counting rules, sweeps, figures
Implement the countable uncle model from the Cryptarchia spec's
counting-only reference rules, and make it the simulator default.
Counting rules (uncles.py, measure.py):
- Only the first block of a fork (parent on the producer's chain) is
referenceable and countable, which makes every reference verifiable
from chain data alone.
- The reference window is derived from a window-absorption parameter,
w_u = W_abs/f slots (W_abs in expected block-intervals, default 10,
bounded W_abs <= 0.6*k), replacing the free-standing uncle_window.
- Selection skips slots already occupied on the producer's chain and
takes at most one uncle per slot.
- The measurement pass re-checks every rule per reference and tallies
rejections as deep_ref_share.
The pre-redesign model is preserved behind --old on tsi-sweep and
tsi-verify. Its RNG key is byte-identical to the pre-uncle_model key,
so --old bit-reproduces the historical runs.
Supporting changes: uncle_model and window_absorption config surface
with validation (config.py, constants.py); accuracy closed form over
the effective q_u (theory.py); plumbing through tsi.py, epoch.py,
sweep.py, blocktree.py, metrics.py, verify.py, figures_pernode.py.
Studies and figures:
- configs/countable-vs-old.yaml -- delay x U grid, run under both
models on the same grid.
- configs/absorption-window.yaml -- accuracy vs W_abs at U=1.
- scripts/plot_countable_vs_old.py renders fig30-fig33 into
reports/tsi/report-figures/.
Tests: tests/test_countable_counting.py (7 cases) covering first-fork
eligibility, derived-window bounds, occupied-slot exclusion, and
per-reference re-checking; extensions to test_uncles.py,
test_config.py, test_slot_counting.py. Full fast suite: 202 passed.
Also adds CLAUDE.md (graphify project instructions) and ignores
editor/local-agent state plus the vendored Equi-X benchmark clone.
The reports/tsi/ prose describing this model is held back for a
separate editorial pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:48:46 +02:00
- **Uncle model (`uncle_model` , CLI `--old` ):** the default **countable** model implements the
spec's counting-only rules (cryptarchia-v1-protocol.md): only the **first block of a fork**
(parent on the producer's chain) is referenceable/countable, the window is **derived** as
`w_u = window_absorption / f` slots (`W` expected block-intervals, default `W = 10` → 300
slots, bounded `W ≤ 0.6·k` ), selection skips slots already occupied on the producer's chain
and picks one uncle per slot, and the measurement pass re-checks every rule per reference
(rejections tallied as `deep_ref_share` ). Passing `--old` to `tsi-sweep` /`tsi-verify` runs
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).
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>
2026-08-07 11:36:01 +02:00
- **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 Eyal– Sirer 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.
2026-07-30 18:57:10 +02:00
- **Metrics:** per-node `D_est` spread (`range` , `IQR` ), canonical-chain **agreement**
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>
2026-08-07 11:36:01 +02:00
(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` —
2026-07-30 18:57:10 +02:00
transient re-convergence.
## Headline result
**Per-node `D_est` disagreement collapses to zero.** Because TSI reads density from a window
buried far past `k` -finality, and all nodes seed the recursion from a common hardcoded
genesis `D` , every node computes the **same** measured density `m` → **identical** `D_est`
(`range ≈ 0` , `agreement_window = 1` ) — *even under a sparse graph with high latency and heavy
tip-level forking* (`agreement_tip` can drop well below 1). This **validates the reduced
model**. Topology/latency instead shift the shared *mean* accuracy (via fork rate → `q` ),
which uncle references recover just as in the reduced model. (Injected heterogeneous
disagreement, which the real protocol never creates, is *preserved* by the common
multiplicative update — a cautionary note, not protocol behaviour.)
## Quick start
```bash
make install # venv + editable install
make test # unit + fast per-node checks
make verify # per-node validation (parity, spread→0, agreement, topology effect)
# Run any configs/<name>.yaml by its stem (auto-discovered); each writes a dated runs/ folder:
make smoke # tiny end-to-end grid + figures (configs/smoke.yaml)
make default # scaled-k divergence/topology sweep + figures (configs/default.yaml)
make fullscale # full-scale (true k) confirmation (configs/fullscale.yaml)
make figures RESULTS=runs/< dir > /results.parquet # re-render figures from a run
# Extra sweep flags: make fullscale SWEEP_ARGS="--batch-size 1 --mem-frac 0.6"
```
## Scale & performance
- **Representation:** one global block tree + `(N × n_blocks)` `float64` arrival matrix `A`
(sub-slot arrivals); topology `path_latency[N,N]` (per-node Dijkstra, once per trajectory).
- **`n_blocks` is NOT `~10·k` in general — it tracks block production.** `n_blocks` is the number
of lottery wins in an epoch, `≈ E·Σᵢφ(wᵢ/D_est)` . At equilibrium that is `~10·k` (≈ 22k at
k=2160), but when `D_est` is far below the true stake — the **collapsed-estimate regime** , e.g. a
small `genesis_d_factor` — `Σ(stake)/D_est = 1/genesis_d_factor` is large and block production
explodes proportionally. At `genesis_d_factor=0.01` , genesis epoch-0 produces ** ~2.0M blocks**
(100× equilibrium) → `A ≈ 15 GB` for a *single* worker; `D_est` self-corrects to equilibrium
within ~2 epochs, so only the earliest epoch(s) are heavy. **Raising `genesis_d_factor` toward
0.1– 0.5 collapses this cost** (0.1 → ~0.22M blocks → ~1.6 GB; 0.5 → ~44k → ~0.4 GB) and does not
change the equilibrium result, which is measured after burn-in.
- **Sliding-window prune (`prune_arrival` , default on):** the arrival matrix never needs per-node
columns for blocks past the horizon — under deterministic latency a block with `slot ≤ t − H` has
reached *every* node, so its column is finalized and dropped. We keep columns only for blocks
inside `max(horizon, uncle_window)` slots in a base-offset buffer, turning the `O(N·n_blocks)`
matrix into `O(N · keep-span-blocks)` . This is what makes the collapsed regime affordable: at
N=1000/k=2160/`gdf=0.01` the buffer is ~tens of MB instead of the ~15 GB full matrix (fork choice,
the parent clamp, uncle selection, and per-node tips all reconstruct exactly from it). It is
**bit-identical** to the full matrix at `jitter_mean == 0` (proven by `test_prune_matches_full_matrix`
across topologies/uncles/`gdf` ); with jitter it falls back to the full matrix (whose safety clamp
keeps the tree valid). Set `prune_arrival: false` to force the full matrix (the parity oracle).
The measurement pass also argmaxes in node-row bands so it adds only a small temporary. Divergence
sweeps run at scaled **k=256** (`configs/default.yaml` ); full-scale k=2160 is validated to **N ≤ 2000** .
- **Worker sizing (auto, RAM-safe):** both `A` (`~N·n_blocks` , incl. the block explosion above)
and `path_latency` (`~N²` ) grow, so the sweep runner sizes the loky pool to fit a RAM budget
(`--mem-frac` , default 0.7 of physical RAM) instead of blindly using every core. The per-worker
estimate realises the seeded stake to compute the **genesis-epoch** block count
(`expected_peak_blocks` ), so it reflects a low-`genesis_d_factor` explosion rather than assuming
`~10·k` . A **calibration probe** measures a real worker's peak RSS (one genesis epoch of the
heaviest config in a spawned process) whenever the estimate is heavy or `N > 2000`
(`--calibrate {auto,always,never}` , default `auto` ; the probe bounds itself to physical RAM so it
fails loud rather than freezing).
- **Fail-loud memory guard (`memguard.py` ):** every worker checks size *before* allocating both
big arrays — the `(N × n_blocks)` `A` (in `build_tree_pernode` ) and the `(N × N)` `path_latency`
(in `build_path_latency` , built first) — and raises `ArrivalMatrixTooLarge` if it would exceed
the budget `TSI_ARRIVAL_BYTES_BUDGET` . The sweep sets that to each worker's RAM share; **unset or
`0` is not "unlimited"** — it resolves to `DEFAULT_BUDGET_FRAC` (0.9) of physical RAM, so a bare
`run_trajectory` , `tsi-verify` , the probe, or a `--mem-frac 0` run all keep an absolute
per-process ceiling. So a mis-estimated block explosion (or a huge `N` ) fails with a clear
message instead of freezing the machine.
- **Cost:** dominated by the per-node fork choice (batched per slot) and the arrival-matrix
fill; the sparse lottery is negligible. Across-config joblib **loky** parallelism reused.
- **Measurement optimisation (`measure.py` ):** the per-node canonical/density/agreement pass
was ~95% of an epoch. It is now **deduped by tip** (nodes sharing a tip share every derived
quantity — high agreement collapses `N` to a handful of computations) and the per-tip chain
walk runs as a cached **numba** kernel (pure-Python fallback if numba is absent). Exact —
bit-identical to the naive loop (`test_measure` ). Measured ** ~9× end-to-end** (heavy config
11.3 s → 1.2 s) and ~14× on measurement-bound configs. numba comes via the `accel` extra
(`pip install -e ".[dev,accel]"` , done by `make install` ).
- **Windowed fork choice (`windowed_fork_choice` , default on):** bounds the block-tree build's
per-slot candidate scan to a horizon of the max path latency plus the fully-propagated best
tip, turning `O(n_blocks^2)` fork choice into `O(n_blocks*H)` . **Exact** when link latency is
deterministic (`jitter_mean == 0` ) — bit-identical to a full scan (parity test). With
`jitter_mean > 0` it becomes a tiny approximation and **warns** ; a safety clamp still keeps
the tree valid, and `windowed_fork_choice=False` forces a guaranteed-exact full scan.
- **Reproducibility:** every draw spawns off `SeedSequence(hash(config))` — child 0 stake,
1 graph, 2 init, 3+e epoch `e` . `graph_seed` /`degree` /`link_latency_*` are part of the
config identity.
- **numpy-version caveat:** the `accel` extra (numba) requires `numpy<2.5` , so installing it
pins numpy to 2.4.x. numpy's `Generator.choice(replace=False)` is **not** stream-stable
across the 2.4↔2.5 boundary, and the sparse lottery uses it heavily only in the degenerate
*collapsed-estimate* regime (`D_est → 0` ⇒ win-prob → 1 ⇒ `count ≈ n_slots` ). So a run on
numpy 2.5 and a run on numpy 2.4 give **identical results for all normal configs** but can
diverge chaotically in that one extreme regime (e.g. `degree=4, link_latency=8` , where the
estimate has already collapsed to ~0.13 — off the safe chart). The differences are tiny
(max |Δ mean_ratio| ≈ 3e-3) and change no conclusion; pin numpy if bit-reproducibility
across environments is required.
## Layout
```
src/tsi_sim/ constants config rng stake lottery topology blocktree(+build_tree_pernode)
uncles(+select_uncles_at_production) tsi(+update_D_vec) epoch engine metrics
theory verify plotting/{style, figures_pernode, make_figures}
configs/ smoke.yaml default.yaml fullscale.yaml
Countable uncle model: spec counting rules, sweeps, figures
Implement the countable uncle model from the Cryptarchia spec's
counting-only reference rules, and make it the simulator default.
Counting rules (uncles.py, measure.py):
- Only the first block of a fork (parent on the producer's chain) is
referenceable and countable, which makes every reference verifiable
from chain data alone.
- The reference window is derived from a window-absorption parameter,
w_u = W_abs/f slots (W_abs in expected block-intervals, default 10,
bounded W_abs <= 0.6*k), replacing the free-standing uncle_window.
- Selection skips slots already occupied on the producer's chain and
takes at most one uncle per slot.
- The measurement pass re-checks every rule per reference and tallies
rejections as deep_ref_share.
The pre-redesign model is preserved behind --old on tsi-sweep and
tsi-verify. Its RNG key is byte-identical to the pre-uncle_model key,
so --old bit-reproduces the historical runs.
Supporting changes: uncle_model and window_absorption config surface
with validation (config.py, constants.py); accuracy closed form over
the effective q_u (theory.py); plumbing through tsi.py, epoch.py,
sweep.py, blocktree.py, metrics.py, verify.py, figures_pernode.py.
Studies and figures:
- configs/countable-vs-old.yaml -- delay x U grid, run under both
models on the same grid.
- configs/absorption-window.yaml -- accuracy vs W_abs at U=1.
- scripts/plot_countable_vs_old.py renders fig30-fig33 into
reports/tsi/report-figures/.
Tests: tests/test_countable_counting.py (7 cases) covering first-fork
eligibility, derived-window bounds, occupied-slot exclusion, and
per-reference re-checking; extensions to test_uncles.py,
test_config.py, test_slot_counting.py. Full fast suite: 202 passed.
Also adds CLAUDE.md (graphify project instructions) and ignores
editor/local-agent state plus the vendored Equi-X benchmark clone.
The reports/tsi/ prose describing this model is held back for a
separate editorial pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:48:46 +02:00
countable-vs-old.yaml absorption-window.yaml (countable-model studies)
Review fixes + high-precision design-band delay study
Acts on a correctness/completeness review of the countable uncle model
and its report material.
Correctness fixes in the report:
- s3.4 quoted 0.998 for W_abs=10 at the 8s budget; the run says 0.9963.
- s1 claimed both models >= 0.996 at U >= 1; countable U=2 delta=8 is
0.9955. Corrected to >= 0.995.
- The s3.2 table presented two cells (U=1 at delta 16 and 32) as model
differences. They are not resolvable: t = 0.46 and 0.47 over 5
replicates. The table now carries +-SEM and a t per cell.
- s3.4 claimed the ~7-block-interval floor "carries over unchanged".
Accuracy is still climbing past W=7 at every delay (8s: 0.989 ->
0.996), so the claim is dropped. The 32s curve is non-monotonic with
replicate SD up to 0.22 and is now flagged as noise, not a trend.
- 1-r was attributed to the first-fork restriction alone; it is the
combined first-fork and capacity loss, which this measurement cannot
separate. Hedged to match fig32's own axis label.
Completeness: the U=0 negative control was swept but never reported.
With no uncles the two models are identical by construction, yet they
differ by -0.23 at delta_max=32 (t=2.1) because they draw independent
RNG streams. That is the noise floor the rest of the grid must clear,
and it is now in s3.2, s9, fig30 and the config header.
New study (configs/fine-delay.yaml, scripts/plot_fine_delay.py, s3.2a,
fig34/fig35): the design band delta_max 1-5 at 40 replicates, both
models. Findings: every U >= 1 cell of both models lands in
0.998-1.001, flat in delay, while U=0 decays 0.810 -> 0.640. No
individual cell resolves a model difference (widest 95% CI +-0.15pp;
max t=2.59 vs Bonferroni 2.94 over 15 cells). Pooled across uncle caps
the first-fork cost is monotone in delay and separates from zero only
at delta_max=5 (-0.0014 +- 0.0007, t=3.7) -- below 0.15% everywhere in
the band, against +-0.9% per-epoch sampling noise.
Code:
- deep_ref_share is identically 0 on every real countable run: for a
chain block B the producer's chain below B is the counting chain
below B, so the counting-side parent-on-chain re-check cannot reject
what selection emitted. It is a drift alarm, not a rate. Documented
as such in measure.py, the plot docstring and the config header, and
pinned by a new end-to-end test.
- Removed annotate_uncles: a second countable implementation that
production never called, while carrying most of the selection test
coverage. Tests now drive select_uncles_at_production through an
annotate_via_production replay helper -- same assertions, live path.
- Added tests for the two previously uncovered branches of the live
selection: the pmin/below chain walk that resolves parent-on-chain
for candidates whose parent sits below the window, and the
occupied-slot exclusion built from the chain walk.
- theory.q_effective and theory.window_miss_prob were unused and
untested. Now used (the prediction figure reconstructs q_u through
the identity the report quotes) and tested. The window_miss_prob test
records that its "~ e^-W" docstring is the f->0 limit: the true decay
is e^-1.017W at f=1/30, 16% off by W=10.
- Shared sem()/recovery_rate() moved into figures_pernode.py; fig30 and
fig33 regenerated with SEM error bars and the U=0 control curve.
- Fixed the pre-existing E501 in bootstrap_dynamics.py; ruff clean.
Report prose reworked to read standalone: the countable model is
described as the rules under analysis and the former model as a
labelled "unrestricted" comparison baseline, with no dated banners and
no round-to-round narration.
Tests: 209 passed (was 202).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 20:48:38 +02:00
fine-delay.yaml (delay 1-5 at 40 replicates: the design band, high precision)
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>
2026-08-07 11:36:01 +02:00
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)
2026-07-30 18:57:10 +02:00
tests/ test_{pernode,config,rng,lottery,blocktree,uncles,tsi_counting,stake,
Review pass: reproduce every number from its data of record, fix what did not
Correctness/completeness review of the report and simulator. Verified against
the committed parquets: the sec 6.6 countable-ceiling table (cap-64 MDP sweep),
sec 6.10 Result 4's depth ceilings, the sec 3.4 uncle-selection table, all
adversary-variant numbers, the rho-boundary row-4 quotes (0.976 at rho=0.91,
4-sigma shortfall at 0.96, max cell 1.0024), and the sec 8.4 capstone table.
Three defects found, all fixed:
1. The collapse event was not reproducible from the committed script. Study D
swept only the default (random) coalition, but the one observed collapse is
a whale cell; the "once in 144 runs" count came from an ad-hoc probe. The
committed sweep now carries the selection axis (96 runs) and reproduces the
event: 1/12 in the whale 50% cell at delta_max = 8, never at 4. All six
fold-related passages now quote the committed sweep, which also retires the
stale "the full dynamics never reach it" wording in the sec 6 arc, the
sec 6.2 intro, row 6 and item 1 -- text that contradicted item 18 since
yesterday's finding.
2. capstone.py's printout could not reproduce the report's sec 8.4 table. The
report's numbers are a per-replicate-tail aggregation (each replicate burns
in against its own early-stop length); the script cut the tail at the ARM's
max epoch, silently dropping any replicate that stopped earlier (7 of 8 in
the adversary arm) and landing one rounding step off on three cells. The
script now aggregates per replicate and prints the SEM; against the existing
parquet it reproduces the table exactly (1.001/0.998, 0.342+-0.009 /
0.343+-0.005, p_ref 1.000/0.990, 8 reps both arms). The report table was
right all along; sec 6.8's p_ref quote (0.989, the per-arm value) is aligned
to 0.990.
3. Small report fixes: slow-beta deflation rounded 0.765 -> "0.77" (now 0.76);
fig13's caption now points at the fig36 ceiling instead of implying free
recovery; row 5 cites the measured slow-beta standing deflation; the
canonical-data paragraph lists the new studies' artifacts; the simulator
README's layout block lists the new tests and scripts.
Adds a unit test for reorg.countable_recovery_from_depths (the one new
function that had none). 236 tests pass; the new-study parquets are copied to
the main checkout's runs/, where every other study's data of record lives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:29:13 +02:00
theory,latency,theory_convergence,countable_counting,
countable_selfish,...}.py
Review fixes + high-precision design-band delay study
Acts on a correctness/completeness review of the countable uncle model
and its report material.
Correctness fixes in the report:
- s3.4 quoted 0.998 for W_abs=10 at the 8s budget; the run says 0.9963.
- s1 claimed both models >= 0.996 at U >= 1; countable U=2 delta=8 is
0.9955. Corrected to >= 0.995.
- The s3.2 table presented two cells (U=1 at delta 16 and 32) as model
differences. They are not resolvable: t = 0.46 and 0.47 over 5
replicates. The table now carries +-SEM and a t per cell.
- s3.4 claimed the ~7-block-interval floor "carries over unchanged".
Accuracy is still climbing past W=7 at every delay (8s: 0.989 ->
0.996), so the claim is dropped. The 32s curve is non-monotonic with
replicate SD up to 0.22 and is now flagged as noise, not a trend.
- 1-r was attributed to the first-fork restriction alone; it is the
combined first-fork and capacity loss, which this measurement cannot
separate. Hedged to match fig32's own axis label.
Completeness: the U=0 negative control was swept but never reported.
With no uncles the two models are identical by construction, yet they
differ by -0.23 at delta_max=32 (t=2.1) because they draw independent
RNG streams. That is the noise floor the rest of the grid must clear,
and it is now in s3.2, s9, fig30 and the config header.
New study (configs/fine-delay.yaml, scripts/plot_fine_delay.py, s3.2a,
fig34/fig35): the design band delta_max 1-5 at 40 replicates, both
models. Findings: every U >= 1 cell of both models lands in
0.998-1.001, flat in delay, while U=0 decays 0.810 -> 0.640. No
individual cell resolves a model difference (widest 95% CI +-0.15pp;
max t=2.59 vs Bonferroni 2.94 over 15 cells). Pooled across uncle caps
the first-fork cost is monotone in delay and separates from zero only
at delta_max=5 (-0.0014 +- 0.0007, t=3.7) -- below 0.15% everywhere in
the band, against +-0.9% per-epoch sampling noise.
Code:
- deep_ref_share is identically 0 on every real countable run: for a
chain block B the producer's chain below B is the counting chain
below B, so the counting-side parent-on-chain re-check cannot reject
what selection emitted. It is a drift alarm, not a rate. Documented
as such in measure.py, the plot docstring and the config header, and
pinned by a new end-to-end test.
- Removed annotate_uncles: a second countable implementation that
production never called, while carrying most of the selection test
coverage. Tests now drive select_uncles_at_production through an
annotate_via_production replay helper -- same assertions, live path.
- Added tests for the two previously uncovered branches of the live
selection: the pmin/below chain walk that resolves parent-on-chain
for candidates whose parent sits below the window, and the
occupied-slot exclusion built from the chain walk.
- theory.q_effective and theory.window_miss_prob were unused and
untested. Now used (the prediction figure reconstructs q_u through
the identity the report quotes) and tested. The window_miss_prob test
records that its "~ e^-W" docstring is the f->0 limit: the true decay
is e^-1.017W at f=1/30, 16% off by W=10.
- Shared sem()/recovery_rate() moved into figures_pernode.py; fig30 and
fig33 regenerated with SEM error bars and the U=0 control curve.
- Fixed the pre-existing E501 in bootstrap_dynamics.py; ruff clean.
Report prose reworked to read standalone: the countable model is
described as the rules under analysis and the former model as a
labelled "unrestricted" comparison baseline, with no dated banners and
no round-to-round narration.
Tests: 209 passed (was 202).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 20:48:38 +02:00
scripts/ plot_countable_vs_old.py (countable-vs-unrestricted comparison figures)
plot_fine_delay.py (design-band accuracy + model gap with 95% CIs)
Review pass: reproduce every number from its data of record, fix what did not
Correctness/completeness review of the report and simulator. Verified against
the committed parquets: the sec 6.6 countable-ceiling table (cap-64 MDP sweep),
sec 6.10 Result 4's depth ceilings, the sec 3.4 uncle-selection table, all
adversary-variant numbers, the rho-boundary row-4 quotes (0.976 at rho=0.91,
4-sigma shortfall at 0.96, max cell 1.0024), and the sec 8.4 capstone table.
Three defects found, all fixed:
1. The collapse event was not reproducible from the committed script. Study D
swept only the default (random) coalition, but the one observed collapse is
a whale cell; the "once in 144 runs" count came from an ad-hoc probe. The
committed sweep now carries the selection axis (96 runs) and reproduces the
event: 1/12 in the whale 50% cell at delta_max = 8, never at 4. All six
fold-related passages now quote the committed sweep, which also retires the
stale "the full dynamics never reach it" wording in the sec 6 arc, the
sec 6.2 intro, row 6 and item 1 -- text that contradicted item 18 since
yesterday's finding.
2. capstone.py's printout could not reproduce the report's sec 8.4 table. The
report's numbers are a per-replicate-tail aggregation (each replicate burns
in against its own early-stop length); the script cut the tail at the ARM's
max epoch, silently dropping any replicate that stopped earlier (7 of 8 in
the adversary arm) and landing one rounding step off on three cells. The
script now aggregates per replicate and prints the SEM; against the existing
parquet it reproduces the table exactly (1.001/0.998, 0.342+-0.009 /
0.343+-0.005, p_ref 1.000/0.990, 8 reps both arms). The report table was
right all along; sec 6.8's p_ref quote (0.989, the per-arm value) is aligned
to 0.990.
3. Small report fixes: slow-beta deflation rounded 0.765 -> "0.77" (now 0.76);
fig13's caption now points at the fig36 ceiling instead of implying free
recovery; row 5 cites the measured slow-beta standing deflation; the
canonical-data paragraph lists the new studies' artifacts; the simulator
README's layout block lists the new tests and scripts.
Adds a unit test for reorg.countable_recovery_from_depths (the one new
function that had none). 236 tests pass; the new-study parquets are copied to
the main checkout's runs/, where every other study's data of record lives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:29:13 +02:00
countable_selfish.py (first-fork ceiling under the selfish MDP; fig36)
adversary_variants.py (whale/jitter/slow-beta variants + the withhold-load sweep)
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>
2026-08-07 11:36:01 +02:00
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)
2026-07-30 18:57:10 +02:00
```
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>
2026-08-07 11:36:01 +02:00
### 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.