mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 19:53:10 +00:00
148 lines
6.4 KiB
Python
148 lines
6.4 KiB
Python
|
|
"""How far can a PROFITABLE adversary deflate the estimate? — REPORT §8.3 item 16 (fig37).
|
||
|
|
|
||
|
|
§6.6 measures the countable recovery ceiling under two adversaries, and both optimise something
|
||
|
|
other than the estimator: revenue (the SSZ objective) and reorg depth. Item 16 recorded the
|
||
|
|
consequence — those are upper bounds on `eta`, not lower bounds on the damage — and asked what an
|
||
|
|
adversary optimising deflation directly would do.
|
||
|
|
|
||
|
|
Two answers, because the obvious question is the wrong one:
|
||
|
|
|
||
|
|
* **Unconstrained**, the deflation optimum is pure abstention: publish nothing, adopt when
|
||
|
|
overtaken, drive `D_hat` to exactly `1 - alpha` and revenue to zero. §6.4 already covers this
|
||
|
|
and shows it is *correct* measurement — a coalition that publishes nothing is not
|
||
|
|
participating, so `1 - alpha` is the right answer for the stake that is.
|
||
|
|
* **Constrained to stay paid**, the question bites. Sweeping the mixed objective
|
||
|
|
`lam * (adversary blocks) - (contribution to D_hat)` traces the profit/deflation frontier;
|
||
|
|
the point of interest is where the revenue *share* reaches `alpha`, i.e. where the adversary
|
||
|
|
does no worse than mining honestly. Everything below that line is self-punishing griefing,
|
||
|
|
already bounded by §6.5.
|
||
|
|
|
||
|
|
The headline: at `alpha = 0.4` an adversary content with break-even revenue pushes `D_hat` to
|
||
|
|
~0.64, against the revenue-optimal policy's 0.81. The revenue-optimal adversary is not the
|
||
|
|
estimator's worst case, and the gap is free — it costs only the selfish premium.
|
||
|
|
|
||
|
|
Note the sweep parameter is not monotone in revenue: selfish mining wins a larger share of a
|
||
|
|
*smaller* pie, so raw adversary block rate is maximised by honest mining and large `lam` returns
|
||
|
|
there. That is fine — the sweep is used to enumerate candidate policies and their measured
|
||
|
|
(share, `D_hat`) pairs, not as a monotone path, and any point on it is a legitimate strategy.
|
||
|
|
|
||
|
|
Run: python scripts/deflation_frontier.py (writes runs/deflation_frontier.parquet + fig37)
|
||
|
|
"""
|
||
|
|
|
||
|
|
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_mdp import deflation_frontier, deflation_optimal_stats, optimal_policy_stats
|
||
|
|
|
||
|
|
HERE = Path(__file__).resolve().parent.parent
|
||
|
|
RUNS = HERE / "runs"
|
||
|
|
FIGS = HERE / "report-figures"
|
||
|
|
RUNS.mkdir(exist_ok=True)
|
||
|
|
FIGS.mkdir(exist_ok=True)
|
||
|
|
|
||
|
|
CAP = 64
|
||
|
|
GAMMA = 0.0
|
||
|
|
ALPHAS = [0.30, 1 / 3, 0.36, 0.38, 0.40, 0.42, 0.45]
|
||
|
|
LAMS = [round(x, 2) for x in np.arange(0.0, 3.05, 0.1)]
|
||
|
|
|
||
|
|
|
||
|
|
def sweep() -> pd.DataFrame:
|
||
|
|
rows = []
|
||
|
|
for a in ALPHAS:
|
||
|
|
ro = optimal_policy_stats(a, GAMMA, cap=CAP)
|
||
|
|
do = deflation_optimal_stats(a, GAMMA, cap=CAP)
|
||
|
|
for lam in LAMS:
|
||
|
|
r = deflation_frontier(a, GAMMA, lam, cap=CAP)
|
||
|
|
r |= dict(revenue_optimal=ro.revenue,
|
||
|
|
dhat_revenue_optimal=ro.dhat_ratio(1.0, True) if ro.deviates else 1.0,
|
||
|
|
dhat_abstention=do.dhat_ratio(1.0, True))
|
||
|
|
rows.append(r)
|
||
|
|
df = pd.DataFrame(rows)
|
||
|
|
df.to_parquet(RUNS / "deflation_frontier.parquet", index=False)
|
||
|
|
return df
|
||
|
|
|
||
|
|
|
||
|
|
def best_profitable(df: pd.DataFrame) -> pd.DataFrame:
|
||
|
|
"""Per alpha, the frontier point minimising D_hat among those paying at least stake share."""
|
||
|
|
out = []
|
||
|
|
for _a, g in df.groupby("alpha"):
|
||
|
|
paid = g[g.reward_per_stake >= 1.0 - 1e-9]
|
||
|
|
if paid.empty:
|
||
|
|
continue
|
||
|
|
out.append(paid.loc[paid.dhat_countable.idxmin()])
|
||
|
|
return pd.DataFrame(out)
|
||
|
|
|
||
|
|
|
||
|
|
def report(df: pd.DataFrame) -> None:
|
||
|
|
best = best_profitable(df)
|
||
|
|
print(f"{'alpha':>6} {'rev-opt rev':>12} {'rev-opt D':>10} | "
|
||
|
|
f"{'best paid rev':>14} {'xstake':>7} {'D':>7} {'extra deflation':>16}")
|
||
|
|
for a, g in df.groupby("alpha"):
|
||
|
|
ro_r, ro_d = g.revenue_optimal.iloc[0], g.dhat_revenue_optimal.iloc[0]
|
||
|
|
b = best[best.alpha == a]
|
||
|
|
if b.empty:
|
||
|
|
print(f"{a:6.3f} {ro_r:12.4f} {ro_d:10.4f} | (nothing profitable deflates)")
|
||
|
|
continue
|
||
|
|
b = b.iloc[0]
|
||
|
|
print(f"{a:6.3f} {ro_r:12.4f} {ro_d:10.4f} | {b.revenue:14.4f} "
|
||
|
|
f"{b.reward_per_stake:7.3f} {b.dhat_countable:7.4f} {ro_d - b.dhat_countable:+16.4f}")
|
||
|
|
print("\n(extra deflation > 0 means the profitable-but-deflating policy beats the "
|
||
|
|
"revenue-optimal one at damaging the estimate, at no cost versus honest mining)")
|
||
|
|
|
||
|
|
|
||
|
|
def fig37(df: pd.DataFrame) -> None:
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
style.apply_style()
|
||
|
|
fig, axes = plt.subplots(1, 2, figsize=(9.6, 3.8))
|
||
|
|
|
||
|
|
# LEFT: the frontier itself for a few alphas — revenue share against the estimate
|
||
|
|
ax = axes[0]
|
||
|
|
for i, a in enumerate([0.36, 0.40, 0.45]):
|
||
|
|
g = df[np.isclose(df.alpha, a)].sort_values("lam")
|
||
|
|
c = style.OKABE_ITO[i + 1]
|
||
|
|
ax.plot(g.revenue, g.dhat_countable, "-o", ms=3, color=c, label=rf"$\alpha={a}$")
|
||
|
|
ax.axvline(a, color=c, lw=0.7, ls=":")
|
||
|
|
ro = g.dhat_revenue_optimal.iloc[0]
|
||
|
|
ax.plot([g.revenue_optimal.iloc[0]], [ro], "*", ms=11, color=c)
|
||
|
|
ax.set_xlabel("adversary revenue share (dotted line = its stake, i.e. break-even)")
|
||
|
|
ax.set_ylabel(r"$\hat D / D^*$")
|
||
|
|
ax.set_title("Profit/deflation frontier (★ = revenue-optimal)")
|
||
|
|
ax.legend(fontsize=7, loc="lower right")
|
||
|
|
|
||
|
|
# RIGHT: how much worse the estimator gets once the adversary stops maximising revenue
|
||
|
|
ax = axes[1]
|
||
|
|
best = best_profitable(df).sort_values("alpha")
|
||
|
|
ref = df.groupby("alpha").agg(d_ro=("dhat_revenue_optimal", "first"),
|
||
|
|
d_ab=("dhat_abstention", "first")).reset_index()
|
||
|
|
ax.plot(ref.alpha, ref.d_ro, "-*", ms=9, color=style.OKABE_ITO[2],
|
||
|
|
label="revenue-optimal (§6.6)")
|
||
|
|
ax.plot(best.alpha, best.dhat_countable, "-o", ms=4, color=style.OKABE_ITO[3],
|
||
|
|
label="best deflation at break-even pay")
|
||
|
|
ax.plot(ref.alpha, ref.d_ab, "--v", ms=4, color=style.OKABE_ITO[1],
|
||
|
|
label=r"abstention ($1-\alpha$, unpaid)")
|
||
|
|
ax.axhline(1.0, color="0.5", lw=0.9, ls="--")
|
||
|
|
ax.set_xlabel(r"adversary stake $\alpha$")
|
||
|
|
ax.set_ylabel(r"$\hat D / D^*$")
|
||
|
|
ax.set_title("The revenue-optimal adversary is not the worst case")
|
||
|
|
ax.legend(fontsize=7, loc="lower left")
|
||
|
|
|
||
|
|
style.save(fig, FIGS / "fig37_deflation_frontier", provenance="scripts/deflation_frontier.py")
|
||
|
|
plt.close(fig)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
print(f"=== profit/deflation frontier (MDP cap={CAP}, gamma={GAMMA}) ===")
|
||
|
|
df = sweep()
|
||
|
|
report(df)
|
||
|
|
fig37(df)
|
||
|
|
print("wrote fig37_deflation_frontier")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|