mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-11 05:33:32 +00:00
57 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3c52aeec0e
|
Settle §6.9's multi-coalition conjecture: refuted for deflation, confirmed for the threshold
§6.9 flagged multi-coalition selfish mining as conjectural on two counts taken from the literature — that splitting a coalition can deflate D-hat FURTHER than one coalition of the same size, and that individually sub-threshold coalitions can be jointly profitable. With rival private chains in the engine, both are now measured (scripts/multi_coalition.py, fig39), and they point opposite ways. DEFLATION — REFUTED. Splitting a fixed stake into K rivals reduces estimator damage, monotonically, at every beta and K tested (t = 4.3 to 14.3): beta K=1 K=2 K=3 K=4 0.20 0.908 0.928 0.922 0.928 0.30 0.778 0.858 0.880 0.880 0.40 0.586* 0.764 0.807 0.823 The mechanism shows in the fork structure: a lone coalition holds one private chain ~2000 blocks deep, while two rivals cut each other to depth ~2. Rivals spend their advantage burying each other instead of honest work. So D-hat ~ 0.70 at alpha = 0.4 IS an upper bound on multi-coalition deflation, not the under-estimate §6.9 warned it might be. (* boundary-affected: 66% of that cell's adversarial blocks were still private at epoch end even with the lead cap. The 0.2 and 0.3 rows have <1% stranding and carry the result alone.) THRESHOLD — CONFIRMED, and 1/3 does not hold at this load: one coalition at 0.200 0.875x t = -2.5 does not pay one coalition at 0.295 1.232x t = 2.7 PAYS two rivals at 0.200 each 1.045x t = 2.5 each PAYS three rivals at 0.133 each 0.852x t = -5.1 does not pay Two results against the folklore. A single coalition already profits at alpha ~ 0.295, because the honest network forks at ~0.48 here so the public chain's HEIGHT advances at only (1-a)*f*(1-fork) while a coalition sharing one view extends privately at the full a*f — the threshold falls with the fork rate. And a 0.20 coalition that does NOT pay alone DOES pay against a second 0.20 rival: the rival displaces honest blocks too and both collect on the disruption. That is the "individually sub-threshold, jointly profitable" case, confirmed at K = 2 and not extending to K >= 3. The practical split: coalition fragmentation is good for TSI's estimator and bad for the incentive argument. "No coalition holds 1/3" is not a safety property at a load where the honest network forks appreciably, which makes rho < 1 an incentive constraint and not only an accuracy one. §8.3 item 2 narrowed accordingly; fig39 marks boundary-affected cells hollow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
097543a5f9
|
Re-measure the W pairing paired, and correct my own severity numbers
The W = 12 pairing is now measured the way a ~0.001 claim has to be: every
integer W from 8 to 15, 32 replicates, and paired_streams so the whole grid runs
on common random numbers (_base_key excludes both uncle_window_anchor and
window_absorption, so a replicate draws one stake vector, one graph and one
lottery for every cell). The earlier unpaired sweep reported +0.0008 against a
standard error of 0.0009 — it could not resolve its own headline.
Paired against today's recipe (uncle-anchored, W = 10):
parent W=10 -0.0056 +- 0.0005 t = -10.4
parent W=11 -0.0024 +- 0.0005 t = -4.5
parent W=12 +0.00004 +- 0.00050 t = 0.1 <- parity
parent W=14 +0.0016 +- 0.0004 t = 3.5
W = 12 is the smallest window reaching parity, and the parity is exact rather
than marginal: W = 11, one interval short, is still resolvably worse. p_ref
agrees at the same window (0.938 vs 0.939) instead of lagging to W = 15 as the
unpaired edition had it. Also states what the sweep makes visible: widening
today's uncle-anchored rule buys +0.0018 on its own, so W = 12 makes the swap
cost-neutral against the CURRENT recipe rather than optimal in absolute terms.
CORRECTIONS to the previous commit, which measured contamination on the wrong
RNG stream. The engine draws stake from seedseq_for(config).spawn(...)[0]; I
used rng_for(config), the root. Both are valid stake draws, neither is the same
vector. Redone properly:
- The capstone draw was NOT contaminated: 0 of 8 replicates over 1.25x its
label, worst 0.369 against 0.30, no majority. My "2 of 8, one a 61%
majority" was wrong and is withdrawn from §8.4 and §9.
- The finding that survives is sharper: on that same mild overshoot the spec's
rule moved 0.001 and the parent-anchored variant moved 0.016. A rule leaning
harder on the reference window is far more sensitive to an oversized
suppressing coalition.
- Genuinely contaminated: §6.12's 12-replicate W sweep (2 majorities, worst
0.720) and §6.8's selfish margin at a=0.3 and a=0.4 (2 and 1 majorities).
§6.5's variants and §6.8's a=0.2 arm are clean; §8.3 item 20 narrowed to the
one sweep that still needs re-running.
- The general severity is worse than first stated, not better: at the report's
geometry a nominal 0.3 realised a majority in 12% of replicates.
Two more defects found on the way:
- stake_for(config) added, because scripts used rng_for and the engine uses
the spawned child — so every script that rebuilt a tree was analysing a
different network than the trajectory it was compared against. All scripts
and tests now use it.
- A coalition member could receive a private block BEFORE its parent: the
arrival was clamped against the PRODUCER's view of the parent and applied to
the whole coalition, so a member still awaiting a public parent got the child
first. Now clamped per member. Caught by the existing arrival-order test once
the stake derivation was corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
516783146d
|
Fix adversary coalition sizing; add rival coalitions and a lead cap
Three engine defects, found while building the multi-coalition study §6.9 flags
as open. The first is the serious one.
1. COALITION SIZING (engine._adversary_mask, `random` selection — the default).
The coalition was the smallest random prefix whose stake reached the target.
Under a Pareto tail a whale straddling the cut carries it far past its label:
over 60 replicates, a nominal adversary_frac of 0.4 realised a MAJORITY in
~10% of them and reached 0.97, and 0.2 reached 0.90. The median was always
on-label, which is why it hid — it distorts the tail, not the centre.
Both other places in the code that size a set by stake had already rejected
this rule: the `whale` arm uses fit-then-close, and _churn_inactive_mask
documents the identical failure ("a 30% label realising up to ~53%"). The
`random` arm kept it. Now fit-then-close in random order, and a draw where
the tail leaves no subset near the label warns instead of silently running a
different attacker. Realised stake is now within 0.1% of its label.
Re-ran the load-bearing studies. §8.4 capstone (2 of 8 replicates
contaminated, one a 61% majority): spec rule 0.994 -> 0.995, p_ref 0.936 ->
0.937. The parent-anchored variant is far more sensitive — 0.974 -> 0.990,
p_ref 0.875 -> 0.923 — because a tighter window and a larger suppressing
coalition compound, so §8.4's argument for the W = 12 pairing rested on
0.021 of cost that is really 0.006. The pairing itself survives re-measurement
and is now better supported: p_ref reaches parity at W = 12 too, not at 15.
§6.8's uncle-margin sweep and §6.5's random-arm variants are flagged as
needing re-measurement (§8.3 item 20), not silently carried.
2. SM1 NEVER TERMINATED under a forking honest network. Textbook SM1 waits while
it leads, assuming the lead returns to zero. But honest blocks fork against
each other, so the public chain's HEIGHT grows at ~(1-a)*f*(1-fork) while a
coalition sharing one view extends privately at the full a*f; past a fork rate
of ~1 - a/(1-a) the private chain outruns the public one and `wait` never
fires. The lead ran to thousands and every block was stranded at the epoch
boundary — 98% of adversarial blocks at alpha=0.4, delta_max=8 — scoring an
attacker that WON the race as having earned nothing. selfish_lead_cap
(default: the finality depth k) publishes a lead that can no longer be caught.
Inert unless `wait` stops terminating; pinned paired.
3. RIVAL COALITIONS (adversary_coalitions = K) for the §6.9 study: K private
chains, each invisible to the others by the same arrival sentinel that hides
them from honest nodes, so they orphan each other as well as the honest chain.
Stake-balanced partition (LPT), K=1 bit-identical to the single-coalition path.
Also corrects §8.4's closing paragraph, which still quoted a pre-countable
D-hat/D of 1.001 and fork rates that contradicted its own table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
74ee06047c
|
Make the model carry-over metric-specific instead of wholesale
§9 claimed that studies predating the countable model "carry over unchanged" because no difference is resolvable at ρ < 1. That is true of accuracy and false of the reference rate, and asserting it wholesale is what let the §8.4 capstone sit at p_ref = 1.000 for weeks after the redesign made it 0.944. Measured per metric off the committed paired band (δ_max 1–5, U ≥ 1, common random numbers, so the uncle rule is the only difference): D̂/D 0.9994 vs 1.0000 +0.00055 t = 4.9 carries over fork rate 0.2256 vs 0.2255 -0.00010 t = -2.0 carries over orphan rate 0.2256 vs 0.2255 -0.00009 t = -2.2 carries over tip agreement 0.9943 vs 0.9950 +0.00070 t = 0.9 carries over p_ref 0.9260 vs 0.9998 +0.07381 t = 155.0 RE-MEASURE p_ref fails in kind, not in degree: the unrestricted rule makes every in-window orphan referenceable, so its ≈1.0 restates the model rather than measuring the design. Any pre-countable p_ref is wrong by ~7 points. Also corrects §6.7(1), which still asserted p_ref ≈ 1 for genuine latency orphans — §6.8 had been corrected to the measured 0.944 but the earlier claim was left standing — and drops "no difference is resolvable" from §9, which contradicted §3.2a's pooled t = 5.7. Adds scripts/model_carryover.py so the verdict is reproducible rather than asserted, and a §9 study-table row. deep_ref_share is unaffected (0 by construction under the countable rule); no other reference-structure metric is quoted from a pre-countable run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c4577e7062
|
Measure the W = 12 pairing instead of asserting it
The report had twice recommended pairing the parent-anchored window with W = 12, argued from the shape of the mechanism -- a parent gap running one to two block-intervals longer consumes that much window -- but never measured. Swept under the adversary that exposed the cost in the first place (the capstone's geometry, 30 % uncle suppression, U = 2, k = 2160, 12 replicates): uncle W=10 (today's recipe) : D-hat 0.9916 p_ref 0.933 parent W=10 : D-hat 0.9851 p_ref 0.913 parent W=12 (proposed pair) : D-hat 0.9908 p_ref 0.924 Parent-anchored at W = 12 lands within +0.0008 of today's recipe -- parity inside one standard error -- so the anchor's cost under a suppressing adversary is bought back exactly by the two extra block-intervals the longer parent gap consumes. p_ref recovers more slowly (matching only at W = 15) but stays far above the ~0.3 the soft rule needs, so it does not bind the recommendation. The operative conclusion, now supported: adopt the anchor change and W = 12 TOGETHER, or neither. At W = 10 the anchor costs 0.0065 under this adversary, which is the one regime where it is not free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
de618d7ea8
|
The sec 8.4 capstone was still reporting pre-countable numbers
Re-running the capstone under both anchors turned up something worse than the anchor question. The uncle-anchored arm should have bit-reproduced the committed capstone, since its key is unchanged -- and it did not. Bisecting against the pre-session source showed my changes are clean (identical trajectory, epochs=21, tail 0.994358 either way); the committed parquet is simply stale. It is dated 24 July and has no uncle_model column at all, so it predates the countable redesign. sec 8.4's headline table has been reporting UNRESTRICTED-model numbers ever since that landed. sec 9's "all studies re-run" note covers the slot-counting fix of 23/24 July, not the countable redesign of 4 August, and the capstone was missed. Corrected, countable model, delta_max = 8, U = 2, W = 10, k = 2160, 8 reps: honest D-hat/D 0.996 (was 1.001) p_ref 0.944 (was 1.000) 30% suppress 0.994 (was 0.998) p_ref 0.936 (was 0.990) Lower, and p_ref materially lower, because the first-fork restriction makes some orphans unreferenceable that the earlier model counted. No recommendation changes: accuracy sits inside the +-0.9% per-epoch noise floor below the hard ceiling of 1, consensus is exact, p_ref stays far above the ~0.3 the soft rule needs. sec 6.8's p_ref quotes came from the same stale run and are corrected too. The anchor result the re-run was for: parent-anchored gives 0.996 honest -- unchanged -- but 0.974 under the 30% suppression adversary, with p_ref 0.875. The effects compound: a tighter effective window leaves an orphan fewer chances to be referenced before its PARENT ages out, and a suppressing adversary removes some of those chances. Neither isolated sweep shows it, because sec 6.12's honest arms hold the adversary at zero and its adversarial arms use a slack cap. That is what the capstone is for, and it is the strongest argument for pairing the anchor change with W = 12 rather than adopting it at W = 10. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a16284ebc5
|
The uncle cap rule survives the anchor change; the window floor does not
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> |
||
|
|
99ce8b76d0
|
Correct sec 6.12: the parent anchor does move the window floor
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> |
||
|
|
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> |
||
|
|
abf2c86285
|
Make the fork-loss validation stand alone
The results document was written as a reply to a briefing note that will not be
circulated, so it depended on a file no reader would have -- and linked to one
that was never committed, so the link was dead on GitHub regardless.
Rewritten to be self-contained: it now states what the added spec section
claims, what was measured, and the verdict, without reference to the brief.
The six claims and six experiments are stated in its own terms rather than by
the brief's C-numbers and E-numbers, and it carries its own reproduction table
mapping each section to the config or script and run directory behind it.
Renamed fork-loss-validation.md accordingly.
The configs and scripts written for these experiments carried the same
dependency in their header comments ("Handoff E5", "Claim C2"); those now
describe what they measure directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
18c15d329c
|
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> |
||
|
|
a82e4cb706
|
E5: the section's number is reproducible at ~8 slots of per-recipient spread
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> |
||
|
|
e5a89004f2
|
Answer the fork-loss handoff: the section's residual is ~17x overstated, and it misses the real bias
Settles E1-E4 and E6 of handoff-fork-loss-validation.md against spec d6fd7648.
E1 costs nothing and reframes everything: analysis-block-times-blend-network.md
sets blending_delay as a FIXED per-hop dwell (the 3d+5 max-delay arithmetic
gives 14 s at d=3 and 11 s at d=2, matching its prose), not a mean or a bound.
The simulator's Uniform(0, delta_max) matches a 2 s dwell in the mean at
delta_max = 4, so D_vis = 8 s and rho = 0.27 -- inside the committed 40-replicate
paired design band, which answers C1-C3 from data of record.
C1 refuted: every U>=1 cell sits at 0.9985-0.9997, not 0.986. C2's mechanism is
right but its size is ~17x over: the paired first-fork cost is 0.08 pp pooled
(95% CI [0.03, 0.13], t = 3.08), resolved only because the arms share streams --
the U=0 negative control is exactly 0.00000 +- 0.00000. C3 is refuted in the
UNFAVOURABLE direction: the no-uncle loss is 33% at N=1000 and 34.6% at N=5000
(42% / 49.5% at delta_max = 8), so the section understates what uncles buy by
about half. C4 stands with ~7x margin (U=3 still recovers at rho = 1.87). C5 is
right in effect, wrong in wording -- the knee is at W_abs ~ 5, so the spec's 10
is ~2x above it, which is "has margin", not "never binds".
C6 is the section's real omission. The deployed estimator quantises the target
rate at PRECISION = 1e3, and measured in the full dynamics that reads
1.01026 +- 0.00056 against a closed form of 1.0101 -- a 1.0% bias ~13x the
first-fork cost the section is concerned with, opposite in sign, removed by a
one-constant change. It could not be measured before because PRECISION was a
module constant pinned at the RECOMMENDED 1e6; f_precision is now a config
field, appended to the RNG key only when non-default so no committed run moves.
Also from the guide: uncle_window_slots now floors rather than rounds, matching
w_u := floor(W/f) (identical at the defaults; matters only for the W and f
sweeps).
Reviewed sec 4.3's argument as sec 6 asks, and it holds -- inclusion stayed soft
("may reference fewer uncles than it could ... and its block remains valid"), so
row 10, the anti-mandate argument and the suppress adversary are all unaffected;
only the CONTENT of a reference became validity-gated. One correction: the
"no incentive to deviate" clause does still exist, so sec 8.5's implication (ii)
is live, not moot.
E5 -- the jitter diagnostic, and the only experiment that could invalidate the
report rather than the section -- is not run and is flagged as such.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f342fd4f45
|
Score the deflation frontier on absolute pay, not revenue share
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> |
||
|
|
7eced7eadf
|
reports/blend: record which evidence predates the seed-derivation change
data/README promised that re-running with an unchanged config reproduces exactly. That is no longer true for three of the eight runs: adding failure domains put n_regions and region_locality into the topology seed, which changes the graph drawn for every config, including those leaving both at their defaults. default, percolation and redundancy predate that change. A spot-checked cell moves by 0.3%, about 1.5 standard errors -- ordinary variation between independent realisations, not a change in behaviour, and within the error bars the report already states. Closed-form quantities are identical either way. The README now says which runs are current, which are not, and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e9b79ce90a
|
blend: attribution evidence at the reported scale, and a figure for the timing study
Two gaps left by the previous review. Section 3.4 quoted the attribution bracket at N=20,000 while the only committed evidence carrying those columns was the timing run at N=2,000, so a reader diffing report against data saw different numbers for the same quantity. Added configs/attribution.yaml and a make target: it records both bounds and the graph hop distance at the reported scale, cheaply, since the adversary and deanonymization metrics are closed-form and the hop distance is a property of the topology. It reproduces the section exactly -- L = 2.58 and neighbourhood confidence 0.640 at degree 8, f_adv 0.2. It also surfaces a result the smaller run could not: degree cuts both ways. A sparser graph has longer routes, so it offers the adversary more upstream places to see the message -- L is 4.18 at degree 4 against 1.93 at degree 16, lifting neighbourhood confidence from 0.61 to 0.72. The low diameter that makes propagation fast also starves the adversary, one of the few places where raising the degree helps anonymity rather than hurting it. Section 3.11 was the only section without a figure. Fig 25 plots MAP success against the effective anonymity set for both release designs: the dashed sets separate far faster than the solid best-guess curves, which is the whole argument for not trusting perplexity alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cad2bf51b2
|
blend: emission-model fidelity, and evidence tooling that covers every section
Second review pass, three findings. Pending cancellations were a set, so a node that proposed twice before its next cover emission forfeited only one and then over-emitted relative to its quota -- the precise uniformity cover traffic exists to preserve. Now a multiset. The timeline drew the block proposer uniformly while quota.py used a stake- weighted lottery, so the two halves of the cover-traffic model disagreed. The timeline now takes the stake array. Concentration is visible in the bookkeeping: a dominant proposer wins most proposals but rarely draws a cover slot to forfeit, so cancellations redeemed fall from 107 to 28 -- the unredeemed backlog being exactly the over-emission the stake ceiling describes. data/report_numbers.py claimed to print every quoted value but covered only sections 3.1-3.5 and 3.8. Extended to 3.9 correlated churn, 3.10 blending, mixing and the quota ceiling, 3.11 the release designs, and the 3.4 attribution bracket; the claim in data/README is corrected to say what it actually does. Neither model fix moves a published number: the proposer identity does not enter blending, mixing or timing, and repeat proposals are rare at the reported rates. Two regression tests pin both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
36c16a0a98
|
blend: align section 3.11 with its committed evidence, and expose every study via make
The 3.11 table carried numbers from the ad-hoc analysis that preceded the sweep. Replaced with the values the checked-in run actually produces (MAP success 0.993/0.905/0.683 clock, 0.989/0.832/0.550 jitter), so every figure in the report is traceable to data/. The minimum-interval control likewise now quotes the committed 10.14s vs 10.22s and 0.858 vs 0.860. cover-traffic was the only study without a make target, and correlated-churn, cover-traffic and timing were missing from the simulator quick-start. Added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fb49a790fc
|
blend: make sections 3.11 and the attribution bracket reproducible
Review finding: the timing study and the neighbourhood-confidence numbers were produced by ad-hoc analysis, not by the simulator. timing_linkability, neighbourhood_confidence and mean_upstream_hops had no callers outside their own modules; min_blend_delay and release_mode were declared on SweepConfig, validated and keyed, but never read by sweep.py, so a YAML setting them was silently ignored; and propagation.py called mix_wait without the minimum, leaving the knob inert on the delay tables of 3.1-3.2. Section 6 promised every number was reproducible and data/README claimed to hold the evidence behind every number -- both were false for 3.11. Now wired end to end: release_designs() is a real sweep axis, the engine measures the timing attack per design and records it in traffic.parquet, and the deanon table carries the full attribution bracket (local confidence, attributable fractions, upstream hops, neighbourhood confidence). Added configs/timing.yaml and a make target. The committed sweep reproduces 3.11: MAP success 0.993/0.905/0.683 for clock and 0.989/0.832/0.550 for jitter across the swept rates, and the minimum interval changes nothing (0.993 vs 0.993). Evidence checked in under data/timing. Three regression tests pin the wiring so a measure cannot go back to living only in analysis. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e4d3d3a0f7
|
blend: close the attribution bracket with a neighbourhood-observability model
The a/d estimator credits the adversary only with the sender own links, which
understates it: to rule out that X was forwarding it need not hold X incoming
link, only have seen the message anywhere upstream. That gives
confidence = 1 / (1 + (1-f_adv)^L)
for L upstream hops, with the local model as the L=1 case.
L is not free -- the graph fixes it. A forwarder sits about halfway along a route
and the peer graph is low-diameter: mean hop distance 5.15 at degree 8, so L ~ 2.6.
Confidence rises from 0.56 to 0.64 at f_adv = 0.2, but 0.9 would need ~10 upstream
hops. The low diameter that makes propagation fast is exactly what starves the
adversary of observation points.
So the bracket closes near the local model rather than near certainty, and the
binary full_deanon treatment is NOT rescued by neighbourhood effects. Both ends
are reported rather than one being chosen, since confident attribution is a
threshold question: an adversary content with 0.64 attributes most senders, one
demanding 0.9 attributes almost none.
What remains unmodelled is an adversary combining this structural evidence with
the timing evidence of 3.11.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9bf61aaaec
|
reports/blend: section 3.11, timing
Prices two ways of delaying at a matched latency budget: the free-running clock (batch at the tick) against an independent per-message draw. Measured by the effective anonymity set of a release, and by MAP success -- how often the observer single best guess is right -- because perplexity flatters a heavy tail. The dominant result is that at the specified cover rate timing linkage is essentially total under BOTH designs: the best guess is right 98-99% of the time. That follows directly from the mixing~0 result of 3.10 -- a relay holds about one message, and delay cannot manufacture ambiguity out of one message. Traffic buys timing protection; delay does not. Between designs the independent draw wins at equal cost, against the intuition that batching should be safer: a tick is a hard boundary the observer can exclude behind, whereas an exponential never excludes anything outright. But the margin is a third of what the set size suggests (68% larger set, 19% better against the best guess), so perplexity alone would have overstated the case for Poisson-style mixing threefold. A minimum release interval changes nothing, structurally: a zero-length gap is instantaneous, so it never covers an arrival and is never sampled. Section 5 updated -- the timing caveat is discharged, and the structural results are now flagged as an upper bound on the anonymity a deployment actually gets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c7fef64d0
|
blend: attribution confidence -- one adversarial peer is not an identification
full_deanon counted any sender with an adversarial peer as identified. Capturing a
cascade tells the adversary WHICH message it is following, not WHO started it:
seeing an honest X transmit is equally consistent with X having received it from a
peer the adversary cannot watch. Separating the two gives
confidence = 1/(2 - a/d) = d/(2d - a)
for a adversarial peers of degree d. The path length does not enter -- the
conditioning event already fixes the relays as adversarial, so an honest X is not
one of them for this message.
The consequence is large. One peer of eight is worth 0.53, barely above the 0.5
prior, and 90% confidence needs a >= 8: every peer, which is the ECLIPSE condition
rather than the observation condition. Measured, attributable_frac_90 equals
eclipsed_frac exactly. At f_adv = 0.2, degree 8 that is 2.6e-6 against an
observed_frac of 0.83 -- the published figure overstates confident origination by
five orders of magnitude.
Stated in the report as a bracket rather than a replacement: full_deanon is the
upper bound on adversary capability, this is the lower bound, and the truth lies
between because the adversary also learns from the sender neighbourhood. Closing
that gap needs a k-hop observability model and is recorded as open in section 5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f51630e509
|
Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation, adversary exposure, deanonymization and time-to-link, reliability under uniform and correlated churn, messaging redundancy, and cover traffic. The pd name no longer describes it. tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend, and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows. The text substitutions are deliberately narrow. pd is also the conventional pandas alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend. rewrite would have corrupted four files. Only package-unambiguous forms were changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the pyproject name. All four import pandas as pd lines are untouched and verified. Both READMEs reframed: peering degree is now presented as the primary axis that ties the others together rather than as the subject, and the relative links, which lost a directory level in the move, are corrected. Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets, the script shims, an end-to-end smoke run, and data/report_numbers.py still reproducing the report tables from the checked-in evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a93311013b
|
reports/blend/pd: section 3.10, cover traffic
Written from configs/cover-traffic.yaml: the rate swept over three decades against three release delays, each timeline paired with the epoch emission budget. The finding that shapes the section is that mixing never happens. At one message per second a relay holds 0.0014 messages and never more than two; even 256x the rate reaches only 0.39, matching Little law exactly. So the anonymity set is entirely blending -- the broadcasts a relay saw between releases -- which follows rate*(2M+1)/3, twice the mean hold. Measured within ~1% over most of the range. That makes delay the cheap lever: both knobs enter linearly, but bandwidth is paid on every link while delay is paid once per hop. An anonymity set of 100 costs 42.9 msg/s at a 3s delay and 4.9 msg/s at 30s. The quota ceiling is the hard edge. A node proposals must fit its emission budget, capping stake at ln(1-q)/ln(1-f) of INFERRED stake -- about 0.1% at the baseline rate once Poisson fluctuation is allowed for. A 9.5% holder overruns by ~65x and is distinguishable by emission count alone, before any path is captured. Two existing sections needed correcting as a result. 3.6-3.7: with cover traffic running, catching an emission is not catching a block, and the large stakers those sections analyse sit one to two orders of magnitude above the quota ceiling, so their binding exposure is the quota rather than the cascade. 5: the timing- correlation adversary was deferred for want of cover traffic and is now unblocked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c42d030f0d
|
pd review: document the new columns, pin the coverage notion, drop dead code
Third review pass over the blend material. Completeness: - the simulator README documented neither frac_reached_live nor the three correlated-churn columns (churn_mode, n_regions, region_locality) that every run now writes, and its model section never described correlated outages at all; - the knowledge graph had no pd nodes -- graphify update had never been run since the simulator was added (2643 -> 2968 nodes). Correctness/coherence: - section 3.5 quotes coverage without saying which coverage, now that 3.9 distinguishes all-node from live-network. It is all-node; under uniform churn the two agree to 0.001, so nothing in 3.5 turns on it. Said so explicitly; - 3.9 named its groups AS/region without noting that link latency ignores them. Regions are failure and peering domains, not latency domains -- real co-located nodes would also be faster, so the clustered delays are if anything pessimistic. Redundancy: - style.band_plot was dead: never called by any figure. Removed, with the two imports it alone needed; - the units sentence appeared verbatim in the header note and again opening the model section. Dropped the second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
25d6463c23
|
Check in the evidence behind the pd report
The simulator gitignores its runs/ directory, so every table and figure in reports/blend/pd rested on data that existed only on one machine. This adds the sweep outputs of record under reports/blend/pd/data -- one directory per study, 1 MB total -- so any number can be checked against its source, or challenged, without re-running hours of compute. report_numbers.py comes with them: run it and it prints every value the report quotes together with its across-topology standard error, straight from these parquets. It reproduces the report tables exactly. Kept: default (8000 rounds/cell), redundancy (9600), percolation (6400), correlated-churn (6400), fullscale (192, the deliberately lighter 1e6 check). Omitted: the smoke runs, and an earlier 144-rounds/cell redundancy grid whose sampling error produced a non-monotonic delivery curve -- superseded, and the reason the kept grid samples 9600. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
73aa71dc90
|
pd report: correlated-outage study, and 10^6 scaling confirmed
Closes the two open items from the review. 3.9 correlated (AS/region) outages, from configs/correlated-churn.yaml at 6400 rounds/cell: clustered failure does NOT fragment the network -- the live coverage holds at 1.000 at every degree through 80% churn, and the percolation threshold of 3.5 never appears, because removing a failure domain removes a whole neighbourhood and leaves every surviving one intact. At degree 4 and 70% churn, scattered failure annihilates the network (live coverage 0.001, delivery 0.000) while the same number of clustered failures still delivers 30% of messages. The cost is that dead domains become unreachable islands, so all-node coverage falls instead. This reverses the caveat previously written, which asserted correlated outages would degrade coverage faster; 3.5 uncorrelated threshold is in fact the conservative case for the operating network. 10^6: the fullscale run confirms the results carry -- adversary closed forms reproduced to 1.6e-4 at a million nodes, coverage under churn indistinguishable from 10^5, delivery still tracking (1-u)^hops, and full delay up only 5.8% from 10^5. It samples 192 rounds/cell against 8000, so it is recorded as a scaling check rather than a source of headline numbers, and the tables are not restated from it. Remaining churn caveat is now the honest one: adversarially placed churn (a cut set rather than a random or clustered set) is still unmodelled, as are heavy-tailed AS sizes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0f125b40c6
|
pd: correlated AS/region churn, and two report caveats corrected
Uncorrelated churn alone was incomplete: real outages take out a datacentre, AS or region as a unit. Adds failure domains and a correlated churn mode, plus the metric needed to tell the two apart. - n_regions / region_locality: nodes belong to equal-sized failure domains, and a configurable share of each node peers inside its own domain. Locality is what makes a failure domain a connectivity domain -- with region-blind peering, dropping whole regions removes a uniformly random set of nodes and is indistinguishable from uniform churn. The locality matchings keep the graph exactly d-regular (they change where peers are, never how many). - churn_mode = uniform | regional, swept per topology so both modes are compared on the same graph at an identical dead-node count. - frac_reached_live: coverage of the *responsive* network, alongside coverage of all nodes. The two move in opposite directions under correlated failure, so one number could not express the result. Measured (degree 4, 20 domains, 75% locality, half the network dead): clustered failure leaves the survivors fully connected -- live coverage 1.000 and delivery equal to the live-relay rate, i.e. nothing lost to routing -- where the same number of scattered failures gives 0.857 live coverage and loses delivery to broken routes. Correlated outages are gentler on the survivors than uniform churn, while stranding the dead domains. Verify check 8 anchors this. Also, per review of the caveats: exact d-regularity is a protocol requirement rather than a modelling simplification, and the timing-correlation adversary is deferred because it is only meaningful once the network emits cover traffic, which this simulator does not yet do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
44e5d25fe3
|
pd report: state the tested size range honestly (10^5, not 10^6)
The headline claimed the findings covered "the sizes tested (10^3-10^6)" and the method note claimed exactness "including 10^6", but no run behind this report exceeds N = 100000 -- 10^6 is a capability of the simulator (make sweep-fullscale) that was never exercised for these numbers. Claims now match the measurements, with an explicit caveat in section 5 distinguishing the three decades actually measured from the extrapolation to 10^6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
81c48a38ab
|
pd review: model completeness, stale numbers, figure coherence
Correctness/completeness pass over the blend material only (TSI untouched). - report Model section (2) was missing two of the six axes: messaging redundancy (R cascades, first-arrival combination) and the emission/linking model (30 s stake-proportional cadence, what counts as linked) were defined only inline in the findings; - method note still claimed 200 rounds x 8 topologies, contradicting the 1000 x 8 the tables now come from; - design guidance carried two superseded numbers: worst-case observation as "+0.15 absolute" (it saturates at 1.000 at degree 8, f_adv 0.2) and the redundancy example (0.34 -> 0.72, measured 0.342 -> 0.713); - figure references were incoherent: Figs 2 and 14 were cited in the text but never shown, and Fig 8 was shown but never cited. All 15 embedded figures are now cited and all citations resolve; - simulator README listed two parquets for smoke (there are three) and omitted redundancy from the propagation/deanon column lists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9b03a68a84
|
Add linkability, messaging redundancy and churn percolation to pd; report
Extends the pd Blend simulator along two axes the deanonymization model opened up, adds the reports/blend/pd report of record, and fixes three correctness defects found while reviewing the result. Linkability over time (pd.linkability): - time to link an emitter ~ 30s*ln(1/(1-alpha))/(stake*q): inversely proportional to stake, so a 5% staker is linked in ~2 days and a 0.001% staker only after ~27 years; - time to certify a node's stake >= theta from the count of attributable observations (relative precision ~1/sqrt(N)): sizing a node costs 100-400x more than identifying it, and sub-0.1% stake is practically unlearnable. Both are closed forms over the exact deanonymization rates and a stake-proportional 30 s emission cadence, checked against a Monte-Carlo of the emission process in verify. Messaging redundancy (R independent cascades per emission, R = 1..4): - `redundancy` knob threaded through config/rng/propagation/engine/metrics/ sweep; a node receives from whichever cascade reaches it first, so arrival times combine element-wise. Delivery and capture both follow 1-(1-x)^R, so redundancy trades reliability against anonymity and divides time-to-link by ~R. Measured: delivery 0.34 -> 0.81 at 30% churn for R = 1 -> 4, while a 1%-staker's time to link falls 10 d -> 2.5 d. - Redundancy buys NO coverage: a cascade only delivers if the sender could already route to its relay, so every delivered cascade floods the sender's own component. Coverage is flat in R to four decimals at every degree. - Near the percolation threshold the cascades fail together rather than independently, so redundancy under-delivers against 1-(1-p1)^R there. Churn percolation (configs/percolation.yaml, verify check 7): - the flood only crosses responsive nodes, so it lives on the responsive sub-graph -- site percolation on a d-regular graph. A network survives churn only up to u_c = 1 - 1/(degree-1); measured collapse lands on the predicted threshold for every degree (3 -> 0.50, 6 -> 0.80, 16 -> 0.93), which inverts into the sizing rule degree > 1 + 1/(1-u). Correctness fixes: - redundancy delay used the fastest cascade's own full delay, which over-states it (min-max vs max-min); now the element-wise earliest arrival, reducing exactly to the single-cascade model at R = 1 (test); - the "redundancy improves coverage" claim was false in both the report and the simulator README -- removed and replaced with the measured result; - per-hop latency is degree-dependent (1.5 s at degree 16 to 2.7 s at degree 3), not a flat 1.6 s; and the worst-case observation figure was averaged over degrees -- at degree 8 and f_adv = 0.2 it is 0.83 -> 1.000. Statistics: round counts raised for resolution rather than speed -- 8000 rounds per cell in the main sweep, 9600 in the redundancy study, 6400 in the percolation study, giving SEM <= 0.009 on every delivery rate and <= 0.04 s on every delay mean. The previous redundancy grid (144 rounds/cell) produced a non-monotonic delivery curve; it is now monotonic and within 0.015 of theory. Adversary and deanonymization metrics remain closed-form and exact. reports/blend/pd: the report of record -- peering-degree trade-offs across speed, observation, eclipse, deanonymization and reliability, plus the time-to-link, stake-inference, redundancy and churn-threshold sections, with 21 figures of record and an explicit sampling-error statement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8cc682aa49
|
Item 16: the revenue-optimal adversary is not the estimator's worst case
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> |
||
|
|
5a8cc4437a
|
Item 5 resolved: the uncle cap is not the binding constraint under a private chain
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> |
||
|
|
ef82ed614d
|
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> |
||
|
|
bed374cdd4
|
Pin the audited spec revision without dating it
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> |
||
|
|
e3da5c8fca
|
Audit sec 8.5 against spec 1.2.1: fix the window rationale and the open-task claim
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> |
||
|
|
c80421e636
|
Scope-variants lede: say which conclusion each variant touches
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
feafe8ec92
|
Close the sec 6.5 scope variants; static withholding can reach the fold
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> |
||
|
|
1a1548b8e5
|
Re-check sec 8.5 against the spec: the header-padding premise is superseded
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>
|
||
|
|
88f31340ad
|
Uncle selection: the spec fixes oldest-first, so measure deviation from it
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> |
||
|
|
0510b20684
|
Countable recovery under a selfish adversary: SM1 hides the first-fork cost
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> |
||
|
|
06364797a8
|
Pair the overload grid; the first-fork cost is resolved at every load
§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> |
||
|
|
3f6f2c4e79
|
Contents: plain nested list, no HTML entities
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> |
||
|
|
c8a23fd1df
|
Report becomes the directory README; rebuild the contents block
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> |
||
|
|
4baccd8d4b
|
Paired design: resolve the design band with common random numbers
The unpaired comparison could not answer the question it was asked. The two uncle models draw independent RNG streams -- uncle_model is in the config key, which is what makes --old bit-reproduce earlier runs -- so the arms differed in stake draw, peering graph and every lottery outcome, each comparison paid the between-run variance twice, and the per-cell floor (+-0.0015) sat an order of magnitude above the effect. Only delta_max = 5 resolved, and only after pooling. Adds `paired_streams`: the RNG root is derived from the model- independent part of the key, so a countable cell and its --old twin get the SAME stake, graph and lottery draws and the uncle rule is the only difference. Each replicate is then a matched pair and the shared variance cancels. Trajectories still diverge after epoch 0 through the genuine feedback (a different counted density changes the next epoch's difficulty), which is the signal. The flag is deliberately NOT in key(): it selects which key the seed is derived from, so including it would perturb every historical seed. Re-verified that --old still bit-reproduces the committed 2026-07-27 rho-boundary parquet, max |delta| = 0. Results (configs/fine-delay-paired.yaml, 40 replicates per arm): - Negative control becomes an IDENTITY check. With U = 0 no reference is taken, so shared streams must give bit-identical trajectories. All 200 replicate pairs differ by exactly 0.0. Unpaired, the same control only had to agree within +-0.025 and drifted by 0.016. - Per-cell SE shrinks by a median 1.6x (1.2-2.1x); widest 95% CI goes +-0.0015 -> +-0.0010. 5/15 cells resolve at |t| >= 2 (0.75 expected by chance); the largest, U=2 at delta_max=4, is t = 4.32 and clears Bonferroni for 15 tests. - The cost is a STEP, not the ramp the unpaired data suggested: delta_max 1-3 unresolved (t = 1.1, 1.8, 1.4), then delta_max 4 AND 5 both resolve at -0.0011 (t = 4.7) and -0.0009 (t = 3.7). Whole-band pooled -0.00060 +- 0.00021, t = 5.7 -- where the unpaired estimate of the same quantity (t = 2.8) had failed correction. So the first-fork restriction costs nothing measurable up to delta_max = 3 and about 0.1% at 4-5 -- an order of magnitude below the +-0.9% per-epoch sampling noise. Two bugs found while building this, both of which would have silently produced a wrong answer: - paired_streams was missing from metrics._CONFIG_FIELDS, so it never reached the parquet; plot_fine_delay.py falls back to the unpaired test when it cannot confirm pairing, so the sweep would have completed and quietly reported the old result. Caught before the run finished; the sweep was restarted and a test now pins the field. - The U=0 control check reported FAILS on a PERFECT control: paired, the gap is exactly 0 so its SE is 0 and t is 0/0. It now checks the gap itself when the streams are shared, and falls back to the t-test only when there is real spread. §3.2a is rewritten around the paired measurement; the unpaired sweep is retained in §9 as the power comparison that motivated it. Figures 34-35 regenerated, with the control annotation and provenance reflecting the design actually used. Tests: 214 passed (was 209). ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
15ad9f285c
|
Tighten the statistics in the design-band section
Re-examining §3.2a's inference rather than its numbers found one claim that was wrong and one that does not survive correction. Wrong: "two independent tests agree on that onset." The gap test (countable - unrestricted) and the vs-1.0 test (countable - 1.0) share the countable arm and are correlated at rho ~ 0.73. The vs-1.0 test is not confirmation, it is the same measurement with one noisy arm removed -- which makes it ~1.4x more sensitive, and is why it flags 4 cells where the gap test flags 1. What IS informative is the contrast between arms: unrestricted shows chance-level deviation, countable shows a shortfall concentrated at every uncle cap at the top of the band. Does not survive: the whole-band pooled gap, -0.00048 +- 0.00033 (t = 2.8). Counting every test in the section -- 15 per-cell gap, 5 pooled-per-delay, 1 whole-band, 30 vs-1.0 = 51 -- Bonferroni requires z = 3.30. It is now labelled unresolved. The sign imbalance (11/15 negative, p = 0.12) is labelled likewise. Holds, and more firmly than claimed: delta_max = 5, pooled t = 3.68. Added a 20 000-draw permutation test on the same cells, which assumes no distributional form: p = 0.0003, with every other delay at p >= 0.21. It clears the 51-test Bonferroni threshold. Replicate distributions are clean (sd ~0.003, |skew| <= 0.5, no outliers), and the peering graph re-rolls per replicate -- `replicate` is in config.key() and the graph is seeded from the full-key spawn hierarchy -- so the replicate SEM captures graph-to-graph variance rather than pseudo-replicating one graph. Added a weighted regression of gap on delay as a single pre-specifiable test with no choice of which delay to inspect: slope -0.00024 +- 0.00012 per slot (t = -2.05). Consistent with a monotone cost, marginal on its own, and reported as such. Added the limitation that actually caps the resolution: the comparison is unpaired. uncle_model enters the config key (which is what makes --old bit-reproduce the earlier runs), so the two arms share neither graph nor lottery draws and each comparison pays the between-run variance twice -- a +-0.0015 per-cell floor, an order of magnitude above the effect. A paired design would cancel most of it and is named as the single change that would most improve the measurement. The §1 summary is corrected to match: indistinguishable from zero up to delta_max = 4, resolved only at the top of the band at ~0.14%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8d1ede2807
|
Redundancy and coherence pass on the merged report
Read as one document rather than four, with an overlap scan over all
146 prose paragraphs.
- §2.2 and Appendix A both carried the full derivation of the on-chain
`f`-rounding offset (37% trigram overlap) — an artefact of the parts
having to stand alone. §2.2 now states the fact, the ~1.010 factor,
and that this report's estimator is unbiased, then defers the
derivation and the spec-change sizing to Appendix A. Residual overlap
is the unavoidable shared notation.
- §7's figure-location note was corrected to the measured placement
(Appendix B and C hold figB1/figB2 and fig17/fig18/fig21; it had said
§9) and extended to fig30–fig35.
Checked and found clean: no verbatim repeated sentences; no other
paragraph pair above 20% overlap that is not a vocabulary coincidence;
no directional reference ("above"/"below") inverted by moving §7–§8
after §6 — the three flagged were numeric comparisons, not navigation;
all 33 TOC entries resolve and every top-level section is listed; 47
anchors and 37 figures resolve in both markdown and rendered HTML.
Tests: 209 passed. ruff clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c5059c2fc8
|
Consolidate the TSI report into one document
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> |
||
|
|
15e876e90a
|
Correctness pass: derive rho in code, and the absolute vs-1.0 test
Two findings from re-reviewing the fine-delay section. 1. The rho values I put in s3.2a were wrong. The report derives rho = f*D_vis with D_vis = hops*delta_max/2 + (hops+1)*ell_mean from a MEASURED ell_mean (1.211 slots at N=1000/degree=6), not from the link_latency_mean parameter (0.5). Hand-substituting a guessed 1.5 inflated every value by ~0.04: the band is rho 0.21-0.41, not 0.25-0.45. To stop that recurring, graph_ell_mean moves out of rho_boundary_analysis.py into figures_pernode.py, joined by a new rho_for() that both scripts and any future quotation go through; plot_fine_delay.py now prints the derived rho per delay. This also exposed an inconsistency in the existing s3.2 table, which rounded delta_max=4 to "rho ~ 0.4" while s3.2a called the same cell 0.36 and prose elsewhere already used 0.56 for delta_max=8. The s3.2 column now carries the derived values (0.36/0.56/0.96/1.76). 2. Testing each cell against the exact target 1.0 -- the same question the gap test asks, without reference to the other model -- corroborates the first-fork onset independently. Unrestricted: 1/15 cells below 1 (t=-2.09, chance). Countable: 4/15, and not scattered -- delta_max=4 at U=1, and ALL THREE caps at delta_max=5 (-0.0012 to -0.0019, t=-2.5..-3.7). A shortfall appearing at every cap at once, only at the top of the band, only under the restricted model, is the first-fork cost seen absolutely. That makes "one uncle slot is sufficient -- not approximately, exactly" too strong as I had written it. s3.2a now states the residual (0.1-0.2% at the top of the band, zero below delta_max=3), reconciles it with the s1 headline, and notes that since all three caps show the same shortfall the residual is not a capacity limit. The bound quoted in s1 moves from "below 0.15%" to "<= 0.2%". Also adds the new run directories to s9's canonical list, which covered every other study but not these. Tests: 209 passed. ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ac6a309e58
|
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> |