Second half of the re-measurement. Sizing U is unaffected: the smallest cap
reaching the 0.98 bar is identical under both anchors at every load tested --
U=1 at rho 0.27 and 0.47, U=2 at 0.87, and neither clears at rho=1.27, which is
past the operating point the recipe permits. U = ceil(rho)+1 carries over
verbatim. At that cap the residual cost stays inside the design regime: -0.003
at rho 0.47 and 0.87, growing to -0.009 only at 1.27.
So the anchor's cost lives in the WINDOW, not the cap, which is what the
mechanism predicts -- the parent gap lengthens the queue's deadline without
changing how fast it drains.
Net effect on the recipe: U unchanged, W floor up one to two block-intervals,
W = 10 still clearing at the deployed point and W = 12 restoring today's margin
at heavier delay. Both sweeps are committed configs and their data archived, so
the re-baselining is reproducible rather than a one-off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
My sec 6.12 claim was "costs no honest recovery at any tested delay". That
measurement held max_uncles = 4 -- a slack cap, which hides the window entirely,
since with four uncle slots the queue drains faster than the window can bind.
Re-running the sec 3.4 absorption sweep at U = 1, where the window IS the binding
constraint, shows the cost is real:
delta=4 knee W 5 -> 6 at W=10: 0.9982 -> 0.9981 (free)
delta=8 knee W 6 -> 8 at W=10: 0.9963 -> 0.9930 (-0.0033)
delta=16 knee W 20 -> 20 at W=10: 0.9658 -> 0.9326 (-0.0332)
The floor rises by one to two block-intervals, exactly what a parent gap running
one block-interval longer than the uncle gap predicts -- the prediction I wrote
into the plan and then failed to test, because the first study was configured
with a cap that masked it.
Both statements are true and the report now carries both: at the recommended
U = ceil(rho)+1 the anchor is free, and at U = 1 it costs measurably from
delta = 8 up. The consequence for sizing is that W should be measured against
the parent gap under the new rule -- W = 12 restores the margin W = 10 has
today, inside the spec's floor(0.6k) bound, so it needs no further spec change.
The delta = 16 row is U-limited rather than window-limited (rho ~ 0.87 against
U = 1, below the recommended cap), and is labelled as such.
The sec 8.5 spec-delta row and the sec 6 arc bullet carried the same overclaim and
are corrected too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
The last and most consequential handoff experiment -- the only one that could
have invalidated the report rather than the spec section. It does not, but the
result is more useful than a refutation: it identifies the assumption the
section's number encodes.
Sweeping per-(block,node) jitter interpolates between the two transport models,
since jitter is precisely what the standalone simulation had and the Blend
cascade does not. At the deployment's operating point, D-hat/D at U=1 runs
0.9983 / 0.9992 / 0.9991 / 0.9971 / 0.9871 for jitter 0/1/2/4/8 slots, with the
share of orphans below their fork's first block rising 0.25% -> 3.30%. So BOTH
branches of the handoff's pass/fail are true at different levels: flat to ~2
slots, and at 8 slots it lands on 0.9871 -- essentially the section's 0.986.
That turns the disagreement into a checkable question. The cascade's variance is
per BLOCK (every recipient moved together by the final flood, harmless to the
estimate); jitter is per RECIPIENT (divergent, and the only kind that makes
forks deeper than one block, which the counting rule cannot recover). Blend
delivers ~0.5 slot of per-recipient spread over a degree-6 graph, so the design
sits an order of magnitude inside the flat region and 8 slots is ~16x what the
transport produces. The section charged per-recipient variance where Blend
delivers per-block; that distinction is the whole disagreement.
Consensus is untouched at every level (range_ratio = 0 and agreement = 1.000 in
all 480 runs), reconfirming sec 6.1 at the deployment's own point.
Partly closes open item 15: the report now carries a measured bound on
per-recipient variance instead of assuming the transport. Spatially CORRELATED
latency is still untested -- jitter is i.i.d. per (block, node), so it cannot
model a region straggling as a bloc, and item 15 is narrowed to that.
Also lands the sec 4.1 item 2 relabel from the handoff guide: the unrestricted
arm is an unreachable CEILING, not a candidate design, because the spec now
rejects a block carrying an entry that fails the counting rules. Sec 2.1 and the
header note say so explicitly rather than calling it "the comparison baseline".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verification of my own item-16 result, which I had flagged as resting on a
step I had not checked: "break-even" was measured as revenue SHARE >= stake
share, and share-to-pay equivalence rests on sec 6.6's issuance argument. It
does not hold as stated.
A revenue share is measured against the canonical block rate, but pay is set by
the estimator. TSI drives COUNTED density -- canonical plus recovered uncles --
to f per slot, so a run yields f/D-hat block-finding events per slot and a
deflating attack makes the lottery easier for everyone. Pay per slot is
adv_rate * f / D-hat against an honest miner's alpha * f, so the correction on
the share ratio is density/D-hat, which is below 1 whenever any uncle is
recovered. Scoring on share credits the attacker with pay it never receives.
The headline survives; one row does not. At alpha = 0.4 the true break-even
point deflates to 0.649 rather than 0.642 (against the revenue-optimal 0.811),
so 16 points of free deflation instead of 17. But at alpha = 0.36 the policy I
reported as paying 1.033x stake actually pays 0.95x, and NO policy in the sweep
both pays and deflates there -- that row is withdrawn. The exposure therefore
opens near alpha ~ 0.37-0.38, above the 1/3 selfish threshold, not at it. That
is a better result for the protocol than the one I published yesterday, which
is the direction these corrections usually do not go.
pay_vs_honest is now computed in deflation_frontier() and is the criterion
best_profitable() selects on, so the claim is reproducible rather than resting
on a hand check. The conversion neglects the multi-winner factor c(f) ~ 1.017
between events and occupied slots (sec 2.1), noted inline and small against a
16-point effect. Downstream citations in sec 1, sec 8.2 and item 16 updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both eta ceilings in sec 6.6 come from adversaries optimising something else
(revenue, reorg depth), so they bound eta from above without bounding the
damage from below. Optimising the estimate directly needs no ratio transform:
each transition consumes exactly one block-finding event, so minimising
D-hat = (canonical + p_ref * countable uncles)/events is a plain average-reward
MDP over the transition table that already carries the orphan counts. One
value-iteration pass, no bisection.
Unconstrained, the answer degenerates -- and usefully. The optimum is pure
abstention: publish nothing, adopt when overtaken, D-hat = 1 - alpha exactly,
revenue zero. That is sec 6.4's withholding, which the report already shows is
CORRECT measurement rather than mis-measurement, so the unconstrained objective
asks the wrong question.
The constrained one bites. Sweeping lam * (adversary blocks) - (contribution to
D-hat) enumerates policies; the line of interest is where revenue SHARE reaches
alpha, i.e. where attacking costs nothing versus mining honestly. At alpha=0.4
such a policy drives D-hat to 0.642 where the revenue-maximiser reaches 0.811
-- 17 points of extra deflation bought with the selfish premium alone. At 0.36
and 0.45 the gaps are 0.082 and 0.103. Below the 1/3 threshold nothing
profitable deflates, so the exposure starts exactly where selfish mining does.
This revises two claims that were about revenue but read as though they were
about the adversary in general: sec 6.7's "the adversary frontier is exactly
optimal selfish mining; no compounding lever remains" and sec 8.2's echo of it.
Both now say the PROFIT frontier is bounded and the estimator frontier is not
the same policy. Note the sweep parameter is deliberately non-monotone in
revenue -- selfish mining takes a bigger share of a smaller pie, so raw block
rate is maximised by honesty and large lam returns there; it enumerates
policies rather than tracing a path.
Also closes a fairness loop these findings opened. Sec 6.7(1) credits uncle
rewards with compensating orphaned honest producers, computed on the SM1 race
where every orphan is a first-fork block. Under a private chain 20-40% of the
honest blocks destroyed are unreferenceable by construction, so those producers
are uncompensatable at ANY w_u -- not underpaid because p_ref is low, but
unreachable because no valid block may name them. The fairness guarantee
inherits the same first-fork ceiling as the density repair. Logged as item 19,
flagged as a protocol-design question rather than something a schedule fixes.
_solve_mdp is refactored into _solve_reward/_greedy_policy/_stationary/
_policy_rates so both objectives share one implementation; optimal_policy_stats
reproduces its committed figures exactly (eta 0.4413, D-hat 0.9447/0.8111 at
alpha=0.4). 251 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the SM1 adversary in the engine, the question item 5 could not ask is now
a measurement. It asked whether the honest-load cap needs margin when the
attack inflates orphaning, on the theory that owed uncles would defer past W.
They do not. Sweeping U x W x alpha against the attack (512 runs, N=1000,
k=256, 8 reps): at the design point and alpha = 0.3, D-hat/D reads
0.729/0.755/0.738/0.758 for U = 1/2/3/4 -- flat within noise -- and no attacked
cell reaches the 0.98 bar at any cap or either window. The honest baseline in
the same sweep reproduces sec 3.4 exactly (U=1 clears at delta=8; delta=16
needs U=2 at W=10 or W=20 at U=1), which is a useful check that the engine
adversary has not disturbed the honest regime.
Splitting the honest orphans by WHY they went unreferenced explains it. Neither
existing metric separates the two causes -- p_ref mixes them, and
deep_ref_share is 0 by construction here because the proposer's candidate
filter drops deep-fork blocks before any reference to one is proposed -- so the
script walks the tree. Countable share (first block of its fork): 97% honest,
76-81% at alpha=0.2, 59-72% at alpha=0.3. Referenced OF those: 90-93% honest,
84-93% and 80-88% under attack. The queue drains at essentially the honest rate
whatever the cap; what collapses is eligibility. An override discards a CHAIN
and only its first block has a parent on the surviving chain, so 20-40% of the
honest work destroyed is unreferenceable by construction. U governs drain
capacity for candidates that exist; it cannot manufacture eligibility.
So U = ceil(rho) + 1 stands unchanged and needs no adversarial margin -- and
the one place the cap does matter is the honest-load reason it was sized for
(U=1 -> 2 lifts the referenced-of-countable rate from 84% to 93% at
alpha = 0.2, then U=4 adds nothing).
This is the fig36 first-fork ceiling reached from an independent direction: a
per-node network simulation with real delays and a real queue, versus a
stationary MDP. Two models sharing no code, agreeing on direction and rough
size, is the strongest available evidence that the ceiling is a property of the
counting rule rather than of either model. Recorded in sec 6.6 and sec 6.8, with
the sec 6.8 structural argument corrected: it holds for orphans that are
referenceable, but a private chain buries most of them out of reach.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The audit note said the released logos-lips copy is still 1.1.0 and a reader
checking it will not find uncle references. That is news, not report text: it
goes stale the moment the RFC lands, and reports/tsi is meant to read as a
standalone timeless document. Keep the provenance pin, drop the rest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Full pass over every spec claim the report makes, against the uncle-references
RFC at 521f20dd (v1.2.1, 2026-08-05). Thirteen claims check out verbatim,
including the two that carry recommendations: PRECISION = 1e3 with
f_p = truncate(f * PRECISION) confirms row 14's premise exactly, and "carry no
fork-choice weight and grant no reward, a proposer has no incentive to deviate"
confirms the uncle-reward row and implication (ii). MAX_UNCLES = 4, w_u = W/f,
W <= floor(0.6k), T = 6k/f, epoch = 10*floor(k/f), the counting rules, duplicate
handling and counting-only status all hold.
Two claims did not.
The W row rebutted "a larger window would gain little". That sentence is gone:
the rationale was rewritten in the same revision that introduced W, and now
justifies the UPPER bound W <= floor(0.6k) on two grounds (the candidate is
still in the proposer's block tree; referencing blocks stay in the window's own
epoch) while asserting of the value only that it "comfortably captures the
forks worth referencing". Rebutting deleted text is worse than useless here,
because the new bound is 1296 slots at k = 2160 -- so this report's own
450-600 widening near rho ~ 1 is already permitted and needs no spec change,
which is a better outcome than the tension the row used to claim. What the
spec still does not give is a LOWER bound, which is what the 7/f floor supplies.
The intro said the spec flags w_u and MAX_UNCLES as "provisional", leaving
their tuning "as an open task of [Analysis] Total Stake Inference". The word
provisional appears in no spec document at any revision. The open task is real
but different: analysis-total-stake-inference.md records that its analysis
covers the design WITHOUT uncle references and that re-analyzing with
uncle-inclusive block counts "is an open task" -- which is what this report
does, so the claim gets stronger by being stated accurately.
Also pins the revision audited, and notes that these rules live on the in-flight
RFC rather than the released logos-lips copy, which is still 1.1.0 with no uncle
references at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Whale coalitions, jitter > 0 and very slow beta were the residual "untested
adversary variants" of open item 11. None moves a conclusion:
- Concentration does not change the deflation (suppression D-hat within noise
at every stake), and a whale coalition reproduces the sec 6.4 withholding law
D-hat -> (1-beta_adv) more cleanly than a random one: 0.9005/0.6997/0.5010
against a predicted 0.9/0.7/0.5. The "lumpier share statistic" worry points
the other way, and for a reason that is about coalition CONSTRUCTION rather
than concentration: a random coalition grows until its stake first reaches
the target, so the last node added overshoots by its own size -- a whale,
under a Pareto tail. Realised block share at a nominal beta_adv = 0.1 is
0.137 +- 0.108. Logged as item 17: the beta_adv axis is a nominal target.
- jitter up to 1 slot changes nothing under attack (notch 0.390 -> 0.410,
attacker share flat, range_ratio identically 0), as sec 6.1 found honestly.
- Slow beta shrinks the notch (0.415 -> 0.080 for beta 1 -> 0.1) at flat
attacker take, but sinks the MEAN estimate to 0.765 at beta = 0.05: the
estimator can no longer track back up during the honest half of the cycle.
Slowing beta buys the defender nothing on either axis.
Unplanned: study A blew past the memory guard, which turned out to be the
sec 6.2 fold being reached. The mechanism is sec 6.2's own -- rho_eff = rho/r,
and withholding deflates r by design, so a 50 % coalition doubles the load onto
rho_eff ~ 1.1 at the design point. Swept directly, the estimate collapses once
in 144 runs at delta_max = 8 (a concentrated 50 % coalition) and never at
delta_max = 4. That retires "not an observed dynamical trap" but is one event,
so the claim is stated as a rare tail and the rate is logged unmeasured as
item 18.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implication (i) quoted "the length of the header does not reveal how many
uncles a block references" from Uncle References. That sentence was removed
from the spec in b809df59 ("Making uncles variable size instead of fixed"):
the field is now a variable-size unpadded list and proposal indistinguishability
is preserved at the message layer, by padding every dispersed payload to
Max_Body_Length. The constraint on a per-reference nephew reward therefore
rests on the voucher being content-dependent -- which the Anonymous Leaders
Reward Protocol still requires it not to be -- and not on header length.
Implications (ii) and (iii) were re-checked against the same revision and stand
unchanged: the "no incentive to deviate" sentence is still there, and the
equal-share content-independent voucher is still the payout model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Open item 11 listed "a random (rather than oldest-first) uncle-selection draw"
as an untested spec sensitivity. The spec does not leave it open: Uncle
Selection in cryptarchia-v1-protocol.md has the proposer take the oldest
candidates first, deterministically, because an uncle expires w_u slots after
its own slot. That is exactly what every result in the report already uses, so
the item is a conformance match, not a gap -- and the simulator comment calling
uncle_random_p "the spec's unbiased coin" cites text the spec no longer has.
What is genuinely open is deviation FROM that rule: selection is proposer-local
and the uncles field is never validated. configs/uncle-selection.yaml measures
the cost. A proposer that includes each candidate on a fair coin instead loses
up to 0.10 in D-hat/D, and 0.063 at the recommended W = 10 once rho ~ 1
(0.902 vs 0.965, t = -8.6). At the design point the margin survives but is
spent: 0.980 vs 0.997 against a 0.98 bar. The loss does not close as W grows,
because a coin wastes opportunities rather than queue capacity and a well-sized
window is precisely what keeps the queue short enough for that to bite.
This matters for the sec 8.5 reward recommendation: the spec argues a proposer
has no incentive to deviate BECAUSE uncles grant no reward, and paying them
removes that argument.
Also adds adversary_selection=whale (the largest holders at matched stake, for
the untested concentration case). The marker is appended to key() only when
non-default so every historical run's seed stays byte-identical, guarded by a
test alongside the paired_streams one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The countable model can reference only the first block of a fork, so a
discarded chain of h honest blocks yields one countable uncle, not h. Sec 6.6
reads the estimator repair off a free knob eta and quotes it at eta = 1 --
attainable under SM1, which acts the moment the honest branch reaches length 1
and so never buries a second block. The optimal SSZ policy waits and does bury
them, and there the deployed counting rules cap eta at 0.44 (alpha = 0.4,
gamma = 0), landing D-hat at 0.81 rather than the 0.94 an unrestricted count
gives -- and the ceiling degrades with alpha while the unrestricted value
improves. So SM1 is a faithful proxy for selfish-mining revenue (0.484 vs
0.489) but not for TSI's estimator damage.
selfish_mdp: carry per-branch orphan counts on the transition table so the
accounting cannot drift from the race logic; optimal_policy_stats solves the
policy's stationary distribution for per-event canonical/orphan rates. The
per-event rates sum to 1 (every block is canonical or orphaned), which the
tests assert as an independent check on the whole derivation.
reorg: the same ceiling for the depth-maximising adversary -- 0.52 at
alpha = 0.30 with the measured honest fork rate -- reached from the other
direction. Neither adversary optimises deflation directly, so both ceilings
are upper bounds on eta; that gap is logged as open item 16.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
§3.2 rested on 5 unpaired replicates while §3.2a used a paired design.
Re-running the delta_max 4/8/16/32 grid with common random numbers and
20 replicates (configs/countable-vs-old-paired.yaml) changes the answer
at the design end.
The cost is resolved at EVERY delay and grows monotonically with load:
delta_max=4 (rho 0.36) -0.0013 t= 4.0 (unpaired: not resolved)
delta_max=8 (rho 0.56) -0.0034 t= 9.8 (unpaired: not resolved)
delta_max=16 (rho 0.96) -0.0102 t=17.5
delta_max=32 (rho 1.76) -0.0228 t=22.4
So §3.2's claim that "at the operating loads (rho < 1) no difference
between the models is detectable at all" was an artefact of the weak
design, not a property of the system. There is a difference; it is just
small — 0.13% and 0.34% at the two sub-unit loads. The new delta_max=4
figure (-0.0013 at 20 reps) independently reproduces §3.2a's (-0.0011 at
40 reps) from a separate sweep.
11 of 12 U>=1 cells resolve individually; max t = 29.0 against a
Bonferroni threshold of 2.87 for twelve tests. The U=0 control is exact:
80/80 replicate pairs differ by 0.0.
New finding at U=1 under overload: the sign FLIPS and the countable rule
wins, +0.0127 (t = 7.6), positive in 19 of 20 pairs. Both models have
collapsed at rho ~ 1.76 with a single uncle slot, but when capacity is
the binding constraint the countable rule's occupied-slot exclusion
means its one reference always recovers a NEW slot, while the
unrestricted rule dedups by block id and can spend that reference on an
orphan whose slot is already counted. Measured recovery agrees:
q_u = 0.591 countable vs 0.579 unrestricted. The slot-vs-block
distinction of §2.1 is worth most exactly where references are scarcest.
Code: paired_gaps and pooled_by_delay move from scripts/plot_fine_delay
into figures_pernode so both plot scripts share one implementation;
plot_countable_vs_old now detects paired runs and uses the
per-replicate difference, falling back to the unpaired two-sample test
otherwise. Two hardcoded reporting values fixed — the Bonferroni
threshold was pinned to 2.935 and printed nan for any grid that was not
15 cells, and a per-cell comparison line had a hardcoded /15 denominator;
both now derive from the grid actually run.
Figures 30-32 regenerated from the paired grid. Tests: 214 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous version used to fake the indent of subsection
lines. That is an HTML entity leaking into markdown: Obsidian renders it
literally rather than as leading space, so the block reads as broken
there.
Rebuilt as a genuine nested markdown list, one subsection per line,
indented four spaces. Four rather than two because python-markdown (used
by scripts/build_html.py) needs four for reliable nesting, while
Obsidian and GitHub accept it equally — so the same source renders
correctly in all three.
Verified the nesting is real in the built HTML (nested <ul> inside <li>,
not a flattened list) and that the block still covers all 47 anchors
with none dangling and none missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The report is now reports/tsi/README.md, so browsing to reports/tsi/
lands on the report itself rather than on an index that points at it.
The old index carried nothing the report lacked except the note that
report-figures/ holds the figures of record (the simulator folder does
not commit its own), which is folded into §9; its section table is
superseded by the report's own contents block. The "[Index]" self-link
in the header is replaced by the simulator link the index used to carry.
The contents block was inconsistent: §2 listed subsection titles, §3 and
§6 listed bare numbers with no titles at all, and the appendices were
crammed onto one line while their subsections went unlisted. Rebuilt
from the document's actual headings so every entry has a real title,
top-level entries carry a one-line gloss, and subsections sit indented
under their parent. It now covers all 47 anchors, including B.1-B.4 and
C.1-C.2 which were previously absent.
scripts/build_html.py follows the rename (DOCS is a single document) and
still renders clean: 47 anchors, 0 broken internal links, 0 unrewritten
.md links, 37 images.
Also adds configs/countable-vs-old-paired.yaml — the paired, 20-replicate
version of the overload grid. §3.2a now rests on a paired design while
§3.2 still rests on 5 unpaired replicates, which is why its U=1 cells at
delta_max 16 and 32 sit unresolved at t ~ 0.5 against a replicate sd of
0.15. That sweep is running; the report is not yet updated from it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four-part split existed because the single report had grown dense
and heavily cross-referenced; splitting traded that for a different
cost, which the merged read makes visible. Section numbers (§1–§9,
Appendices A–C) were already the stable identifiers, so the parts were
a packaging choice, not a structural one.
reports/tsi/tsi-report.md is now the whole report. Parts are
interleaved back into section order — §1, §2–§5, §6, §7–§8, §9 +
appendices — which is NOT concatenation order: Part 1 carried §1, §7
and §8, so appending files in sequence would have put §7–§8 ahead of
§2. Every cross-file link collapses to an internal anchor; all 47
anchors resolve, all 37 figure embeds resolve, and no line of prose was
lost (verified by diffing normalised content lines with link targets
stripped — 0 lost, additions are the new header and table of contents).
Coherence fixes the merge exposed, all artefacts of the split:
- The roadmap paragraph described "four parts (see the index)" and is
now a section-order roadmap, with its circular self-link to §1
dropped.
- §7's figure-location note pointed readers at "the other parts". It
now names the actual sections, and it was also WRONG about three
figures: fig17/fig18/fig21 are in Appendix C and figB1/figB2 in
Appendix B, not §9. It had also never been updated for fig30–fig35.
- §9's "throughout this part" is now "throughout".
README.md becomes a proper index — a section table pointing into the
one document — rather than a list of four files.
scripts/split_report.py is deleted: a one-time migration that produced
the split, now both obsolete and pointing the wrong way.
scripts/build_html.py was already broken before this change — it still
read the report from tsi-sim-pernode/, where the files stopped living
when they moved to reports/tsi/. Retargeted at reports/tsi/ and the
single document; verified end-to-end (0 broken internal anchors, 0
unrewritten .md links, 37 images in the rendered HTML). Its output is
now gitignored, as its docstring always claimed it was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Applied the reconstructed round-4 review to the TSI parameter-selection report
set (reports/tsi) and executed the follow-ups.
Report (reports/tsi):
- Applied the must+should findings across README + parts 1-4: cross-part numeric
corrections, figure-caption fixes, spec reconciliation, and cross-file companions
(hops-degradation and notch/reward numbers, tip-agreement ordering, density-window
timing, VRF -> ZK Proof-of-Leadership, w_u window/reward gloss).
- Editorial pass for timeless voice (no "now adopted / merged / coin" narration) and
a gentle spec-safety framing (recommendations are thresholds; the protocol's
MAX_UNCLES=4 sits safely above them).
- Added the fork-rate-vs-scale table (6.10), defined "grinding gain", promoted the
clock-skew study to its own paragraph, added the correlated-latency caveat, and
moved fig27/fig28 beside their discussion.
- Documented the Blend cascade in 2: hops propagate over the shared gossip graph
(not direct links), the final broadcast comes from the last relay, relays are
blind forwarders.
Simulator (tools/simulators/tsi/tsi-sim-pernode):
- Docstring/dead-code fixes: theory.block_count_ceiling (legacy framing), measure,
reorg (catch-up reading), metrics (removed two dead helpers), config (fixed_point
10^-6; clock_skew_max/lottery_chunks documented inert), stake_vs_delay.
- Generator correctness + regenerated figures: figures_pernode.CONFIG_COLS now
exhaustive (f no longer pooled); rho_boundary_analysis SEM across replicates +
hollow floored markers + de-hardcoded ell_mean (measured from the run's graph);
appendix_fluct per-N sigma + ~18x title (figB2); bootstrap_dynamics driving
estimate so fig1 epoch-0 matches genesis.
- pytest: 186 passed; report links 528/0 dangling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>