2026-07-30 18:57:10 +02:00
|
|
|
|
"""Relative stake estimate vs network delay (fig16).
|
|
|
|
|
|
|
|
|
|
|
|
Shows the report's central relationship: the recovered *relative stake* ``D̂/D`` as a function of the
|
Round-4 TSI report review: apply findings, editorial pass, code + figure fixes
Applied the reconstructed round-4 review to the TSI parameter-selection report
set (reports/tsi) and executed the follow-ups.
Report (reports/tsi):
- Applied the must+should findings across README + parts 1-4: cross-part numeric
corrections, figure-caption fixes, spec reconciliation, and cross-file companions
(hops-degradation and notch/reward numbers, tip-agreement ordering, density-window
timing, VRF -> ZK Proof-of-Leadership, w_u window/reward gloss).
- Editorial pass for timeless voice (no "now adopted / merged / coin" narration) and
a gentle spec-safety framing (recommendations are thresholds; the protocol's
MAX_UNCLES=4 sits safely above them).
- Added the fork-rate-vs-scale table (6.10), defined "grinding gain", promoted the
clock-skew study to its own paragraph, added the correlated-latency caveat, and
moved fig27/fig28 beside their discussion.
- Documented the Blend cascade in 2: hops propagate over the shared gossip graph
(not direct links), the final broadcast comes from the last relay, relays are
blind forwarders.
Simulator (tools/simulators/tsi/tsi-sim-pernode):
- Docstring/dead-code fixes: theory.block_count_ceiling (legacy framing), measure,
reorg (catch-up reading), metrics (removed two dead helpers), config (fixed_point
10^-6; clock_skew_max/lottery_chunks documented inert), stake_vs_delay.
- Generator correctness + regenerated figures: figures_pernode.CONFIG_COLS now
exhaustive (f no longer pooled); rho_boundary_analysis SEM across replicates +
hollow floored markers + de-hardcoded ell_mean (measured from the run's graph);
appendix_fluct per-N sigma + ~18x title (figB2); bootstrap_dynamics driving
estimate so fig1 epoch-0 matches genesis.
- pytest: 186 passed; report links 528/0 dangling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 13:13:03 +02:00
|
|
|
|
mean block-visibility delay ``D_vis`` (seconds), one curve per uncle cap ``U``. Accuracy holds at
|
|
|
|
|
|
the 1.0 exact-recovery bound while the load ``ρ = f·D_vis`` stays below ``⌈U⌉``, then collapses — so
|
|
|
|
|
|
larger delay needs more uncles. Blend transport, f = 1/30 (30 s blocks); delay swept via the
|
2026-07-30 18:57:10 +02:00
|
|
|
|
per-hop blending budget ``blend_delay_max``.
|
|
|
|
|
|
|
|
|
|
|
|
Run: python scripts/stake_vs_delay.py (writes runs/stake_vs_delay.parquet + fig16)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
|
|
from tsi_sim import topology
|
|
|
|
|
|
from tsi_sim.config import SimConfig
|
|
|
|
|
|
from tsi_sim.engine import run_trajectory
|
|
|
|
|
|
from tsi_sim.plotting import style
|
|
|
|
|
|
|
|
|
|
|
|
HERE = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
RUNS = HERE / "runs"
|
|
|
|
|
|
FIGS = HERE / "report-figures"
|
|
|
|
|
|
RUNS.mkdir(exist_ok=True)
|
|
|
|
|
|
FIGS.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
F = 1.0 / 30.0
|
|
|
|
|
|
HOPS = 3
|
|
|
|
|
|
DELAYS = [1.0, 4.0, 8.0, 12.0, 16.0, 20.0, 26.0, 32.0, 40.0] # per-hop blending budget (s)
|
|
|
|
|
|
UNCLES = [0, 1, 2, 3]
|
|
|
|
|
|
BASE = dict(n_nodes=800, k=48, epochs=16, stake_dist="uniform", topology="blend", degree=8,
|
|
|
|
|
|
link_latency_mean=0.3, link_latency_dist="geo", blend_hops=HOPS, uncle_window=300,
|
|
|
|
|
|
genesis_d_factor=0.5, f=F)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def d_vis(delay: float) -> float:
|
|
|
|
|
|
"""Mean visibility delay D_vis = hops·δ/2 + (hops+1)·ℓ_mean, ℓ_mean = mean shortest-path."""
|
|
|
|
|
|
cfg = SimConfig(**BASE, blend_delay_max=delay, max_uncles=1)
|
|
|
|
|
|
pl = topology.build_path_latency(cfg, np.random.default_rng(np.random.SeedSequence(0)))
|
|
|
|
|
|
off = pl[~np.eye(pl.shape[0], dtype=bool)]
|
|
|
|
|
|
l_mean = float(off[np.isfinite(off)].mean())
|
|
|
|
|
|
return HOPS * delay / 2.0 + (HOPS + 1) * l_mean
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sweep() -> pd.DataFrame:
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for delay in DELAYS:
|
|
|
|
|
|
dv = d_vis(delay)
|
|
|
|
|
|
for U in UNCLES:
|
|
|
|
|
|
reps = []
|
|
|
|
|
|
for r in range(5):
|
|
|
|
|
|
df = pd.DataFrame(run_trajectory(SimConfig(
|
|
|
|
|
|
replicate=r, blend_delay_max=delay, max_uncles=U, **BASE)))
|
|
|
|
|
|
reps.append(df[df.epoch >= 8].mean_ratio.mean())
|
|
|
|
|
|
ratio = float(np.mean(reps))
|
|
|
|
|
|
sem = float(np.std(reps) / np.sqrt(len(reps)))
|
|
|
|
|
|
rows.append(dict(delay=delay, d_vis=dv, rho=F * dv, U=U, ratio=ratio, sem=sem))
|
|
|
|
|
|
print(f"delay={delay:4.0f}s D_vis={dv:5.1f} rho={F*dv:4.2f} U={U}: D̂/D={ratio:.3f}")
|
|
|
|
|
|
out = pd.DataFrame(rows)
|
|
|
|
|
|
out.to_parquet(RUNS / "stake_vs_delay.parquet")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fig16(df: pd.DataFrame) -> None:
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
style.apply_style()
|
|
|
|
|
|
fig, ax = plt.subplots(figsize=(7.2, 4.4))
|
|
|
|
|
|
ax.axhline(1.0, color="0.5", lw=0.9, ls="--", label="exact recovery (1.0)")
|
|
|
|
|
|
for i, U in enumerate(UNCLES):
|
|
|
|
|
|
s = df[df.U == U].sort_values("d_vis")
|
|
|
|
|
|
ax.errorbar(s.d_vis, s.ratio, yerr=s["sem"], fmt="-o", ms=4, capsize=2,
|
|
|
|
|
|
color=style.OKABE_ITO[i], label=f"U = {U}")
|
|
|
|
|
|
# rho = 1, 2, 3 boundaries: D_vis = k/f <-> rho = f*D_vis = k
|
|
|
|
|
|
for k in (1, 2, 3):
|
|
|
|
|
|
dv = k / F
|
|
|
|
|
|
if dv <= df.d_vis.max() * 1.02:
|
|
|
|
|
|
ax.axvline(dv, color="0.7", lw=0.7, ls=":")
|
|
|
|
|
|
ax.text(dv, 0.32, f"ρ={k}", rotation=90, va="bottom", ha="right", fontsize=7,
|
|
|
|
|
|
color="0.4")
|
|
|
|
|
|
ax.set_xlabel(r"mean block-visibility delay $D_{\rm vis}$ (s) [load $\rho = f\,D_{\rm vis}$]")
|
|
|
|
|
|
ax.set_ylabel(r"relative stake estimate $\hat D / D$")
|
|
|
|
|
|
ax.set_title(r"Recovered relative stake vs delay (blend, $f=1/30$): "
|
|
|
|
|
|
r"$U$ must grow with $\rho=\lceil f D_{\rm vis}\rceil$")
|
|
|
|
|
|
ax.set_ylim(0.3, 1.02) # bounded by 1: cap at the exact-recovery bound, no above-1 headroom
|
|
|
|
|
|
ax.legend(fontsize=8, loc="lower left")
|
|
|
|
|
|
style.save(fig, FIGS / "fig16_stake_vs_delay", provenance="scripts/stake_vs_delay.py")
|
|
|
|
|
|
plt.close(fig)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
|
print("=== relative stake estimate vs delay ===")
|
|
|
|
|
|
df = sweep()
|
|
|
|
|
|
fig16(df)
|
|
|
|
|
|
print("wrote fig16_stake_vs_delay")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|