mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 11:43:20 +00:00
Review pass: reproduce every number from its data of record, fix what did not
Correctness/completeness review of the report and simulator. Verified against the committed parquets: the sec 6.6 countable-ceiling table (cap-64 MDP sweep), sec 6.10 Result 4's depth ceilings, the sec 3.4 uncle-selection table, all adversary-variant numbers, the rho-boundary row-4 quotes (0.976 at rho=0.91, 4-sigma shortfall at 0.96, max cell 1.0024), and the sec 8.4 capstone table. Three defects found, all fixed: 1. The collapse event was not reproducible from the committed script. Study D swept only the default (random) coalition, but the one observed collapse is a whale cell; the "once in 144 runs" count came from an ad-hoc probe. The committed sweep now carries the selection axis (96 runs) and reproduces the event: 1/12 in the whale 50% cell at delta_max = 8, never at 4. All six fold-related passages now quote the committed sweep, which also retires the stale "the full dynamics never reach it" wording in the sec 6 arc, the sec 6.2 intro, row 6 and item 1 -- text that contradicted item 18 since yesterday's finding. 2. capstone.py's printout could not reproduce the report's sec 8.4 table. The report's numbers are a per-replicate-tail aggregation (each replicate burns in against its own early-stop length); the script cut the tail at the ARM's max epoch, silently dropping any replicate that stopped earlier (7 of 8 in the adversary arm) and landing one rounding step off on three cells. The script now aggregates per replicate and prints the SEM; against the existing parquet it reproduces the table exactly (1.001/0.998, 0.342+-0.009 / 0.343+-0.005, p_ref 1.000/0.990, 8 reps both arms). The report table was right all along; sec 6.8's p_ref quote (0.989, the per-arm value) is aligned to 0.990. 3. Small report fixes: slow-beta deflation rounded 0.765 -> "0.77" (now 0.76); fig13's caption now points at the fig36 ceiling instead of implying free recovery; row 5 cites the measured slow-beta standing deflation; the canonical-data paragraph lists the new studies' artifacts; the simulator README's layout block lists the new tests and scripts. Adds a unit test for reorg.countable_recovery_from_depths (the one new function that had none). 236 tests pass; the new-study parquets are copied to the main checkout's runs/, where every other study's data of record lives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bed374cdd4
commit
ef82ed614d
@ -481,7 +481,7 @@ Read this before using the numbers: the regime they hold in, and the margins tha
|
||||
[§1](#s1)–[§5](#s5) characterise an honest, jitter-free regime, in which the two headline claims (one global estimate `D̂`; uncle cap `U` = the load `⌈ρ⌉` suffices, where the **load** `ρ = f·D_vis` is the number of blocks the network produces per block-visibility delay `D_vis` — [§3.3](#s3-3)) cannot fail by construction. Eleven studies test those limits, forming one arc from network noise to a full adversary and its incentives, then out to the chain structure the estimate rides on and the wall-clock cadence it runs at:
|
||||
|
||||
- **[§6.1](#s6-1)** consensus survives jitter (positive result).
|
||||
- **[§6.2](#s6-2)** the estimator is a load-feedback loop, bistable near `ρ≈1` in a fitted static map — a caveat, not reached in the full dynamics.
|
||||
- **[§6.2](#s6-2)** the estimator is a load-feedback loop, bistable near `ρ≈1` in a fitted static map — reached by the full dynamics only as a rare tail event under heavy withholding.
|
||||
- **[§6.3](#s6-3)** uncle-suppression grinding — a bounded attack.
|
||||
- **[§6.4](#s6-4)** static block withholding — deflates `D̂` but faithfully tracks *active* stake.
|
||||
- **[§6.5](#s6-5)** dynamic withhold-rejoin grinding — unprofitable, non-persistent.
|
||||
@ -513,9 +513,9 @@ Jitter feeds exactly the tip-level churn that [§3.1](#s3-1) showed never reache
|
||||
<a id="s6-2"></a>
|
||||
### 6.2 The estimator is a load-feedback loop, bistable near ρ≈1 in theory (`fig7`)
|
||||
|
||||
The worry: the estimator feeds back on itself — a low estimate makes the lottery easier, more blocks collide, fewer get counted, and the estimate drops further. Could that spiral? The answer: only in a fitted static model, right at the load boundary `ρ ≈ 1`; the simulated network never reaches the spiral, but the boundary is real and is why we provision `ρ < 1` with margin.
|
||||
The worry: the estimator feeds back on itself — a low estimate makes the lottery easier, more blocks collide, fewer get counted, and the estimate drops further. Could that spiral? The answer: the fold sits right at the load boundary `ρ ≈ 1` in a fitted static model, and the simulated network reaches it only as a rare tail event — once in 96 runs, under a concentrated 50 % withholding coalition at a heavy blending budget (the caveat at the end of this section). The boundary is real, and is why we provision `ρ < 1` with margin.
|
||||
|
||||
Because the controller drives the *counted* density to the target block rate `f` (blocks per slot; `f = 1/30`, one block every 30 slots = 30 s — defined in the [§3.3](#s3-3) symbol table, and swept in [§3.6](#s3-6)), the realised proposal rate is `f/r` (`r` = the recovered accuracy `D̂/D`; under-recovery means `r < 1`), so the realised load is `ρ_eff = ρ/r > ρ` whenever the estimate is under-recovered — this single effect explains both the U=0 finite-size drift and the near-integer `⌈ρ⌉` under-shoots. Fitting the effective counted density `q_eff` ([§4](#s4), eq. 4) as `q_eff ≈ 1/(1 + 0.71·ρ_eff)` and solving the self-consistent fixed point `r = ln(1−f)/ln(1−f/q_eff(ρ/r))` gives a single reachable stable branch that **folds into a collapsed low branch at `ρ ≈ 1.08`** — the map is bistable at the recipe's own operating boundary. (`fig7`'s shaded band opens earlier, at `ρ ≈ 0.66`: that is where the unstable middle root rises above the low-`r` seed of the branch scan, so from `ρ ≈ 0.66` up a sufficiently deflated start already falls into the collapse basin, even though the upper branch itself survives to the fold.) So the honest-regime "`U = ⌈ρ⌉` suffices" line, computed on the high branch, is an upper edge, not a safe interior; the `+1` margin partly absorbs the `ρ_eff > ρ` gap. *Caveat:* this bistability is a property of the *fitted static* `q_eff(ρ)` map, and the full per-node dynamics essentially never reach the fold — no grinding schedule flips to the collapsed branch ([§6.3](#s6-3)(i)) and a withhold pulse always recovers ([§6.5](#s6-5)(iv)). "Essentially" is doing real work, though, and the mechanism that closes the gap is this section's own: because the realised load is `ρ_eff = ρ/r`, an estimate deflated to `r` multiplies the load by `1/r`, and *withholding deflates `r` by design* — a 50 % coalition doubles the load, which at the design point `ρ ≈ 0.56` lands on `ρ_eff ≈ 1.1`, past the fold. Swept directly (`scripts/adversary_variants.py`, static withholding, `β_adv ∈ {0.3, 0.5}` × `δ_max ∈ {4, 8}` s × 12 replicates), the estimate stays on the upper branch in every cell at `δ_max = 4`, and single epochs dive far below it and recover (worst epoch 0.015 at `β_adv` = 0.3, tail mean 0.61). At `δ_max = 8` it collapses **once in 144 runs** — a concentrated 50 % withholding coalition, which produces blocks at the collapsed rate and never recovers within the run. So the fold is *primarily* a provisioning warning, but it is not unreachable: it is a rare tail event that a large withholding coalition at a heavy blending budget can enter, and one more reason the [§8](#s8) `ρ < 1` rule wants margin rather than equality. Characterising that tail — its frequency against `β_adv`, `ρ` and coalition concentration — needs a dedicated study and is not attempted here.
|
||||
Because the controller drives the *counted* density to the target block rate `f` (blocks per slot; `f = 1/30`, one block every 30 slots = 30 s — defined in the [§3.3](#s3-3) symbol table, and swept in [§3.6](#s3-6)), the realised proposal rate is `f/r` (`r` = the recovered accuracy `D̂/D`; under-recovery means `r < 1`), so the realised load is `ρ_eff = ρ/r > ρ` whenever the estimate is under-recovered — this single effect explains both the U=0 finite-size drift and the near-integer `⌈ρ⌉` under-shoots. Fitting the effective counted density `q_eff` ([§4](#s4), eq. 4) as `q_eff ≈ 1/(1 + 0.71·ρ_eff)` and solving the self-consistent fixed point `r = ln(1−f)/ln(1−f/q_eff(ρ/r))` gives a single reachable stable branch that **folds into a collapsed low branch at `ρ ≈ 1.08`** — the map is bistable at the recipe's own operating boundary. (`fig7`'s shaded band opens earlier, at `ρ ≈ 0.66`: that is where the unstable middle root rises above the low-`r` seed of the branch scan, so from `ρ ≈ 0.66` up a sufficiently deflated start already falls into the collapse basin, even though the upper branch itself survives to the fold.) So the honest-regime "`U = ⌈ρ⌉` suffices" line, computed on the high branch, is an upper edge, not a safe interior; the `+1` margin partly absorbs the `ρ_eff > ρ` gap. *Caveat:* this bistability is a property of the *fitted static* `q_eff(ρ)` map, and the full per-node dynamics essentially never reach the fold — no grinding schedule flips to the collapsed branch ([§6.3](#s6-3)(i)) and a withhold pulse always recovers ([§6.5](#s6-5)(iv)). "Essentially" is doing real work, though, and the mechanism that closes the gap is this section's own: because the realised load is `ρ_eff = ρ/r`, an estimate deflated to `r` multiplies the load by `1/r`, and *withholding deflates `r` by design* — a 50 % coalition doubles the load, which at the design point `ρ ≈ 0.56` lands on `ρ_eff ≈ 1.1`, past the fold. Swept directly (`scripts/adversary_variants.py`, static withholding, `β_adv ∈ {0.3, 0.5}` × `δ_max ∈ {4, 8}` s × coalition selection {random, whale} × 12 replicates — 96 runs), the estimate stays on the upper branch in every cell at `δ_max = 4`, and single epochs dive far below it and recover (worst epoch 0.015 at `β_adv` = 0.3, tail mean 0.61). At `δ_max = 8` it collapses **once in 96 runs** — one replicate of the concentrated (whale) 50 % coalition, which produces blocks at the collapsed rate and never recovers within the run. So the fold is *primarily* a provisioning warning, but it is not unreachable: it is a rare tail event that a large withholding coalition at a heavy blending budget can enter, and one more reason the [§8](#s8) `ρ < 1` rule wants margin rather than equality. Characterising that tail — its frequency against `β_adv`, `ρ` and coalition concentration — needs a dedicated study and is not attempted here.
|
||||
|
||||

|
||||
|
||||
@ -607,7 +607,7 @@ The curve is **within `0.09` for `β = 0.5` and `β = 1.0`** at every duty — p
|
||||
|
||||
**The TSI coupling (`fig13`, right).** Selfish mining orphans honest blocks (23.7 % of all blocks at `α = 0.4`, `γ = 0`), so the *counted* canonical density falls and TSI **deflates `D̂` to `D*·(density fraction)`** — `0.70` at `α = 0.4` with no uncles: a 30 % under-report of active stake, a safety harm layered on the reward theft. But that under-count is precisely what **uncle references fix** ([§3.2](#s3-2)): counting orphaned honest blocks back into the density restores `D̂` toward `D*` — to `0.82` at `η = 0.5` and `0.94` at `η = 1` (uncle-recovery fraction `η ∈ [0,1]`, distinct from the integer uncle cap `U`). So the mechanism provisioned for honest latency recovery **doubles as the mitigation for selfish mining's estimator deflation** — this restores the *estimate* `D̂` (a safety win), independent of the reward schedule. Whether *rewarding* those uncles also cuts the selfish *profit* premium is a separate, conditional question — it can *backfire* without care and needs a soft inclusion rule — taken up in [§6.7](#s6-7)–[§6.8](#s6-8).
|
||||
|
||||

|
||||

|
||||
|
||||
**But `η` is not free — the first-fork restriction caps it, and SM1 hides that (`fig36`).** `fig13`'s recovery curve reads `η` as a knob over `[0,1]`, and against **SM1** the full range is attainable: SM1 acts the moment the honest branch reaches length 1 (match at a 1-lead, override at a 2-lead, publish-one above it), so it never buries a second block behind the first, every orphan it makes is the first block of its fork, and the countable model ([§2.1](#s2-1)) can reference all of them — measured `η = 1.000000` exactly, at every `α` and `γ` tested. The **optimal** policy — the one invoked above as the true profit frontier — does not oblige. It *waits*, then overrides a run of `h` honest blocks at once; that run is one chain, so only its first block has a parent on the referencing chain and the deployed rules recover **one** uncle from it, not `h`. Solving the SSZ MDP for its stationary orphan structure (`scripts/countable_selfish.py`, cap = 64) gives the ceiling directly (`γ = 0`):
|
||||
|
||||
@ -655,7 +655,7 @@ The backfire is *unconditional* below the `⅓` threshold and holds at higher `
|
||||
|
||||
[§6.7](#s6-7)(3) shows a purely *voluntary* uncle reward can backfire on selfish mining. The tempting fix — a *mandatory* inclusion rule that makes a block **invalid** if it omits an owed uncle — cannot be encoded safely: "owed" means "the producer could have seen it," which depends on per-node gossip timing that no node can prove. An **objective** encoding (slot-gap `s_block − s_uncle ≥ g`) lets an adversary *withhold-then-late-release* an uncle to retroactively **invalidate honest blocks** (censorship); a **visibility-based** encoding is *subjective*, so the same block is valid to some nodes and invalid to others — a **consensus fork** with no adversary at all. **We therefore make inclusion a soft rule:** *a block is always valid; a producer that includes an available in-window uncle earns the nephew reward `w_n`, and the orphan producer earns `w_u` — omission simply forfeits those rewards.* No validity edge, so no fork and no censorship weapon.
|
||||
|
||||
**The emergent `p_ref` is high — now measured, not assumed.** The per-node engine records `p_ref` directly (the fraction of in-window orphans referenced as uncles by some canonical block): at the recommended `W = 300` it is **≈ 1.0 honestly** and **0.989 under a 30 %-stake coalition that produces its share of the (straddling) canonical blocks and suppresses references on them** ([§8](#s8) capstone). That suppression case *is* the reference-suppression channel a selfish attacker would use — an honest orphan loses its reference only if *every* one of its in-window straddlers is a suppressing coalition block — so even with 30 % of the straddlers hostile, barely 1 % of honest orphans go unreferenced, comfortably above the `p_ref ≳ 0.3` the soft rule needs for `α ≥ 0.4` (`fig15`). The one selfish channel this measurement does *not* exercise is the attacker *orphaning extra* honest blocks via a private-chain override (the per-node engine has no private-chain strategy): there we rely on the structural argument below — published honest orphans are straddled by *honest* blocks within `W`, which the attacker does not control — and read the payoff off the `fig15` `p_ref` sweep rather than a single measured point. The earlier analytic sweeps used `p_ref ≈ 0.85` as a conservative stand-in; the suppression measurement confirms it is, if anything, pessimistic.
|
||||
**The emergent `p_ref` is high — now measured, not assumed.** The per-node engine records `p_ref` directly (the fraction of in-window orphans referenced as uncles by some canonical block): at the recommended `W = 300` it is **≈ 1.0 honestly** and **0.990 under a 30 %-stake coalition that produces its share of the (straddling) canonical blocks and suppresses references on them** ([§8](#s8) capstone). That suppression case *is* the reference-suppression channel a selfish attacker would use — an honest orphan loses its reference only if *every* one of its in-window straddlers is a suppressing coalition block — so even with 30 % of the straddlers hostile, barely 1 % of honest orphans go unreferenced, comfortably above the `p_ref ≳ 0.3` the soft rule needs for `α ≥ 0.4` (`fig15`). The one selfish channel this measurement does *not* exercise is the attacker *orphaning extra* honest blocks via a private-chain override (the per-node engine has no private-chain strategy): there we rely on the structural argument below — published honest orphans are straddled by *honest* blocks within `W`, which the attacker does not control — and read the payoff off the `fig15` `p_ref` sweep rather than a single measured point. The earlier analytic sweeps used `p_ref ≈ 0.85` as a conservative stand-in; the suppression measurement confirms it is, if anything, pessimistic.
|
||||
|
||||
**Why a soft rule still works — the reference rate is emergent and high (`fig15`).** Under a soft rule `p_ref` is not the attacker's to set to 0: an honest orphan was **published** (honest miners publish immediately), so *any honest canonical block that sees it within `W`* references it for `w_n`. The attacker only withholds references on **its own** canonical blocks; the honest blocks that follow do the referencing. So `p_ref` sits **high** in practice, and `fig15` reads off the payoff along it — at a realistic honest-referencer rate `p_ref ≈ 0.85`, the selfish attacker's reward share is already **below block-only and near stake** (`0.420` at `α = 0.4`, vs `0.520` at the suppressed `p_ref = 0` and `0.484` block-only) with honest recovery `0.83–0.93`. For `α ≥ 0.4` the crossover below block-only is at `p_ref ≈ 0.3` (the grey band); for a near-threshold `α ≈ 0.35` a small residual backfire (`≤ +0.03` for `p_ref ≳ 0.3`, rising to `+0.05` at the unreachable `p_ref = 0`; share still near stake) persists until `p_ref ≈ 0.75`. So the soft rule buys **most** of the (unattainable) hard-mandate benefit, plus the full fairness and hiding-disincentive of [§6.7](#s6-7)(1–2), with **none** of the fork/censorship hazard.
|
||||
|
||||
@ -797,8 +797,8 @@ Everything below is assembled from this report's own measurements; each row cite
|
||||
| 2 | block rate `f` | **1/30** (given) — treat as a coupled input | `f` moves every threshold: `U_min ∝ f`, `W_min ∝ 1/f` (recovery stays at 1.0 at all rates); re-derive `U`, `W`, `ρ` if `f` ever changes | [§3.6](#s3-6), fig6 |
|
||||
| 3 | uncle window `W` | **300 slots = 10/f**; widen to **450–600 (15–20/f)** when operating near the load boundary `ρ ≈ 1` | hard floor ≈ 7/f set by block spacing (N-invariant, fig25); below it no uncle count recovers (delay 16, W = 100: even U = 4 only 0.94); `W` is the buffer for block-production *fluctuations* — at `ρ ≈ 1`, W = 600 recovers what W = 300 loses (fig25), a cheap alternative to the `+1` uncle; the 10/f margin also covers the uncle-selection sensitivity (oldest-first vs a random draw; [§3.4](#s3-4)) and keeps the emergent reference rate `p_ref` ([§6.7](#s6-7)) high for the reward design | [§3.4](#s3-4)/3.5, figs 4, 5, 22, 25; [§6.8](#s6-8) |
|
||||
| 4 | uncle cap `U` | **`⌈ρ⌉ + 1`** (Blend, the deployment target); **1** suffices for plain direct gossip (ρ ≈ 0.04) | `U < ρ` never recovers; `⌈ρ⌉` is tight but under-shoots just below integer ρ — at ρ = 0.96, U = 1 fails at 0.962 ± 0.006 (3σ below the 0.98 bar): the direct evidence for the +1 margin, corroborated by the dedicated 40-cell ρ-boundary sweep (U = 1 only ≈ 0.966 at ρ = 0.96, a 4σ shortfall, and already ≈ 0.976 at ρ = 0.91 — `⌈ρ⌉` under-shoots from ρ ≈ 0.9, fig26); one uncle lifts every tested delay to full recovery (≈ 1.0) at N = 10 000 | [§3.3](#s3-3)/3.5/3.6, figs 6, 16, 20, 26 |
|
||||
| 5 | TSI learning rate `β` | **1** (`BETA_DEFAULT`) | one-epoch tracking of active stake: any withhold notch is erased in one epoch; attacker profitability is β-independent, so slowing β buys the defender nothing. *Disclosure:* the one regime where distortion per unit of network reward forfeited exceeds 1 (bounded ≤ 2×) is β ≥ 1 (rejoin overshoot, across the tested coalitions) — a protocol-chosen gain, not an adversary lever; and because `β = 1` tracks with a one-epoch (≈ 7.5-day) lag, `D̂` reflects participating stake from ~1–2 weeks ago and cannot resolve *sub-epoch* participation cycles (the estimator's Nyquist limit) — the correct trade for organic churn, detailed in [§6.11](#s6-11) | [§6.5](#s6-5), [§6.11](#s6-11), figs 10–12 |
|
||||
| 6 | operating point | **`ρ = f·D_vis < 1` with margin** | the fitted static feedback map folds near ρ ≈ 1 (provisioning warning — the full dynamics never reach it); grinding is weak at ρ < 1 (1.04×) but cheap at ρ > 1 (1.43×); chronic ρ > 1 under-count persists after any attack ends. ρ < 1 is simultaneously the honest-accuracy and the adversarial safeguard | [§6.2](#s6-2) fig7; [§6.3](#s6-3) fig8; [§6.5](#s6-5)(iv) fig12 |
|
||||
| 5 | TSI learning rate `β` | **1** (`BETA_DEFAULT`) | one-epoch tracking of active stake: any withhold notch is erased in one epoch; attacker profitability is β-independent, so slowing β buys the defender nothing — measured down to β = 0.05, where the smaller notch is paid for with a *deeper standing deflation* (mean `D̂` 0.85 → 0.76) at unchanged attacker take ([§6.5](#s6-5) scope note). *Disclosure:* the one regime where distortion per unit of network reward forfeited exceeds 1 (bounded ≤ 2×) is β ≥ 1 (rejoin overshoot, across the tested coalitions) — a protocol-chosen gain, not an adversary lever; and because `β = 1` tracks with a one-epoch (≈ 7.5-day) lag, `D̂` reflects participating stake from ~1–2 weeks ago and cannot resolve *sub-epoch* participation cycles (the estimator's Nyquist limit) — the correct trade for organic churn, detailed in [§6.11](#s6-11) | [§6.5](#s6-5), [§6.11](#s6-11), figs 10–12 |
|
||||
| 6 | operating point | **`ρ = f·D_vis < 1` with margin** | the fitted static feedback map folds near ρ ≈ 1 — primarily a provisioning warning, but the full dynamics *can* reach it as a rare tail event under heavy withholding (once in 96 runs, [§6.2](#s6-2)); grinding is weak at ρ < 1 (1.04×) but cheap at ρ > 1 (1.43×); chronic ρ > 1 under-count persists after any attack ends. ρ < 1 is simultaneously the honest-accuracy and the adversarial safeguard | [§6.2](#s6-2) fig7; [§6.3](#s6-3) fig8; [§6.5](#s6-5)(iv) fig12 |
|
||||
| 7 | genesis / initialization | **genesis `D̂` is a single protocol constant, identical at every node** — never client-configurable | consensus is an *initialization* property, not a restoring force: an injected per-node spread is conserved forever (max/min ≈ 3.0, flat), while a *common* wrong guess self-corrects in ~2 epochs | [Appendix C](#sC) (fig21); [§3.2](#s3-2) fig1 |
|
||||
| 8 | uncle validity precondition | **a counted uncle must be a real lottery winner for its slot — already enforced on-chain by the spec's ZK Proof-of-Leadership** | fabrication is cryptographically impossible (the ZK Proof-of-Leadership cannot be forged); this precondition underlies every [§6.3](#s6-3)–[§6.5](#s6-5) bound and caps uncle issuance at `≤ U·(w_u + w_n)` per block | [§6.3](#s6-3), [§6.7](#s6-7)(a) |
|
||||
| 9 | uncles in the density | **recovered uncles enter the TSI counted density** (independent of rewarding them) | restores `D̂` under honest latency (0.64–0.74 → 1.0) *and*, partially, under selfish orphaning — a safety win with no reward-schedule dependence. Size the *partial*: honest recovery is full because latency orphans are single blocks, but an optimally-played selfish attack discards multi-block forks of which the countable rule reaches only the first, so recovery there is `0.70 → 0.81` at α = 0.4 (not the `0.94` an unrestricted count would give) and falls further as α grows | [§3.2](#s3-2) fig2; [§6.6](#s6-6) fig13, fig36 |
|
||||
@ -823,7 +823,7 @@ Everything below is assembled from this report's own measurements; each row cite
|
||||
<a id="s8-3"></a>
|
||||
### 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; the full dynamics never reach it, but "U = ⌈ρ⌉ suffices" is an upper edge, not a safe interior — row 6 is a hard provisioning requirement, not a preference.
|
||||
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).
|
||||
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.
|
||||
@ -833,14 +833,14 @@ Everything below is assembled from this report's own measurements; each row cite
|
||||
8. **Residual ~1 % offset — resolved here, pending in the spec ([§2.2](#s2-2), row 14, [Appendix A](#sA)).** After the slot-counting fix the only systematic offset is the 10⁻³ rounding of `f` (≈ 1 % `f/f_p` over-estimate); this report's estimator applies the row-14 precision bump, but the **spec still uses 10⁻³** and should adopt it.
|
||||
9. **Multi-coalition reorg — bounded, but worse than a single coalition ([§6.9](#s6-9), [§6.10](#s6-10)).** The [§6.10](#s6-10) tail is single-coalition. For `K` competing private-chain coalitions with total stake `α_tot`, the deepest reorg any one coalition (stake `α_i`) can drive against the public chain is bounded by `α_eff,i = α_i / (α_i + (1−α_tot)(1−o′))`, where `o′` is the public fork rate *inflated by every coalition's released-block orphaning*. Two channels make competition **worse** than a single colluding `α_tot` coalition: the rival coalitions' stake leaves the public chain (the honest term carries `(1−α_tot)`, not `(1−α_i)`), and their mutual orphaning lifts `o′` above the honest baseline `o` — in the limit `o′→1` even a sub-majority coalition faces a stalled public chain and reorgs unboundedly. So the single-coalition tail is best-case-for-the-defender; the safe design response is the same rule as everywhere else — size against `α_tot`, and keep `ρ < 1` so the honest baseline `o` stays small and caps how far mutual orphaning can inflate `o′`.
|
||||
10. **Soft-rule bribery is cheap per block ([§6.9](#s6-9)).** A bribe > `w_n` buys one block's omitted references — bounded in effect (other honest blocks in `W` still reference), but the hard-mandate defence is unavailable *by design*.
|
||||
11. **Untested adversary variants — now measured ([§6.5](#s6-5)).** What remains untested is only the adaptive (feedback) schedule, and that case is closed on other grounds in [§6.7](#s6-7). The rest of the list is resolved: whale coalitions leave the deflation unchanged and reproduce the [§6.4](#s6-4) law more cleanly than a random coalition does; `jitter > 0` moves nothing under attack, as it moves nothing honestly; very slow `β` shrinks the notch but deepens the standing deflation (0.85 → 0.77 as `β` 0.25 → 0.05) at unchanged attacker take. Uncle selection leaves the list too, but as a conformance match rather than a result — the spec fixes deterministic oldest-first, the rule every result here uses; what nothing validates is *deviation* from it, whose cost [§3.4](#s3-4) measures (up to `−0.10` in `D̂/D`, and `−0.063` at the recommended `W` when `ρ ≈ 1`). Two things the measurements *added* to this list: the collapsed-branch tail event of [§6.2](#s6-2), and the coalition-construction overshoot of item 17.
|
||||
11. **Untested adversary variants — now measured ([§6.5](#s6-5)).** What remains untested is only the adaptive (feedback) schedule, and that case is closed on other grounds in [§6.7](#s6-7). The rest of the list is resolved: whale coalitions leave the deflation unchanged and reproduce the [§6.4](#s6-4) law more cleanly than a random coalition does; `jitter > 0` moves nothing under attack, as it moves nothing honestly; very slow `β` shrinks the notch but deepens the standing deflation (0.85 → 0.76 as `β` 0.25 → 0.05) at unchanged attacker take. Uncle selection leaves the list too, but as a conformance match rather than a result — the spec fixes deterministic oldest-first, the rule every result here uses; what nothing validates is *deviation* from it, whose cost [§3.4](#s3-4) measures (up to `−0.10` in `D̂/D`, and `−0.063` at the recommended `W` when `ρ ≈ 1`). Two things the measurements *added* to this list: the collapsed-branch tail event of [§6.2](#s6-2), and the coalition-construction overshoot of item 17.
|
||||
12. **Network partition/heal — reasoned, not simulated ([§6.1](#s6-1)).** TSI reads its density window well past `k`-finality (it closes `4·⌊k/f⌋` slots before the estimate is used), so a partition that heals *within* the finality depth is invisible: all honest nodes reconverge on one canonical chain before the measurement window closes and compute an identical `D̂` — the same finality-window argument that makes jitter and clock skew harmless ([§6.1](#s6-1)). A partition lasting *longer* than finality is a consensus-layer safety failure (the chain forks irreparably) that TSI inherits but does not worsen — it adds no partition vulnerability of its own. A direct partition-and-heal simulation is left as future work.
|
||||
13. **Reward payout mechanism unspecified ([§8.5](#s8-5)).** Rows 10–11 give the incentive constraints, not the encoding. Header-count indistinguishability (Blend) and the equal-share, content-independent voucher of the Anonymous Leaders Reward Protocol are the binding constraints on any `w_u`/`w_n` schedule, and reconciling them — including a payout path for a producer whose block is off-chain — is spec work left open here.
|
||||
14. **Temporal resolution — `D̂` is stale by design ([§6.11](#s6-11)).** With `β = 1`, `D̂` tracks active stake at a one-epoch (≈ 7.5-day) lag, so it reflects participating stake from ~1–2 weeks ago and updates only weekly; a genuinely *sub-epoch* swing (daily or intra-week churn) falls below the estimator's Nyquist limit and is not tracked as a cycle at all. This is the correct trade for organic churn (row 5), but it bounds how fast TSI can follow real participation changes — flagged, not a defect.
|
||||
15. **Correlated latency untested ([§2](#s2)).** Both link distributions draw each link *independently*, so the sweeps probe the latency marginal, not its spatial structure; geographically **correlated** latency (regional clustering that lets co-located nodes fork as a bloc) is not modelled. In the primary Blend regime the per-hop mixing delay dominates the geographic link term, so this is expected to stay second-order — but it is untested.
|
||||
16. **The deflation-optimal adversary is unquantified ([§6.6](#s6-6)).** The countable recovery ceilings measured there come from adversaries optimising something *else* — revenue (the SSZ MDP: `η = 0.44` at α = 0.4) and reorg depth (`η = 0.52` at α = 0.30). An adversary optimising the *estimator* directly would trade revenue for fork depth, since every block buried behind a fork's first is permanently uncountable while a deeper fork costs it only the marginal revenue of waiting. Both measured ceilings are therefore upper bounds on `η` — best-case-for-the-defender — not lower bounds on the damage; sizing the deflation-optimal policy is future work.
|
||||
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 144 runs at `δ_max` = 8 s (a concentrated 50 % coalition) 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.
|
||||
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.
|
||||
|
||||
<a id="s8-4"></a>
|
||||
### 8.4 Capstone: the whole recipe, in one run
|
||||
@ -928,7 +928,7 @@ Sweep studies are committed configs, run with `make <name>` (writes a dated `run
|
||||
|
||||
Because the two models draw independent RNG streams, every countable-vs-unrestricted comparison is **unpaired**, and its resolution is set by the replicate spread rather than by the effect size. Each comparison sweep therefore includes a `U = 0` arm as a **negative control**: with no uncles the models are identical by construction, so the measured `U = 0` gap is a direct reading of the noise floor at that delay and replicate count. At `δ_max = 32` with 5 replicates that floor is ≈ 0.23 in `D̂/D` — larger than several real effects elsewhere in the grid — which is why [§3.2](#s3-2) reports a `t` statistic per cell and why the design regime is measured separately at 40 replicates ([§3.2a](#s3-2a)).
|
||||
|
||||
All 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`.
|
||||
All 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). 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/`).
|
||||
|
||||
|
||||
@ -158,7 +158,10 @@ configs/ smoke.yaml default.yaml fullscale.yaml
|
||||
countable-vs-old.yaml absorption-window.yaml (countable-model studies)
|
||||
fine-delay.yaml (delay 1-5 at 40 replicates: the design band, high precision)
|
||||
tests/ test_{pernode,config,rng,lottery,blocktree,uncles,tsi_counting,stake,
|
||||
theory,latency,theory_convergence,countable_counting,...}.py
|
||||
theory,latency,theory_convergence,countable_counting,
|
||||
countable_selfish,...}.py
|
||||
scripts/ plot_countable_vs_old.py (countable-vs-unrestricted comparison figures)
|
||||
plot_fine_delay.py (design-band accuracy + model gap with 95% CIs)
|
||||
countable_selfish.py (first-fork ceiling under the selfish MDP; fig36)
|
||||
adversary_variants.py (whale/jitter/slow-beta variants + the withhold-load sweep)
|
||||
```
|
||||
|
||||
@ -140,38 +140,46 @@ def study_withhold_load() -> pd.DataFrame:
|
||||
`1 - beta_adv` BY DESIGN (§6.4), so a 50 % coalition doubles the load — and at the design
|
||||
point `rho ~ 0.56` that lands on `rho_eff ~ 1.1`, past the fold.
|
||||
|
||||
This sweeps the blending budget under static withholding at `beta_adv` 0.3/0.5 and records
|
||||
how often the estimate collapses, which is the direct test of "never reached in the dynamics".
|
||||
This sweeps the blending budget under static withholding at `beta_adv` 0.3/0.5 — for BOTH
|
||||
coalition selections, since the one observed collapse was a whale cell — and records how often
|
||||
the estimate collapses, which is the direct test of "never reached in the dynamics".
|
||||
"""
|
||||
def cell(badv: float, delay: float, rep: int) -> dict:
|
||||
def cell(badv: float, delay: float, selection: str, rep: int) -> dict:
|
||||
cfg = SimConfig(**{**WHALE_BASE, "blend_delay_max": delay},
|
||||
adversary_frac=badv, adversary_strategy="withhold", replicate=rep)
|
||||
adversary_frac=badv, adversary_strategy="withhold",
|
||||
adversary_selection=selection, replicate=rep)
|
||||
t, collapsed = _tail_or_collapse(cfg)
|
||||
row = dict(beta_adv=badv, blend_delay_max=delay, rep=rep, collapsed=collapsed)
|
||||
row = dict(beta_adv=badv, blend_delay_max=delay, selection=selection, rep=rep,
|
||||
collapsed=collapsed)
|
||||
if t is not None:
|
||||
row |= dict(mean_ratio=float(t.mean_ratio.mean()),
|
||||
min_ratio=float(t.mean_ratio.min()),
|
||||
adv_block_share=float(t.adv_block_share.mean()))
|
||||
return row
|
||||
|
||||
jobs = [(b, d, r) for b in (0.3, 0.5) for d in (4.0, 8.0) for r in range(REPS)]
|
||||
jobs = [(b, d, s, r) for b in (0.3, 0.5) for d in (4.0, 8.0)
|
||||
for s in ("random", "whale") for r in range(REPS)]
|
||||
df = pd.DataFrame(Parallel(n_jobs=N_JOBS, backend="loky", inner_max_num_threads=1)(
|
||||
delayed(cell)(b, d, r) for b, d, r in jobs))
|
||||
delayed(cell)(b, d, s, r) for b, d, s, r in jobs))
|
||||
df.to_parquet(RUNS / "adversary_variants_withhold_load.parquet", index=False)
|
||||
return df
|
||||
|
||||
|
||||
def _report_withhold_load(df: pd.DataFrame) -> None:
|
||||
print("\n=== D. static withholding vs the §6.2 fold (rho_eff = rho / r) ===")
|
||||
print(f"{'b_adv':>6} {'delta':>6} {'collapsed':>10} {'D-hat/D':>18} {'worst epoch':>12}")
|
||||
print(f"{'b_adv':>6} {'delta':>6} {'sel':>7} {'collapsed':>10} {'D-hat/D':>18} "
|
||||
f"{'worst epoch':>12}")
|
||||
for badv in sorted(df.beta_adv.unique()):
|
||||
for delay in sorted(df.blend_delay_max.unique()):
|
||||
g = df[(df.beta_adv == badv) & (df.blend_delay_max == delay)]
|
||||
ok = g[~g.collapsed]
|
||||
mr = f"{ok.mean_ratio.mean():8.4f}+-{ok.mean_ratio.std(ddof=1):.4f}" if len(ok) > 1 \
|
||||
else f"{'n/a':>16}"
|
||||
worst = f"{ok.min_ratio.min():12.4f}" if len(ok) else f"{'n/a':>12}"
|
||||
print(f"{badv:6.1f} {delay:6.1f} {int(g.collapsed.sum()):5d}/{len(g):<4d} {mr} {worst}")
|
||||
for sel in sorted(df.selection.unique()):
|
||||
g = df[(df.beta_adv == badv) & (df.blend_delay_max == delay)
|
||||
& (df.selection == sel)]
|
||||
ok = g[~g.collapsed]
|
||||
mr = (f"{ok.mean_ratio.mean():8.4f}+-{ok.mean_ratio.std(ddof=1):.4f}"
|
||||
if len(ok) > 1 else f"{'n/a':>16}")
|
||||
worst = f"{ok.min_ratio.min():12.4f}" if len(ok) else f"{'n/a':>12}"
|
||||
print(f"{badv:6.1f} {delay:6.1f} {sel:>7} {int(g.collapsed.sum()):5d}/{len(g):<4d}"
|
||||
f" {mr} {worst}")
|
||||
|
||||
|
||||
def _report_whale(df: pd.DataFrame) -> None:
|
||||
|
||||
@ -41,11 +41,17 @@ def main() -> None:
|
||||
df.to_parquet(out / "capstone.parquet", index=False)
|
||||
print("=== Capstone: recommended config, all metrics together (equilibrium tail) ===")
|
||||
for adv, g in df.groupby("adv"):
|
||||
t = g[g.epoch >= g.epoch.max() // 2]
|
||||
# Per-REPLICATE tail: early_stop ends replicates at different epochs, so a per-arm cut
|
||||
# (epoch >= arm_max//2) would silently drop any replicate that stopped before the cut
|
||||
# and skew the tail toward the slow-converging ones. The report's §8.4 numbers are the
|
||||
# per-replicate aggregation; keep this printout matching them.
|
||||
t = pd.concat([r[r.epoch >= r.epoch.max() // 2] for _, r in g.groupby("replicate")])
|
||||
per_rep = t.groupby("replicate").fork_rate.mean()
|
||||
sem = per_rep.std(ddof=1) / (len(per_rep) ** 0.5)
|
||||
print(f"adversary {adv:.0%}: D̂/D {t.mean_ratio.mean():.4f} "
|
||||
f"range_ratio {t.range_ratio.max():.4f} agreement {t.agreement_window.min():.4f} "
|
||||
f"fork_rate {t.fork_rate.mean():.3f} max_reorg_depth {t.max_reorg_depth.max()} "
|
||||
f"p_ref {t.p_ref.mean():.3f}")
|
||||
f"fork_rate {per_rep.mean():.3f}+-{sem:.3f}(SEM over {len(per_rep)} reps) "
|
||||
f"max_reorg_depth {t.max_reorg_depth.max()} p_ref {t.p_ref.mean():.3f}")
|
||||
print(f"wrote {out/'capstone.parquet'} ({len(df)} rows)")
|
||||
|
||||
|
||||
|
||||
@ -76,6 +76,17 @@ def test_attacker_self_uncle_is_capped_too():
|
||||
assert 0.5 < s.countable_recovery_adv < 1.0
|
||||
|
||||
|
||||
def test_reorg_countable_recovery_from_depths():
|
||||
# A depth-d reorg discards one chain of d blocks -> 1 countable uncle: runs / blocks.
|
||||
from tsi_sim.reorg import countable_recovery_from_depths
|
||||
|
||||
assert countable_recovery_from_depths(np.array([], dtype=np.int64)) == 1.0
|
||||
assert countable_recovery_from_depths(np.array([1, 1, 1])) == 1.0 # SM1-like: all depth-1
|
||||
assert countable_recovery_from_depths(np.array([3, 1, 2])) == 0.5 # 3 runs / 6 blocks
|
||||
# and it is the depth-weighted harmonic sense of "share": deeper reorgs drag it down
|
||||
assert countable_recovery_from_depths(np.array([10])) == 0.1
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_cap_convergence():
|
||||
# The orphan shape converges more slowly in cap than the revenue does; check the drift is
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user