Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions chain.toml
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,10 @@ expected_gpu = "" # was "NVIDIA L40S" (DEC-CA-0002)
# launch-env injection + the provisioner health gate; digest computed
# orchestrator-side). Folded into contract_digest — re-pin ⇒ trainer AND
# validators restart together at an epoch boundary, pods pull the new image.
train_image_digest = "sha256:43c07d4207d7693517d74203f6783fdd2c2f91e3d475f38db38e4798ae97785e"
# ^ ghcr cascade-worker:worker-v0.9.0 — built 2026-09-16 09:38 UTC from this
train_image_digest = "sha256:205ff68242ae78fc923c8a5c7d2c4a4eccdd8afc7c7739cae99187475ba96d6f"
# ^ LIVE SYNC 2026-11-13: worker-v0.13.0 (main 4fbed80, built 2026-09-28) = the
# deployed pin since 2026-09-28; main still named v0.9.0 below.
# (was) ghcr cascade-worker:worker-v0.9.0 — built 2026-09-16 09:38 UTC from this
# branch (publish-worker run 35080201768): budget_denomination (DEC-CA-0042)
# parsed on the pod, PLUS check_loss_finite (840e900, 2026-09-14) which the
# v0.8.0 image (main@9346a52) never carried — three NaN legs trained to the
Expand Down Expand Up @@ -352,14 +354,14 @@ funded_pods = "rent"
payer_vault_dir = "payer_vault" # cascade-intake --vault-dir (work_root-relative)
funded_pod_sku = "" # unused with funded_pod_skus set (per-round pick)
# Worker image funded pods boot — keep = the fleet's pinned digest.
funded_pod_image = "ghcr.io/tensorlink-ai/cascade-worker:worker-v2.0.0-rc11@sha256:2fd193cad6be7bef87f110246a2074de84cde44511e153a21d1d0301d93cba64"
funded_pod_image = "ghcr.io/tensorlink-ai/cascade-worker:worker-v2.0.0-rc15@sha256:5d41d94bc0c0d3ae3dfd042b0cc7e4d900c7d1f48173808436c12bf764a67641" # LIVE since 2026-10-31 (rc15 = main 5e3fade); synced to main 2026-11-13
# sha256 of that image's cascade/**/*.py (scripts/worker_code_fingerprint.py
# <repo>@<digest>). Every rented final/funded pod must hash its files to this
# BEFORE a leg is dispatched — the digest env a template injects is echoed
# faithfully by a host that started a STALE image under the pinned tag
# (2026-09-12: Lium 91.224.44.222/.223 ran worker-v0.7.0 as "v0.8.0"; three
# miners' legs stalled and were burned). Re-derive on every image re-pin.
worker_code_fingerprint = "33b0d42fad8fa46b93c09ce1abb3d1d07a349905c171b795671cc68f06d7e907" # 271 files, worker-v2.0.0-rc11 (2fd193ca…); registry walk, rc10 walk reproduced its pin 2026-10-30
worker_code_fingerprint = "2d8261aa40e86d3821f8fbeac0d60a0ac7528434a686c17e204c2b20198e293c" # 274 files, worker-v2.0.0-rc15 (5d41d94b…); registry walk = local 5e3fade source = on-pod hash, 2026-10-31
funded_ready_timeout_seconds = 900.0
# How a funded leg's checkpoint leaves its payer pod. "harvest" (default):
# CREDENTIAL-FREE pods — the worker trains --local-only (nothing at all in the
Expand Down Expand Up @@ -1219,7 +1221,7 @@ king_ladder = true
# so a worker-image repin keeps the carried king and the finished legs. 0 =
# decided on chain; a typed era-start block is the owner override. Repin the
# image only AFTER the switch era has started.
image_decoupled_from_block = 0
image_decoupled_from_block = 9493200 # TYPED 2026-11-13 (DEC-CA-0067): = the block the v2b-69630f92 vote resolved (locked in at 9486000, 85.9% of stake, applied at the era start 9493200 = Thu 12 Nov 22:23 UTC) — behaviour-identical; frees its note segment. Without it a validator whose rolling-era-king vote is still signalled overflows the 128-byte note and REFUSES to start (seen 2026-11-13 00:13 UTC).
# Bundled with the same block (part of the vote's hash): the v2 private
# verdict budget becomes this many windows. Inert until the switch.
eval_n_windows_after = 4000
Expand Down Expand Up @@ -1348,7 +1350,7 @@ router_lr_scale = 0.1
route_bias_rate_scale = 0.25
# Fit the existing 50B unique-corpus cap; the full reference preset is 27,500.
training_steps = 23841
train_image_digest = "sha256:2fd193cad6be7bef87f110246a2074de84cde44511e153a21d1d0301d93cba64"
train_image_digest = "sha256:5d41d94bc0c0d3ae3dfd042b0cc7e4d900c7d1f48173808436c12bf764a67641" # rc15, LIVE since 2026-10-31 (was rc11 2fd193ca…); synced to main 2026-11-13
expected_gpu = ""
long_series = "crop"
power_ema_scope = "leg"
Expand Down
6 changes: 5 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,16 @@ def cfg() -> ChainConfig:
raw["netconfig"]["owner_hotkey"] = ""
raw["scoring"]["forfeit_from_block"] = 0 # DEC-CA-0067 typed blocks: see scoring=
raw["scoring"]["margin_v2_from_block"] = 0
raw["v2"]["image_decoupled_from_block"] = 0 # the typed v2b switch: see v2=
return replace(c, training=replace(c.training, expected_gpu="",
train_image_digest=""),
raw=raw,
netconfig=replace(c.netconfig, owner_hotkey=""),
v2=replace(c.v2, launch_at_utc="", commit_open_at_utc="",
commit_from_block=0, prelaunch_burn_readiness=""),
commit_from_block=0, prelaunch_burn_readiness="",
# the release types the decided v2b switch (real-
# chain state); fixtures keep the vote undecided
image_decoupled_from_block=0),
# corpus_target_points ships ARMED (DEC-CA-0031): a points-
# denominated drain would override every fixture's small
# corpus_n_series with a 67M-point draw. Tests that exercise
Expand Down
12 changes: 10 additions & 2 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,16 @@ def test_activation_block_must_be_on_both_grids(tmp_path):
"""A seam that is not a boundary of both grids leaves the round spanning it
with two different lengths depending on which side you ask from."""

