mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 19:53:10 +00:00
142 lines
7.0 KiB
Python
142 lines
7.0 KiB
Python
"""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
|