mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 19:53:10 +00:00
Implement the countable uncle model from the Cryptarchia spec's counting-only reference rules, and make it the simulator default. Counting rules (uncles.py, measure.py): - Only the first block of a fork (parent on the producer's chain) is referenceable and countable, which makes every reference verifiable from chain data alone. - The reference window is derived from a window-absorption parameter, w_u = W_abs/f slots (W_abs in expected block-intervals, default 10, bounded W_abs <= 0.6*k), replacing the free-standing uncle_window. - Selection skips slots already occupied on the producer's chain and takes at most one uncle per slot. - The measurement pass re-checks every rule per reference and tallies rejections as deep_ref_share. The pre-redesign model is preserved behind --old on tsi-sweep and tsi-verify. Its RNG key is byte-identical to the pre-uncle_model key, so --old bit-reproduces the historical runs. Supporting changes: uncle_model and window_absorption config surface with validation (config.py, constants.py); accuracy closed form over the effective q_u (theory.py); plumbing through tsi.py, epoch.py, sweep.py, blocktree.py, metrics.py, verify.py, figures_pernode.py. Studies and figures: - configs/countable-vs-old.yaml -- delay x U grid, run under both models on the same grid. - configs/absorption-window.yaml -- accuracy vs W_abs at U=1. - scripts/plot_countable_vs_old.py renders fig30-fig33 into reports/tsi/report-figures/. Tests: tests/test_countable_counting.py (7 cases) covering first-fork eligibility, derived-window bounds, occupied-slot exclusion, and per-reference re-checking; extensions to test_uncles.py, test_config.py, test_slot_counting.py. Full fast suite: 202 passed. Also adds CLAUDE.md (graphify project instructions) and ignores editor/local-agent state plus the vendored Equi-X benchmark clone. The reports/tsi/ prose describing this model is held back for a separate editorial pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
99 lines
4.0 KiB
Python
99 lines
4.0 KiB
Python
"""Corrected slot-based density counting: one count per slot, never more."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from tsi_sim.blocktree import BlockTree
|
|
from tsi_sim.config import SimConfig
|
|
from tsi_sim.engine import run_trajectory
|
|
from tsi_sim.theory import block_count_ceiling
|
|
from tsi_sim.tsi import density_m
|
|
|
|
|
|
def make_tree(slots, parents, heights, uncles):
|
|
n = len(slots)
|
|
return BlockTree(
|
|
slot=np.array(slots, np.int64),
|
|
parent=np.array(parents, np.int64),
|
|
height=np.array(heights, np.int64),
|
|
leader=np.zeros(n, np.int64),
|
|
uncles=uncles,
|
|
)
|
|
|
|
|
|
def test_same_slot_co_winner_uncle_not_counted():
|
|
"""An uncle sharing a canonical block's slot must not add a count (slot already won)."""
|
|
# canonical 1(slot0), 3(slot2); orphan 2 ALSO at slot0 (co-winner), referenced by 3.
|
|
tree = make_tree(
|
|
slots=[-1, 0, 0, 2],
|
|
parents=[-1, 0, 0, 1],
|
|
heights=[0, 1, 1, 2],
|
|
uncles=[(), (), (), (2,)],
|
|
)
|
|
canonical = [3, 1]
|
|
assert density_m(tree, canonical, T=10) == 2 # slots {0, 2} — uncle adds nothing
|
|
assert density_m(tree, canonical, T=10, legacy_block_count=True) == 3 # the old bug
|
|
|
|
|
|
def test_multiple_uncles_same_slot_count_once():
|
|
"""Two referenced orphans in the same (non-canonical) slot count as one recovered slot."""
|
|
# canonical 1(slot0), 4(slot3); orphans 2 and 3 BOTH at slot1, both referenced.
|
|
tree = make_tree(
|
|
slots=[-1, 0, 1, 1, 3],
|
|
parents=[-1, 0, 0, 0, 1],
|
|
heights=[0, 1, 1, 1, 2],
|
|
uncles=[(), (), (), (), (2, 3)],
|
|
)
|
|
canonical = [4, 1]
|
|
assert density_m(tree, canonical, T=10) == 3 # slots {0, 1, 3}
|
|
assert density_m(tree, canonical, T=10, legacy_block_count=True) == 4 # the old bug
|
|
|
|
|
|
def test_distinct_slot_uncles_still_counted():
|
|
"""The fix must not lose genuinely distinct recovered slots."""
|
|
tree = make_tree(
|
|
slots=[-1, 0, 1, 2, 3],
|
|
parents=[-1, 0, 0, 0, 1],
|
|
heights=[0, 1, 1, 1, 2],
|
|
uncles=[(), (), (), (), (2, 3)],
|
|
)
|
|
canonical = [4, 1]
|
|
assert density_m(tree, canonical, T=10) == 4 # slots {0, 1, 2, 3}
|
|
|
|
|
|
def test_zero_delay_equilibrium_is_one_not_ceiling():
|
|
"""The c(f) ceiling was the bug: corrected counting equilibrates at 1.0 with uncles.
|
|
|
|
Holds under the (default) countable model too: at zero delay the only orphans are
|
|
same-slot co-winners, which countable selection never references (occupied slot) and
|
|
which add nothing to the slot count anyway. 5 replicates / 0.02 tolerance because the
|
|
countable model's key() draws a different RNG stream than the historical runs the old
|
|
3-rep/0.015 margin was tuned on.
|
|
"""
|
|
base = dict(n_nodes=300, stake_dist="uniform", topology="full_mesh", latency=0,
|
|
max_uncles=2, uncle_window=300, k=64, epochs=24, genesis_d_factor=1.0)
|
|
tails = []
|
|
for rep in range(5):
|
|
df = pd.DataFrame(run_trajectory(SimConfig(**base, replicate=rep)))
|
|
tails.append(df[df.epoch >= 8].mean_ratio.mean())
|
|
assert abs(np.mean(tails) - 1.0) < 0.02
|
|
|
|
|
|
def test_legacy_flag_reproduces_the_ceiling():
|
|
# OLD model on purpose: the c(f) ceiling arises from referencing same-slot co-winners
|
|
# and counting them per block id. The countable model never references a same-slot
|
|
# co-winner (its slot is already occupied on the chain), so under it the legacy flag
|
|
# has nothing to double-count and this historical bug cannot be reproduced.
|
|
base = dict(n_nodes=300, stake_dist="uniform", topology="full_mesh", latency=0,
|
|
max_uncles=2, uncle_window=300, k=64, epochs=24, genesis_d_factor=1.0,
|
|
uncle_model="old")
|
|
tails = []
|
|
for rep in range(3):
|
|
df = pd.DataFrame(run_trajectory(
|
|
SimConfig(**base, legacy_block_count=True, replicate=rep)))
|
|
tails.append(df[df.epoch >= 8].mean_ratio.mean())
|
|
c = block_count_ceiling(SimConfig().f)
|
|
assert abs(np.mean(tails) - c) < 0.015
|