mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 11:43:20 +00:00
87 lines
3.9 KiB
Python
87 lines
3.9 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
|
||
|
|
|
||
|
|
|
||
|
|
@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
|