diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index c41af16..5931d67 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -465,16 +465,36 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint { } } + let finalized_slot = store.latest_finalized().slot; + // Ensure target is in justifiable slot range // // Walk back until we find a slot that satisfies justifiability rules // relative to the latest finalized checkpoint. - while !slot_is_justifiable_after(target_block.slot, store.latest_finalized().slot) { + while target_block.slot > finalized_slot + && !slot_is_justifiable_after(target_block.slot, finalized_slot) + { target_block_root = target_block.parent_root; target_block = store .get_block(&target_block_root) .expect("parent block exists"); } + // Ensure target is at or after the source (latest_justified) to maintain + // the invariant: source.slot <= target.slot. When a block advances + // latest_justified between safe_target updates (interval 2), the walk-back + // above can land on a slot behind the new justified checkpoint. + // + // See https://github.com/blockblaz/zeam/blob/697c293879e922942965cdb1da3c6044187ae00e/pkgs/node/src/forkchoice.zig#L654-L659 + let latest_justified = store.latest_justified(); + if target_block.slot < latest_justified.slot { + warn!( + target_slot = target_block.slot, + justified_slot = latest_justified.slot, + "Attestation target walked behind justified source, clamping to justified" + ); + return latest_justified; + } + Checkpoint { root: target_block_root, slot: target_block.slot,