import re

import pytest

from cascade.shared.config import DEFAULT_CHAIN_TOML, load_chain_config

src = DEFAULT_CHAIN_TOML.read_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)
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.
Expand All @@ -312,11 +317,14 @@ def test_activation_block_must_be_on_both_grids(tmp_path):


def test_cadence_keys_must_be_set_together(tmp_path):
import re

import pytest

from cascade.shared.config import DEFAULT_CHAIN_TOML, load_chain_config

src = DEFAULT_CHAIN_TOML.read_text()
src = re.sub(r"^image_decoupled_from_block\s*=.*$", "image_decoupled_from_block = 0",
DEFAULT_CHAIN_TOML.read_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"
Expand Down
8 changes: 7 additions & 1 deletion tests/unit/test_image_after_repin.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ def test_parse_is_all_or_none_and_one_digest():

def _with(tmp_path, **kv):
text = (REPO / "chain.toml").read_text()
# the release types the decided v2b switch; these tests arm the AFTER pins
# with the vote still open (unbound schedule)
text, n = re.subn(r"(?m)^image_decoupled_from_block\s*=.*$",
"image_decoupled_from_block = 0", text)
assert n == 1
for k, v in kv.items():
text, n = re.subn(rf"(?m)^{k}\s*=.*$", f'{k} = "{v}"', text)
assert n == 1, k
Expand Down Expand Up @@ -119,7 +124,8 @@ def acfg():
funded_sku_wall_measured_image=""),
scoring=replace(c.scoring, era_king_from_block=B0, tenure_blocks_from_block=B0,
cohort_maxt_increment_from_block=B0),
v2=replace(c.v2, from_block=B0, launch_at_utc="", **AFTER),
v2=replace(c.v2, from_block=B0, launch_at_utc="", image_decoupled_from_block=0,
**AFTER), # the vote still open
training=replace(c.training, v2_from_block=B0))
return bind_image_after(c)

Expand Down
44 changes: 31 additions & 13 deletions tests/unit/test_image_decoupled_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,29 @@ def test_prove_numerics_digest_needs_the_leg_image():
# ── the knobs ────────────────────────────────────────────────────────────────


