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
27 changes: 26 additions & 1 deletion cascade/shared/calendar_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,32 @@ def check_calendar_launch(cfg, client) -> None:
"""
if not dated_launch_pending(cfg):
return
check_launch_loads(cfg, int(client.finalized_block()))
head = int(client.finalized_block())
if launch_date_passed(cfg, client, head):
# 2026-11-13: the trial below arms v2 at the era AFTER the head — right
# before the date, wrong after it: the real block is in the past
# (mainnet 9399600), so a typed later switch (``[v2]
# image_decoupled_from_block = 9493200``, #457) "precedes" the fake
# trial block and every node that pulled main refused to start. Past
# the date, the first poll's :func:`resolve_calendar_launch` parses the
# config at the REAL block from finalized timestamps and refuses a
# config that cannot load there.
return
check_launch_loads(cfg, head)


def launch_date_passed(cfg, client, head: int) -> bool:
"""The finalized head is at or past ``[v2] launch_at_utc`` (chain time).
``False`` when the head's timestamp cannot be read — the pre-date trial
then runs as before (refusing is the safe side)."""
target = launch_timestamp(cfg.v2.launch_at_utc)
if target is None:
return False
try:
stamp = float(client.block_timestamp(int(head)))
except Exception: # noqa: BLE001 — unreadable chain time: keep the trial
return False
return math.isfinite(stamp) and stamp >= target


def _first_finalized_at(client, head, target):
Expand Down
37 changes: 37 additions & 0 deletions tests/unit/test_calendar_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,43 @@ 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():
# 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
# that pulled main: "image_decoupled_from_block=9493200 precedes [v2]
# from_block=9500400".
from cascade.shared.activation import apply_activation

shipped = load_chain_config()
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)
head = 9497650
past = SimpleNamespace(finalized_block=lambda: head,
block_timestamp=lambda block: launch + (block - 9399000) * 12.0)
check_calendar_launch(live, past) # starts (was: ValueError)
# ... and the date really is behind the head: the real resolution lands on
# the live v2 block and that config loads with the typed switch
resolved = resolve_calendar_launch(live, past)
assert resolved.v2.from_block == 9399600
assert resolved.v2.image_decoupled_from_block == 9493200
# BEFORE the date the trial still runs (and still refuses a bad config)
early = SimpleNamespace(finalized_block=lambda: 9390000,
block_timestamp=lambda block: launch - 3600.0)
check_calendar_launch(live, early)
raw = effective_raw(live)
raw["netconfig"]["owner_hotkey"] = ""
with pytest.raises(ValueError, match="launch_at_utc"):
check_calendar_launch(parse_chain_config(raw), early)
# an unreadable head timestamp keeps the trial (the safe side)
def boom(block):
raise RuntimeError("pruned")
dark = SimpleNamespace(finalized_block=lambda: 9390000, block_timestamp=boom)
with pytest.raises(ValueError, match="launch_at_utc"):
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()
raw = effective_raw(cfg)
Expand Down
Loading