4 Commits

Author SHA1 Message Date
Marcin Pawlowski
f1433cbbb3
Anchor the uncle reference window to the parent, not the uncle
The spec bounds an uncle's own slot (0 < sl_A - sl_U <= w_u) but leaves its
PARENT unconstrained beyond lying on the referencing chain. So a block minted
NOW, built on a chain block from arbitrarily far back, is a legal first-fork
uncle: recent by its own slot, ancient by its parent's. Verifying it means
deriving the epoch state and ledger root as of that ancient parent, per
reference, and those are precisely the inputs the counting rules require -- so
the work cannot be amortised. It costs the adversary nothing beyond lottery
wins it already has; it just builds them somewhere useless.

Measured with a deep_parent coalition. At the deployed operating point a 30%
adversary moves the MEDIAN counted reference's reach from 54 slots back to
20,144, and the worst case to 76,778 -- the epoch boundary, ~21 hours of
history, ~256x the nominal window. It is not a tail effect.

The fix is a SUBSTITUTION, not an additional rule. A block strictly postdates
its parent and a referenced uncle strictly precedes its referencer, so
sl_A - sl_U < sl_A - sl_parent(U) <= w_u: bounding the parent bounds the uncle
for free, and a both-windows variant would be identical to the parent one. Both
invariants are pinned in a new test_slot_ordering.py rather than argued -- the
user asked to confirm sl_A > sl_U explicitly, and it turns out to be
load-bearing for the whole implication, so it is tested at three geometries
plus a hand-built counting case.

Under the parent anchor the same coalition reaches 292/300/300 slots at
delta_max 4/8/16 -- capped by construction. Honest recovery is unaffected:
0.9993 -> 0.9999, 0.9969 -> 0.9986, 0.9791 -> 0.9858, no loss anywhere within
one to two SEM, because a latency orphan's parent is recent by construction.

One finding that sharpens the case: at delta_max = 16 the HONEST uncle-anchored
arm already reaches 315 slots, past its own w_u = 300. Under the current rule
w_u is not a bound on validation reach even with no adversary present. It only
becomes a state-retention bound once anchored to the parent.

Recorded as sec 6.12 with fig38, a new row in the sec 8.5 spec deltas, both new
knobs in sec 7, and the study in sec 9. uncle_window_anchor and the deep_parent
strategy are appended to the RNG key only when non-default, so no committed run
is reseeded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:22:50 +02:00
Marcin Pawlowski
3708d03e22
Private-chain (SM1) adversary in the per-node engine
Sec 6.8 recorded that "the per-node engine has no private-chain strategy", which is
why every selfish result came from the global race model with uncle recovery as
a free knob eta -- and why open item 5 (does the uncle cap need margin under
attack-inflated orphaning?) could not be sized: a knob has no queue to overflow.

adversary_strategy="selfish" adds it. The coalition mines one shared private
chain and releases under the classic SM1 rules in (a, h) form: adopt when the
public chain wins, match at equal length, override at a one-block lead, else
wait. Only VISIBILITY is modelled -- the coalition's mining needs no special
case, because a member's fork choice already builds on the private tip whenever
it leads (that tip has the greatest height among blocks the member can see) and
falls back to the public chain exactly when the public chain overtakes, which
is the adopt branch. So the private chain forms, extends and is abandoned
emergently, and the code that had to be written is the arrival matrix.

Design notes worth keeping:
- Private blocks reuse the sentinel `withhold` already had (never-arrives), so
  the existing exclusions from canonical-tip selection apply unchanged; release
  flips it back and gossips DIRECTLY from the producer, bypassing Blend, since
  an adversary has no privacy budget to respect and wants the race won.
- A private chain breaks the windowed horizon's premise (a hidden block is old
  enough to look fully-propagated while no honest node has it, and it becomes
  visible LATER, which the one-way frontier pointer cannot revisit), so selfish
  forces the exact full scan and full matrix.
- Blocks still hidden at epoch end are abandoned and hidden from the coalition
  too, or the canonical-tip search would crown a chain no honest node saw.

Validated against Eyal-Sirer at sub-slot latency: revenue share 0.0356 vs an
exact 0.0356 at alpha = 0.1, and above the closed form at higher alpha by just
the margin the alpha_eff fork-amplification correction predicts (0.498 vs 0.484
at alpha = 0.4, with fork rate 0.38).

Adds p_ref_honest: the reference rate over orphans produced OUTSIDE the
coalition. Under a private-chain attack this diverges sharply from p_ref, and
only the honest one measures the repair the report credits to uncle counting --
an attacker's own discarded blocks are its loss to bear.

test_fork unpacks fork_stats positionally, so its three call sites take the new
fifth value. 247 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:46:57 +02:00
Marcin Pawlowski
bd2ac7b7be
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
Marcin Pawlowski
24da2fc8b3
Importing tsi-sim v3 2026-07-30 18:57:10 +02:00