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>
This commit is contained in:
Marcin Pawlowski 2026-08-10 12:52:10 +02:00
parent 6f28882510
commit 86058fb3f0
No known key found for this signature in database
2 changed files with 9 additions and 9 deletions

View File

@ -695,16 +695,16 @@ The backfire is *unconditional* below the `⅓` threshold and holds at higher `
**What sets `p_ref` — the window `W` and visibility (the residual).** `p_ref` rises toward 1 as `W` grows (more following honest blocks get a chance to reference a given orphan) and falls only when an orphan **ages out of `W` before any honest referencer sees it** — i.e. under a *deep reorg* longer than `W`, or genuine propagation loss. This is exactly the "can't guarantee a node sees every fork in the window" limit, and it is `W`-tunable: the [§3.4](#s3-4) sizing `W ≈ 10/f` is already generous, and the residual adversarial lever (drive `p_ref` down via reorgs deeper than `W`) is bounded — deep reorgs need large `α`, are rare, and risk the attacker's private chain. One proviso remains, about *degree* not *direction*: even at `p_ref = 1` the selfish share is `≈ α` with a small residual premium that **grows with `α`**`≈ 0` near the `⅓` threshold, `+0.006` at `α = 0.4`, `+0.014` at `α = 0.46` — not exact break-even.
**Under a private chain the uncle cap is not the binding constraint — the fork structure is.** The engine carries a private-chain coalition (`adversary_strategy: selfish`, SM1 release rules), so the whole loop is present: the attack orphans honest blocks in runs, the survivors queue for the `U` uncle slots of each canonical block, and whatever does not drain within `W` ages out. Sweeping the cap against the attack (`scripts/selfish_uncle_margin.py`; N = 1 000, k = 256, 8 replicates, `U ∈ {1,2,3,4}` × `W ∈ {10, 20}`) settles the question the honest-load rule left open. **Raising `U` does not buy the estimate back.** At the design point (`δ_max` = 8 s) and `α` = 0.3, `D̂/D` reads 0.729 / 0.755 / 0.738 / 0.758 for `U` = 1 / 2 / 3 / 4 — flat within replicate noise — and no attacked cell reaches the 0.98 recovery bar at any cap or either window, while the honest baseline in the same sweep reproduces [§3.4](#s3-4) exactly (`U` = 1 clears at `δ_max` = 8; `δ_max` = 16 needs `U` = 2 at `W` = 10, or `W` = 20 at `U` = 1).
**Under a private chain the uncle cap is not the binding constraint — the fork structure is.** The engine carries a private-chain coalition (`adversary_strategy: selfish`, SM1 release rules), so the whole loop is present: the attack orphans honest blocks in runs, the survivors queue for the `U` uncle slots of each canonical block, and whatever does not drain within `W` ages out. Sweeping the cap against the attack (`scripts/selfish_uncle_margin.py`; N = 1 000, k = 256, 8 replicates, `U ∈ {1,2,3,4}` × `W ∈ {10, 20}`, re-run after the [§9](#s9) coalition-sizing fix) settles the question the honest-load rule left open. **Raising `U` does not buy the estimate back.** At the design point (`δ_max` = 8 s) and `α` = 0.3, `D̂/D` reads 0.757 / 0.769 / 0.775 / 0.766 for `U` = 1 / 2 / 3 / 4 — flat within replicate noise — and no attacked cell reaches the 0.98 recovery bar at any cap or either window, while the honest baseline in the same sweep reproduces [§3.4](#s3-4) exactly (`U` = 1 clears at `δ_max` = 8; `δ_max` = 16 needs `U` = 2 at `W` = 10, or `W` = 20 at `U` = 1).
The reason is visible once the honest orphans are split by *why* they went unreferenced — structurally unreachable, or eligible but starved of a slot. (Neither `p_ref` nor `deep_ref_share` separates these: the first mixes them, and the second is 0 by construction here, because the proposer's candidate filter drops deep-fork blocks before any reference to one is ever proposed.) Walking the tree directly (same script, 6 replicates):
| | honest | `α` = 0.2 | `α` = 0.3 |
|---|---|---|---|
| honest orphans that are **countable** (first block of their fork) | 97 % | 7681 % | 5972 % |
| of those, **referenced** within `W` | 9093 % | 8493 % | 8088 % |
| honest orphans that are **countable** (first block of their fork) | 97 % | 77 % | 5455 % |
| of those, **referenced** within `W` | 9094 % | 7992 % | 7088 % |
The queue is draining. Countable orphans are referenced at essentially the honest rate whatever the attack, and the one place the cap matters is the honest-load reason it was sized for — going `U` = 1 → 2 lifts that rate from 84 % to 93 % at `α` = 0.2, then `U` = 4 adds nothing. What collapses is the *countable* share: an override discards a **chain**, and only its first block has a parent on the surviving chain ([§2.1](#s2-1)), so 2040 % of the honest work the attacker destroys is unreferenceable by construction. `U` governs drain capacity for candidates that exist; it cannot manufacture eligibility. This is the [§6.6](#s6-6) first-fork ceiling arriving from a completely independent direction — a per-node network simulation rather than a stationary MDP — and it is why `U = ⌈ρ⌉ + 1` needs no adversarial margin: the margin would be spent on a queue that is not the bottleneck.
The queue is draining. Countable orphans are referenced at essentially the honest rate whatever the attack, and the one place the cap matters is the honest-load reason it was sized for — going `U` = 1 → 2 lifts that rate from 79 % to 92 % at `α` = 0.2, then `U` = 4 adds nothing. What collapses is the *countable* share: an override discards a **chain**, and only its first block has a parent on the surviving chain ([§2.1](#s2-1)), so 2040 % of the honest work the attacker destroys is unreferenceable by construction. `U` governs drain capacity for candidates that exist; it cannot manufacture eligibility. This is the [§6.6](#s6-6) first-fork ceiling arriving from a completely independent direction — a per-node network simulation rather than a stationary MDP — and it is why `U = ⌈ρ⌉ + 1` needs no adversarial margin: the margin would be spent on a queue that is not the bottleneck.
**Reward-side attack surface (bounded, self-defeating).** *(a) Farming* — orphaning a real VRF win to self-uncle it — pays `w_u + w_n` against `1`, so it is loss-making under the [§6.7](#s6-7)(a) invariant **`w_u + w_n < 1`**; in the model the attacker's share *falls* as it self-uncles more (`0.406 → 0.394`). *(b)* Because honest referencers count the orphans, a flood *inflates* `D̂` rather than deflating it — the opposite of what a deflation attack needs. *(c) Nephew leakage* to the attacker is bounded by `w_n` and dominated by the `w_u` that flows to the honest producer.
@ -981,7 +981,7 @@ Everything below is assembled from this report's own measurements; each row cite
17. **The `β_adv` axis is a nominal target, not a realised share ([§6.5](#s6-5)).** A random coalition is grown until its stake first reaches `β_adv`, so the last node added overshoots by its own size — under a Pareto tail, potentially a whale. The realised block share at a nominal `β_adv` = 0.1 is 0.137 ± 0.108, and that variance dominates the mean at small `β_adv`. The conclusions here are drawn from monotone trends across the axis and are unaffected, but a reader should not read `β_adv` as an achieved share; a whale coalition (filled largest-first to the target) lands on it precisely and is the cleaner instrument where the exact share matters.
18. **The collapsed-branch tail is uncharacterised ([§6.2](#s6-2)).** Static withholding reaches the collapsed branch once in 96 runs — one replicate of the concentrated (whale) 50 % coalition at `δ_max` = 8 s — and never at `δ_max` = 4 s. One reproducible event is enough to retire "not an observed dynamical trap" but far too thin to give the event a rate; its frequency against `β_adv`, `ρ` and coalition concentration is unmeasured.
19. **Structurally uncompensatable orphans ([§6.7](#s6-7)(1), [§6.8](#s6-8)).** The uncle reward refunds `w_u` per *referenced* orphan, but 2040 % of the honest blocks a private-chain attack destroys are unreferenceable by construction — buried behind the first block of a discarded run. Those producers cannot be compensated at any `w_u`, so the fairness guarantee inherits the first-fork ceiling exactly as the density repair does. No reward schedule fixes this; it is a property of the counting rule. Whether that is acceptable — honest miners bearing an uninsurable loss precisely when attacked — is a protocol-design question this report raises rather than settles.
20. **Two adversary studies await re-measurement ([§9](#s9)).** The coalition-sizing defect — a Pareto whale straddling the prefix cut could carry the coalition past its label, up to a majority in ~10 % of replicates — was fixed, and the load-bearing studies ([§8.4](#s8-4) capstone under both anchors, [§6.12](#s6-12)'s `W` pairing) were re-run. One was not: [§6.8](#s6-8)'s selfish uncle-margin sweep, where 2 of 8 replicates at `α` = 0.3 and 1 of 8 at `α` = 0.4 were running majority coalitions (worst 0.602 and 0.707). It reports a *directional* result — whether a larger `U` clears a recovery bar — and a too-large adversary does not flip that direction, which is why it is not withdrawn. But its **levels and thresholds should not be quoted as measured** until re-run. Its `α` = 0.2 arm, [§6.5](#s6-5)'s scope variants, the `whale` arms, every honest sweep, and the [§6.2](#s6-2)/[§6.5](#s6-5) collapse event (a whale coalition) were clean.
20. **Coalition sizing — found, fixed, and every affected study re-run ([§9](#s9)).** The adversarial coalition was built by a cumulative-prefix cut, which under a Pareto tail let one whale carry it past its label — at the report's geometry a nominal 0.3 realised a majority in 12 % of replicates. Every study with an adversary and the default `random` selection has been re-measured against the corrected construction: the [§8.4](#s8-4) capstone under both anchors, [§6.12](#s6-12)'s `W` pairing (also re-designed as a paired sweep), [§6.12](#s6-12)'s `fig38` effort arm, and [§6.8](#s6-8)'s selfish uncle-margin sweep. **No conclusion was overturned**; one was materially resized (the parent anchor's cost under suppression, 0.021 → 0.006) and a number of levels moved in the third decimal. The `whale` arms, every honest sweep, and the [§6.2](#s6-2)/[§6.5](#s6-5) collapse event were never affected. What remains is a *residual* rather than an open item: a Pareto draw can leave `adversary_frac` unreachable outright (one holder above the target), which now raises a warning and leaves that replicate running a weaker adversary than its label — it dilutes an attacked arm toward the honest baseline, so it is conservative, but a sweep quoting levels should report how many of its replicates warned.
<a id="s8-4"></a>
### 8.4 Capstone: the whole recipe, in one run
@ -1110,15 +1110,15 @@ The construction is now fit-then-close in random order: take every node that sti
| [§8.4](#s8-4) capstone, spec rule | 0 of 8 | 0.369 vs 0.30 | `D̂/D` 0.994 → 0.995, `p_ref` 0.936 → 0.937 |
| [§8.4](#s8-4)/[§6.12](#s6-12) capstone, parent anchor | 0 of 8 | 0.369 vs 0.30 | `D̂/D` 0.974 → **0.990**, `p_ref` 0.875 → **0.923** — the one changed conclusion |
| [§6.12](#s6-12) `W` pairing, the 12-replicate edition | 2 of 12 (**2 majorities**) | 0.720 vs 0.30 | re-run *and* re-designed as a paired sweep — same value for `W`, now resolved; see [§6.12](#s6-12) |
| [§6.8](#s6-8) selfish uncle margin, `α` = 0.3 | 2 of 8 (**2 majorities**) | 0.602 vs 0.30 | not re-run — see below |
| [§6.8](#s6-8) selfish uncle margin, `α` = 0.4 | 1 of 8 (**1 majority**) | 0.707 vs 0.40 | not re-run — see below |
| [§6.8](#s6-8) selfish uncle margin, `α` = 0.3 | 2 of 8 (**2 majorities**) | 0.602 vs 0.30 | re-run: `D̂/D` at `U` = 14 moved 0.729/0.755/0.738/0.758 → 0.757/0.769/0.775/0.766, conclusion unchanged |
| [§6.8](#s6-8) selfish uncle margin, `α` = 0.4 | 1 of 8 (**1 majority**) | 0.707 vs 0.40 | re-run; still far below the bar at every cap |
| [§6.8](#s6-8) selfish uncle margin, `α` = 0.2 | 0 of 8 | 0.202 vs 0.20 | clean as run |
| [§6.5](#s6-5) scope variants, `random` arm | 0 of 12 at `β_adv` = 0.3 and 0.5 | 0.345 vs 0.30; 0.567 vs 0.50 | clean as run |
| [§6.12](#s6-12) `uncle_parent_window` | 0 of 10 | 0.346 vs 0.30 | clean as run |
Note the [§8.4](#s8-4) rows: that draw contained **no** oversized coalition, and the spec's own recipe barely moved — yet the parent-anchored variant moved by 0.016 on the same replicates. A rule that leans harder on the reference window is more sensitive to a mildly oversized suppressing coalition than the spec's rule is, which is why the two rows differ by an order of magnitude on identical inputs.
The [§6.8](#s6-8) rows at `α` = 0.3 and 0.4 are the ones genuinely contaminated, and they are stated as outstanding rather than quietly carried: that sweep reports a *directional* result (does a larger `U` clear a recovery bar), and a too-large adversary does not flip the direction, but its **levels and thresholds should not be quoted as measured** until it is re-run. That is [§8.3](#s8-3) item 20.
The [§6.8](#s6-8) rows at `α` = 0.3 and 0.4 were the genuinely contaminated ones, and that sweep has now been re-run: the levels moved in the third decimal, the honest baseline still reproduces [§3.4](#s3-4) exactly (`U` = 1 clears at `δ_max` = 8; `δ_max` = 16 needs `U` = 2 at `W` = 10 or `W` = 20 at `U` = 1), and no attacked cell reaches the 0.98 bar at any cap or either window — the conclusion the section draws. Nothing in the table is now carried on pre-fix numbers.
The capstone was re-run under the countable model (and both window anchors) after it was found still carrying pre-redesign numbers; its parquet now carries an `anchor` column. All other studies were **re-run on 2026-07-23/24 with the corrected slot-counting mechanism** ([§2.1](#s2-1)) and the early-stop optimisation; the resilient batch is `scripts/run_all_reruns.sh` (per-step log in `runs/rerun_status.log`). Canonical run directories (latest): fullscale N=5000/10000 = `2026-07-24_094519_fullscale`; fullscale N=1000/2000 = `2026-07-23_171803_fullscale-small`; uncle-window = `2026-07-24_001456`; window-uncles = `2026-07-24_014240`; block-rate = `2026-07-24_043943`; blend-hops-delay = `2026-07-24_064052`; window-scale = `2026-07-24_085234`; latency-shape = `2026-07-24_090014_expdist`; stake-tail = `2026-07-24_090044_pareto133`; heterogeneous-start = `2026-07-24_090114_default`; N-scaling = the `nscaling-{a,b}` + `nscaling32-{a,b}` runs; adversary grids = `runs/adversary_grid/`; jitter = `runs/jitter_grid/`; bootstrap = `runs/bootstrap_fullscale/`; fluctuation = `runs/fluctuation_u0.parquet`; fork-rate = `runs/fork_rate_vs_delay.parquet`; ρ-boundary = `2026-07-27_195627_rho-boundary`. Referencing-model studies: countable-vs-unrestricted (paired, primary) = `2026-08-05_120804_cvo-paired-countable` / `2026-08-05_123403_cvo-paired-old`; the superseded unpaired pair = `2026-08-04_103536_cvo-countable` / `2026-08-04_104010_cvo-old`; window absorption = `2026-08-04_104633_absorption-window`; fine delay band (unpaired) = `2026-08-04_191441_fine-countable` / `2026-08-04_195353_fine-old`; fine delay band (paired, primary) = `2026-08-05_111109_fine-paired-countable` / `2026-08-05_113218_fine-paired-old`. Adversarial-countability studies: uncle-selection deviation = `2026-08-05_152617_uncle-selection`; countable-selfish MDP = `runs/countable_selfish.parquet` + `runs/countable_selfish_reorg.parquet`; §6.5 scope variants and the withhold-load sweep = `runs/adversary_variants_{whale,jitter,beta,withhold_load}.parquet`.

View File

@ -47,7 +47,7 @@ RUNS.mkdir(exist_ok=True)
EPOCHS = 16
REPS = 8
N_JOBS = 6
N_JOBS = 12
BASE = dict(n_nodes=1000, stake_dist="pareto", topology="blend", degree=6,
link_latency_mean=0.5, link_latency_dist="geo", blend_hops=3,