Marcin Pawlowski f51630e509
Rename the simulator and report from pd to blend
The study started as a peering-degree question and grew well past it: propagation,
adversary exposure, deanonymization and time-to-link, reliability under uniform
and correlated churn, messaging redundancy, and cover traffic. The pd name no
longer describes it.

tools/simulators/blend/pd/ -> tools/simulators/blend/, package src/pd -> src/blend,
and reports/blend/pd/ -> reports/blend/. Moved with git mv so history follows.

The text substitutions are deliberately narrow. pd is also the conventional pandas
alias, and pandas genuinely has a pd.plotting submodule, so a blanket pd. -> blend.
rewrite would have corrupted four files. Only package-unambiguous forms were
changed: from pd.X, -m pd.X, pd.<our module>, PD_BYTES_BUDGET, src/pd, and the
pyproject name. All four import pandas as pd lines are untouched and verified.

Both READMEs reframed: peering degree is now presented as the primary axis that
ties the others together rather than as the subject, and the relative links, which
lost a directory level in the move, are corrected.

Verified after the move: ruff clean, 101 tests, 45 verify anchors, make targets,
the script shims, an end-to-end smoke run, and data/report_numbers.py still
reproducing the report tables from the checked-in evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:59:59 +02:00

179 lines
7.3 KiB
Python

