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
19 changes: 11 additions & 8 deletions src/lean_spec/subspecs/forkchoice/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,21 @@ def from_anchor(
# Read the slot at which the anchor block was proposed.
anchor_slot = anchor_block.slot

# Initialize checkpoints from this state.
# Seed both checkpoints from the anchor block itself.
#
# We explicitly set the root to the anchor block root.
# The state internally might have zero-hash checkpoints (if genesis),
# but the Store must treat the anchor block as the justified/finalized point.
# The store treats the anchor as the new "genesis" for fork choice:
# all history below it is pruned. The justified and finalized checkpoints
# therefore point at the anchor block with the anchor's own slot,
# regardless of what the anchor state's embedded checkpoints say.
anchor_checkpoint = Checkpoint(root=anchor_root, slot=anchor_slot)

return cls(
time=Interval.from_slot(anchor_slot),
config=state.config,
head=anchor_root,
safe_target=anchor_root,
latest_justified=state.latest_justified.model_copy(update={"root": anchor_root}),
latest_finalized=state.latest_finalized.model_copy(update={"root": anchor_root}),
latest_justified=anchor_checkpoint,
latest_finalized=anchor_checkpoint,
blocks={anchor_root: anchor_block},
states={anchor_root: state},
validator_id=validator_id,
Expand Down Expand Up @@ -515,8 +518,8 @@ def on_block(
# Keep the checkpoint with the higher slot.
# On slot ties, prefer the store's own checkpoint.
#
# The store's checkpoint is pinned to the anchor block root at init.
# The anchor state may hold a pre-anchor root.
# The store's checkpoint is pinned to the anchor at init and only
# moves forward via real justification/finalization events.
# On ties the store's view is authoritative.
latest_justified = max(
self.latest_justified, post_state.latest_justified, key=lambda c: c.slot
Expand Down
26 changes: 18 additions & 8 deletions tests/consensus/devnet/fc/test_checkpoint_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ def test_store_init_from_non_genesis_anchor(
-----------------------------

- Head points to the anchor block.
- Latest justified and latest finalized both reference the anchor root.
- Latest justified and latest finalized both reference the anchor root
at the anchor's own slot (beacon-chain seeding convention).
- Store clock is at the anchor slot (no pre-anchor intervals tracked).
- The anchor block is the only entry in the store's block map.

Expand Down Expand Up @@ -78,9 +79,12 @@ def test_store_init_from_non_genesis_anchor(
time=anchor_time_intervals,
head_slot=ANCHOR_SLOT,
head_root_label="genesis",
latest_justified_slot=anchor_state.latest_justified.slot,
# Both checkpoints are seeded from the anchor itself:
# slot = anchor.slot, root = anchor_root. The anchor
# state's embedded checkpoint slots are intentionally ignored.
latest_justified_slot=ANCHOR_SLOT,
latest_justified_root_label="genesis",
latest_finalized_slot=anchor_state.latest_finalized.slot,
latest_finalized_slot=ANCHOR_SLOT,
latest_finalized_root_label="genesis",
safe_target_root_label="genesis",
labels_in_store=["genesis"],
Expand Down Expand Up @@ -135,9 +139,11 @@ def test_extend_chain_from_non_genesis_anchor(
checks=StoreChecks(
head_slot=Slot(11),
head_root_label="block_11",
latest_justified_slot=anchor_state.latest_justified.slot,
# Empty blocks carry no attestations, so neither checkpoint
# advances past the anchor seeding.
latest_justified_slot=ANCHOR_SLOT,
latest_justified_root_label="genesis",
latest_finalized_slot=anchor_state.latest_finalized.slot,
latest_finalized_slot=ANCHOR_SLOT,
latest_finalized_root_label="genesis",
labels_in_store=["genesis", "block_11"],
),
Expand All @@ -151,7 +157,9 @@ def test_extend_chain_from_non_genesis_anchor(
checks=StoreChecks(
head_slot=Slot(12),
head_root_label="block_12",
latest_justified_slot=ANCHOR_SLOT,
latest_justified_root_label="genesis",
latest_finalized_slot=ANCHOR_SLOT,
latest_finalized_root_label="genesis",
labels_in_store=["genesis", "block_11", "block_12"],
),
Expand All @@ -165,7 +173,9 @@ def test_extend_chain_from_non_genesis_anchor(
checks=StoreChecks(
head_slot=Slot(13),
head_root_label="block_13",
latest_justified_slot=ANCHOR_SLOT,
latest_justified_root_label="genesis",
latest_finalized_slot=ANCHOR_SLOT,
latest_finalized_root_label="genesis",
labels_in_store=["genesis", "block_11", "block_12", "block_13"],
),
Expand Down Expand Up @@ -238,9 +248,9 @@ def test_fork_off_non_genesis_anchor(
# unambiguously heavier.
#
# Source is pinned to the anchor block because the store
# only knows about blocks at or after the anchor. The
# anchor state's internal justified root still points to
# pre-anchor history that was discarded on checkpoint sync.
# only knows about blocks at or after the anchor, and the
# store's latest_justified checkpoint is seeded at
# (anchor.slot, anchor_root).
BlockStep(
block=BlockSpec(
slot=Slot(12),
Expand Down
Loading