From c881aa26a4c0b784a58053186101dfbec84265d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:22:13 -0300 Subject: [PATCH 1/3] fix: check against finalized slot for justifiable search This is just in case. --- crates/blockchain/src/store.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index c41af16..93da4a7 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -465,11 +465,15 @@ 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) From 19543b238338e8411cd3c0b673ac86f7812083bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:48:26 -0300 Subject: [PATCH 2/3] fix: clamp attestation target to source When a block advances latest_justified during processing (interval 0), safe_target remains stale until interval 2. This causes get_attestation_target() to walk back past the new justified checkpoint, producing attestations where source.slot > target.slot. Clamp the target to latest_justified when it falls behind, matching Zeam's approach. Closes #97 --- crates/blockchain/src/store.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 93da4a7..b30a6c6 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -479,6 +479,20 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint { .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. + 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, From 9ee480d938b44306515c2233e87ab95a0ee141a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:07:33 -0300 Subject: [PATCH 3/3] chore: link to Zeam's equivalent --- crates/blockchain/src/store.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index b30a6c6..5931d67 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -483,6 +483,8 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint { // 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!(