def test_knobs_ship_off_and_inert():
for path, after in (("chain.toml", 4000), ("chain.testnet.toml", 300)):
cfg = load_chain_config(REPO / path)
assert cfg.v2.image_decoupled_from_block == 0
assert cfg.v2.eval_n_windows_after == after
assert cfg.v2.prior_train_image_digests == ()
assert cfg.activation.resolved_image_decoupled_block == 0
assert not image_decoupled_at(cfg, 10**9)
assert v2_eval_windows_at(cfg, 10**9) == int(cfg.v2.eval_n_windows)
def test_knobs_ship_typed_on_mainnet_and_off_on_testnet():
# mainnet: the v2b-69630f92 vote resolved (locked 9486000, 85.9% of stake,
# applied at the era start 9493200) and the release TYPES that block
# (2026-11-13) to free its note segment; nothing is resolved from chain
cfg = load_chain_config(REPO / "chain.toml")
assert cfg.v2.image_decoupled_from_block == 9493200
assert cfg.v2.eval_n_windows_after == 4000
assert cfg.v2.prior_train_image_digests == ()
assert cfg.activation.resolved_image_decoupled_block == 0
assert image_decoupled_at(cfg, 10**9) and v2_eval_windows_at(cfg, 10**9) == 4000
# on the live 900 grid (the fleet's resolved rollover) the settlement AT
# the switch block is the previous era's last; the next one is under the rule
live = replace(cfg, round=replace(cfg.round, rolling_from_block=9136800, epoch_blocks=900,
epoch_blocks_prev=3600, epoch_activation_block=9136800))
assert not image_decoupled_at(live, 9493200) and image_decoupled_at(live, 9494100)
# testnet: still decided on chain, inert until then
cfg = load_chain_config(REPO / "chain.testnet.toml")
assert cfg.v2.image_decoupled_from_block == 0
assert cfg.v2.eval_n_windows_after == 300
assert cfg.v2.prior_train_image_digests == ()
assert cfg.activation.resolved_image_decoupled_block == 0
assert not image_decoupled_at(cfg, 10**9)
assert v2_eval_windows_at(cfg, 10**9) == int(cfg.v2.eval_n_windows)


def _with(tmp_path, **kv):
Expand Down Expand Up @@ -305,7 +319,7 @@ def vcfg():
forfeit_from_block=0, forfeit_successor_hotkey="",
win_margin_start_v2=0.0, win_margin_end_v2=0.0,
margin_warmup_blocks_v2=0, margin_v2_from_block=0),
v2=replace(c.v2, prelaunch_burn=0.0),
v2=replace(c.v2, prelaunch_burn=0.0, image_decoupled_from_block=0), # vote open
eval=replace(c.eval, shared_ladder_windows=0))


