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>
This commit is contained in:
Marcin Pawlowski 2026-08-10 12:42:54 +02:00
parent 097543a5f9
commit 3c52aeec0e
No known key found for this signature in database
3 changed files with 48 additions and 7 deletions

View File

@ -488,7 +488,7 @@ Read this before using the numbers: the regime they hold in, and the margins tha
- **[§6.6](#s6-6)** selfish / private-chain withholding — the one profitable lever (above the classic stake threshold, quantified with the optimal-strategy MDP); uncle-*counting* restores about half of the estimator damage, bounded by the countable rule's one-uncle-per-fork ceiling.
- **[§6.7](#s6-7)** block/uncle *rewards* — robustly compensate honest orphans and disincentivise hiding, but a *voluntary* reward can backfire on selfish mining.
- **[§6.8](#s6-8)** a *soft* (reward-weighted) inclusion rule — the fork-safe choice (a validity rule cannot prove which forks a producer saw). The emergent reference rate stays high (honest blocks reference the published orphans), so the backfire mostly vanishes; the residual is set by the uncle window `W` (how far back a block may reach to reference an orphan; recommended 300 slots = 10/f, [§3.4](#s3-4)) and visibility.
- **[§6.9](#s6-9)** multiple coalitions and bribery — the abstention commons is bounded and structure-independent; multi-coalition *selfish* mining is flagged, not solved.
- **[§6.9](#s6-9)** multiple coalitions and bribery — the abstention commons is bounded and structure-independent; multi-coalition *selfish* mining is now measured, and splits: splitting a coalition **reduces** estimator deflation (so one coalition is the worst case), but two sub-threshold rivals can **each** profit, so `⅓` is not a safety property at a forking load.
- **[§6.10](#s6-10)** fork rate and reorg depth — uncles and `ρ < 1` keep reorganisations shallow and keep even a 30 % coalition below the effective-majority cliff.
- **[§6.12](#s6-12)** the reference window bounds the uncle but not the historical state a validator must reach — a 30 % coalition drives it to the epoch boundary, and anchoring the window to the uncle's *parent* caps it, free at the recommended cap but raising the window floor by one to two block-intervals.
- **[§6.11](#s6-11)** organic stake churn against the ~7.5-day epoch cadence — tracked with a one-epoch lag; a bounded rate wobble, never a consensus or accuracy failure.
@ -711,13 +711,39 @@ The queue is draining. Countable orphans are referenced at essentially the hones
The consolidated reward-schedule recommendation is in [§8](#s8) (rows 1011 of the selection table).
<a id="s6-9"></a>
### 6.9 Multiple coalitions and bribery: the abstention commons is bounded; multi-coalition selfish mining stays open
### 6.9 Multiple coalitions and bribery: splitting a coalition helps the estimator and hurts the threshold
Everything above assumed a single coalition; this section asks what changes when several attack at once — or bribe others to.
**Withholding commons (solid).** `D̂` deflation depends only on the *total* participating stake ([§6.4](#s6-4)), not on how it is partitioned — this is **exact by construction** (the withhold mask is per-node membership with no sub-coalition label), so `K` abstaining coalitions summing to `β` deflate `D̂` exactly as one coalition of size `β`, and the [§6.2](#s6-2) fold is no more reachable by stacking than by a single coalition of the same `β`. Each is *individually* unprofitable ([§6.5](#s6-5)) and, given others already deflate, the best response is to *mine* at the depressed difficulty rather than join — so the abstention commons is not an equilibrium; pure griefers pay their aggregate forfeit for a bounded, linear deflation.
**Multi-coalition selfish mining (conjectural).** Here the abstention argument does **not** transfer: competing *selfish* miners orphan *each other*, and the literature shows the profitability threshold can fall **below** the single-miner `⅓` — several individually-sub-threshold coalitions may be *jointly* profitable, and total orphaning (hence raw `D̂` deflation) can **exceed** the single-coalition [§6.6](#s6-6) value, so `D̂ ≈ 0.70` at `α = 0.4` is *not* a multi-coalition upper bound. Our two-type model cannot represent a `K`-agent reference game, so this is flagged, not solved; TSI *safety* rests on the [§6.8](#s6-8) soft rule's honest-referencer `D̂`-recovery, which is itself `p_ref`-limited (`W`/visibility) and degrades — gracefully, never forking — as orphan volume rises. Sizing this properly is future work.
**Multi-coalition selfish mining — measured, and it splits into a good half and a bad half.** The abstention argument does not transfer here: competing *selfish* miners orphan *each other*, and the literature holds that the profitability threshold can fall **below** the single-miner `⅓`, with total orphaning — hence raw `D̂` deflation — *exceeding* the single-coalition value. Both halves are now simulated rather than cited. `adversary_coalitions = K` splits a fixed adversarial stake into `K` near-equal rivals, each running its own private chain with its own view: a rival's unreleased blocks are invisible to every other coalition by the same arrival sentinel that hides them from honest nodes, so the chains race each other as well as the public chain, which is the entire mechanism the conjecture rests on (`scripts/multi_coalition.py`, `fig39`, 10 replicates, the [§8](#s8) design point `δ_max` = 8).
**The deflation half is refuted: one coalition is the worst case.** Splitting the same stake *reduces* the damage to the estimator, monotonically and decisively — at every `β` and every `K` tested, `t` between 4.3 and 14.3:
| total `β` | `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 is visible in the fork structure: a lone coalition holds one deep private chain (measured max fork depth ~2 000 blocks at `β` = 0.4), while rivals cut each other short (depth ~2 at `K` = 2). Rival coalitions spend their advantage orphaning *each other's* blocks instead of honest ones, so less honest work is displaced and the counted density recovers. **`D̂ ≈ 0.70` at `α` = 0.4 is therefore an upper bound on multi-coalition deflation after all**, not the under-estimate [§6.9](#s6-9) previously warned it might be. (\* the `β` = 0.4, `K` = 1 cell is boundary-affected — 66 % of its adversarial blocks were still private when the epoch ended even with the lead cap of [§9](#s9) — so read it as directional. The `β` = 0.2 and 0.3 rows have <1 % stranding and carry the result on their own.)
**The threshold half is confirmed, and `⅓` does not hold at this load.** Per-coalition profitability — a coalition's share of the canonical chain against its own stake, `>1` meaning selfish mining pays:
| configuration | stake each | canonical / stake | `t` vs 1 | replicates > 1 |
|---|---|---|---|---|
| one coalition at `β` = 0.20 | 0.200 | 0.875 | 2.5 | 2/10 |
| one coalition at `β` = 0.295 | 0.295 | **1.232** | 2.7 | 9/10 |
| **two rivals at `β` = 0.40** | **0.200 each** | **1.045** | 2.5 | 9/10 |
| three rivals at `β` = 0.40 | 0.133 each | 0.852 | 5.1 | 0/10 |
| four rivals at `β` = 0.40 | 0.094 each | 0.691 | 4.6 | 0/10 |
![Fig 39 — rival selfish coalitions: splitting a fixed adversarial stake into K rivals reduces estimator deflation at every beta (left), while per-coalition profitability shows two 0.20 rivals each clearing the pay line that a lone 0.20 coalition does not (right).](report-figures/fig39_multi_coalition.png)
Two results, both against the `⅓` folklore. A *single* coalition already profits at `α` ≈ 0.295, below the threshold — because the honest network forks at ~0.48 here, so the public chain's *height* advances at only `(1α)·f·(1fork)` while a coalition sharing one view extends privately at the full `α·f`; the threshold falls with the fork rate, and at this load it is under 0.3. And more sharply: **a 0.20 coalition that does *not* pay on its own (0.875) *does* pay when a second 0.20 rival is present (1.045)**. The rival is not competition in the usual sense — it displaces honest blocks too, and both coalitions collect on the disruption. That is precisely the "individually sub-threshold, jointly profitable" case, confirmed at `K` = 2. It does not extend: at `K` ≥ 3 each rival is too small and every cell is unprofitable.
So the two halves of the conjecture point opposite ways, and the practical reading follows the split. **For TSI's estimator, coalition fragmentation is good news** — the single-coalition bound holds and [§6.6](#s6-6)'s figures are worst-case. **For the incentive argument it is bad news**: "no coalition holds `⅓`" is not a safety property at a load where the honest network forks appreciably, and two sub-threshold coalitions can each profit. Sizing that properly against `ρ` — the threshold is a function of the fork rate, which is a function of the load the recipe already bounds — is the remaining piece, and it is why the `ρ < 1` rule of [§8](#s8) is an incentive constraint and not only an accuracy one.
**Bribery on the reference game.** An attacker profiting from a deflated `D̂` can bribe canonical miners to *omit* references. Under the soft rule the miner forgoes only the nephew reward `w_n`, so a bribe `> w_n` (`≈ 0.03`) suffices — *cheap*, and here the hard-mandate "prohibitive (a full block)" defence is unavailable *by design* (we rejected validity-based inclusion for fork-safety, [§6.8](#s6-8)). But its *impact* is small: bribing away one block's references lowers `p_ref` only marginally, since the many *other* honest blocks within `W` still reference the orphan; to hold `p_ref` low the attacker must bribe (or control) a large fraction of canonical producers over the whole window. So soft-rule bribery is cheap per block but bounded in effect, and — like the withholding-commons bribe (pay coalitions to abstain, untouched by any inclusion rule) — reduces to the bounded, self-punishing griefing of [§6.5](#s6-5).
@ -937,7 +963,7 @@ Everything below is assembled from this report's own measurements; each row cite
### 8.3 Residual risks and open items
1. **The ρ ≈ 1 fold ([§6.2](#s6-2)).** The fitted static map is bistable at the recipe's own boundary, and the full dynamics reach it as a rare tail event (item 18); "U = ⌈ρ⌉ suffices" is an upper edge, not a safe interior — row 6 is a hard provisioning requirement, not a preference.
2. **Multi-coalition selfish mining is open ([§6.9](#s6-9)).** The literature threshold can fall below ⅓ and total orphaning can exceed the single-coalition value, so `D̂ ≈ 0.70` at α = 0.4 is not an upper bound. Flagged, not solved. The [§6.6](#s6-6) countable ceiling adds a second unmodelled dimension: competing coalitions orphan each other in *runs* as well as in volume, and the recoverable share depends on that shape, not just on the total (item 16).
2. **Multi-coalition selfish mining — half resolved ([§6.9](#s6-9)).** Both halves of the conjecture are now simulated with rival private chains. The *deflation* half is **refuted**: splitting a fixed stake into `K` rivals reduces `D̂` damage monotonically (`t` = 4.314.3 across `β` and `K`), because rivals bury each other's chains instead of honest work — so the single-coalition figures of [§6.6](#s6-6) are an upper bound after all. The *threshold* half is **confirmed**: a lone coalition already profits at `α` ≈ 0.295, and a 0.20 coalition that does not pay alone (0.875×) does pay against a second 0.20 rival (1.045×). The threshold falls with the honest fork rate, so `ρ < 1` is an incentive constraint and not only an accuracy one. What remains is mapping the threshold as a function of `ρ` rather than at the single design point tested, and `K` ≥ 3 equilibria (every such cell was unprofitable here, so no coalition has an incentive to split that far, but that is an observation and not a proof).
3. **Near-threshold reward backfire ([§6.7](#s6-7)[§6.8](#s6-8)).** At α ≈ 0.35 a small backfire (≤ +0.03 above block-only for `p_ref ≳ 0.3`, rising to +0.05 at the unreachable `p_ref = 0`) persists until `p_ref ≳ 0.75`; even `p_ref = 1` leaves a premium growing with α.
4. **No re-convergence ([Appendix C](#sC)).** Any mechanism that could inject persistent per-node `D̂` disagreement (genesis/clock exploit) would never be self-corrected — safety rests entirely on row 7.
5. **Adversarial `U` margin — resolved: none is needed, and none would help ([§6.8](#s6-8)).** The concern was that under attack-inflated orphaning the honest-load cap would leave owed uncles deferring past `W`. Measured with a private-chain coalition in the engine, the queue is not the bottleneck: countable orphans are referenced at the honest rate (8493 %) whatever the cap, and raising `U` past the recommended 2 moves `D̂` not at all. The loss is structural — an override buries 2040 % of the honest blocks it destroys behind the first block of the discarded run, where the counting rule cannot reach them. `U = ⌈ρ⌉ + 1` therefore stands unchanged, and what is *not* solved is the deflation itself, tracked as items 2 and 16 rather than as a sizing question.
@ -1052,6 +1078,7 @@ Sweep studies are committed configs, run with `make <name>` (writes a dated `run
| **the deployed operating point** (`δ_max` = 4 from the spec's Blend profile; the three `f`-precision arms) | `scripts/spec_point.py` (`f_precision`); `runs/spec_point.parquet` | [§2.2](#s2-2), [§8.5](#s8-5), [Appendix A](#sA) |
| **per-recipient delay variance** (jitter 08 slots × `U`, both referencing models, exact oracle) | `scripts/spec_jitter.py`, `configs/spec-point-jitter.yaml`; `runs/spec_jitter.parquet` | [§6.1](#s6-1), [§8.3](#s8-3) item 15 |
| **the spec point at scale and by window** (`N` = 5000; `W_abs` sweep at `δ_max` = 4) | `configs/spec-point-n5000.yaml` (run twice, `--old`), `configs/spec-point-window.yaml` | [§3.4](#s3-4), [§8.5](#s8-5) |
| **rival selfish coalitions** (`K` private chains at a fixed total stake; deflation and per-coalition profitability) | `scripts/multi_coalition.py` (`adversary_coalitions`); `runs/multi_coalition{,_split}.parquet`; fig39 | [§6.9](#s6-9), [§8.3](#s8-3) item 2 |
| **which metrics survive the model change** (the per-metric carry-over verdict, off the paired band) | `scripts/model_carryover.py`, from the paired `fine-paired-*` runs | [§9](#s9) below, [§6.8](#s6-8), [§8.4](#s8-4) |
**Uncle-model convention.** The simulator's default is the **countable** model — first-fork candidates only, derived window `w_u = W/f`, occupied-slot exclusion, per-reference counting rules ([§2.1](#s2-1)). The **unrestricted** baseline is preserved in the code and selected with `--old` on `tsi-sweep`/`tsi-verify`. Its RNG key is byte-identical to the pre-restriction key, so `--old` **bit-reproduces the earlier runs**: a `rho-boundary` cell (δ_max = 8, U = 2, k = 256, N = 1 000) re-run under `--old` matches the committed `2026-07-27_195627_rho-boundary` parquet with `max |Δ| = 0` on every epoch and every metric. Studies in the table above that predate the countable default were produced under the unrestricted model and reproduce exactly under `--old`.
@ -1095,7 +1122,7 @@ The [§6.8](#s6-8) rows at `α` = 0.3 and 0.4 are the ones genuinely contaminate
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`.
Figures are embedded from [`report-figures/`](report-figures) and versioned here alongside the report; the simulator folder does **not** commit its own generated figures, so the copies checked in here are the figures of record. Figures are in `report-figures/` (`fig1``fig29`, plus [Appendix B](#sB)'s `figB1``figB2`; numbering is generation order, not order of appearance). Committed generators: `fig1` (bootstrap, k=2160) by `scripts/bootstrap_dynamics.py`; `fig2`,`fig4`,`fig5`,`fig17``fig22` by `scripts/regenerate_extra_figs.py` from the latest sweeps (`fig3` hops×delay×U grid by `scripts/hops_delay_grid.py`, `fig6` (block-rate `U_min` grid + ρ-collapse) rendered ad hoc from `runs/2026-07-24_043943_block-rate` with no committed generator; `fig26` deficit-vs-ρ by `scripts/rho_boundary_analysis.py`) (fullscale-derived `fig17``fig20` pool both sizes in that run, N = 5 000 and N = 10 000 — the generators filter on stake_dist/topology/degree/init_dest only, never on `n_nodes`); `fig8`,`fig9` by `scripts/adversary_figs.py` from `runs/adversary_grid/`; `fig10``fig12` by `scripts/dynamic_withhold.py`; `fig13``fig15` by `scripts/selfish_mining.py`/`selfish_rewards.py`/`reward_mandate.py`; `fig16` by `scripts/stake_vs_delay.py`; `fig23``fig24` by `scripts/nscaling_analysis.py`; `fig25` by `scripts/window_scale_analysis.py`; `fig27``fig28` by `scripts/reorg_depth.py` (fork rates via `--measure`; private-chain model `src/tsi_sim/reorg.py`); `fig29` by `scripts/churn.py`; `figB1``figB2` by `scripts/appendix_fluct.py`. `fig7` (feedback fixed-point) is an analytic overlay; `fig30``fig33` (countable-vs-unrestricted accuracy, `q_u`-prediction check, recovery rate, absorption-window sweep) by `scripts/plot_countable_vs_old.py` from the `cvo-countable`/`cvo-old`/`absorption-window` runs; `fig34``fig35` (design-regime accuracy and the model gap with 95 % CIs) by `scripts/plot_fine_delay.py` from the `fine-countable`/`fine-old` runs; `fig36` (the countable recovery ceiling under a selfish adversary) by `scripts/countable_selfish.py`, which solves the MDP directly and writes `runs/countable_selfish.parquet` + `runs/countable_selfish_reorg.parquet` (no sweep input); `fig37` (the profit/deflation frontier) by `scripts/deflation_frontier.py` from `runs/deflation_frontier.parquet`, likewise solver-only; `fig38` (the reference-window anchor) by `scripts/uncle_parent_window.py`. Every figure type the per-node simulator generates appears in this report, and the fork-rate/reorg-depth study closes the previous reproducibility gap for the adversarial figures (`fig8`,`fig9` now have committed generators from `runs/adversary_grid/`).
Figures are embedded from [`report-figures/`](report-figures) and versioned here alongside the report; the simulator folder does **not** commit its own generated figures, so the copies checked in here are the figures of record. Figures are in `report-figures/` (`fig1``fig39`, plus [Appendix B](#sB)'s `figB1``figB2`; numbering is generation order, not order of appearance). Committed generators: `fig1` (bootstrap, k=2160) by `scripts/bootstrap_dynamics.py`; `fig2`,`fig4`,`fig5`,`fig17``fig22` by `scripts/regenerate_extra_figs.py` from the latest sweeps (`fig3` hops×delay×U grid by `scripts/hops_delay_grid.py`, `fig6` (block-rate `U_min` grid + ρ-collapse) rendered ad hoc from `runs/2026-07-24_043943_block-rate` with no committed generator; `fig26` deficit-vs-ρ by `scripts/rho_boundary_analysis.py`) (fullscale-derived `fig17``fig20` pool both sizes in that run, N = 5 000 and N = 10 000 — the generators filter on stake_dist/topology/degree/init_dest only, never on `n_nodes`); `fig8`,`fig9` by `scripts/adversary_figs.py` from `runs/adversary_grid/`; `fig10``fig12` by `scripts/dynamic_withhold.py`; `fig13``fig15` by `scripts/selfish_mining.py`/`selfish_rewards.py`/`reward_mandate.py`; `fig16` by `scripts/stake_vs_delay.py`; `fig23``fig24` by `scripts/nscaling_analysis.py`; `fig25` by `scripts/window_scale_analysis.py`; `fig27``fig28` by `scripts/reorg_depth.py` (fork rates via `--measure`; private-chain model `src/tsi_sim/reorg.py`); `fig29` by `scripts/churn.py`; `figB1``figB2` by `scripts/appendix_fluct.py`. `fig7` (feedback fixed-point) is an analytic overlay; `fig30``fig33` (countable-vs-unrestricted accuracy, `q_u`-prediction check, recovery rate, absorption-window sweep) by `scripts/plot_countable_vs_old.py` from the `cvo-countable`/`cvo-old`/`absorption-window` runs; `fig34``fig35` (design-regime accuracy and the model gap with 95 % CIs) by `scripts/plot_fine_delay.py` from the `fine-countable`/`fine-old` runs; `fig36` (the countable recovery ceiling under a selfish adversary) by `scripts/countable_selfish.py`, which solves the MDP directly and writes `runs/countable_selfish.parquet` + `runs/countable_selfish_reorg.parquet` (no sweep input); `fig37` (the profit/deflation frontier) by `scripts/deflation_frontier.py` from `runs/deflation_frontier.parquet`, likewise solver-only; `fig38` (the reference-window anchor) by `scripts/uncle_parent_window.py`; `fig39` (rival selfish coalitions) by `scripts/multi_coalition.py` from `runs/multi_coalition{,_split}.parquet`. Every figure type the per-node simulator generates appears in this report, and the fork-rate/reorg-depth study closes the previous reproducibility gap for the adversarial figures (`fig8`,`fig9` now have committed generators from `runs/adversary_grid/`).
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View File

@ -191,11 +191,25 @@ def fig39(acc: pd.DataFrame, spl: pd.DataFrame) -> None:
ax.legend(fontsize=7)
ax = axes[1]
flagged = False
for i, b in enumerate(BETAS):
g = spl[spl.beta == b].groupby("K")[["stake_share", "canonical_share"]].mean()
ax.plot(g.index, g.canonical_share / g.stake_share, marker="o", ms=4,
color=style.OKABE_ITO[i + 1], label=rf"$\beta$ = {b:.1f}")
strand = spl[spl.beta == b].groupby("K").stranded.mean()
ratio = g.canonical_share / g.stake_share
ax.plot(g.index, ratio, marker="o", ms=4, color=style.OKABE_ITO[i + 1],
label=rf"$\beta$ = {b:.1f}")
# A cell whose private chains were still hidden at the epoch boundary is measuring the
# boundary, not the attack. Draw it hollow so the curve cannot be read as if every point
# carried the same weight — the §6.9 text says the same thing in words.
bad = strand > 0.2
if bad.any():
flagged = True
ax.plot(ratio.index[bad], ratio[bad], "o", ms=9, mfc="none", mew=1.4,
color=style.OKABE_ITO[i + 1], zorder=5)
ax.axhline(1.0, color="0.5", lw=0.9, ls="--")
if flagged:
ax.annotate("hollow: private chains\nstranded at epoch end\n(read as directional)",
xy=(0.30, 0.04), xycoords="axes fraction", fontsize=6, color="0.35")
ax.set_xticks(KS)
ax.set_xlabel("number of rival coalitions $K$")
ax.set_ylabel("canonical share / own stake\n(per coalition; > 1 = selfish mining pays)")