142 lines
7.0 KiB
Python
Raw Normal View History

2026-07-30 18:57:10 +02:00
"""Selfish / private-chain withholding model (§6.6)."""
import numpy as np
import pytest
from tsi_sim.selfish import (
RaceResult,
RewardParams,
honest_reward_recovery,
race_from_alpha,
reward_shares,
selfish_revenue_closed_form,
selfish_threshold,
simulate_selfish,
tsi_dhat_ratio,
)
from tsi_sim.selfish_mdp import optimal_selfish_revenue
@pytest.mark.parametrize("gamma", [0.0, 0.5, 1.0])
@pytest.mark.parametrize("alpha", [0.1, 0.2, 1 / 3, 0.4, 0.45])
def test_race_matches_eyal_sirer_closed_form(alpha, gamma):
# The SM1 simulation must reproduce the analytic relative revenue within MC noise.
rng = np.random.default_rng(12345)
r = race_from_alpha(alpha, 2_000_000, gamma, rng)
assert abs(r.revenue_share - selfish_revenue_closed_form(alpha, gamma)) < 0.004
@pytest.mark.parametrize("gamma", [0.0, 0.5, 1.0])
def test_block_conservation(gamma):
# Every mined block is exactly one of: adversary-canonical, honest-canonical, or orphaned
# (honest or adversary). This must hold at all gamma (the tie race must account for its blocks).
r = race_from_alpha(0.4, 1_000_000, gamma, np.random.default_rng(7))
assert r.adv + r.hon + r.orphan_hon + r.orphan_adv == r.events
def test_honest_only_has_no_orphans_and_stake_share():
# alpha = 0 -> every block honest, none orphaned, adversary share 0.
rng = np.random.default_rng(1)
r = race_from_alpha(0.0, 100_000, 0.0, rng)
assert r.adv == 0 and r.orphan_hon == 0 and r.orphan_adv == 0
assert r.revenue_share == 0.0 and r.density_fraction == 1.0
def test_selfish_is_profitable_above_threshold_only():
# Below the gamma=0 threshold (1/3) selfish under-earns; above it over-earns its stake.
rng = np.random.default_rng(7)
below = race_from_alpha(0.25, 2_000_000, 0.0, rng)
above = race_from_alpha(0.40, 2_000_000, 0.0, rng)
assert below.revenue_share < 0.25 # honest mining is better here
assert above.revenue_share > 0.40 # selfish premium: earns more than its stake
assert abs(selfish_threshold(0.0) - 1 / 3) < 1e-9
def test_selfish_orphans_honest_blocks_and_deflates_density():
# A profitable selfish attack orphans honest blocks, so the counted canonical density < 1,
# which is exactly what deflates D_hat; uncle recovery restores it monotonically toward 1.
rng = np.random.default_rng(3)
r = race_from_alpha(0.4, 1_000_000, 0.0, rng)
assert r.orphan_hon > 0
assert r.density_fraction < 1.0
d0 = tsi_dhat_ratio(r, uncle_recovery=0.0)
d_half = tsi_dhat_ratio(r, uncle_recovery=0.5)
d1 = tsi_dhat_ratio(r, uncle_recovery=1.0)
assert d0 < d_half < d1 <= 1.0 + 1e-9
assert abs(d0 - r.density_fraction) < 1e-9
def test_simulate_selfish_is_deterministic_given_stream_and_seed():
is_adv = np.random.default_rng(0).random(50_000) < 0.35
a = simulate_selfish(is_adv, 0.5, np.random.default_rng(99))
b = simulate_selfish(is_adv, 0.5, np.random.default_rng(99))
assert isinstance(a, RaceResult) and (a.adv, a.hon) == (b.adv, b.hon)
# --- optimal-selfish MDP (Sapirshtein) ---------------------------------------------------------
@pytest.mark.parametrize("alpha,gamma", [(0.1, 0.0), (0.3, 0.0), (0.4, 0.0), (0.4, 0.5)])
def test_optimal_selfish_dominates_sm1_and_honest(alpha, gamma):
# The optimal policy is never worse than SM1 or than honest mining (both are feasible policies).
opt = optimal_selfish_revenue(alpha, gamma, cap=16, iters=1500)
assert opt >= selfish_revenue_closed_form(alpha, gamma) - 3e-3
assert opt >= alpha - 3e-3
def test_optimal_selfish_is_honest_below_threshold():
# Below the gamma=0 threshold (1/3) no deviation beats honest: optimal == alpha.
assert abs(optimal_selfish_revenue(0.25, 0.0, cap=16, iters=1500) - 0.25) < 3e-3
# --- uncle-reward model ------------------------------------------------------------------------
def test_uncle_reward_shrinks_selfish_share_and_compensates_honest():
# A profitable selfish attack orphans honest blocks. Paying uncle rewards to those orphans
# raises honest total reward, so the attacker's REWARD share falls below its block share, and
# honest miners recover more of their mined value.
r = race_from_alpha(0.4, 500_000, 0.0, np.random.default_rng(2))
block_share = r.revenue_share
s0 = reward_shares(r, RewardParams(w_uncle=0.0)).adv_reward_share
s_half = reward_shares(r, RewardParams(w_uncle=0.5)).adv_reward_share
s_full = reward_shares(r, RewardParams(w_uncle=1.0)).adv_reward_share
assert abs(s0 - block_share) < 1e-9 # no uncle reward -> reward share == block share
assert s_full < s_half < s0 # more uncle reward -> smaller attacker share
# honest fairness: recovery rises monotonically with the uncle reward toward 1.0
rec0 = honest_reward_recovery(r, RewardParams(w_uncle=0.0))
rec1 = honest_reward_recovery(r, RewardParams(w_uncle=1.0))
assert rec0 < rec1 <= 1.0 + 1e-9
def test_uncle_reward_is_noop_without_orphans():
# Honest mining (no orphans) -> uncle rewards change nothing; share stays at the stake.
r = race_from_alpha(0.0, 50_000, 0.0, np.random.default_rng(5))
assert reward_shares(r, RewardParams(w_uncle=1.0, w_nephew=0.5)).adv_reward_share == 0.0
assert honest_reward_recovery(r, RewardParams(w_uncle=1.0)) == 1.0
def test_uncle_rewards_backfire_without_mandate_but_mandate_neutralises():
# The strategic result (report §6.7/§6.8): without a mandate the attacker suppresses honest
# references AND self-uncles its own lost blocks, so an uncle reward RAISES its share above the
# block share; a mandatory-inclusion schedule instead drives it down to ~stake (break-even).
r = race_from_alpha(0.4, 1_000_000, 0.0, np.random.default_rng(1))
block = r.revenue_share
rp_supp = RewardParams(w_uncle=1.0, p_ref=0.0, p_ref_adv=1.0)
suppress = reward_shares(r, rp_supp).adv_reward_share
mandate = reward_shares(r, RewardParams.mandatory(w_uncle=1.0)).adv_reward_share
assert suppress > block # self-uncle + suppression -> uncle reward helps the attacker
assert mandate < block # forced honest-orphan compensation -> premium shrinks
assert abs(mandate - 0.40) < 0.03 # pushed to ~stake (break-even)
def test_self_uncle_recovery_is_monotone_in_p_ref_adv():
# Recovering more of the attacker's own lost blocks (higher p_ref_adv) raises its reward share.
r = race_from_alpha(0.42, 800_000, 0.5, np.random.default_rng(4))
s = [reward_shares(r, RewardParams(w_uncle=0.8, p_ref=0.0, p_ref_adv=pa)).adv_reward_share
for pa in (0.0, 0.5, 1.0)]
assert s[0] < s[1] < s[2]
def test_farming_profitable_iff_wu_plus_wn_exceeds_one():
# A self-farmer orphaning a canonical win to uncle it collects w_uncle (producer) + w_nephew
# (self-nephew); the marginal per-slot payoff vs an honest block (=1) is exactly w_u + w_n.
for wu, wn, profitable in [(0.5, 0.3, False), (0.875, 0.03125, False), (0.9, 0.15, True)]:
assert ((wu + wn) > 1.0) == profitable