Expand Down Expand Up @@ -745,6 +759,7 @@ 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
feature = A.primary_feature(cfg).name
if rollover_typed:
cfg = A.apply_activation(cfg, LIVE_ROLLOVER[1])
Expand Down Expand Up @@ -778,6 +793,7 @@ def test_note_with_a_locked_cfg_segment_and_the_v2b_type_rule():
type its block into chain.toml (behaviour-identical) and its segment
leaves the note."""
cfg = load_chain_config(REPO / "chain.toml")
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)):
low = 10 ** (digits - 1)
Expand All @@ -802,9 +818,9 @@ 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
blocks — a deployment that applied the pins but not the typed blocks."""
/ v2b blocks — a deployment that applied the pins but not the typed blocks."""
text = (REPO / "chain.toml").read_text()
for k in ("forfeit_from_block", "margin_v2_from_block"):
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
p = tmp_path / "untyped.toml"
Expand Down Expand Up @@ -862,8 +878,10 @@ def test_note_budget_worst_case_scales_to_8_digit_blocks():
"""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."""
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")
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) == ""
assert "TRIM" in A.v2b_note_problem(cfg, rec, head_block=9_800_000)
Expand Down
7 changes: 5 additions & 2 deletions tests/unit/test_image_decoupled_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ def test_pre_lock_receipts_carry_no_switch_and_match_the_v3_golden():

from .receipt_fixture import make_scored_receipt

runner = ValidatorRunner(cfg=load_chain_config(REPO / "chain.toml"))
release = load_chain_config(REPO / "chain.toml")
untyped = replace(release, v2=replace(release.v2, image_decoupled_from_block=0))
runner = ValidatorRunner(cfg=untyped)
assert runner.image_decoupled_block == 0 # nothing locked, nothing typed
receipt, _, _ = make_scored_receipt()
stamped = replace(receipt, image_decoupled_block=runner.image_decoupled_block)
Expand Down Expand Up @@ -100,7 +102,8 @@ def test_option_b_drops_the_prelaunch_segment_and_option_a_still_needed(tmp_path
release = load_chain_config(REPO / "chain.toml")
assert A.v2b_note_problem(release, rec, head_block=9_420_000) == ""
assert A.v2b_note_problem(_retired(release), rec, head_block=9_420_000) == ""
both = A.own_signal_payload(_retired(release), rec, None, None, A.ActivationRecord(), None,
open_vote = replace(release, v2=replace(release.v2, image_decoupled_from_block=0))
both = A.own_signal_payload(_retired(open_vote), rec, None, None, A.ActivationRecord(), None,
None, A.ActivationRecord(feature=V2B, lock_block=9504000,
activation_block=9507600,
source="tally"))
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_lane_c_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def test_loader_arms_lane_c_on_an_era_boundary(tmp_path):
v2 = 10_000 * era
raw = copy.deepcopy(cfg.raw)
raw["v2"]["from_block"] = v2
raw["v2"]["image_decoupled_from_block"] = 0 # the typed v2b switch: vote open here
# DEC-CA-0063 needs [netconfig] owner_hotkey once v2 is typed; lane C only
raw["v2"]["bench_source"] = "trainer"
# Lane C takes its allocation from B, which now launches independently.
Expand Down
14 changes: 11 additions & 3 deletions tests/unit/test_shared_ladder.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,15 @@ def test_shipped_values_parse_and_default_off(tmp_path):
assert shared_ladder_configured(shipped.eval)
assert not shared_ladder_active(shipped.eval, 9493200) # the old era's last settlement
assert shared_ladder_active(shipped.eval, 9494100)
assert shipped.quantile_head_at(9494100) and not shipped.quantile_head_at(9493200) or \
not shipped.v2.image_decoupled_from_block # aligned when v2b is applied
# the release types both switches at the same era (v2b 9493200 → the
# quantile head from its first settlement; v2 live from 9399600 on chain)
assert shipped.v2.image_decoupled_from_block == 9493200
# ... on the fleet's resolved 900 grid: the repo file ships rolling and v2
# undecided (3600 grid, where 9494100 is not a boundary at all)
live = replace(shipped, v2=replace(shipped.v2, from_block=9399600),
round=replace(shipped.round, rolling_from_block=9136800, epoch_blocks=900,
epoch_blocks_prev=3600, epoch_activation_block=9136800))
assert live.quantile_head_at(9494100) and not live.quantile_head_at(9493200)
assert not A.shared_ladder_feature(shipped).decided_on_chain # typed: nothing signalled
# The vote's name is pinned: a changed count or ladder is a FRESH vote.
assert A.shared_ladder_feature_name(4000, HORIZONS) == "lad-556b1b98"
Expand Down Expand Up @@ -496,7 +503,8 @@ def test_note_carries_the_segment_and_stays_under_the_budget_with_every_mainnet_
shipped = load_chain_config(DEFAULT_CHAIN_TOML)
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))
shipped = replace(shipped, eval=replace(shipped.eval, shared_ladder_from_block=0),
v2=replace(shipped.v2, image_decoupled_from_block=0)) # both votes open
assert shipped.activation.enabled
lad = A.shared_ladder_feature(shipped)
v2b = A.image_decoupled_feature(shipped)
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/test_v2_golive.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ def test_mainnet_ships_inert_with_the_v2_contract_scheduled():
bench_budget_hours=6.0, bench_sweep_hours=2.0,
# DEC-CA-0067: bundled budget, inert until the v2b vote
eval_n_windows_after=4000, quantile_head_after=True,
image_decoupled_feature="v2b-69630f92")
image_decoupled_feature="v2b-69630f92",
# 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 cfg.lane_b.from_block == 0 and not cfg.lane_b.active(10**9)
v2c = cfg.training.v2_contract
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/test_v2b_seed_epoch_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def base():
cfg = load_chain_config(ROOT / "chain.toml")
era = era_length_blocks(cfg.round, 0)
gate = 10_000 * era
cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate),
training=replace(cfg.training, v2_from_block=gate))
cfg = replace(cfg, v2=replace(cfg.v2, from_block=gate, image_decoupled_from_block=0),
training=replace(cfg.training, v2_from_block=gate)) # the vote still open
return cfg, era


Expand Down Expand Up @@ -499,7 +499,7 @@ def vcfg():
forfeit_from_block=0, forfeit_successor_hotkey="",
win_margin_start_v2=0.0, win_margin_end_v2=0.0,
margin_warmup_blocks_v2=0, margin_v2_from_block=0),
v2=replace(c.v2, prelaunch_burn=0.0),
v2=replace(c.v2, prelaunch_burn=0.0, image_decoupled_from_block=0), # vote open
eval=replace(c.eval, shared_ladder_windows=0))


Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_validator_bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ def test_loader_refuses_v2_go_live_without_a_main_validator(tmp_path):
text = re.sub(r"(?m)^(\[v2\]\n(?:[^\[][^\n]*\n)*?from_block\s*=\s*)\d+", rf"\g<1>{v2}",
(root / "chain.toml").read_text(encoding="utf-8"))
assert f"from_block = {v2}" in text
# the shipped file types the v2b switch (9493200); keep it OPEN here so the
# loader's "precedes [v2] from_block" check does not fire before the one under test
text = re.sub(r"(?m)^image_decoupled_from_block\s*=\s*\d+", "image_decoupled_from_block = 0", text)
text = text.replace(f'owner_hotkey = "{cfg.netconfig.owner_hotkey}"', 'owner_hotkey = ""', 1)
p = tmp_path / "chain.toml"
p.write_text(text, encoding="utf-8")
Expand Down
Loading