diff --git a/crates/design-challenge-task/src/emit.rs b/crates/design-challenge-task/src/emit.rs index 948e440a6..d5fdf4228 100644 --- a/crates/design-challenge-task/src/emit.rs +++ b/crates/design-challenge-task/src/emit.rs @@ -16,10 +16,16 @@ pub struct DesignEmitPlan { pub pin_block: u64, } +/// Max epochs to walk back on catch-up (Finney public RPC prunes ~256 blocks). +const MAX_CATCHUP_EPOCHS: u64 = 16; + /// Decide whether/which epoch the design filler should emit. /// -/// - Catch up `last_emitted+1` when behind by more than one epoch (repairs the -/// end-of-epoch relabel race that skipped alternate epochs in prod). +/// - **Cold start** (`last_emitted == 0`): emit the **current** epoch immediately. +/// The in-process cursor resets to 0 on every process boot; walking from +/// epoch 1 pins a pruned block and fails with `SubnetOwnerHotkey not found`. +/// - Catch up `last_emitted+1` when behind (capped to [`MAX_CATCHUP_EPOCHS`]) +/// so end-of-epoch relabel skips can recover without exceeding prune depth. /// - Otherwise wait until the last [`DESIGN_EMIT_LATE_BLOCKS`] of the current /// epoch so admin awards can submit Score leaves first. #[must_use] @@ -34,12 +40,24 @@ pub fn design_emit_plan( return None; } let tempo = tempo.max(1); + // Cold start after deploy/restart: do not catch up from epoch 1. + if last_emitted == 0 { + return Some(DesignEmitPlan { + epoch: current_epoch, + pin_block: current_last_epoch_block, + }); + } if last_emitted >= current_epoch { return None; } // Sequential catch-up for skipped epochs (award path / boundary race). if last_emitted + 1 < current_epoch { - let target = last_emitted + 1; + let gap = current_epoch.saturating_sub(last_emitted); + let target = if gap > MAX_CATCHUP_EPOCHS { + current_epoch.saturating_sub(MAX_CATCHUP_EPOCHS) + } else { + last_emitted + 1 + }; let epochs_back = current_epoch.saturating_sub(target); let pin_block = current_last_epoch_block.saturating_sub(epochs_back.saturating_mul(tempo)); return Some(DesignEmitPlan { @@ -82,6 +100,25 @@ mod tests { assert_eq!(p.pin_block, 8_815_687 - (24423 - 24413) * 360); } + #[test] + fn emit_plan_cold_start_emits_current_immediately() { + let p = design_emit_plan(0, 24424, 10, 360, 8_816_047).unwrap(); + assert_eq!( + p, + DesignEmitPlan { + epoch: 24424, + pin_block: 8_816_047 + } + ); + } + + #[test] + fn emit_plan_caps_catchup_to_prune_window() { + let p = design_emit_plan(100, 24424, 10, 360, 8_816_047).unwrap(); + assert_eq!(p.epoch, 24424 - 16); + assert_eq!(p.pin_block, 8_816_047 - 16 * 360); + } + #[test] fn emit_plan_noop_when_already_emitted_current() { assert!(design_emit_plan(11, 11, 350, 360, 1000).is_none()); diff --git a/crates/design-challenge/src/lib.rs b/crates/design-challenge/src/lib.rs index a7c319460..dab755e1d 100644 --- a/crates/design-challenge/src/lib.rs +++ b/crates/design-challenge/src/lib.rs @@ -84,6 +84,18 @@ mod tests { assert_eq!(p.pin_block, 8_815_687 - (24423 - 24413) * 360); } + #[test] + fn emit_plan_cold_start_emits_current_immediately() { + let p = design_emit_plan(0, 24424, 10, 360, 8_816_047).unwrap(); + assert_eq!( + p, + DesignEmitPlan { + epoch: 24424, + pin_block: 8_816_047 + } + ); + } + #[test] fn emit_plan_noop_when_already_emitted_current() { assert!(design_emit_plan(11, 11, 350, 360, 1000).is_none());