145 lines
5.7 KiB
Python
Raw Normal View History

2026-07-30 18:57:10 +02:00
"""Optimal selfish mining + uncle-reward incentive design — REPORT §6.6 / §6.7 (fig14).
Two questions:
A. How much does the *optimal* (Sapirshtein MDP) selfish strategy beat SM1, and where is the
profitability threshold? (fig14, left)
B. Do block/uncle REWARDS defuse the attack? Paying an uncle reward to orphaned honest blocks
compensates them, so the selfish attacker's *reward* share falls below its block share and the
profitability threshold moves up. (fig14, right)
Adversarial framing (see report §6.7): uncle rewards (i) compensate honestly-orphaned producers,
(ii) disincentivise hiding (a withheld block never propagates -> can never be an uncle -> forfeits
both block and uncle reward), and (iii) shrink the selfish premium. The reward scheme's own attack
surface "uncle farming" (deliberately orphaning your own blocks to collect uncle rewards) is
bounded because uncles must be real VRF winners and an uncle pays w_uncle < 1 < a canonical block.
Run: python scripts/selfish_rewards.py (writes runs/selfish_rewards.parquet + fig14)
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pandas as pd
from tsi_sim.plotting import style
from tsi_sim.selfish import (
RewardParams,
honest_reward_recovery,
race_from_alpha,
reward_shares,
selfish_revenue_closed_form,
)
from tsi_sim.selfish_mdp import optimal_selfish_revenue
HERE = Path(__file__).resolve().parent.parent
RUNS = HERE / "runs"
FIGS = HERE / "report-figures"
RUNS.mkdir(exist_ok=True)
FIGS.mkdir(exist_ok=True)
N_EVENTS = 4_000_000
ALPHAS = [0.10, 0.15, 0.20, 0.25, 0.30, 1 / 3, 0.36, 0.40, 0.43, 0.46]
W_UNCLES = [0.0, 0.25, 0.5, 1.0]
def sweep_optimal() -> pd.DataFrame:
"""Optimal (MDP) vs SM1 vs honest revenue, plus reward-share under each uncle reward (g=0)."""
rng = np.random.default_rng(7)
rows = []
for alpha in ALPHAS:
for gamma in (0.0, 0.5):
opt = optimal_selfish_revenue(alpha, gamma, cap=40, iters=3000)
rows.append(dict(kind="revenue", alpha=alpha, gamma=gamma,
sm1=selfish_revenue_closed_form(alpha, gamma), optimal=opt))
# reward-share (gamma=0 SM1 race) under each uncle reward
r = race_from_alpha(alpha, N_EVENTS, 0.0, rng)
for wu in W_UNCLES:
rp = RewardParams(w_uncle=wu, p_ref=1.0)
rows.append(dict(kind="reward", alpha=alpha, w_uncle=wu,
block_share=r.revenue_share,
reward_share=reward_shares(r, rp).adv_reward_share,
honest_recovery=honest_reward_recovery(r, rp)))
out = pd.DataFrame(rows)
out.to_parquet(RUNS / "selfish_rewards.parquet")
return out
def _threshold(alphas, shares):
"""First alpha where share > alpha (profitability boundary), by linear interp; None if never."""
a = np.array(alphas)
d = np.array(shares) - a
for i in range(1, len(a)):
if d[i - 1] <= 0 < d[i]:
t = a[i - 1] + (a[i] - a[i - 1]) * (-d[i - 1]) / (d[i] - d[i - 1])
return float(t)
return None
def report(df: pd.DataFrame) -> None:
rev = df[df.kind == "revenue"]
print("optimal (MDP) vs SM1 revenue, gamma=0 / 0.5:")
for alpha in (1 / 3, 0.4, 0.46):
for g in (0.0, 0.5):
row = rev[(np.isclose(rev.alpha, alpha)) & (rev.gamma == g)].iloc[0]
print(f" a={alpha:.3f} g={g}: optimal={row.optimal:.3f} SM1={row.sm1:.3f} "
f"(gap {row.optimal-row.sm1:+.3f})")
rw = df[df.kind == "reward"]
print("\nuncle reward -> selfish profitability threshold (gamma=0, SM1):")
for wu in W_UNCLES:
s = rw[rw.w_uncle == wu].sort_values("alpha")
thr = _threshold(s.alpha.tolist(), s.reward_share.tolist())
rec = s[np.isclose(s.alpha, 0.40)].honest_recovery.iloc[0]
print(f" w_uncle={wu}: threshold alpha* = {thr if thr is None else round(thr,3)} "
f"(honest reward recovery @a=0.4: {rec:.3f})")
def fig14(df: pd.DataFrame) -> None:
import matplotlib.pyplot as plt
style.apply_style()
fig, axes = plt.subplots(1, 2, figsize=(9.6, 3.8))
aa = np.array(ALPHAS)
# LEFT: optimal vs SM1 vs honest revenue
ax = axes[0]
rev = df[df.kind == "revenue"]
ax.plot(aa, aa, color="0.5", lw=0.9, ls="--", label="honest (= stake)")
for i, g in enumerate((0.0, 0.5)):
s = rev[rev.gamma == g].sort_values("alpha")
c = style.OKABE_ITO[i]
ax.plot(s.alpha, s.optimal, "-o", ms=4, color=c, label=rf"optimal, $\gamma={g}$")
ax.plot(s.alpha, s.sm1, ":", lw=1.4, color=c, label=rf"SM1, $\gamma={g}$")
ax.set_xlabel(r"adversary stake $\alpha$")
ax.set_ylabel("revenue share")
ax.set_title("Optimal selfish (MDP) vs SM1")
ax.legend(fontsize=7, loc="upper left")
# RIGHT: reward-share vs alpha under uncle rewards (gamma=0)
ax = axes[1]
rw = df[df.kind == "reward"]
ax.plot(aa, aa, color="0.5", lw=0.9, ls="--", label="break-even (= stake)")
for i, wu in enumerate(W_UNCLES):
s = rw[rw.w_uncle == wu].sort_values("alpha")
ax.plot(s.alpha, s.reward_share, "-o", ms=4, color=style.OKABE_ITO[i],
label=rf"$w_u={wu}$")
ax.set_xlabel(r"adversary stake $\alpha$")
ax.set_ylabel("attacker reward share")
ax.set_title("Uncle rewards shrink the selfish premium")
ax.legend(fontsize=7, loc="upper left")
style.save(fig, FIGS / "fig14_optimal_rewards", provenance="scripts/selfish_rewards.py")
plt.close(fig)
def main() -> None:
print("=== optimal-selfish + uncle-reward sweep ===")
df = sweep_optimal()
report(df)
fig14(df)
print("wrote fig14_optimal_rewards")
if __name__ == "__main__":
main()