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

63 lines
2.6 KiB
Python

"""Concurrent block-proposal analysis.
Every lottery win is a block *proposal*. Two proposals produced within ``L`` slots of each
other cannot see one another (a block becomes visible only after the network latency ``L``),
so they are mutually concurrent — competing forks. Bucketing the timeline into
non-overlapping windows of ``L`` slots and counting proposals per bucket gives a direct
view of how many proposals are concurrent, and the busiest bucket is the peak number of
concurrent proposals. This is the quantity that bounds how many uncles can appear, so it
informs the ``MAX_UNCLES`` choice.
"""
from __future__ import annotations
from dataclasses import replace
import numpy as np
from .config import SimConfig
from .lottery import sample_wins, win_probs
from .rng import seedseq_for
from .stake import make_stake
def window_counts(winner_slots: np.ndarray, epoch_len: int, bucket: int) -> np.ndarray:
"""Proposals per non-overlapping ``bucket``-slot window over ``[0, epoch_len)``."""
if epoch_len <= 0:
return np.empty(0, np.int64)
bucket = max(int(bucket), 1)
n_windows = (epoch_len + bucket - 1) // bucket
if winner_slots.size == 0:
return np.zeros(n_windows, np.int64)
return np.bincount(winner_slots // bucket, minlength=n_windows)
def proposal_slots(config: SimConfig, replicate: int = 0) -> np.ndarray:
"""Simulate one epoch of block proposals at the *true* lottery difficulty (D=D_true).
Returns the sorted slots at which proposals (all lottery winners, including forks) occur.
Independent of the TSI trajectory — the proposal process depends only on stake, ``f``,
and ``epoch_len`` — so this is a cheap, self-contained re-simulation for the plots.
"""
cfg = replace(config, replicate=replicate)
root = seedseq_for(cfg)
children = root.spawn(2)
stake = make_stake(cfg, np.random.default_rng(children[0]))
p_win = win_probs(stake, float(stake.sum()), cfg.f)
winner_slots, _ = sample_wins(p_win, cfg.epoch_len, np.random.default_rng(children[1]))
return winner_slots
def concurrency_stats(config: SimConfig, replicate: int = 0) -> dict:
"""Per-bucket proposal-count stats for one simulated epoch (bucket = ``max(L, 1)``)."""
ws = proposal_slots(config, replicate)
bucket = max(config.latency, 1)
counts = window_counts(ws, config.epoch_len, bucket)
return {
"bucket": bucket,
"counts": counts,
"max": int(counts.max()) if counts.size else 0,
"mean": float(counts.mean()) if counts.size else 0.0,
"p99": float(np.percentile(counts, 99)) if counts.size else 0.0,
}