"""The emission-quota stake ceiling: exact bind, the D_hat/D normalisation, and epoch compliance."""
import math
import numpy as np
from blend.quota import (
alpha_max,
assign_stake,
emission_quota_per_slot,
expected_blocks_per_epoch,
inferred_alpha,
max_alpha_for_confidence,
quota_exceedance_prob,
quota_per_epoch,
s_max_true,
simulate_epoch_emissions,
win_prob,
)
F = 1.0 / 30.0
def test_quota_is_one_emission_per_slot_network_wide():
n = 20_000
assert emission_quota_per_slot(n) * n == 1.0 # whole network emits once per slot
assert emission_quota_per_slot(n, 4.0) * n == 4.0 # the multiplier scales it
def test_alpha_max_is_where_the_win_rate_equals_the_quota():
n = 20_000
a = alpha_max(n, F)
assert abs(win_prob(a, F) - emission_quota_per_slot(n)) < 1e-15
def test_alpha_max_is_below_the_q_over_f_approximation():
"""q/f is a small-q expansion and errs optimistic, so the exact bind must be lower."""
for n in (1_000, 20_000, 10**6):
exact = alpha_max(n, F)
approx = emission_quota_per_slot(n) / F
assert exact < approx
assert abs(approx / exact - 1) < 0.02 # ~1.7% at f = 1/30
def test_alpha_max_scales_inversely_with_network_size_and_with_cover_rate():
assert abs(alpha_max(20_000, F) / alpha_max(200_000, F) - 10.0) < 0.01
assert abs(alpha_max(20_000, F, 8.0) / alpha_max(20_000, F, 1.0) - 8.0) < 0.01
def test_true_stake_ceiling_is_scaled_by_the_inference_ratio():
"""The lottery uses sigma/D_hat, so the ceiling in TRUE stake carries the D_hat/D factor."""
n = 20_000
a = alpha_max(n, F)
assert s_max_true(n, F, 1.0) == a # accurate estimator: no correction
assert abs(s_max_true(n, F, 0.74) - 0.74 * a) < 1e-15 # deflated estimate tightens it
assert s_max_true(n, F, 0.64) < s_max_true(n, F, 0.74) < a
def test_expected_blocks_equal_the_quota_at_alpha_max():
n, S = 20_000, 648_000
a = alpha_max(n, F)
assert abs(expected_blocks_per_epoch(a, F, S) - quota_per_epoch(n, S)) < 1e-6
def test_a_node_at_the_mean_bind_overruns_about_half_the_time():
n, S = 20_000, 648_000
p = quota_exceedance_prob(alpha_max(n, F), F, n, S)
assert 0.35 < p < 0.65 # mean bind is a coin flip, as expected
def test_confidence_ceiling_is_stricter_than_the_mean_bind():
n, S = 20_000, 648_000
safe = max_alpha_for_confidence(F, n, S, confidence=0.99)
assert safe < alpha_max(n, F)
assert quota_exceedance_prob(safe, F, n, S) <= 0.01 + 1e-9
assert 0.5 < safe / alpha_max(n, F) < 0.9 # Poisson noise eats real headroom
def test_exceedance_matches_a_direct_simulation():
"""Closed-form exceedance vs drawing epochs of block wins."""
n, S = 2_000, 20_000
a = alpha_max(n, F) * 0.8
closed = quota_exceedance_prob(a, F, n, S)
rng = np.random.default_rng(0)
quota = quota_per_epoch(n, S)
wins = rng.binomial(S, win_prob(a, F), size=20_000)
emp = float(np.mean(wins > math.floor(quota)))
assert abs(closed - emp) < max(0.01, 0.1 * closed)
# --- stake distribution and the measured ceiling --------------------------------------------------
def test_stake_distributions_normalise_and_zipf_is_heavy_tailed():
n = 5_000
rng = np.random.default_rng(0)
uni = assign_stake(n, "uniform", rng)
zipf = assign_stake(n, "zipf", rng, zipf_a=1.0)
for s in (uni, zipf):
assert abs(s.sum() - 1.0) < 1e-12
assert (s > 0).all()
assert np.allclose(uni, 1.0 / n)
assert zipf.max() > 50 * uni.max() # a real head, unlike the flat case
def test_inferred_alpha_divides_by_the_estimator_ratio():
"""The lottery weighs sigma/D_hat, so a low estimate inflates every node's alpha."""
s = np.array([0.001, 0.01])
assert np.allclose(inferred_alpha(s, 1.0), s)
assert np.allclose(inferred_alpha(s, 0.5), s * 2.0)
def test_uniform_stake_stays_inside_the_quota_at_scale():
"""At 1/N each, every node's block rate is f/N -- far under a 1/N emission budget."""
n, S = 20_000, 648_000
s = assign_stake(n, "uniform", np.random.default_rng(1))
r = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(2))
assert r["compliant_frac"] == 1.0
assert r["overrun"].sum() == 0
def test_heavy_tailed_stake_makes_the_head_overrun_its_quota():
n, S = 20_000, 648_000
s = assign_stake(n, "zipf", np.random.default_rng(3), zipf_a=1.0)
r = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(4))
assert 0.0 < r["compliant_frac"] < 1.0 # the head breaks, the tail does not
assert r["min_overrun_stake"] > r["stake"].min() # it is the large holders that break
assert r["overrun"][np.argmax(s)] > 0 # the biggest staker certainly does
def test_the_measured_ceiling_matches_the_closed_form():
"""Where compliance actually breaks must bracket the analytic alpha_max."""
n, S = 20_000, 648_000
s = assign_stake(n, "zipf", np.random.default_rng(5), zipf_a=0.8)
r = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(6))
predicted = alpha_max(n, F)
assert r["max_compliant_stake"] < 3.0 * predicted
assert r["min_overrun_stake"] > 0.3 * predicted
def test_a_low_stake_estimate_tightens_the_measured_ceiling():
"""D_hat/D is an input, and lowering it must push more nodes over their quota."""
n, S = 20_000, 648_000
s = assign_stake(n, "zipf", np.random.default_rng(7), zipf_a=1.0)
accurate = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(8),
stake_inference_ratio=1.0)
deflated = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(8),
stake_inference_ratio=0.64)
assert deflated["compliant_frac"] < accurate["compliant_frac"]
assert deflated["max_compliant_stake"] <= accurate["max_compliant_stake"]
def test_more_cover_traffic_raises_the_ceiling():
"""The quota is the budget, so paying more cover traffic admits more concentrated stake."""
n, S = 20_000, 648_000
s = assign_stake(n, "zipf", np.random.default_rng(9), zipf_a=1.0)
lean = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(10), cover_rate_mult=1.0)
rich = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(10), cover_rate_mult=32.0)
assert rich["compliant_frac"] > lean["compliant_frac"]
assert rich["max_compliant_stake"] > lean["max_compliant_stake"]
def test_exceedance_survives_a_large_quota():
"""Regression: exp(-lam) underflows past lam ~ 745, which silently collapsed a hand-rolled
Poisson CDF to 0 and reported every node as exceeding. Raising the cover rate reaches that
regime immediately, so the mean bind must still be a coin flip at every rate."""
n, S = 20_000, 648_000
for rate in (1.0, 16.0, 64.0, 256.0):
p = quota_exceedance_prob(alpha_max(n, F, rate), F, n, S, rate)
assert 0.4 < p < 0.6, (rate, p)
def test_the_safe_ceiling_approaches_the_mean_bind_as_the_quota_grows():
"""Poisson noise shrinks relative to the mean, so the headroom needed for confidence shrinks."""
n, S = 20_000, 648_000
ratios = [max_alpha_for_confidence(F, n, S, 0.99, r) / alpha_max(n, F, r)
for r in (1.0, 16.0, 256.0)]
assert all(b > a for a, b in zip(ratios, ratios[1:], strict=False))
assert ratios[-1] > 0.9