mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 19:53:10 +00:00
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>
98 lines
4.5 KiB
Python
98 lines
4.5 KiB
Python
"""Countable (first-fork) uncle recovery under a selfish adversary (§6.6).
|
|
|
|
The countable model can reference only the first block of a fork, so a discarded *chain* of
|
|
honest blocks yields one countable uncle however long it is. These tests pin the two ends of
|
|
that: SM1 never buries a second block (so the restriction costs nothing), while the optimal
|
|
policy waits and does (so it costs a factor of ~2 in recoverable orphans).
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from tsi_sim.selfish import race_from_alpha, selfish_threshold
|
|
from tsi_sim.selfish_mdp import optimal_policy_stats
|
|
|
|
FAST = dict(cap=16, iters=1500)
|
|
|
|
|
|
@pytest.mark.parametrize("gamma", [0.0, 0.5, 1.0])
|
|
@pytest.mark.parametrize("alpha", [0.2, 1 / 3, 0.4, 0.45])
|
|
def test_sm1_orphans_are_all_countable(alpha, gamma):
|
|
# SM1 acts as soon as the honest branch reaches length 1, so every orphan it makes is the
|
|
# first block of its fork: the first-fork restriction costs SM1 exactly nothing.
|
|
r = race_from_alpha(alpha, 200_000, gamma, np.random.default_rng(3))
|
|
assert r.orphan_hon_runs == r.orphan_hon
|
|
assert r.countable_recovery == 1.0
|
|
|
|
|
|
@pytest.mark.parametrize("gamma", [0.0, 0.5])
|
|
def test_optimal_policy_block_conservation(gamma):
|
|
# Every block-finding event yields exactly one block, which ends up canonical or orphaned.
|
|
# Per-event rates must therefore sum to 1 — the same invariant test_selfish asserts for SM1.
|
|
s = optimal_policy_stats(0.4, gamma, **FAST)
|
|
total = s.density_fraction + s.orphan_hon_blocks + s.orphan_adv_blocks
|
|
assert abs(total - 1.0) < 1e-9
|
|
|
|
|
|
@pytest.mark.parametrize("gamma", [0.0, 0.5])
|
|
def test_optimal_policy_buries_orphans(gamma):
|
|
# Above the profitability threshold the optimum waits before overriding, so it discards
|
|
# multi-block honest chains that the first-fork rule cannot recover.
|
|
s = optimal_policy_stats(0.4, gamma, **FAST)
|
|
assert s.deviates
|
|
assert s.orphan_hon_runs < s.orphan_hon_blocks
|
|
assert s.countable_recovery < 0.7 # measured ~0.44 (gamma=0) / ~0.55 (gamma=0.5)
|
|
|
|
|
|
def test_below_threshold_does_not_deviate():
|
|
# Below the threshold the optimum is honest mining; the MDP is indifferent across policies
|
|
# there, so the orphan structure of an arbitrary greedy tie-break must not be reported.
|
|
alpha = 0.25
|
|
assert alpha < selfish_threshold(0.0)
|
|
s = optimal_policy_stats(alpha, 0.0, **FAST)
|
|
assert not s.deviates
|
|
assert s.orphan_hon_blocks == 0.0
|
|
assert s.density_fraction == 1.0
|
|
|
|
|
|
def test_countable_dhat_is_below_unrestricted():
|
|
s = optimal_policy_stats(0.4, 0.0, **FAST)
|
|
# With no references the two models agree; with them, countable recovers strictly less.
|
|
assert s.dhat_ratio(p_ref=0.0, countable=True) == s.dhat_ratio(p_ref=0.0, countable=False)
|
|
assert s.dhat_ratio(p_ref=1.0, countable=True) < s.dhat_ratio(p_ref=1.0, countable=False)
|
|
# and both are bounded by the no-attack value
|
|
assert s.dhat_ratio(p_ref=1.0, countable=False) <= 1.0
|
|
# monotone in the reference rate
|
|
assert (s.dhat_ratio(p_ref=0.0, countable=True)
|
|
< s.dhat_ratio(p_ref=0.5, countable=True)
|
|
< s.dhat_ratio(p_ref=1.0, countable=True))
|
|
|
|
|
|
def test_attacker_self_uncle_is_capped_too():
|
|
# The attacker's abandoned secret chain is also one chain, so it can self-uncle only its
|
|
# first block — the §6.7(a) farming channel is narrower than the block count suggests.
|
|
s = optimal_policy_stats(0.4, 0.0, **FAST)
|
|
assert s.orphan_adv_runs < s.orphan_adv_blocks
|
|
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
|
|
# small where the report quotes numbers.
|
|
a = optimal_policy_stats(0.4, 0.0, cap=48)
|
|
b = optimal_policy_stats(0.4, 0.0, cap=64)
|
|
assert abs(a.countable_recovery - b.countable_recovery) < 2e-3
|
|
assert abs(a.revenue - b.revenue) < 1e-3
|