85 Commits

Author SHA1 Message Date
Marcin Pawlowski
333231c6dd
Set up the high-power W sweep and its analysis
Prepares a finer, higher-power re-measurement of where the parent-anchored window
crosses today's recipe. The integer sweep put the crossing between W = 11
(t = -4.5) and W = 12 (t = 0.1) — located to about one block-interval and no
better, on a ~0.0005 standard error.

configs/pref-window-anchor-fine.yaml attacks three variance sources at once:
half-integer W from 9 to 14 (w_u = 270..420 slots, all genuinely distinct
windows, not relabelled duplicates); 96 replicates instead of 32; and early_stop
off at 30 epochs, so each run contributes a full 15-epoch equilibrium sample
instead of ~10. The replicate count averages out between-run variance; the epoch
count attacks the per-epoch +-0.9% sampling noise that replicates cannot touch.
2112 runs, paired throughout.

scripts/w_pairing_analysis.py reports the paired table, locates the crossing
(smallest W not resolvably worse, plus the interpolated zero), and — the part
that matters for an unattended run — counts replicates whose stake draw cannot
realise adversary_frac and repeats the whole analysis without them.

That last check already pays for itself on the committed integer sweep: 2 of its
32 replicates ran 0.09 and 0.06 coalitions against a 0.30 label. Being weaker
they pull cells toward parity, which would make the crossing look smaller than
it is. Dropping them changes nothing — crossing stays at W = 12, interpolated
zero at 11.98 — so the committed recommendation is robust to it, and §6.12 now
says so.

