2026-07-30 18:57:10 +02:00

145 lines
5.7 KiB
Python

"""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()