diff --git a/chain.toml b/chain.toml index 26502020..b7a7375d 100644 --- a/chain.toml +++ b/chain.toml @@ -284,13 +284,13 @@ ops_gpu_cooldown_seconds = 10800 # operators (a validator on a stale value silently picks a different snapshot). # NOT part of contract_digest ([training] only). The DEC-CA-0045 rollover # switches it to [activation] epoch_blocks_after. -epoch_blocks = 3600 # ≈12h at a 12s block time +epoch_blocks = 900 # DECIDED rollover (rolling-era-king, lock 9133200 → 9136800): 3600 → 900 (3h) round_hours = 12.0 # informational: wall-clock span of an epoch # Scheduled switch (0/0 = steady state). Last used: 7200 → 3600 at 8726400, # 2026-07-29. To schedule another, the block must be a boundary of BOTH grids # (load-checked); set both back to 0 once it is safely past. -epoch_blocks_prev = 0 -epoch_activation_block = 0 +epoch_blocks_prev = 3600 # DECIDED rollover typed (see rolling_from_block) +epoch_activation_block = 9136800 # DECIDED rollover typed (see rolling_from_block) # Rolling intake + era king (DEC-CA-0043, block-gated). Trainer POLICY: from # this block challengers train the moment they are funded, legs cross # boundaries, every boundary publishes ONE settlement manifest (era king + @@ -305,7 +305,11 @@ epoch_activation_block = 0 # the six external validators run a release carrying the era envelope. # With every key at 0 the block is DECIDED ON CHAIN by validator stake # ([activation] below, DEC-CA-0045); typing a block here overrides that. -rolling_from_block = 0 +# DECIDED 2026-10-31 (lock-in 9133200 → 9136800, the block the trainer's +# published 900-block settlements have followed since). Typed 2026-11-13 so a +# restarting node needs no activation history or archive RPC; a node whose own +# record says otherwise follows this typed value (configured_rollover). +rolling_from_block = 9136800 # Settlements per era (consecutive boundaries sharing one set of training # seeds, one init and one cached king checkpoint): 4 on the 3h grid = 12h # eras — today's king-leg cadence with 3h verdicts inside it. @@ -692,7 +696,7 @@ increment_from_block = 9046800 # = cohort_maxt_from_block (both flip at the eve # Do NOT type "v2" / a block after the rollover here: it breaks that ordering # (apply_activation refuses it) and would move live pre-v2 era-king # settlements back to level units (2026-10-20 review; DEC-CA-0039 note). -cohort_maxt_increment_from_block = 0 +cohort_maxt_increment_from_block = 9136800 # DECIDED rollover typed (what apply_activation sets) # Multivariate scoring activation (DEC-CA-0041): from this block a multivariate # window's channels are averaged into ONE per-window contribution (GIFT-Eval # weighting — the window counts once, not once per channel), so the round point @@ -742,12 +746,12 @@ margin_v2_from_block = 9194400 # TYPED 2026-11-01 (DEC-CA-0067): = the bloc # pinned). Must equal [round] rolling_from_block and be >= # cohort_maxt_increment_from_block >= cohort_maxt_from_block (load-checked). # 0 = off (bit-identical to the ungated validator). -era_king_from_block = 0 +era_king_from_block = 9136800 # DECIDED rollover typed # Tenure / ripeness in BLOCKS from the rollover (CONSENSUS): the margin decay # and the promotion clock keep their wall-time length across the 3600 → 900 # grid switch instead of running 4× faster. Both *_blocks values are inert # until tenure_blocks_from_block is set (= ROLLOVER). -tenure_blocks_from_block = 0 +tenure_blocks_from_block = 9136800 # DECIDED rollover typed margin_warmup_blocks = 28800 # = 8 rounds × 3600 blocks (~4 d) — same decay as today cascade_reign_blocks = 18000 # = 5 rounds × 3600 blocks (~2.5 d) — same ripeness as today king_resync_max_blocks = 18000 # = king_resync_max_rounds (5) × 3600 blocks — same valve as today @@ -1123,7 +1127,9 @@ dormant_after_blocks = 0 [v2] launch_at_utc = "2026-10-30T12:00:00Z" commit_open_at_utc = "2026-10-30T09:00:00Z" # V2 reveals queue here; training waits for v2 -commit_from_block = 0 # resolved from finalized timestamps, or explicitly pinned by operator +# Pinned 2026-11-13: block 9395601 is the first finalized block at/after +# commit_open_at_utc (stamped 2026-10-30T09:00:00Z; archive read). +commit_from_block = 9395601 prelaunch_burn_readiness = "r2" # cascade v2 (DEC-CA-0050): Cascade-MOE-28M king lineage (lane A) + the # domain-expert lane (lane B). Finalized chain timestamps resolve launch_at_utc @@ -1135,7 +1141,10 @@ prelaunch_burn_readiness = "r2" # router bites once the routing gate opens, receipts stamp `v2_block`, and # lane A keeps 100 % until another lane launches. Planned order: C first # (A/C 75/25), then B (A/B/C 50/25/25); activation remains separate for each. -from_block = 0 +# Pinned 2026-11-13 = what launch_at_utc resolved to: the 12:00:00Z crossing is +# block 9396501, the next era start on the decided grid is 9136800 + 73×3600. +# With both blocks typed a restart reads no historical timestamps (no archive). +from_block = 9399600 # Rides the same go-live (DEC-CA-0054): every v2 settlement enforces the # bench gate (the mode; the RULE is DEC-CA-0054's three suites + six-number # geomean only until bench_gate_v2_from_block below, which on mainnet is the @@ -1201,6 +1210,11 @@ reign_warm_start_blocks = 21600 # to burn_uid (miners keep their split of the rest); it ends at the first v2 # settlement. prelaunch_burn_from_block = types it instead (override). prelaunch_burn = 0.95 +# DECIDED (prelaunch-95-r2: lock 9486000 → 9486900, the block every node seen +# carries); typed 2026-11-13 (owner go). Behaviour-identical: the burn applies +# only before the first v2 settlement (emission_policy), and v2 is active from +# 9399600 < 9486900, so it never applied — typing frees the note segment. +prelaunch_burn_from_block = 9486900 # The carried (toto2) king's court is paid at most 5 % of total emissions from # the v2 block until a king is crowned at a v2 settlement (lane B unchanged). carryover_king_share = 0.05 diff --git a/decided_votes.toml b/decided_votes.toml index 77bcb3f0..ee811bfa 100644 --- a/decided_votes.toml +++ b/decided_votes.toml @@ -25,8 +25,11 @@ feature = "rolling-era-king" lock_block = 9133200 activation_block = 9136800 source = "config" -type_it = false -reason = "nodes resolved it differently: the operator node 9133200→9136800, one external validator 9374400→9378000 (it restarted late and adopted other notes). Typing either value in main would override a block some nodes decided; the operator types its own on its deployed file only." +# typed in chain.toml 2026-11-13 (owner decision): 9136800 is the block the +# trainer's published 900-block settlements have followed since. One external +# validator recorded 9374400→9378000 (it restarted late and adopted other +# notes); the grids coincide from 9378000 on, so its live scoring is unchanged +# and the typed value wins over its record (configured_rollover). [[vote]] feature = "forfeit-42b1a486" @@ -47,8 +50,10 @@ feature = "prelaunch-95-r2" lock_block = 9486000 activation_block = 9486900 source = "tally" -type_it = false -reason = "HELD (owner decision pending, 2026-11-13): decided and applied on every node seen (operator + the external validator's note both carry 9486000:9486900); typing [v2] prelaunch_burn_from_block = 9486900 is behaviour-identical and frees another note segment, but it is a release chain.toml edit the owner has not yet approved. Next one-line PR." +# typed in chain.toml 2026-11-13 (owner go): decided on every node seen +# (operator + the external validator's note both carry 9486000:9486900). +# Behaviour-identical: the burn applies only before the first v2 settlement +# and v2 is active from 9399600, so it never applied. [[vote]] feature = "v2b-69630f92" diff --git a/docs/AGENT_MINING.md b/docs/AGENT_MINING.md index c24effd1..68d539ba 100644 --- a/docs/AGENT_MINING.md +++ b/docs/AGENT_MINING.md @@ -397,7 +397,7 @@ deployment or qualification succeeded: | Template | V2 block | Loaded B gate | C / MarketCast blocks | Random-init eras | B pool | CPU generation | |---|---:|---:|---|---:|---|---| -| `chain.toml` | 0 | 0 | 0 / 0 | 14 | 25% | enabled for shards; 90 min configured | +| `chain.toml` | 9399600 | 0 | 0 / 0 | 14 | 25% | enabled for shards; 90 min configured | | `chain.testnet.toml` | 8190000 | 8190001 | 0 / 0 | 0 | 50% | off; 90 min configured | diff --git a/tests/conftest.py b/tests/conftest.py index 58d692b5..ce26eaae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,7 +109,7 @@ def isolated(runner): @pytest.fixture(scope="session") -def cfg() -> ChainConfig: +def cfg(tmp_path_factory) -> ChainConfig: """The mainnet template with its ENFORCING pins neutralized. chain.toml carries the real launch pins (expected_gpu, the worker-image @@ -122,7 +122,14 @@ def cfg() -> ChainConfig: """ from dataclasses import replace - c = load_chain_config(REPO_ROOT / "chain.toml") + from tests.unit.predecision_main import predecision_path + + # The release TYPES the decided rollover (9136800) and the resolved v2 + # launch (9399600) since 2026-11-13 — real-chain state, like the pins. + # Fixtures run on tiny fake block clocks, so they start from the release + # file with those blocks untyped (the vote/launch undecided), exactly the + # file every fixture ran on before. Shipped-file facts: load chain.toml. + c = load_chain_config(predecision_path(tmp_path_factory.mktemp("predecision"))) # Tests using fake block clocks also reparse raw configuration (satellites, # overlays). Neutralize the real calendar there as well as on cfg.v2. import copy diff --git a/tests/unit/predecision_main.py b/tests/unit/predecision_main.py new file mode 100644 index 00000000..0c81a274 --- /dev/null +++ b/tests/unit/predecision_main.py @@ -0,0 +1,63 @@ +"""The release chain.toml as it shipped BEFORE 2026-11-13 typed the decided +rollover and the resolved v2 launch: every DEC-CA-0043 rollover key back to +its vote-open value and ``[v2] from_block`` / ``commit_from_block`` back to 0. + +Tests that exercise the on-chain rollover vote or the dated-launch resolution +from a fresh node's point of view load this; tests about the shipped release +load ``chain.toml`` itself. ``test_predecision_main_is_the_exact_inverse`` +pins that this is the exact inverse of the typed lines — and that applying the +decided rollover + resolving the launch lands back on the shipped config. +""" +from __future__ import annotations + +import re +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] + +# (section, key, pre-decision value) — exactly the lines typed on 2026-11-13. +UNTYPED = ( + ("round", "epoch_blocks", "3600"), + ("round", "epoch_blocks_prev", "0"), + ("round", "epoch_activation_block", "0"), + ("round", "rolling_from_block", "0"), + ("scoring", "cohort_maxt_increment_from_block", "0"), + ("scoring", "era_king_from_block", "0"), + ("scoring", "tenure_blocks_from_block", "0"), + ("v2", "commit_from_block", "0"), + ("v2", "from_block", "0"), + # absent before (the vote decided it): None = delete the typed line + ("v2", "prelaunch_burn_from_block", None), +) + + +def predecision_text(text: str | None = None) -> str: + """``chain.toml`` text with the decided blocks untyped (section-scoped: + only the key inside its own table is rewritten).""" + text = (REPO / "chain.toml").read_text() if text is None else text + for section, key, value in UNTYPED: + head = re.search(rf"(?m)^\[{re.escape(section)}\]\s*$", text) + assert head, section + nxt = re.search(r"(?m)^\[", text[head.end():]) + end = head.end() + (nxt.start() if nxt else len(text) - head.end()) + if value is None: + body, n = re.subn(rf"(?m)^{re.escape(key)}\s*=[^\n]*\n", "", + text[head.end():end], count=1) + else: + body, n = re.subn(rf"(?m)^{re.escape(key)}(\s*)=\s*[^\n#]*?(\s*#.*)?$", + rf"{key}\g<1>= {value}", text[head.end():end], count=1) + assert n == 1, (section, key) + text = text[:head.end()] + body + text[end:] + return text + + +def predecision_path(tmp_path) -> Path: + p = Path(tmp_path) / "chain.predecision.toml" + p.write_text(predecision_text()) + return p + + +def predecision_main(tmp_path): + from cascade.shared.config import load_chain_config + + return load_chain_config(predecision_path(tmp_path)) diff --git a/tests/unit/test_activation.py b/tests/unit/test_activation.py index 9a2f8aa4..e4bdacf0 100644 --- a/tests/unit/test_activation.py +++ b/tests/unit/test_activation.py @@ -17,6 +17,7 @@ import cascade.shared.activation as A from cascade.shared.config import load_chain_config from cascade.shared.era import era_king_active, rolling_active, tenure_blocks_active +from tests.unit.predecision_main import predecision_main, predecision_text REPO = Path(__file__).resolve().parents[2] FEATURE = "rolling-era-king" @@ -27,11 +28,13 @@ @pytest.fixture -def cfg(): - """The shipped chain.toml with its DEC-CA-0048 forfeiture, DEC-CA-0049 - margin-v2 bar and DEC-CA-0055 pre-launch burn DISARMED: these tests model - the plain fleet (none configured) and arm one explicitly.""" - c = load_chain_config(REPO / "chain.toml") +def cfg(tmp_path_factory): + """The release chain.toml as it shipped BEFORE the decided rollover was + typed (the rollover still open to the stake vote — what these tests + exercise), with its DEC-CA-0048 forfeiture, DEC-CA-0049 margin-v2 bar and + DEC-CA-0055 pre-launch burn DISARMED: these tests model the plain fleet + (none configured) and arm one explicitly.""" + c = predecision_main(tmp_path_factory.mktemp("predecision")) return replace(c, scoring=replace(c.scoring, forfeit_hotkeys=(), forfeit_from_block=0, forfeit_successor_hotkey="", win_margin_start_v2=0.0, win_margin_end_v2=0.0, margin_warmup_blocks_v2=0, @@ -383,17 +386,24 @@ def test_startup_activation_arms_from_the_notes_and_persists(cfg, tmp_path): # ── config loading ─────────────────────────────────────────────────────────── -def test_shipped_configs_carry_the_activation_section(): +def test_shipped_configs_carry_the_activation_section(tmp_path): main = load_chain_config(REPO / "chain.toml") assert main.activation.enabled and main.activation.feature == FEATURE assert main.activation.threshold == 0.51 and main.activation.epoch_blocks_after == 900 - assert main.round.rolling_from_block == 0 # the fleet decides the block + # the fleet's decided rollover is typed into the release (2026-11-13) + assert main.round.rolling_from_block == 9136800 + assert A.configured_rollover(main) == 9136800 + # before it was typed the release left the block to the fleet's vote + pre = predecision_main(tmp_path) + assert pre.activation.enabled and pre.activation.feature == FEATURE + assert pre.round.rolling_from_block == 0 and A.configured_rollover(pre) == 0 test = load_chain_config(REPO / "chain.testnet.toml") assert test.activation.enabled and A.configured_rollover(test) == 600 # typed in wins def test_loader_rejects_a_bad_threshold_or_grid(tmp_path): - src = (REPO / "chain.toml").read_text(encoding="utf-8") + # the pre-decision release: the grid checks bite on an untyped rollover + src = predecision_text() p = tmp_path / "chain.toml" p.write_text(src.replace("threshold = 0.51", "threshold = 1.5"), encoding="utf-8") with pytest.raises(ValueError, match="threshold"): diff --git a/tests/unit/test_bench_gate_v2.py b/tests/unit/test_bench_gate_v2.py index b7c70d34..0dcdedec 100644 --- a/tests/unit/test_bench_gate_v2.py +++ b/tests/unit/test_bench_gate_v2.py @@ -388,11 +388,15 @@ def test_parse_key(): parse_bench_gate_v2_from_block({"bench_gate_v2_from_block": bad}) -def test_shipped_configs(): +def test_shipped_configs(tmp_path): from cascade.shared.config import load_chain_config + from tests.unit.predecision_main import predecision_main main = load_chain_config("chain.toml") - assert main.v2.from_block == 0 and main.v2.bench_gate_v2_from_block == 0 + # 2026-11-13: main types the resolved v2 launch; the gate's "v2" follows it + assert main.v2.from_block == 9399600 and main.v2.bench_gate_v2_from_block == 9399600 + before = predecision_main(tmp_path) + assert before.v2.from_block == 0 and before.v2.bench_gate_v2_from_block == 0 assert main.scoring.bench_gate_tolerance == 0.01 assert main.scoring.bench_gate_min_win_rate == 0.5 test = load_chain_config("chain.testnet.toml") diff --git a/tests/unit/test_calendar_launch.py b/tests/unit/test_calendar_launch.py index c1c5fb82..30fa300a 100644 --- a/tests/unit/test_calendar_launch.py +++ b/tests/unit/test_calendar_launch.py @@ -14,6 +14,7 @@ ) from cascade.shared.config import load_chain_config, parse_chain_config from cascade.shared.netconfig import effective_raw +from tests.unit.predecision_main import predecision_main def _chain(target, head): @@ -22,12 +23,16 @@ def _chain(target, head): current_block=lambda: pytest.fail("must use finalized timestamps")) -def test_release_has_the_noon_utc_launch_and_separate_update_vote(): +def test_release_has_the_noon_utc_launch_and_separate_update_vote(tmp_path): cfg = load_chain_config() - assert cfg.v2.from_block == 0 + # 2026-11-13: main types the resolved launch (pre-decision file left it 0) + assert (cfg.v2.from_block, cfg.v2.commit_from_block) == (9399600, 9395601) + assert predecision_main(tmp_path).v2.from_block == 0 assert cfg.v2.launch_at_utc == "2026-10-30T12:00:00Z" assert cfg.v2.prelaunch_burn == 0.95 - assert cfg.v2.prelaunch_burn_from_block == 0 + # 2026-11-13: the decided pre-launch burn vote is typed (inert post-v2) + assert cfg.v2.prelaunch_burn_from_block == 9486900 + assert predecision_main(tmp_path).v2.prelaunch_burn_from_block == 0 assert cfg.activation.threshold == 0.51 assert prelaunch_burn_feature(cfg).name == "prelaunch-95-r2" @@ -45,8 +50,8 @@ def test_old_release_votes_cannot_activate_the_new_release_burn(): assert current.locked and current.signed == ("updated",) -def test_calendar_uses_the_same_finalized_era_for_live_and_late_nodes(): - cfg = load_chain_config() +def test_calendar_uses_the_same_finalized_era_for_live_and_late_nodes(tmp_path): + cfg = predecision_main(tmp_path) raw = effective_raw(cfg) raw["netconfig"]["owner_hotkey"] = cfg.manifest.validator_hotkey cfg = parse_chain_config(raw) @@ -64,8 +69,8 @@ def test_calendar_uses_the_same_finalized_era_for_live_and_late_nodes(): assert resolve_calendar_launch(first, object()) is first -def test_a_node_observing_launch_immediately_needs_only_recent_timestamps(): - cfg = load_chain_config() +def test_a_node_observing_launch_immediately_needs_only_recent_timestamps(tmp_path): + cfg = predecision_main(tmp_path) raw = effective_raw(cfg) raw["netconfig"]["owner_hotkey"] = cfg.manifest.validator_hotkey cfg = parse_chain_config(raw) @@ -93,8 +98,8 @@ def test_launch_requires_an_unambiguous_utc_timestamp(bad): launch_timestamp(bad) -def test_failed_or_nonfinite_chain_time_does_not_guess_a_launch(): - cfg = load_chain_config() +def test_failed_or_nonfinite_chain_time_does_not_guess_a_launch(tmp_path): + cfg = predecision_main(tmp_path) chain = _chain(launch_timestamp(cfg.v2.launch_at_utc), 30001) chain.block_timestamp = lambda _: float("nan") with pytest.raises(ValueError, match="unresolved"): @@ -105,9 +110,9 @@ def test_failed_or_nonfinite_chain_time_does_not_guess_a_launch(): assert resolve_calendar_launch(typed, object()) is typed -def test_service_start_refuses_a_dated_launch_whose_v2_config_cannot_load(): +def test_service_start_refuses_a_dated_launch_whose_v2_config_cannot_load(tmp_path): # A deployment missing the main-validator pin is refused before launch. - cfg = load_chain_config() + cfg = predecision_main(tmp_path) head = SimpleNamespace(finalized_block=lambda: 30000) check_calendar_launch(cfg, head) assert cfg.netconfig.owner_hotkey == cfg.manifest.validator_hotkey @@ -118,7 +123,7 @@ def test_service_start_refuses_a_dated_launch_whose_v2_config_cannot_load(): check_calendar_launch(replace(cfg, v2=replace(cfg.v2, launch_at_utc="")), object()) -def test_shipped_file_starts_after_the_launch_date_with_the_typed_v2b_switch(): +def test_predecision_file_starts_after_the_launch_date_with_the_typed_v2b_switch(tmp_path): # 2026-11-13 regression: main types [v2] image_decoupled_from_block = # 9493200 (#457); v2 launched 2026-10-30 at 9399600. The startup trial # armed v2 at the era AFTER the head (9500400) and refused every node @@ -126,7 +131,7 @@ def test_shipped_file_starts_after_the_launch_date_with_the_typed_v2b_switch(): # from_block=9500400". from cascade.shared.activation import apply_activation - shipped = load_chain_config() + shipped = predecision_main(tmp_path) # the pre-2026-11-13 main (v2 launch still dated) assert shipped.v2.image_decoupled_from_block == 9493200 and not shipped.v2.from_block live = apply_activation(shipped, 9136800) # the fleet's 900 grid launch = launch_timestamp(shipped.v2.launch_at_utc) @@ -155,8 +160,8 @@ def boom(block): check_calendar_launch(parse_chain_config(raw), dark) -def test_a_pruned_late_joiner_gets_an_actionable_error_not_a_guess(): - cfg = load_chain_config() +def test_a_pruned_late_joiner_gets_an_actionable_error_not_a_guess(tmp_path): + cfg = predecision_main(tmp_path) raw = effective_raw(cfg) raw["netconfig"]["owner_hotkey"] = cfg.manifest.validator_hotkey cfg = parse_chain_config(raw) @@ -173,9 +178,9 @@ def pruned(block): resolve_calendar_launch(cfg, chain) -def test_an_enforced_v2_gate_cannot_be_configured_with_a_capped_sweep(): +def test_an_enforced_v2_gate_cannot_be_configured_with_a_capped_sweep(tmp_path): # Launch benchmarks must be complete: a capped sweep is partial by design. - cfg = load_chain_config() + cfg = predecision_main(tmp_path) raw = effective_raw(cfg) raw["netconfig"]["owner_hotkey"] = cfg.manifest.validator_hotkey raw["v2"]["from_block"] = 43200 @@ -183,3 +188,51 @@ def test_an_enforced_v2_gate_cannot_be_configured_with_a_capped_sweep(): raw["eval"]["cascade_bench_max_series"] = 3 with pytest.raises(ValueError, match="complete benchmark suites"): parse_chain_config(raw) + + +def test_shipped_main_restarts_on_a_lite_rpc_without_reading_history(tmp_path): + # 2026-11-13: an external validator restarting on main had no archive RPC. + # Main now types the decided rollover and the resolved launch, so neither + # the startup trial nor the first poll reads a single block timestamp. + from dataclasses import fields, is_dataclass + + from cascade.shared.activation import ( + apply_activation, + apply_prelaunch_burn_activation, + configured_rollover, + ) + + shipped = load_chain_config() + assert configured_rollover(shipped) == 9136800 + assert (shipped.v2.from_block, shipped.v2.commit_from_block) == (9399600, 9395601) + head = 9502000 + + def no_history(block): + raise AssertionError(f"read the timestamp of block {block}") + + lite = SimpleNamespace(finalized_block=lambda: head, block_timestamp=no_history, + current_block=lambda: head) + check_calendar_launch(shipped, lite) + assert resolve_calendar_launch(shipped, lite) is shipped + # ... and it is exactly what a node on the pre-2026-11-13 file computes once + # the fleet's rollover applies and the dated launch resolves on the real + # chain: 9395601 stamped 09:00:00Z, 9396501 the 12:00:00Z crossing. + before = predecision_main(tmp_path) + opening = launch_timestamp(before.v2.commit_open_at_utc) + real = SimpleNamespace(finalized_block=lambda: head, + block_timestamp=lambda b: opening + (b - 9395601) * 12.0) + live = apply_prelaunch_burn_activation( + resolve_calendar_launch(apply_activation(before, 9136800), real), 9486900) + assert (live.v2.from_block, live.v2.commit_from_block) == (9399600, 9395601) + diffs = [] + for f in fields(shipped): + a, b = getattr(live, f.name), getattr(shipped, f.name) + if f.name == "raw": + continue + if is_dataclass(a): + diffs += [(f.name, g.name) for g in fields(a) if getattr(a, g.name) != getattr(b, g.name)] + elif a != b: + diffs.append((f.name,)) + # the only difference is the marker that the rollover was resolved, not typed + assert diffs == [("activation", "resolved_block"), + ("activation", "resolved_prelaunch_burn_block")] diff --git a/tests/unit/test_cohort_duel.py b/tests/unit/test_cohort_duel.py index 1f6e69f2..e60ae56a 100644 --- a/tests/unit/test_cohort_duel.py +++ b/tests/unit/test_cohort_duel.py @@ -763,6 +763,14 @@ def _mainnet(): return load_chain_config(Path(__file__).resolve().parents[2] / "chain.toml") +def _predecision(tmp_path): + """chain.toml as shipped BEFORE 2026-11-13 typed the decided rollover (the + rollover keys back at 0, resolved by ``apply_activation``).""" + from .predecision_main import predecision_main + + return predecision_main(tmp_path) + + def _mainnet_rollover(cfg) -> int: """A rollover block the mainnet [activation] could resolve: a boundary of the loaded 3600 grid, an era start of the 900 × 4 grid after it, after @@ -771,8 +779,8 @@ def _mainnet_rollover(cfg) -> int: return (int(cfg.scoring.cohort_maxt_from_block) // era + 10) * era -def test_mainnet_v2_cohort_settlement_is_judged_in_increment_units(cfg): - """chain.toml types ``cohort_maxt_increment_from_block = 0``, but the +def test_mainnet_v2_cohort_settlement_is_judged_in_increment_units(cfg, tmp_path): + """chain.toml typed ``cohort_maxt_increment_from_block = 0``, but the mainnet era machinery is decided on chain (DEC-CA-0045) and ``apply_activation`` fills that key with the rollover block (the era king is judged under the increment-unit max-T, load-checked ``era_king >= @@ -790,8 +798,15 @@ def test_mainnet_v2_cohort_settlement_is_judged_in_increment_units(cfg): from cascade.shared.config import cohort_maxt_increment_active from cascade.validator.loop import ValidatorRunner - main = _mainnet() - assert main.scoring.cohort_maxt_increment_from_block == 0 # as typed + # the release now TYPES the decided rollover (2026-11-13) — what + # apply_activation filled in from the vote on the pre-decision file + shipped = _mainnet() + assert shipped.scoring.cohort_maxt_increment_from_block == 9136800 + assert shipped.scoring.era_king_from_block == shipped.round.rolling_from_block == 9136800 + main = _predecision(tmp_path) + assert main.scoring.cohort_maxt_increment_from_block == 0 # as typed pre-decision + assert (apply_activation(main, 9136800).scoring.cohort_maxt_increment_from_block + == shipped.scoring.cohort_maxt_increment_from_block) R = _mainnet_rollover(main) resolved = apply_activation(main, R) s = resolved.scoring @@ -846,7 +861,7 @@ def fake_eval(entry, windows): assert C.check_duel_cohort(receipt, typed).status == C.FAIL -def test_mainnet_cohort_increment_gate_cannot_trail_the_rollover(cfg): +def test_mainnet_cohort_increment_gate_cannot_trail_the_rollover(cfg, tmp_path): """Why chain.toml does not type ``cohort_maxt_increment_from_block = "v2"``: a gate later than the rollover breaks the load-checked ordering ``era_king >= cohort_maxt_increment`` — ``apply_activation`` refuses it — and would @@ -855,7 +870,7 @@ def test_mainnet_cohort_increment_gate_cannot_trail_the_rollover(cfg): from cascade.shared.activation import apply_activation - main = _mainnet() + main = _predecision(tmp_path) # the vote-open file: a hypothetical rollover R R = _mainnet_rollover(main) assert apply_activation(main, R).scoring.cohort_maxt_increment_from_block == R era_after = 900 * int(main.round.era_settlements) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 54fc133d..a1216162 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -292,15 +292,17 @@ def test_activation_block_must_be_on_both_grids(tmp_path): import pytest - from cascade.shared.config import DEFAULT_CHAIN_TOML, load_chain_config + from cascade.shared.config import load_chain_config + from tests.unit.predecision_main import predecision_text # The shipped template types the decided v2b switch (an era start on the # live 900 grid); the synthetic 3600-grid seam below is not that grid. src = re.sub(r"^image_decoupled_from_block\s*=.*$", "image_decoupled_from_block = 0", - DEFAULT_CHAIN_TOML.read_text(), flags=re.M) + predecision_text(), flags=re.M) p = tmp_path / "chain.toml" - # The shipped template is in the steady state (0/0); build the scheduled - # pair explicitly so the test does not depend on a live seam being armed. + # The pre-decision release (the shipped one now types the decided 3600 → + # 900 seam) is in the steady state (0/0); build the scheduled pair + # explicitly so the test does not depend on a live seam being armed. steady = ("epoch_blocks_prev = 0\nepoch_activation_block = 0") assert steady in src @@ -321,10 +323,12 @@ def test_cadence_keys_must_be_set_together(tmp_path): import pytest - from cascade.shared.config import DEFAULT_CHAIN_TOML, load_chain_config + from cascade.shared.config import load_chain_config + from tests.unit.predecision_main import predecision_text + # the pre-decision release: its seam keys are in the steady state (0/0) src = re.sub(r"^image_decoupled_from_block\s*=.*$", "image_decoupled_from_block = 0", - DEFAULT_CHAIN_TOML.read_text(), flags=re.M) + predecision_text(), flags=re.M) p = tmp_path / "chain.toml" steady = ("epoch_blocks_prev = 0\nepoch_activation_block = 0") p.write_text(src.replace(steady, "epoch_blocks_prev = 0\n" diff --git a/tests/unit/test_dec55_config_wire.py b/tests/unit/test_dec55_config_wire.py index af3b74fb..a2d8daa4 100644 --- a/tests/unit/test_dec55_config_wire.py +++ b/tests/unit/test_dec55_config_wire.py @@ -27,11 +27,13 @@ CKPT = format_trained_pointer("cascade/ckpt@sha256:" + "b" * 64) -def test_mainnet_and_testnet_values(): +def test_mainnet_and_testnet_values(tmp_path): v2 = load_chain_config(ROOT / "chain.toml").v2 assert (v2.random_init_eras, v2.reign_warm_start_blocks, v2.prelaunch_burn, v2.carryover_king_share, v2.king_ladder) == (14, 21600, 0.95, 0.05, True) - assert v2.prelaunch_burn_from_block == 0 # decided by the vote + assert v2.prelaunch_burn_from_block == 9486900 # decided by the vote, typed 2026-11-13 + from tests.unit.predecision_main import predecision_main + assert predecision_main(tmp_path).v2.prelaunch_burn_from_block == 0 # the vote decided it t = load_chain_config(ROOT / "chain.testnet.toml").v2 assert (t.random_init_eras, t.reign_warm_start_blocks, t.prelaunch_burn, t.carryover_king_share, t.king_ladder) == (0, 0, 0.0, 0.0, False) @@ -64,8 +66,17 @@ def test_loader_refuses_non_bool_ladder(tmp_path): # ── the pre-launch burn vote ───────────────────────────────────────────────── -def test_prelaunch_feature_rides_the_note(): - cfg = load_chain_config(ROOT / "chain.toml") +def test_prelaunch_feature_rides_the_note(tmp_path): + from tests.unit.predecision_main import predecision_main + + # the shipped release types the decided block (2026-11-13): an owner + # override, nothing signalled for it + shipped = load_chain_config(ROOT / "chain.toml") + s = A.prelaunch_burn_feature(shipped) + assert s.name == "prelaunch-95-r2" and not s.decided_on_chain and s.typed_block == 9486900 + assert "prelaunch-95-r2" not in (A.own_signal_payload(shipped, A.ActivationRecord()) or "") + # the vote itself, on the pre-decision release file + cfg = predecision_main(tmp_path) spec = A.prelaunch_burn_feature(cfg) assert spec is not None and spec.name == "prelaunch-95-r2" and spec.decided_on_chain assert A.prelaunch_burn_feature_name(0.955) == "prelaunch-95.5" @@ -81,6 +92,7 @@ def test_prelaunch_feature_rides_the_note(): def test_note_never_exceeds_the_commitment_field(): cfg = load_chain_config(ROOT / "chain.toml") + cfg = replace(cfg, v2=replace(cfg.v2, prelaunch_burn_from_block=0)) # the burn vote open lp = A.ActivationRecord(feature=A.primary_feature(cfg).name, lock_block=91830000, activation_block=91833600) lf = A.ActivationRecord(feature=A.forfeit_feature(cfg).name, lock_block=91854000, @@ -93,6 +105,7 @@ def test_note_never_exceeds_the_commitment_field(): def test_apply_prelaunch_activation_is_one_way_and_boundary_aligned(): cfg = load_chain_config(ROOT / "chain.toml") + cfg = replace(cfg, v2=replace(cfg.v2, prelaunch_burn_from_block=0)) # the burn vote open eb = cfg.round.epoch_blocks b = 2600 * eb armed = A.apply_prelaunch_burn_activation(cfg, b) diff --git a/tests/unit/test_dec55_credentials_audit.py b/tests/unit/test_dec55_credentials_audit.py index b795c276..aa311771 100644 --- a/tests/unit/test_dec55_credentials_audit.py +++ b/tests/unit/test_dec55_credentials_audit.py @@ -14,9 +14,10 @@ from cascade.funding.intake import FundingIntake, canonical_fund_message from cascade.funding.queue import FundedQueue from cascade.funding.vault import PayerKeyVault -from cascade.shared.config import load_chain_config from cascade.shared.era import era_for_block, era_length_blocks, v2_first_era, v2_first_warm_era +from .predecision_main import predecision_main + ROOT = Path(__file__).resolve().parents[2] HK = "5FakeHotkeyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" REF = "cascade-gen-abc123@sha256:" + "a" * 64 @@ -101,8 +102,9 @@ def test_route_is_wired(tmp_path): assert it.vault.get(HK) == "sk-new" -def test_audit_accepts_only_legal_seed_epoch_starts(): - cfg = load_chain_config(ROOT / "chain.toml") +def test_audit_accepts_only_legal_seed_epoch_starts(tmp_path): + # synthetic v2 gate on the pre-decision main (no typed rollover / v2 launch) + cfg = predecision_main(tmp_path) era = era_length_blocks(cfg.round, 0) gate = 10_000 * era cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate), diff --git a/tests/unit/test_dec55_emission.py b/tests/unit/test_dec55_emission.py index 47c00b7a..683d6629 100644 --- a/tests/unit/test_dec55_emission.py +++ b/tests/unit/test_dec55_emission.py @@ -29,6 +29,8 @@ @pytest.fixture(scope="module") def typed(): cfg = load_chain_config(ROOT / "chain.toml") + # the live cadence, with the burn VOTE reopened (typed 2026-11-13) + cfg = replace(cfg, v2=replace(cfg.v2, prelaunch_burn_from_block=0)) era = cfg.round.epoch_blocks * cfg.round.era_settlements gate = 10_000 * era cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate), diff --git a/tests/unit/test_dec55_key_slash_escalation.py b/tests/unit/test_dec55_key_slash_escalation.py index bd34756b..ac3dda67 100644 --- a/tests/unit/test_dec55_key_slash_escalation.py +++ b/tests/unit/test_dec55_key_slash_escalation.py @@ -19,7 +19,8 @@ from cascade.shared.settlement_state import settlement_state from cascade.validator.state import ChampionState, apply_forfeit, dumps, loads -from .test_dec55_validator import KING, ROOT, _Client, _manifest, _receipt, _runner +from .predecision_main import predecision_main +from .test_dec55_validator import KING, _Client, _manifest, _receipt, _runner LANE_A = 0.5 # lane A's pool on this config (lane B live) @@ -27,10 +28,10 @@ @pytest.fixture def env(tmp_path): """The DEC-CA-0055 validator fixture (``test_dec55_validator.env``).""" - from cascade.shared.config import load_chain_config from cascade.shared.era import era_length_blocks - cfg = load_chain_config(ROOT / "chain.toml") + # synthetic v2 gate on the pre-decision main (no typed rollover / v2 launch) + cfg = predecision_main(tmp_path) era = era_length_blocks(cfg.round, 0) gate = 10_000 * era cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate), diff --git a/tests/unit/test_dec55_validator.py b/tests/unit/test_dec55_validator.py index acb36420..1f640b3a 100644 --- a/tests/unit/test_dec55_validator.py +++ b/tests/unit/test_dec55_validator.py @@ -14,7 +14,6 @@ from cascade.audit.checks import check_weights from cascade.shared.bench_report import BenchEntry, BenchReport -from cascade.shared.config import load_chain_config from cascade.shared.emission import COURT_BLOCKS from cascade.shared.era import ( EraSpec, @@ -37,6 +36,8 @@ from cascade.validator.loop import ValidatorRunner from cascade.validator.state import ChampionState +from .predecision_main import predecision_main + ROOT = Path(__file__).resolve().parents[2] KING, OLD = "5King", "5Old" CID = "alice/gen@sha256:" + "a" * 64 @@ -67,7 +68,8 @@ def set_weight_vector(self, vec): @pytest.fixture def env(tmp_path): - cfg = load_chain_config(ROOT / "chain.toml") + # synthetic v2 gate on the pre-decision main (no typed rollover / v2 launch) + cfg = predecision_main(tmp_path) era = era_length_blocks(cfg.round, 0) gate = 10_000 * era cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate), diff --git a/tests/unit/test_decided_votes.py b/tests/unit/test_decided_votes.py index 3d5fb702..8d52919f 100644 --- a/tests/unit/test_decided_votes.py +++ b/tests/unit/test_decided_votes.py @@ -62,9 +62,10 @@ def test_repo_chain_toml_and_record_are_in_sync(shipped, record): assert kinds["forfeit-42b1a486"] == D.OK assert kinds["margin-v2-d30eb443"] == D.OK assert kinds["lad-556b1b98"] == D.OK - for held in ("rolling-era-king", "prelaunch-95-r2"): - assert kinds[held] == D.HELD - assert record.vote(held).reason + assert kinds["rolling-era-king"] == D.OK # typed 2026-11-13 (9136800) + assert record.vote("rolling-era-king").type_it + assert kinds["prelaunch-95-r2"] == D.OK # typed 2026-11-13 (9486900) + assert not [v.feature for v in record.votes if not v.type_it] # nothing held assert all(f.kind == D.OK for f in _by(findings, "pins")) assert {f.key for f in _by(findings, "pins")} == set(D.PIN_KEYS) assert record.as_of_block >= max(v.activation_block for v in record.votes @@ -123,16 +124,34 @@ def test_typed_block_that_disagrees_with_the_decision_is_mismatch(shipped, recor def test_held_row_is_never_asked_for(shipped, record): - # rolling is open in the shipped file and HELD in the record: no TYPE - (f,) = _by(D.drift_report(shipped, record), "rolling-era-king") - assert f.kind == D.HELD and not f.fails - assert "9374400" in f.detail # the known disagreement - # ... but a typed value that is not the recorded one is still a MISMATCH + # HELD rows against the pre-2026-11-13 file (rollover + burn vote untyped): + # the guard reports them with their reason and never asks to type them + import tempfile + + from tests.unit.predecision_main import predecision_text + held = D.DecidedRecord(as_of_block=record.as_of_block, votes=( + D.DecidedVote("rolling-era-king", 9133200, 9136800, "config", type_it=False, + reason="nodes disagree: 9374400→9378000"), + D.DecidedVote("prelaunch-95-r2", 9486000, 9486900, "tally", type_it=False, + reason="owner decision pending"),)) + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "c.toml" + p.write_text(predecision_text()) + untyped = load_chain_config(p) + (f,) = _by(D.drift_report(untyped, held), "rolling-era-king") + assert f.kind == D.HELD and not f.fails and "9374400" in f.detail + (f,) = _by(D.drift_report(untyped, held), "prelaunch-95-r2") + assert f.kind == D.HELD and not f.fails and "owner decision pending" in f.detail + + +def test_typed_rollover_other_than_the_decided_one_is_a_mismatch(shipped, record): + # the external node's own resolution (9378000) typed instead: MISMATCH cfg = replace(shipped, round=replace(shipped.round, rolling_from_block=9378000, epoch_blocks=900, epoch_blocks_prev=3600, epoch_activation_block=9378000)) (f,) = _by(D.drift_report(cfg, record), "rolling-era-king") - assert f.kind == D.MISMATCH + assert f.kind == D.MISMATCH and f.fails + assert f.expected == 9136800 and f.actual == 9378000 def test_pin_drift(shipped, record): @@ -153,8 +172,18 @@ def test_stale_row_and_unknown_feature(shipped, record): kinds = {f.feature: f.kind for f in findings} assert kinds["v2b-deadbeef"] == D.STALE assert kinds["promotion-carry"] == D.UNKNOWN - # the open votes of the config with no row at all are reported as pending - assert kinds["rolling-era-king"] == D.PENDING and kinds["prelaunch-95-r2"] == D.PENDING + # the shipped file has no open vote left (all typed 2026-11-13) ... + assert "prelaunch-95-r2" not in kinds and "rolling-era-king" not in kinds + # ... while open votes with no row at all are reported as pending + import tempfile + + from tests.unit.predecision_main import predecision_text + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "c.toml" + p.write_text(predecision_text()) + untyped = load_chain_config(p) + kinds = {f.feature: f.kind for f in D.drift_report(untyped, stale)} + assert kinds["prelaunch-95-r2"] == D.PENDING and kinds["rolling-era-king"] == D.PENDING def test_state_report(record): @@ -211,7 +240,7 @@ def test_cli_in_sync_and_drifted(tmp_path, capsys): rc = audit_main(["decided", "--config", str(REPO / "chain.toml")]) out = capsys.readouterr().out - assert rc == 0 and "in sync" in out and "[HELD] prelaunch-95-r2" in out + assert rc == 0 and "in sync" in out and "[HELD]" not in out # nothing held since 2026-11-13 src = re.sub(r"(?m)^image_decoupled_from_block\s*=\s*\d+", "image_decoupled_from_block = 0", (REPO / "chain.toml").read_text(encoding="utf-8")) diff --git a/tests/unit/test_era.py b/tests/unit/test_era.py index 16b65cc6..b9bfd609 100644 --- a/tests/unit/test_era.py +++ b/tests/unit/test_era.py @@ -11,10 +11,20 @@ from cascade.shared import era as E from cascade.shared.config import load_chain_config +from tests.unit.predecision_main import predecision_main, predecision_path REPO = Path(__file__).resolve().parents[2] +def _pre(tmp_path: Path) -> Path: + """The pre-decision release chain.toml (rollover untyped) — the base the + synthetic small-block loader scenarios below are built on. Written to its + own subdirectory so ``_toml_with`` never overwrites its source.""" + d = tmp_path / "predecision" + d.mkdir(exist_ok=True) + return predecision_path(d) + + def _armed(cfg, rollover: int, *, settlements: int = 4, prev: int = 0): round_cfg = replace(cfg.round, rolling_from_block=rollover, era_settlements=settlements, @@ -185,15 +195,27 @@ def _fmt(v): return p -def test_shipped_tomls_parse_the_knobs(): +def test_shipped_tomls_parse_the_knobs(tmp_path): main = load_chain_config(REPO / "chain.toml") - assert main.round.rolling_from_block == 0 - assert main.scoring.era_king_from_block == 0 - assert main.scoring.tenure_blocks_from_block == 0 + # the decided DEC-CA-0043 rollover is typed into the release (2026-11-13) + assert main.round.rolling_from_block == 9136800 + assert main.scoring.era_king_from_block == 9136800 + assert main.scoring.tenure_blocks_from_block == 9136800 assert main.round.era_settlements == 4 - assert main.scoring.margin_warmup_blocks == 8 * main.round.epoch_blocks - assert main.scoring.cascade_reign_blocks == main.scoring.cascade_reign_days * main.round.epoch_blocks - assert main.scoring.king_resync_max_blocks == main.scoring.king_resync_max_rounds * main.round.epoch_blocks + # the *_blocks values keep the wall time of the PRE-rollover 3600 grid + assert (main.round.epoch_blocks_prev, main.round.epoch_blocks) == (3600, 900) + assert main.scoring.margin_warmup_blocks == 8 * main.round.epoch_blocks_prev + assert main.scoring.cascade_reign_blocks == main.scoring.cascade_reign_days * main.round.epoch_blocks_prev + assert main.scoring.king_resync_max_blocks == main.scoring.king_resync_max_rounds * main.round.epoch_blocks_prev + # before it was typed every rollover key was 0 and the grid was 3600 + pre = predecision_main(tmp_path) + assert pre.round.rolling_from_block == 0 + assert pre.scoring.era_king_from_block == 0 + assert pre.scoring.tenure_blocks_from_block == 0 + assert pre.round.era_settlements == 4 + assert pre.scoring.margin_warmup_blocks == 8 * pre.round.epoch_blocks + assert pre.scoring.cascade_reign_blocks == pre.scoring.cascade_reign_days * pre.round.epoch_blocks + assert pre.scoring.king_resync_max_blocks == pre.scoring.king_resync_max_rounds * pre.round.epoch_blocks test = load_chain_config(REPO / "chain.testnet.toml") eb = test.round.epoch_blocks # armed at the first ERA boundary (the rollover must start an era) @@ -209,7 +231,7 @@ def test_shipped_tomls_parse_the_knobs(): def test_loader_rejects_unequal_rollover_keys(tmp_path): - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=7200, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=7200, era_king_from_block=10800, tenure_blocks_from_block=7200, cohort_maxt_from_block=1, cohort_maxt_increment_from_block=1) with pytest.raises(ValueError, match="same block"): @@ -217,14 +239,14 @@ def test_loader_rejects_unequal_rollover_keys(tmp_path): def test_loader_rejects_a_partially_set_rollover(tmp_path): - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=7200, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=7200, cohort_maxt_from_block=1, cohort_maxt_increment_from_block=1) with pytest.raises(ValueError, match="ONE block"): load_chain_config(p) def test_loader_rejects_a_misaligned_rollover(tmp_path): - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=3601, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=3601, era_king_from_block=3601, tenure_blocks_from_block=3601, cohort_maxt_from_block=1, cohort_maxt_increment_from_block=1) with pytest.raises(ValueError, match="boundary"): @@ -234,7 +256,7 @@ def test_loader_rejects_a_misaligned_rollover(tmp_path): def test_loader_requires_the_rollover_on_both_grids(tmp_path): # rollover 10800 is a boundary of 3600 but the grid switch to 900 must # land on the same block - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=10800, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=10800, era_king_from_block=10800, tenure_blocks_from_block=10800, epoch_blocks=900, epoch_blocks_prev=3600, epoch_activation_block=14400, @@ -244,7 +266,7 @@ def test_loader_requires_the_rollover_on_both_grids(tmp_path): def test_loader_requires_the_stack_order(tmp_path): - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=14400, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=14400, era_king_from_block=14400, tenure_blocks_from_block=14400, cohort_maxt_from_block=1, cohort_maxt_increment_from_block=28800) with pytest.raises(ValueError, match="stacks on"): @@ -254,7 +276,7 @@ def test_loader_requires_the_stack_order(tmp_path): def test_loader_requires_the_rollover_to_start_an_era(tmp_path): # 7200 is a boundary of the 3600 grid but not of the 4-settlement era # grid (14400): the first era would have started 7200 blocks earlier. - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=7200, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=7200, era_king_from_block=7200, tenure_blocks_from_block=7200, cohort_maxt_from_block=1, cohort_maxt_increment_from_block=1) with pytest.raises(ValueError, match="start an era"): @@ -265,7 +287,7 @@ def test_loader_requires_the_rollover_to_switch_the_grid(tmp_path): # every rollover key set and aligned, but no epoch_blocks_prev / # epoch_activation_block: rolling intake would run on the 12h grid with # 48h eras, silently - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=14400, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=14400, era_king_from_block=14400, tenure_blocks_from_block=14400, cohort_maxt_from_block=3600, cohort_maxt_increment_from_block=7200) with pytest.raises(ValueError, match="switch the grid"): @@ -277,12 +299,12 @@ def test_loader_requires_rent_and_the_jit_king_for_rolling(tmp_path): tenure_blocks_from_block=14400, epoch_blocks=900, epoch_blocks_prev=3600, epoch_activation_block=14400, cohort_maxt_from_block=3600, cohort_maxt_increment_from_block=7200) - assert load_chain_config(_toml_with(REPO / "chain.toml", tmp_path, **recipe)) + assert load_chain_config(_toml_with(_pre(tmp_path), tmp_path, **recipe)) with pytest.raises(ValueError, match="funded_pods"): - load_chain_config(_toml_with(REPO / "chain.toml", tmp_path, funded_pods="off", + load_chain_config(_toml_with(_pre(tmp_path), tmp_path, funded_pods="off", **recipe)) with pytest.raises(ValueError, match="funded_king_rent"): - load_chain_config(_toml_with(REPO / "chain.toml", tmp_path, + load_chain_config(_toml_with(_pre(tmp_path), tmp_path, funded_king_rent=False, **recipe)) @@ -304,7 +326,7 @@ def test_resync_valve_keeps_its_wall_time_across_the_grid_switch(cfg): def test_loader_accepts_the_rollover_recipe(tmp_path): - p = _toml_with(REPO / "chain.toml", tmp_path, rolling_from_block=14400, + p = _toml_with(_pre(tmp_path), tmp_path, rolling_from_block=14400, era_king_from_block=14400, tenure_blocks_from_block=14400, epoch_blocks=900, epoch_blocks_prev=3600, epoch_activation_block=14400, diff --git a/tests/unit/test_fix_launch_review.py b/tests/unit/test_fix_launch_review.py index b3c342ae..943bf81a 100644 --- a/tests/unit/test_fix_launch_review.py +++ b/tests/unit/test_fix_launch_review.py @@ -15,20 +15,21 @@ from cascade.miner.training_context import UnresolvedLaunch, configured_context from cascade.miner.verify import VerifyReport -from cascade.shared.config import load_chain_config, parse_chain_config +from cascade.shared.config import parse_chain_config from cascade.shared.manifest import TrainedEntry, contract_digest from cascade.shared.netconfig import effective_raw from cascade.trainer import sandbox as sandbox_mod from cascade.trainer.corpus import CorpusError from cascade.trainer.worker import _pod_fault +from tests.unit.predecision_main import predecision_main, predecision_path ROOT = Path(__file__).resolve().parents[2] -def _shipped_with_main_validator(): - """chain.toml as shipped (dated launch, from_block 0) plus the one key the - operator must type before launch, so the v2 config loads.""" - cfg = load_chain_config(ROOT / "chain.toml") +def _shipped_with_main_validator(tmp_path): + """chain.toml as shipped before 2026-11-13 (dated launch, from_block 0) plus + the one key the operator must type before launch, so the v2 config loads.""" + cfg = predecision_main(tmp_path) raw = effective_raw(cfg) raw["netconfig"]["owner_hotkey"] = cfg.manifest.validator_hotkey return parse_chain_config(raw) @@ -36,8 +37,8 @@ def _shipped_with_main_validator(): # ── miner tooling judges admission the way the trainer does ────────────────── -def test_at_block_cannot_pin_an_unresolved_dated_launch_offline(): - shipped = load_chain_config(ROOT / "chain.toml") +def test_at_block_cannot_pin_an_unresolved_dated_launch_offline(tmp_path): + shipped = predecision_main(tmp_path) # dated-launch scenario: the pre-2026-11-13 file with pytest.raises(UnresolvedLaunch, match="--at-block"): configured_context(shipped, 9_999_999) assert configured_context(shipped, None).training.base_arch == shipped.training.base_arch @@ -45,11 +46,13 @@ def test_at_block_cannot_pin_an_unresolved_dated_launch_offline(): assert configured_context(typed, 43200 + 1).training is not None -def test_verify_at_block_is_a_usage_error_while_the_launch_is_dated(example_generator_dir, capsys): +def test_verify_at_block_is_a_usage_error_while_the_launch_is_dated(example_generator_dir, capsys, + tmp_path): from cascade.miner.cli import main + # dated-launch scenario: the pre-2026-11-13 file rc = main(["verify", str(example_generator_dir), "--quick", "--at-block", "9999999", - "--json", "--chain-toml", str(ROOT / "chain.toml")]) + "--json", "--chain-toml", str(predecision_path(tmp_path))]) doc = json.loads(capsys.readouterr().out) assert rc == 2 and doc["exit_code"] == 2 assert doc["error"]["code"] == "launch_unresolved" @@ -110,8 +113,8 @@ def test_a_generator_printing_unshare_errors_cannot_relabel_its_own_crash(monkey # ── an enforced gate refuses a capped sweep however it is configured ───────── -def test_capped_sweep_is_refused_while_the_launch_is_still_dated(): - cfg = load_chain_config(ROOT / "chain.toml") +def test_capped_sweep_is_refused_while_the_launch_is_still_dated(tmp_path): + cfg = predecision_main(tmp_path) # dated-launch scenario: the pre-2026-11-13 file raw = effective_raw(cfg) raw["netconfig"]["owner_hotkey"] = cfg.manifest.validator_hotkey raw["eval"]["cascade_bench_max_series"] = 3 @@ -120,10 +123,10 @@ def test_capped_sweep_is_refused_while_the_launch_is_still_dated(): parse_chain_config(raw) -def test_an_overlay_cannot_cap_the_sweep_or_dodge_the_check_by_reverting_the_rule(): +def test_an_overlay_cannot_cap_the_sweep_or_dodge_the_check_by_reverting_the_rule(tmp_path): from cascade.shared.netconfig import NetConfigError, build_config - base = _shipped_with_main_validator() + base = _shipped_with_main_validator(tmp_path) network = {"netuid": base.subnet.netuid, "chain": base.netconfig.network_id} era = base.round.epoch_blocks * base.round.era_settlements diff --git a/tests/unit/test_forfeit_activation.py b/tests/unit/test_forfeit_activation.py index 67b6e5c5..88a147d5 100644 --- a/tests/unit/test_forfeit_activation.py +++ b/tests/unit/test_forfeit_activation.py @@ -18,8 +18,8 @@ import pytest import cascade.shared.activation as A -from cascade.shared.config import load_chain_config from cascade.shared.era import forfeit_successor, forfeited_hotkeys +from tests.unit.predecision_main import predecision_main REPO = Path(__file__).resolve().parents[2] PRIMARY = "rolling-era-king" @@ -29,11 +29,13 @@ @pytest.fixture -def cfg(): - """The shipped chain.toml with its DEC-CA-0048 forfeiture, DEC-CA-0049 - margin-v2 bar and DEC-CA-0055 pre-launch burn DISARMED: these tests model - the plain fleet (none configured) and arm one explicitly.""" - c = load_chain_config(REPO / "chain.toml") +def cfg(tmp_path_factory): + """The release chain.toml as it shipped BEFORE the decided rollover was + typed (rollover still open to the stake vote), with its DEC-CA-0048 + forfeiture, DEC-CA-0049 margin-v2 bar and DEC-CA-0055 pre-launch burn + DISARMED: these tests model the plain fleet (none configured) and arm one + explicitly.""" + c = predecision_main(tmp_path_factory.mktemp("predecision")) return replace(c, scoring=replace(c.scoring, forfeit_hotkeys=(), forfeit_from_block=0, forfeit_successor_hotkey="", win_margin_start_v2=0.0, win_margin_end_v2=0.0, margin_warmup_blocks_v2=0, diff --git a/tests/unit/test_image_decoupled_contract.py b/tests/unit/test_image_decoupled_contract.py index 80afa9c9..93eb31fe 100644 --- a/tests/unit/test_image_decoupled_contract.py +++ b/tests/unit/test_image_decoupled_contract.py @@ -37,6 +37,7 @@ prove_numerics_digest, ) +from .predecision_main import predecision_main, predecision_text from .test_activation import FakeChain, _v REPO = Path(__file__).resolve().parents[2] @@ -770,7 +771,8 @@ def test_mainnet_note_carries_v2b_within_the_field(rollover_typed): rollover resolved from signals); True is the main validator's deployed shape (rolling_from_block typed).""" cfg = load_chain_config(REPO / "chain.toml") - cfg = replace(cfg, v2=replace(cfg.v2, image_decoupled_from_block=0)) # the vote still open + cfg = replace(cfg, v2=replace(cfg.v2, image_decoupled_from_block=0, # the vote still open + prelaunch_burn_from_block=0)) # (typed 2026-11-13) feature = A.primary_feature(cfg).name if rollover_typed: cfg = A.apply_activation(cfg, LIVE_ROLLOVER[1]) @@ -797,13 +799,14 @@ def test_mainnet_note_carries_v2b_within_the_field(rollover_typed): assert len(note.encode("utf-8")) <= A.NOTE_MAX_BYTES -def test_note_with_a_locked_cfg_segment_and_the_v2b_type_rule(): +def test_note_with_a_locked_cfg_segment_and_the_v2b_type_rule(tmp_path): """The DEC-CA-0057 ``cfg-`` overlay segment rides last. With 7-digit blocks a locked cfg- and a locked v2b both fit; past block 10,000,000 (8 digits) the locked v2b is trimmed — the rule: once v2b is DECIDED, type its block into chain.toml (behaviour-identical) and its segment - leaves the note.""" - cfg = load_chain_config(REPO / "chain.toml") + leaves the note. Judged on the pre-decision release (rollover untyped, so + the primary segment rides the note as on a vote-form node).""" + cfg = predecision_main(tmp_path) cfg = replace(cfg, v2=replace(cfg.v2, image_decoupled_from_block=0)) # the vote still open v2b = A.image_decoupled_feature(cfg).name for digits, fits in ((7, True), (8, False)): @@ -829,8 +832,10 @@ def test_note_with_a_locked_cfg_segment_and_the_v2b_type_rule(): def _untyped_blocks(tmp_path): """The release chain.toml (pins set) WITHOUT the typed forfeit / margin-v2 - / v2b blocks — a deployment that applied the pins but not the typed blocks.""" - text = (REPO / "chain.toml").read_text() + / v2b blocks — a deployment that applied the pins but not the typed blocks + (so neither the 2026-11-13 typed rollover / v2 launch: the pre-decision + release text).""" + text = predecision_text() for k in ("forfeit_from_block", "margin_v2_from_block", "image_decoupled_from_block"): text, n = re.subn(rf"(?m)^{k}\s*=.*$", f"{k} = 0", text) assert n == 1, k @@ -885,13 +890,14 @@ def test_the_trainer_never_refuses_for_the_note_budget(tmp_path): assert "v2b_note_problem" not in inspect.getsource(trainer_loop) -def test_note_budget_worst_case_scales_to_8_digit_blocks(): +def test_note_budget_worst_case_scales_to_8_digit_blocks(tmp_path): """Review 4: the worst case uses the digit count blocks reach within the horizon; 8 digits (head unknown, or within ~30 days of 10,000,000) trims the release config's v2b segment beside a locked cfg- overlay — the documented rule: type v2b's block once it is decided (the release now - does; this judges the vote-form config).""" - cfg = load_chain_config(REPO / "chain.toml") + does; this judges the vote-form config — the pre-decision release, whose + rollover is untyped so its primary segment rides the note).""" + cfg = predecision_main(tmp_path) cfg = replace(cfg, v2=replace(cfg.v2, image_decoupled_from_block=0)) rec = _live_records()[0] assert A.v2b_note_problem(cfg, rec, head_block=9_420_000) == "" diff --git a/tests/unit/test_image_decoupled_merge.py b/tests/unit/test_image_decoupled_merge.py index a01c2adc..5a25dc38 100644 --- a/tests/unit/test_image_decoupled_merge.py +++ b/tests/unit/test_image_decoupled_merge.py @@ -173,10 +173,15 @@ def test_note_parts_refactor_keeps_the_note_bytes(tmp_path): """own_signal_payload is built from _own_note_parts: same note as the trim of the parts, for every shape the tests above use.""" rec, fr, mr = _live_records() - for cfg in (_untyped_blocks(tmp_path), load_chain_config(REPO / "chain.toml")): + untyped = _untyped_blocks(tmp_path) + for cfg in (untyped, load_chain_config(REPO / "chain.toml")): for rf, rm in ((fr, mr), (None, None)): note = A.own_signal_payload(cfg, rec, rf, rm, A.ActivationRecord()) - head, extras, tail = A._own_note_parts(cfg, rec, rf, rm, A.ActivationRecord()) + parts = A._own_note_parts(cfg, rec, rf, rm, A.ActivationRecord()) + if parts is None: # every vote typed (release since 2026-11-13): no note + assert cfg is not untyped and note is None + continue + head, extras, tail = parts dropped = A.note_trimmed_segments(cfg, rec, rf, rm, A.ActivationRecord()) kept = [s for s in extras if s.feature not in dropped] assert note == A.format_signals(head, *kept, *tail) diff --git a/tests/unit/test_king_check_audit.py b/tests/unit/test_king_check_audit.py index 3ab83b38..f6b68bc2 100644 --- a/tests/unit/test_king_check_audit.py +++ b/tests/unit/test_king_check_audit.py @@ -27,6 +27,7 @@ from cascade.validator.loop import ValidatorRunner from cascade.validator.state import ChampionState +from .predecision_main import predecision_path from .receipt_fixture import make_scores ROOT = Path(__file__).resolve().parents[2] @@ -56,7 +57,8 @@ def set_weight_vector(self, vec): @pytest.fixture def env(tmp_path): - cfg = load_chain_config(ROOT / "chain.toml") + # hypothetical v2 gate at 10_000 eras: built on the pre-decision release + cfg = load_chain_config(predecision_path(tmp_path)) era = era_length_blocks(cfg.round, 0) gate = 10_000 * era cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate), diff --git a/tests/unit/test_lane_b_merge_state.py b/tests/unit/test_lane_b_merge_state.py index e4c0dcdd..33df005d 100644 --- a/tests/unit/test_lane_b_merge_state.py +++ b/tests/unit/test_lane_b_merge_state.py @@ -222,7 +222,7 @@ def test_f2_strict_rebuild_refuses_overlap_and_non_finite(): check_finite_rows(c, {0: [1]}, names) # other rows are fine -def test_merge_guard_config_and_load_check(): +def test_merge_guard_config_and_load_check(tmp_path): lb = parse_lane_b({"from_block": 1001, "domains": ["energy"]}) assert lb.merge_guard_from_block == 1000 # "v2" default assert lb.merge_guard_at_era(1000) and not lb.merge_guard_at_era(999) @@ -248,7 +248,12 @@ def test_merge_guard_config_and_load_check(): with pytest.raises(ValueError, match="era boundary"): load_chain_config(raw=raw) main = load_chain_config("chain.toml").lane_b - assert main.leg_funding == "miner" and main.merge_guard_from_block == 0 # = v2 (unset) + # 2026-11-13: main types [v2] from_block, so the "v2" default resolves to it + assert main.leg_funding == "miner" and main.merge_guard_from_block == 9399600 # = v2 + from tests.unit.predecision_main import predecision_main + + before = predecision_main(tmp_path).lane_b + assert before.leg_funding == "miner" and before.merge_guard_from_block == 0 # = v2 (unset) def test_validator_era_init_refuses_overlapping_merges_under_the_guard(cfg, tmp_path): diff --git a/tests/unit/test_launch_readiness_regressions.py b/tests/unit/test_launch_readiness_regressions.py index cf415062..ec503577 100644 --- a/tests/unit/test_launch_readiness_regressions.py +++ b/tests/unit/test_launch_readiness_regressions.py @@ -87,10 +87,11 @@ def test_worker_release_requires_matching_fingerprint_and_image_pins(cfg): funded_pod_image="fixture/image@sha256:" + "b" * 64))) -def test_calendar_replay_uses_finalized_time_not_the_receipts_claim(): +def test_calendar_replay_uses_finalized_time_not_the_receipts_claim(tmp_path): + from tests.unit.predecision_main import predecision_main from tests.unit.test_calendar_launch import _chain - cfg = load_chain_config() + cfg = predecision_main(tmp_path) # dated-launch scenario: the pre-2026-11-13 file raw = effective_raw(cfg) raw["netconfig"]["owner_hotkey"] = cfg.manifest.validator_hotkey cfg = parse_chain_config(raw) diff --git a/tests/unit/test_margin_activation.py b/tests/unit/test_margin_activation.py index d596f293..953ccc35 100644 --- a/tests/unit/test_margin_activation.py +++ b/tests/unit/test_margin_activation.py @@ -164,9 +164,12 @@ def test_cohort_maxt_increment_knob_parses_and_defaults_off(tmp_path): cohort_maxt_increment_active, load_chain_config, ) + from tests.unit.predecision_main import predecision_text + # The pre-decision release (rollover untyped): with the decided rollover + # typed, era_king_from_block stacks on this key, so it cannot go absent. bare = re.sub(r"^cohort_maxt_increment_from_block\s*=.*$", "", - DEFAULT_CHAIN_TOML.read_text(), flags=re.M) + predecision_text(), flags=re.M) p = tmp_path / "chain.toml" p.write_text(bare) cfg = load_chain_config(p) diff --git a/tests/unit/test_margin_v2.py b/tests/unit/test_margin_v2.py index e6257cd8..3e3ecea1 100644 --- a/tests/unit/test_margin_v2.py +++ b/tests/unit/test_margin_v2.py @@ -28,6 +28,7 @@ load_chain_config, margin_v2_active, ) +from tests.unit.predecision_main import predecision_main REPO = Path(__file__).resolve().parents[2] PRIMARY = "rolling-era-king" @@ -36,11 +37,12 @@ @pytest.fixture -def cfg(): - """The shipped chain.toml (it carries the v2 bar, decided on chain) with - the DEC-CA-0048 forfeiture and the DEC-CA-0055 pre-launch burn disarmed, - so each test arms what it needs.""" - c = load_chain_config(REPO / "chain.toml") +def cfg(tmp_path_factory): + """The release chain.toml as it shipped BEFORE the decided rollover was + typed (rollover still open to the stake vote; it carries the v2 bar, + decided on chain) with the DEC-CA-0048 forfeiture and the DEC-CA-0055 + pre-launch burn disarmed, so each test arms what it needs.""" + c = predecision_main(tmp_path_factory.mktemp("predecision")) return replace(c, scoring=replace(c.scoring, forfeit_hotkeys=(), forfeit_from_block=0, forfeit_successor_hotkey="", margin_v2_from_block=0), # no v2 schedule ⇒ no DEC-CA-0067 "v2b-" vote either diff --git a/tests/unit/test_netconfig_core.py b/tests/unit/test_netconfig_core.py index 1a5264c7..a6ba8f10 100644 --- a/tests/unit/test_netconfig_core.py +++ b/tests/unit/test_netconfig_core.py @@ -16,9 +16,14 @@ from cascade.shared.config import load_chain_config, parse_chain_config from cascade.shared.hippius import ObjectNotFound +from .predecision_main import predecision_text + REPO = Path(__file__).resolve().parents[2] with (REPO / "chain.toml").open("rb") as _fh: SHIPPED_RAW = tomllib.load(_fh) +# the release as it was before 2026-11-13 typed the decided rollover and the +# v2 launch: the base for hypothetical overlays / votes at test blocks +PREDECISION_RAW = tomllib.loads(predecision_text()) # A typed DEC-CA-0043 rollover (3600 -> 900 grid at R, eras of 4 x 900 after # it), so only the overlay's own cfg- segment is ever voted on in these tests. @@ -38,7 +43,7 @@ def owner_keypair(uri: str = "//Owner"): def base_raw(owner: str | None = None) -> dict: - raw = copy.deepcopy(SHIPPED_RAW) + raw = copy.deepcopy(PREDECISION_RAW) raw["round"].update(rolling_from_block=R, epoch_blocks=900, epoch_blocks_prev=3600, epoch_activation_block=R) raw["scoring"].update(era_king_from_block=R, tenure_blocks_from_block=R, @@ -104,8 +109,14 @@ def test_netconfig_table_defaults_inert_and_refuses_typos(): assert c.netconfig.enabled else: assert c.netconfig.owner_hotkey == "" and not c.netconfig.enabled + # the shipped release (v2 launch typed, validator bench source armed) + # needs its [netconfig] owner: dropping the table is refused raw = copy.deepcopy(SHIPPED_RAW) raw.pop("netconfig", None) + with pytest.raises(ValueError, match="needs \\[netconfig\\] owner_hotkey"): + parse_chain_config(raw) + raw = copy.deepcopy(PREDECISION_RAW) # pre-launch: a missing table is inert + raw.pop("netconfig", None) missing = parse_chain_config(raw).netconfig assert missing.owner_hotkey == "" and not missing.enabled raw["netconfig"] = {"owner_hotkeys": "x"} @@ -385,7 +396,10 @@ def test_config_at_walks_the_history_by_block(): def test_resolved_activation_blocks_survive_an_overlay(): - raw = copy.deepcopy(SHIPPED_RAW) + # the release now TYPES the decided rollover (2026-11-13): nothing to resolve + typed_main = load_chain_config(REPO / "chain.toml") + assert A.configured_rollover(typed_main) == 9136800 and A.resolved_rollover(typed_main) == 0 + raw = copy.deepcopy(PREDECISION_RAW) # the vote-open file the fleet resolved raw["netconfig"] = {"owner_hotkey": OWNER.ss58_address} raw["scoring"].update(forfeit_hotkeys=[], win_margin_start_v2=0.0, win_margin_end_v2=0.0, margin_warmup_blocks_v2=0, forfeit_from_block=0, diff --git a/tests/unit/test_predecision_main.py b/tests/unit/test_predecision_main.py new file mode 100644 index 00000000..43ff9aa1 --- /dev/null +++ b/tests/unit/test_predecision_main.py @@ -0,0 +1,47 @@ +"""The pre-decision helper is the EXACT inverse of the typed decided blocks. + +Scenario fixtures (and the session ``cfg``) run on ``predecision_text()``; if +a later release types another decided block without adding it to +``UNTYPED``, those fixtures would silently run on a partly typed file. This +pins that the helper differs from the shipped file on exactly ``UNTYPED`` and +on nothing else. +""" +from __future__ import annotations + +import tomllib + +from tests.unit.predecision_main import REPO, UNTYPED, predecision_text + + +def _flat(doc, prefix=""): + out = {} + for k, v in doc.items(): + key = f"{prefix}{k}" + if isinstance(v, dict): + out.update(_flat(v, key + ".")) + else: + out[key] = v + return out + + +def test_predecision_main_is_the_exact_inverse(): + shipped = _flat(tomllib.loads((REPO / "chain.toml").read_text())) + before = _flat(tomllib.loads(predecision_text())) + removed = {f"{s}.{k}" for s, k, v in UNTYPED if v is None} + assert shipped.keys() - before.keys() == removed and before.keys() <= shipped.keys() + differ = {k for k in before if shipped[k] != before[k]} | removed + want = {f"{section}.{key}" for section, key, _ in UNTYPED} + # every untyped line really is typed in the release, and nothing else moved + assert differ == want + for section, key, value in UNTYPED: + if value is not None: + assert before[f"{section}.{key}"] == tomllib.loads(f"x = {value}")["x"] + # the decided values themselves (a bad edit to either side fails here) + assert {k: shipped[k] for k in want} == { + "round.epoch_blocks": 900, "round.epoch_blocks_prev": 3600, + "round.epoch_activation_block": 9136800, "round.rolling_from_block": 9136800, + "scoring.cohort_maxt_increment_from_block": 9136800, + "scoring.era_king_from_block": 9136800, "scoring.tenure_blocks_from_block": 9136800, + "v2.commit_from_block": 9395601, "v2.from_block": 9399600, + "v2.prelaunch_burn_from_block": 9486900, + } diff --git a/tests/unit/test_satellite_notorch.py b/tests/unit/test_satellite_notorch.py index 1e7a57f7..57f70a00 100644 --- a/tests/unit/test_satellite_notorch.py +++ b/tests/unit/test_satellite_notorch.py @@ -25,9 +25,12 @@ from pathlib import Path from cascade.shared.config import load_chain_config from tests.unit import test_satellite_replay as T + from tests.unit.predecision_main import predecision_path audit_main._fetch_text = lambda cfg, key: (_ for _ in ()).throw(RuntimeError("offline")) - cfg = T._keyed(load_chain_config(Path("chain.toml"))) + # small fake blocks: replay on the pre-decision release (no typed rollover) + with tempfile.TemporaryDirectory() as pre: + cfg = T._keyed(load_chain_config(predecision_path(pre))) store = T.DictStore() T.make_settlement(cfg, store, 0) with tempfile.TemporaryDirectory() as d: diff --git a/tests/unit/test_shared_ladder.py b/tests/unit/test_shared_ladder.py index 7365f8a4..744fca08 100644 --- a/tests/unit/test_shared_ladder.py +++ b/tests/unit/test_shared_ladder.py @@ -504,7 +504,8 @@ def test_note_carries_the_segment_and_stays_under_the_budget_with_every_mainnet_ assert A.shared_ladder_feature(shipped).name not in ( A.own_signal_payload(shipped, A.ActivationRecord()) or "").split(":") shipped = replace(shipped, eval=replace(shipped.eval, shared_ladder_from_block=0), - v2=replace(shipped.v2, image_decoupled_from_block=0)) # both votes open + v2=replace(shipped.v2, image_decoupled_from_block=0, + prelaunch_burn_from_block=0)) # the votes open (typed 2026-11-13) assert shipped.activation.enabled lad = A.shared_ladder_feature(shipped) v2b = A.image_decoupled_feature(shipped) diff --git a/tests/unit/test_v2_bundle.py b/tests/unit/test_v2_bundle.py index 2ac0f8b4..1f9d9c23 100644 --- a/tests/unit/test_v2_bundle.py +++ b/tests/unit/test_v2_bundle.py @@ -9,6 +9,7 @@ from cascade.shared.config import load_chain_config from cascade.shared.era import bench_gate_configured, bench_gate_mode_at +from tests.unit.predecision_main import predecision_main ROOT = Path(__file__).resolve().parents[2] @@ -18,12 +19,28 @@ def mainnet(): return load_chain_config(ROOT / "chain.toml") -def test_mainnet_bundle_is_inert_until_v2(mainnet): - assert mainnet.v2.from_block == 0 - assert mainnet.training.corpus_mode == "stream_cpu" # the live contract - assert mainnet.training.at_block(10**9).corpus_mode == "stream_cpu" - assert bench_gate_mode_at(mainnet, 10**9) == "off" - assert not bench_gate_configured(mainnet) +@pytest.fixture(scope="module") +def predecision(tmp_path_factory): + """main before 2026-11-13 typed the resolved v2 launch (from_block 0).""" + return predecision_main(tmp_path_factory.mktemp("predecision")) + + +def test_mainnet_bundle_is_inert_until_v2(mainnet, predecision): + # 2026-11-13: main types the resolved launch, so the bundle switches at 9399600 + assert mainnet.v2.from_block == mainnet.training.v2_from_block == 9399600 + assert mainnet.training.corpus_mode == "stream_cpu" # the pre-v2 contract + assert mainnet.training.at_block(9399600 - 1).corpus_mode == "stream_cpu" + assert mainnet.training.at_block(9399600).corpus_mode == "shards" + assert mainnet.training.at_block(10**9).corpus_mode == "shards" + assert bench_gate_mode_at(mainnet, 9399600) == "off" # judges the last pre-v2 era + assert bench_gate_mode_at(mainnet, 10**9) == "enforce" + assert bench_gate_configured(mainnet) + # the pre-decision file stays inert until v2 is typed + assert predecision.v2.from_block == 0 + assert predecision.training.corpus_mode == "stream_cpu" + assert predecision.training.at_block(10**9).corpus_mode == "stream_cpu" + assert bench_gate_mode_at(predecision, 10**9) == "off" + assert not bench_gate_configured(predecision) def test_v2_contract_carries_the_bundle(mainnet): @@ -58,11 +75,12 @@ def test_shards_contract_needs_the_tier(tmp_path): load_chain_config(p) -def test_generation_lead_applies_once_v2_is_typed(mainnet): +def test_generation_lead_applies_once_v2_is_typed(predecision): from types import SimpleNamespace from cascade.trainer.runner_generation import _GenerationMixin + mainnet = predecision # v2 still dated (from_block 0) lead = _GenerationMixin._generation_lead_seconds assert lead(SimpleNamespace(cfg=mainnet)) == 0.0 # v2 not typed: no lead era = mainnet.round.epoch_blocks * mainnet.round.era_settlements @@ -78,11 +96,12 @@ def _typed(mainnet): training=replace(mainnet.training, v2_from_block=gate)), gate, era -def test_settlement_waits_for_the_kings_bench(mainnet): +def test_settlement_waits_for_the_kings_bench(predecision): from types import SimpleNamespace from cascade.trainer.rolling import RollingScheduler + mainnet = predecision # hypothetical typed gate on the pre-decision file cfg, gate, era = _typed(mainnet) me = SimpleNamespace(cfg=cfg) ready = [SimpleNamespace(hotkey="A", bench={"x": 1}), SimpleNamespace(hotkey="B", bench=None)] diff --git a/tests/unit/test_v2_golive.py b/tests/unit/test_v2_golive.py index d3a5351a..88d153ca 100644 --- a/tests/unit/test_v2_golive.py +++ b/tests/unit/test_v2_golive.py @@ -36,12 +36,15 @@ def _text(name="chain.toml"): # ── config: independent era-aligned launches ──────────────────────────────── -def test_mainnet_ships_inert_with_the_v2_contract_scheduled(): +def test_mainnet_ships_inert_with_the_v2_contract_scheduled(tmp_path): + from tests.unit.predecision_main import predecision_main + cfg = load_chain_config(ROOT / "chain.toml") # DEC-CA-0054 rides the go-live: the bench gate enforces from the v2 block # DEC-CA-0055's launch economics ship configured but inert until the block # (the pre-launch burn waits for its on-chain vote: no start block yet) - assert cfg.v2 == V2Config(from_block=0, launch_at_utc="2026-10-30T12:00:00Z", + before = predecision_main(tmp_path) # main before 2026-11-13 typed the launch + assert before.v2 == V2Config(from_block=0, launch_at_utc="2026-10-30T12:00:00Z", commit_open_at_utc="2026-10-30T09:00:00Z", prelaunch_burn_readiness="r2", bench_gate_mode="enforce", random_init_eras=14, @@ -59,13 +62,21 @@ def test_mainnet_ships_inert_with_the_v2_contract_scheduled(): # the vote resolved (locked 9486000, 85.9%): typed # 2026-11-13 to free its note segment image_decoupled_from_block=9493200) - assert not cfg.v2.active(10**9) + assert not before.v2.active(10**9) + assert before.training.at_block(10**9) is before.training + # 2026-11-13: main types the resolved launch; v2-derived blocks follow it + assert cfg.v2 == replace(before.v2, from_block=9399600, commit_from_block=9395601, + bench_gate_v2_from_block=9399600, + bench_source_from_block=9399600, + prelaunch_burn_from_block=9486900) # the decided burn vote, typed + assert cfg.v2.active(9399600) and not cfg.v2.active(9399600 - 1) assert cfg.lane_b.from_block == 0 and not cfg.lane_b.active(10**9) v2c = cfg.training.v2_contract assert v2c is not None and v2c.base_arch == "cascade_pyramid" assert (v2c.router_lr_scale, v2c.route_bias_rate_scale) == (0.1, 0.25) - # inert: every block resolves to the toto2 contract, same object - assert cfg.training.at_block(10**9) is cfg.training + # pre-v2 blocks resolve to the toto2 contract (same object), v2 blocks to v2's + assert cfg.training.at_block(9399600 - 1) is cfg.training + assert cfg.training.at_block(10**9).base_arch == "cascade_pyramid" assert cfg.lane_b.domains == ("econ_fin", "energy", "healthcare", "nature", "sales", "transport", "web_cloudops") assert (cfg.lane_b.full_pay_tiers, cfg.lane_b.half_life_rounds, cfg.lane_b.floor, diff --git a/tests/unit/test_v2_launch_emissions.py b/tests/unit/test_v2_launch_emissions.py index c5d179ef..915a85bd 100644 --- a/tests/unit/test_v2_launch_emissions.py +++ b/tests/unit/test_v2_launch_emissions.py @@ -25,7 +25,8 @@ def typed(): gate = 10_000 * era # the shipped DEC-CA-0055 timing keys zeroed: each test arms what it models cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate, random_init_eras=0, - reign_warm_start_blocks=0), + reign_warm_start_blocks=0, + prelaunch_burn_from_block=0), # typed 2026-11-13: the vote reopened training=replace(cfg.training, v2_from_block=gate)) return cfg, gate, era @@ -72,11 +73,22 @@ def test_carried_king_is_capped_until_a_v2_crowning(typed): assert emission_policy(cfg, boundary=mid + era, king_since_block=mid).inert -def test_inert_by_default(): +def test_inert_by_default(tmp_path): + from tests.unit.predecision_main import predecision_main + + # main before 2026-11-13: v2 untyped and the burn not yet voted ⇒ inert + before = predecision_main(tmp_path) + assert before.v2.prelaunch_burn_from_block == 0 + assert emission_policy(before, boundary=10**9, king_since_block=None).inert + # 2026-11-13: main types v2 at 9399600 ⇒ inert through the settlement AT the + # block, then a pre-v2 king is capped at the carryover share cfg = load_chain_config(ROOT / "chain.toml") - # the shipped chain.toml: v2 untyped and the burn not yet voted ⇒ inert - assert cfg.v2.prelaunch_burn_from_block == 0 - assert emission_policy(cfg, boundary=10**9, king_since_block=None).inert + # the decided burn block is typed, but it follows the v2 block: never applies + assert cfg.v2.prelaunch_burn_from_block == 9486900 > cfg.v2.from_block + assert emission_policy(cfg, boundary=9399600, king_since_block=None).inert + assert (emission_policy(cfg, boundary=10**9, king_since_block=None) + == EmissionPolicy(lane_a_cap=cfg.v2.carryover_king_share) and not + emission_policy(cfg, boundary=10**9, king_since_block=None).inert) assert apply_emission_policy([1.0, 0.0], EmissionPolicy(), burn_uid=0) == [1.0, 0.0] diff --git a/tests/unit/test_v2_submission_window.py b/tests/unit/test_v2_submission_window.py index cfb81ced..907ae610 100644 --- a/tests/unit/test_v2_submission_window.py +++ b/tests/unit/test_v2_submission_window.py @@ -18,13 +18,13 @@ launch_timestamp, resolve_calendar_launch, ) -from cascade.shared.config import load_chain_config +from tests.unit.predecision_main import predecision_main -def test_opening_and_launch_resolve_independently_for_live_and_late_nodes(): +def test_opening_and_launch_resolve_independently_for_live_and_late_nodes(tmp_path): from tests.unit.test_calendar_launch import _chain - cfg = load_chain_config() + cfg = predecision_main(tmp_path) # dated-launch scenario: the pre-2026-11-13 file launch = launch_timestamp(cfg.v2.launch_at_utc) assert launch - launch_timestamp(cfg.v2.commit_open_at_utc) == 3 * 3600 before = resolve_calendar_launch(cfg, _chain(launch, 29100)) @@ -168,11 +168,11 @@ def unavailable(request, timeout): assert calls[0][0][0] == "launch_rules_unavailable" and calls[0][0][2] == 3 -def test_running_intake_remembers_opening_when_the_node_later_prunes_it(): +def test_running_intake_remembers_opening_when_the_node_later_prunes_it(tmp_path): from cascade.funding.main import ChainLaunchAdmission from tests.unit.test_calendar_launch import _chain - cfg = load_chain_config() + cfg = predecision_main(tmp_path) # dated-launch scenario: the pre-2026-11-13 file target = launch_timestamp(cfg.v2.launch_at_utc) chain = _chain(target, 29101) admission = ChainLaunchAdmission(cfg, chain) diff --git a/tests/unit/test_v2b_seed_epoch_switch.py b/tests/unit/test_v2b_seed_epoch_switch.py index f1defbb8..1dd97189 100644 --- a/tests/unit/test_v2b_seed_epoch_switch.py +++ b/tests/unit/test_v2b_seed_epoch_switch.py @@ -30,6 +30,8 @@ v2b_switch_era, ) +from .predecision_main import predecision_main + ROOT = Path(__file__).resolve().parents[2] MAIN, TEST = "v2b-69630f92", "v2b-c1ffa858" # rule /3, quantile head on @@ -37,9 +39,10 @@ @pytest.fixture -def base(): +def base(tmp_path): """Mainnet chain.toml with v2 at a fixed era (ladder on, as shipped).""" - cfg = load_chain_config(ROOT / "chain.toml") + # synthetic v2 gate on the pre-decision main (no typed rollover / v2 launch) + cfg = predecision_main(tmp_path) era = era_length_blocks(cfg.round, 0) gate = 10_000 * era cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate, image_decoupled_from_block=0), diff --git a/tests/unit/test_validator_bench.py b/tests/unit/test_validator_bench.py index f9f81e9a..bb159758 100644 --- a/tests/unit/test_validator_bench.py +++ b/tests/unit/test_validator_bench.py @@ -39,6 +39,8 @@ from cascade.validator.state import genesis from cascade.validator.validator_bench import ValidatorBenchHold +from .predecision_main import predecision_main + CID = "alice/gen@sha256:" + "a" * 64 KING_PTR = format_trained_pointer("cascade/king@sha256:" + "b" * 64) CHAL_PTR = format_trained_pointer("cascade/chal@sha256:" + "c" * 64) @@ -199,13 +201,17 @@ def test_check_bench_source_refusals(): check_bench_source(v2=V2Config(), owner_hotkey="", era_blocks=3600) -def test_mainnet_arms_validator_source_with_v2_testnet_stays_trainer(): +def test_mainnet_arms_validator_source_with_v2_testnet_stays_trainer(tmp_path): from pathlib import Path root = Path(__file__).resolve().parents[2] main = load_chain_config(root / "chain.toml") assert main.v2.bench_source == "validator" - assert main.v2.bench_source_from_block == 0 # "v2" while from_block = 0 + # 2026-11-13: main types [v2] from_block = 9399600, so "v2" resolves to it + assert main.v2.bench_source_from_block == main.v2.from_block == 9399600 + pre = predecision_main(tmp_path) + assert pre.v2.bench_source == "validator" + assert pre.v2.bench_source_from_block == 0 # "v2" while from_block = 0 test = load_chain_config(root / "chain.testnet.toml") assert test.v2.bench_source == "trainer" and test.v2.bench_source_from_block == 0