mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-06 19:23:26 +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>
85 lines
3.4 KiB
Python
85 lines
3.4 KiB
Python
"""Countable-model counting: measurement kernels vs the tsi.py reference oracle.
|
|
|
|
Hand-built tree exercising every counting rule on baked references:
|
|
canonical 1(s0) -> 2(s2) -> 3(s5) -> 4(s10, tip); orphans 5(s1, parent 1, first fork),
|
|
6(s6, parent 5, DEEP), 7(s7, parent 1, first fork), 8(s7, parent 2, first fork, same slot
|
|
as 7). References: block2 -> (5,), block3 -> (1,) [a canonical block], block4 -> (6, 7, 8).
|
|
|
|
With w = 5 and T = 20 the countable verdicts are: 5 counted (d=1); 1 skipped (on chain);
|
|
6 deep-rejected (parent is an orphan); 7 counted (d=3); 8 counted by id but its slot is
|
|
already recovered by 7 (slot dedup). Old model counts every reference.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from tsi_sim.blocktree import BlockTree
|
|
from tsi_sim.measure import measure
|
|
from tsi_sim.tsi import countable_refs, density_m
|
|
|
|
W = 5
|
|
T = 20
|
|
|
|
|
|
def _tree() -> BlockTree:
|
|
tree = BlockTree(
|
|
slot=np.array([-1, 0, 2, 5, 10, 1, 6, 7, 7], np.int64),
|
|
parent=np.array([-1, 0, 1, 2, 3, 1, 5, 1, 2], np.int64),
|
|
height=np.array([0, 1, 2, 3, 4, 2, 3, 2, 3], np.int64),
|
|
leader=np.array([-1, 0, 1, 2, 3, 4, 5, 6, 7], np.int64),
|
|
uncles=[() for _ in range(9)],
|
|
)
|
|
tree.uncles[2] = (5,)
|
|
tree.uncles[3] = (1,)
|
|
tree.uncles[4] = (6, 7, 8)
|
|
return tree
|
|
|
|
|
|
CANONICAL = [4, 3, 2, 1]
|
|
ACTIVE = np.array([0, 1, 2, 5, 6, 7, 10], np.int64) # 7 distinct active slots in T
|
|
|
|
|
|
def test_countable_refs_oracle():
|
|
assert countable_refs(_tree(), CANONICAL, W) == {5, 7, 8}
|
|
|
|
|
|
def test_density_m_countable_and_old():
|
|
tree = _tree()
|
|
# countable: honest slots {0,2,5,10} + recovered slots {1, 7} -> 6
|
|
assert density_m(tree, CANONICAL, T, countable=True, w=W) == 6
|
|
# old model: every reference counts -> recovered slots {1, 6, 7} -> 7
|
|
assert density_m(tree, CANONICAL, T) == 7
|
|
|
|
|
|
@pytest.mark.parametrize("use_numba", [False, True])
|
|
def test_measure_countable_matches_oracle(use_numba):
|
|
tree = _tree()
|
|
A = np.zeros((2, tree.n_blocks)) # both nodes received everything
|
|
ms = measure(tree, A, ACTIVE, T, cutoff=15, use_numba=use_numba,
|
|
countable=True, w=W)
|
|
np.testing.assert_array_equal(ms.m, [6, 6]) # = density_m countable
|
|
np.testing.assert_allclose(ms.q, 4 / 7) # canonical slots / active
|
|
np.testing.assert_allclose(ms.q_eff, 6 / 7) # + recovered slots
|
|
np.testing.assert_array_equal(ms.ref_total, [5, 5]) # ids 5,1,6,7,8 examined
|
|
np.testing.assert_array_equal(ms.ref_deep, [1, 1]) # id 6 rejected as deep
|
|
|
|
|
|
@pytest.mark.parametrize("use_numba", [False, True])
|
|
def test_measure_old_counts_all_refs(use_numba):
|
|
tree = _tree()
|
|
A = np.zeros((2, tree.n_blocks))
|
|
ms = measure(tree, A, ACTIVE, T, cutoff=15, use_numba=use_numba)
|
|
np.testing.assert_array_equal(ms.m, [7, 7]) # = density_m old
|
|
np.testing.assert_allclose(ms.q_eff, 7 / 7)
|
|
np.testing.assert_array_equal(ms.ref_deep, [0, 0]) # no rule to reject on
|
|
|
|
|
|
def test_window_recheck_rejects_stale_reference():
|
|
# A baked reference outside the counting window is uncounted under countable
|
|
# (the old model still counts it): shrink w below block4 -> uncle 7 distance (d=3).
|
|
tree = _tree()
|
|
assert countable_refs(tree, CANONICAL, 2) == {5} # 7, 8 now out of window (d=3)
|
|
assert density_m(tree, CANONICAL, T, countable=True, w=2) == 5
|