(My earlier check claimed 0 of 32 were off-label; it reconstructed the config
wrongly. The runtime warnings were right.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:22:56 +02:00
Marcin Pawlowski
86058fb3f0
Re-run §6.8's selfish uncle-margin sweep; close the coalition-sizing item
The last study still carrying pre-fix numbers. It was the most contaminated of
them — 2 of 8 replicates at alpha=0.3 and 1 of 8 at alpha=0.4 were running
majority coalitions — so it needed re-running before its levels could be quoted.

Every conclusion reproduces:

  - alpha=0.3, delta=8: D-hat/D 0.757/0.769/0.775/0.766 across U=1..4 (was
    0.729/0.755/0.738/0.758). Still flat in U — raising the cap does not buy the
    estimate back, which is the section's point.
  - No attacked cell reaches the 0.98 bar at any cap or either window.
  - The honest baseline still reproduces §3.4 exactly: U=1 clears at delta=8;
    delta=16 needs U=2 at W=10, or W=20 at U=1.
  - The structural decomposition holds: countable share falls 97% -> 77% -> 54%
    with alpha while referenced-of-countable stays high (70-92%), so the loss is
    the first-fork restriction and not a drained queue.

§8.3 item 20 closes: every adversary study with the default selection has now
been re-measured, no conclusion was overturned, and the one materially resized
number was the parent anchor's cost under suppression (0.021 -> 0.006). What
replaces it is a residual worth stating rather than an open task — a Pareto draw
can leave adversary_frac unreachable outright, which now warns and leaves that
replicate with a weaker adversary than its label. That biases an attacked arm
toward the honest baseline, so it is conservative, but a sweep quoting levels
should report how many of its replicates warned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:52:10 +02:00
Marcin Pawlowski
6f28882510
Correct §6.9's fork-depth figures to the post-fix measurement
The depths quoted for the rival-coalition mechanism came from an exploratory run
made before the coalition-sizing fix, where beta=0.4 was realising 0.47. Measured
properly (median over 6 replicates, deepest fork off the canonical chain):

  K=1  865 blocks (784-964)    K=2  17    K=3  9    K=4  7

not the "~2000 and ~2" first written. The mechanism is unchanged and the contrast
is still stark. Also notes why the lead cap does not bound this: it caps the
LEAD, not the chain length, so a private chain can run long while the public
chain keeps pace beside it — the near-tie race that produces the deepest forks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:49:29 +02:00
Marcin Pawlowski
563db81b42
Re-run fig38's effort arm on the engine's stake vector
uncle_parent_window.py has two arms: recovery, which calls run_trajectory, and
effort, which rebuilds a tree by hand. Only the second was affected by the
stake_for correction — it drew stake from rng_for while the trajectory it sits
beside drew from the spawned child, so the two arms described different networks.

The recovery numbers are unchanged (they always came from the engine). The
effort numbers move but the conclusion does not:

  delta=4,  30% adversary   median reach 20 144 -> 11 563   max ~76 770
  delta=8,  30% adversary   median reach  6 390 ->  2 465
  delta=16, 30% adversary   median reach    872 ->    206
  parent-anchored, same adversary: 293 / 298 / 300 slots, capped at w_u

A 30% deep-parent coalition still drags the median counted reference from ~55
slots back to over 11 000 and the worst case to the epoch boundary, and the
parent anchor still caps it at the window by construction. §6.12's table and
prose updated to the re-measured values.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:46:38 +02:00
Marcin Pawlowski
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>
2026-08-10 12:42:54 +02:00
Marcin Pawlowski
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>
2026-08-10 12:37:21 +02:00
Marcin Pawlowski
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>
2026-08-10 12:21:00 +02:00
Marcin Pawlowski
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>
2026-08-10 11:47:51 +02:00
Marcin Pawlowski
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>
2026-08-10 11:34:40 +02:00
Marcin Pawlowski
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>
2026-08-10 11:06:39 +02:00
Marcin Pawlowski
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>
2026-08-07 18:34:16 +02:00
Marcin Pawlowski
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>
2026-08-07 18:27:08 +02:00
Marcin Pawlowski
525d081a3c
Keep the deployed rule as the default; make the proposal an explicit arm
I had planned to flip uncle_window_anchor's default to "parent" so new configs
would measure the proposed design by default. Tried it, and it breaks a
documented guarantee: key() appends the anchor only when it is not "uncle", so
with "parent" as the default an --old run's key is no longer byte-identical to
the pre-redesign key and --old stops bit-reproducing historical runs (sec 9).
o.key() == o._base_key() fails outright.

Reverted, and on reflection the default was wrong for a second reason anyway.
The report's job is to describe the protocol as deployed and to RECOMMEND
changes; the default should therefore be the deployed rule, with the proposal
as an explicit arm. That is exactly the convention fixed_point already follows
(default exact f = the analysis convention, explicit True = spec-faithful).
Both reasons are recorded on the field.

To make the distinction visible rather than implicit, the spec-as-is studies
now pin uncle_window_anchor: uncle explicitly -- spec_point.py, spec_jitter.py
and the three spec-point-*.yaml configs answer "what does the DEPLOYED chain
do", so they must not drift onto a proposal if a default ever moves.

Adds uncle_window_anchor as a sweep axis (SweepConfig field plus _SWEEP_AXES),
and configs/absorption-window-anchor.yaml: the sec 3.4 absorption sweep re-run
under both anchors at three delays. That study is the one that can move a
recommendation -- the W >= 7/f floor was measured against the uncle gap, and the
parent gap runs about one block-interval longer, so the floor should sit higher
and the margin behind W = 10/f shrink.

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:22:50 +02:00
Marcin Pawlowski
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>
2026-08-07 11:59:37 +02:00
Marcin Pawlowski
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>
2026-08-07 11:59:37 +02:00
Marcin Pawlowski
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>
2026-08-07 11:59:37 +02:00
Marcin Pawlowski
c202ad9c17
Add deep_orphan_share: the structural observable behind the first-fork cost
E5 needs to watch whether per-recipient delay variance manufactures the
depth->=2 forks the countable rule cannot reach, and no recorded metric measured
that. p_ref conflates "unreachable by construction" with "eligible but never
picked up" -- the distinction that turned out to be the whole answer to item 5 --
and deep_ref_share is 0 by construction under the countable model, since the
proposer's candidate filter drops deep-fork blocks before any reference to one
is proposed. deep_orphan_share is the fraction of in-window orphans sitting
below the first block of their fork, computed from the depth array fork_stats
already builds.

Also fixes a splat-unpack in test_selfish_engine that silently re-bound to the
wrong quantities when fork_stats grew this field (it read deep_orphan_share as
p_ref_honest). fork_stats has now gained a field twice; both call sites unpack
by position explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 11:59:37 +02:00
Marcin Pawlowski
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>
2026-08-07 11:59:37 +02:00
Marcin Pawlowski
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>
2026-08-07 11:59:37 +02:00
Marcin Pawlowski
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>
2026-08-06 18:00:02 +02:00
Marcin Pawlowski
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>
2026-08-06 18:00:02 +02:00
Marcin Pawlowski
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>
2026-08-06 18:00:02 +02:00
Marcin Pawlowski
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>
2026-08-06 18:00:01 +02:00
Marcin Pawlowski
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>
2026-08-06 18:00:01 +02:00
Marcin Pawlowski
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>
2026-08-06 18:00:01 +02:00
Marcin Pawlowski
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>
2026-08-06 18:00:00 +02:00
Marcin Pawlowski
0c72a39e38
blend: show the clock design is not handicapped by the adversary assumption
The jitter-vs-clock verdict rested on handing the clock adversary the tick
schedule, which is the conservative choice but also the harsher one -- a real
observer sees only ticks that released something. Rather than assume, both are
now measurable, and they are provably equivalent.

A silent tick implies nothing was pending at it, and any arrival older than the
previous release has demonstrably already left. So the candidate window bounded by
the true previous tick and the one bounded by the previous observed release hold
the same arrivals. Confirmed identical to 1e-9 in both the sparse regime (one hold
per relay) and the dense one (56 holds per relay), where the two coincide for
different reasons.

That removes the objection to the comparison: jitter beats the clock at a matched
delay budget on merit, not because the clock was given a stronger opponent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:00:00 +02:00
Marcin Pawlowski
4915444c3e
blend: cover the two new release fields in the key() test
Caught by the test immediately: key() enumerates every SimConfig field, so
min_blend_delay and release_mode needed alternative values.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:00:00 +02:00
Marcin Pawlowski
57be929af7
blend: minimum release interval, and jitter vs clock under a timing attack
Two release designs at a MATCHED delay budget, so they differ only in how they
delay: clock (batch at free-running ticks, the existing model) and jitter (each
message waits its own exponential draw, mean set equal to the clock residual).
Plus min_blend_delay, which forbids intervals shorter than it.

Minimum interval -- a negative result, and provably so. A zero-length gap is
instantaneous, so it never covers an arrival and is never sampled by the residual
or by the size-biased interval. Excluding it therefore leaves the mean hold
exactly unchanged, and with it blending and linkability; what it does change is
E[S], the gap between release opportunities. Confirmed analytically and in
simulation: 1.168s vs 1.167s at M=3.

Timing attack -- the effective anonymity set of a release (perplexity of the
observer posterior over which arrival produced it), plus MAP success, the chance
its single best guess is right. The second matters because perplexity flatters a
heavy tail: an exponential never fully excludes an old arrival, so it can look
unlinkable while still being guessed correctly.

At the baseline rate BOTH designs fail almost completely -- MAP success 0.98-0.99,
effective set ~1. A relay handles so little traffic that in->out matching is
trivial, which follows directly from the mixing~0 result. Traffic, not delay, is
what buys timing protection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:00:00 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:59 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:59 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:59 +02:00
Marcin Pawlowski
8a3a43b366
pd: fix a Poisson underflow that broke the quota ceiling at higher cover rates
Found by the cover-traffic sweep: the 99%-safe stake ceiling FELL as the cover
rate rose, while the mean bind rose to 0.38 -- backwards.

quota_exceedance_prob summed the Poisson CDF by hand starting from exp(-lam).
That underflows to zero past lam ~ 745, so the CDF collapsed to 0 and the function
reported every node as exceeding its quota, which drove the bisection in
max_alpha_for_confidence to a meaningless answer. The default rate is unaffected
(lam ~ 32), but raising the cover rate reaches the broken regime immediately,
because the quota and the tolerable block count grow in proportion.

Replaced with scipy poisson.sf. Exceedance at the mean bind is now ~0.5 at every
rate, as it must be, and the safe/mean ratio rises 0.65 -> 0.81 -> 0.90 -> 0.95 ->
0.98 across the swept rates: Poisson noise shrinks relative to a growing quota, so
less headroom is needed for the same confidence. Two regression tests pin both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:58 +02:00
Marcin Pawlowski
1865b4110d
pd: verify anchors for the blending law and the emission ceiling
Check 9 -- cover traffic on a timeline. Blending follows rate*(2M+1)/3 (7.39 vs
7.41 predicted), the mean hold is the renewal residual (2M+1)/6, and mixing is
nil at the baseline rate: 0.007 concurrent holds, max 2. That last one is the
substantive finding rather than a sanity check -- at one message per second a
relay has nothing to mix, so the anonymity comes entirely from what it has seen.

Check 10 -- the emission quota. The measured breakpoint brackets the closed form
(band [0.127%, 0.159%] against a predicted 0.1475%), and deflating D_hat/D to
0.64 pushes more nodes over their quota, as the (D_hat/D)*alpha_max form requires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:58 +02:00
Marcin Pawlowski
61d72bb5f5
pd: wire cover traffic through the sweep as a fourth table
engine gains a cover_rates axis: each rate plays a timeline through the same
graph and pairs it with the epoch emission budget, which needs no graph and so is
computed alongside rather than inside the window. Seeds are separate streams
(traffic_seedseq for the timeline and clocks, stake_seedseq for the stake draw
and budget), so the stake distribution is independent of the topology and of the
message schedule.

sweep writes traffic.parquet only when a cover-traffic study actually ran, so
every existing config keeps producing exactly three tables. quota_summary reports
the measured ceiling beside the predicted one in the same row, so a run can be
checked against the closed form instead of asked to be believed.

Two figures: blending against cover rate and release delay with the
rate*(2M+1)/3 law overlaid, and the quota ceiling with the measured transition
band against the prediction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:58 +02:00
Marcin Pawlowski
f718f5f954
pd: stake distribution, so the emission ceiling is measured not asserted
The quota ceiling was closed-form only. This adds per-node stake so a run can
show nodes actually breaking it.

- assign_stake: uniform, or heavy-tailed zipf (s ~ 1/rank^a), which is what makes
  the ceiling bite -- the head sits orders of magnitude above it, the tail far below;
- inferred_alpha: converts true relative stake to the sigma/D_hat the lottery
  actually weighs, so a low estimate inflates every node alpha;
- simulate_epoch_emissions: measures the budget over a full epoch. Overrun happens
  at epoch scale and needs no graph, so this is cheap: proposals are Binomial over
  the epoch slots, a proposal cancels the next cover, and a node stays at exactly
  its quota until its wins no longer fit -- at which point it emits more often than
  everyone else, which is the signal cover traffic exists to suppress.

Measured against the closed form at N=20,000, zipf stake, over an epoch: the
predicted ceiling falls inside the transition band every time, and at D_hat/D = 1
the smallest overrunning node sits at 0.1468% against a predicted 0.1475%. The
D_hat/D normalisation is confirmed empirically -- deflating the estimate to 0.64
pulls the measured ceiling down with it, as the (D_hat/D)*alpha_max form requires.
With heavy-tailed stake 99.7% of nodes comply and only the head breaks; the
largest holder at 9.5% stake is some 65x over its allowance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:58 +02:00
Marcin Pawlowski
2248a048d4
pd: cover the new cover-traffic config fields in the key() test
The key() coverage test enumerates every SimConfig field, so the five cover-
traffic knobs had to be given alternative values. Caught by the test itself
immediately after the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:58 +02:00
Marcin Pawlowski
e35804f29d
pd: cover traffic -- emission quota and the blending timeline
First half of the cover-traffic work: the two new modules and their tests.

quota.py -- the emission budget. Cover traffic gives every node the same number
of emissions per epoch, which only holds while a node block proposals fit inside
its quota. The bind is exact: alpha_max = ln(1-q)/ln(1-f), where alpha is stake
relative to the INFERRED total D_hat, since that is the denominator the lottery
threshold is derived from. In true stake the ceiling carries the estimator ratio,
s_max = (D_hat/D)*alpha_max, with D_hat/D an input rather than an assumption. The
familiar q/f is a small-q approximation that runs 1.7% high and so overstates the
tolerable stake. Sitting on the mean bind overruns the quota half the time, so
max_alpha_for_confidence gives the ceiling that holds with stated probability.

traffic.py -- the timeline. The rest of the simulator samples independent rounds
and draws each hold from the stationary residual, which has no notion of time and
so can never let two messages meet at a relay. Here every node owns one
free-running clock shared by all messages through it, extended lazily so only the
relays actually visited grow one. A clock sampled once still reproduces
mixclock.mix_wait, so single-message statistics are unchanged.

It separates two quantities that are easy to conflate: mixing (messages a relay
holds at once) and blending (messages it has SEEN between consecutive releases).
Blending is the anonymity set -- every broadcast reaches every node, so an
observer cannot tell which of them the relay forwarded. Gaps sampled at a release
are size-biased, so blending is rate*(2M+1)/3, twice the mean hold, not
rate*M/2 as a naive reading gives. Measured within 1-4% of that at M = 3, 10, 30
and linear in the cover rate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:57 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:57 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:57 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:57 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:56 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:56 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:56 +02:00
Marcin Pawlowski
b5782619d8
pd smoke: exercise the multi-cascade path end-to-end
smoke.yaml never set redundancy > 1, so the R-cascade aggregation and the two
redundancy figures were only covered by unit tests, never by the end-to-end run.
Adding redundancy: [1, 2] takes smoke from 17 to 19 of the 21 figure builders
(only delay_vs_N and the churn-percolation figure need grids smoke does not have).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:56 +02:00
Marcin Pawlowski
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>
2026-08-06 17:59:55 +02:00
Marcin Pawlowski
6ad63ce2f3
Add pd: peering-degree Blend Monte-Carlo graph simulator
Static-graph simulator quantifying how a node's peering degree trades off
propagation speed, adversary exposure, deanonymization, and reliability in the
Blend network. Scales to 1e6 nodes (sparse CSR + sampled Dijkstra); the
adversary and deanonymization metrics are exact at every N.

Model (ms): seeded d-regular peer graph (matching-union), Blend cascade
(sender -> blend_hops timed-release mix relays -> final flood), geographic link
base + exponential transport jitter, per-node processing lag, free-running
release-clock mixing.

Metrics:
- propagation: full-delay mean/p50/p90/p99, path/broadcast split, coverage times
- reliability: message success-delivery-rate ~ (1-unresponsive_frac)^blend_hops
  and flood coverage, with unresponsive nodes modelled as routing holes
- adversary (exact): observed/eclipsed fractions, random + worst-case placement
- deanonymization (exact): P(whole blend path adversarial) ~ f_adv^blend_hops,
  and full deanonymization (path adversarial AND honest sender peered with an
  adversary) = deanon_rate * observed_frac

Deterministic blake2b seed streams, three parquet tables, joblib parallelism,
memguard, an analytic verify harness, 50 unit tests, and an auto-installing
Makefile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:55 +02:00
madxor
ab5dcc66f5
Merge pull request #1 from logos-blockchain/tsi-countable-uncle-model
TSI: consolidate the report, adopt the countable uncle model, and revise the selfish-mining repair
2026-08-06 17:31:03 +02:00
Marcin Pawlowski
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>
2026-08-06 14:54:52 +02:00