mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-09 20:53:34 +00:00
Verification of my own item-16 result, which I had flagged as resting on a step I had not checked: "break-even" was measured as revenue SHARE >= stake share, and share-to-pay equivalence rests on sec 6.6's issuance argument. It does not hold as stated. A revenue share is measured against the canonical block rate, but pay is set by the estimator. TSI drives COUNTED density -- canonical plus recovered uncles -- to f per slot, so a run yields f/D-hat block-finding events per slot and a deflating attack makes the lottery easier for everyone. Pay per slot is adv_rate * f / D-hat against an honest miner's alpha * f, so the correction on the share ratio is density/D-hat, which is below 1 whenever any uncle is recovered. Scoring on share credits the attacker with pay it never receives. The headline survives; one row does not. At alpha = 0.4 the true break-even point deflates to 0.649 rather than 0.642 (against the revenue-optimal 0.811), so 16 points of free deflation instead of 17. But at alpha = 0.36 the policy I reported as paying 1.033x stake actually pays 0.95x, and NO policy in the sweep both pays and deflates there -- that row is withdrawn. The exposure therefore opens near alpha ~ 0.37-0.38, above the 1/3 selfish threshold, not at it. That is a better result for the protocol than the one I published yesterday, which is the direction these corrections usually do not go. pay_vs_honest is now computed in deflation_frontier() and is the criterion best_profitable() selects on, so the claim is reproducible rather than resting on a hand check. The conversion neglects the multi-winner factor c(f) ~ 1.017 between events and occupied slots (sec 2.1), noted inline and small against a 16-point effect. Downstream citations in sec 1, sec 8.2 and item 16 updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
158 lines
7.0 KiB
Python
158 lines
7.0 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 that actually pay.
|
|
|
|
"Pays" means ABSOLUTE reward per unit time at least matching honest mining
|
|
(``pay_vs_honest >= 1``), not merely a revenue *share* at least matching stake. The two
|
|
differ by ``density / dhat``: the share's denominator is the canonical block rate, but the
|
|
pay rate is set by the estimator, which holds counted density at ``f`` per slot. Using the
|
|
share alone credits the attacker with pay it does not receive — at alpha = 0.36 it marks
|
|
points as break-even that are ~5 % short.
|
|
"""
|
|
out = []
|
|
for _a, g in df.groupby("alpha"):
|
|
paid = g[g.pay_vs_honest >= 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} {'pay/hon':>8} {'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} | (no policy in the sweep both pays "
|
|
f"and 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.pay_vs_honest:8.3f} {b.dhat_countable:7.4f} {ro_d - b.dhat_countable:+16.4f}")
|
|
print("\n(extra deflation > 0 means the paying-but-deflating policy beats the revenue-optimal "
|
|
"one at damaging the estimate, at no cost versus honest mining; 'pay/hon' is ABSOLUTE "
|
|
"reward per unit time, not revenue share)")
|
|
|
|
|
|
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()
|