Skip to content
Draft
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
18 changes: 13 additions & 5 deletions src/lean_spec/spec/forks/lstar/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1723,12 +1723,19 @@ def get_attestation_target(self, store: LstarStore) -> Checkpoint:
# Start from current head
target_block_root = store.head

# Walk back toward safe target (up to `JUSTIFICATION_LOOKBACK_SLOTS` steps)
# Walk back toward safe target (up to `JUSTIFICATION_LOOKBACK_SLOTS` steps).
#
# If safe target is stale behind the finalized checkpoint, the finalized
# slot becomes the lower bound. Target selection must never inspect
# candidate slots before the finalized boundary.
safe_target_slot = store.blocks[store.safe_target].slot
finalized_slot = store.latest_finalized.slot
lower_bound_slot = safe_target_slot if safe_target_slot > finalized_slot else finalized_slot

# This ensures the target doesn't advance too far ahead of safe target,
# providing a balance between liveness and safety.
for _ in range(int(JUSTIFICATION_LOOKBACK_SLOTS)):
if store.blocks[target_block_root].slot > store.blocks[store.safe_target].slot:
if store.blocks[target_block_root].slot > lower_bound_slot:
target_block_root = store.blocks[target_block_root].parent_root
else:
break
Expand All @@ -1737,9 +1744,10 @@ def get_attestation_target(self, store: LstarStore) -> Checkpoint:
#
# Walk back until we find a slot that satisfies justifiability rules
# relative to the latest finalized checkpoint.
while not store.blocks[target_block_root].slot.is_justifiable_after(
store.latest_finalized.slot
):
while store.blocks[target_block_root].slot > finalized_slot:
target_slot = store.blocks[target_block_root].slot
if target_slot.is_justifiable_after(finalized_slot):
break
target_block_root = store.blocks[target_block_root].parent_root

# Create checkpoint from selected target block
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,31 @@ def test_attestation_target_respects_justifiable_slots(
# The target slot must be justifiable after the finalized slot
assert target.slot.is_justifiable_after(finalized_slot)

def test_attestation_target_ignores_stale_safe_target_below_finalized(
self,
observer_store: Store,
spec: LstarSpec,
) -> None:
"""A stale safe target behind finalization must not pull target selection below it."""
store = observer_store
roots: dict[Slot, Bytes32] = {}

for slot_num in range(1, 7):
slot = Slot(slot_num)
proposer = ValidatorIndex(slot_num % len(store.states[store.head].validators))
store, block, _ = spec.produce_block_with_signatures(store, slot, proposer)
roots[slot] = hash_tree_root(block)

finalized = Checkpoint(root=roots[Slot(4)], slot=Slot(4))
store.latest_justified = finalized
store.latest_finalized = finalized
assert store.blocks[store.safe_target].slot == Slot(0)

target = spec.get_attestation_target(store)

assert target.root == finalized.root
assert target.slot == finalized.slot

def test_attestation_target_consistency_with_head(
self, observer_store: Store, spec: LstarSpec
) -> None:
Expand Down
Loading