Skip to content

Commit 1ec4866

Browse files
authored
forkchoice store: add _compute_lmd_ghost_head in core store (#190)
1 parent 7962398 commit 1ec4866

4 files changed

Lines changed: 70 additions & 224 deletions

File tree

src/lean_spec/subspecs/forkchoice/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@
55
providing the core functionality for determining the canonical chain head.
66
"""
77

8-
from .helpers import (
9-
get_fork_choice_head,
10-
)
118
from .store import Store
129

1310
__all__ = [
1411
"Store",
15-
"get_fork_choice_head",
1612
]

src/lean_spec/subspecs/forkchoice/helpers.py

Lines changed: 0 additions & 67 deletions
This file was deleted.

src/lean_spec/subspecs/forkchoice/store.py

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@
3636
from lean_spec.subspecs.containers.slot import Slot
3737
from lean_spec.subspecs.ssz.hash import hash_tree_root
3838
from lean_spec.types import (
39+
ZERO_HASH,
3940
Bytes32,
4041
Uint64,
4142
ValidatorIndex,
4243
is_proposer,
4344
)
4445
from lean_spec.types.container import Container
4546

46-
from .helpers import get_fork_choice_head
47-
4847

4948
class Store(Container):
5049
"""
@@ -485,6 +484,69 @@ def on_block(self, signed_block_with_attestation: SignedBlockWithAttestation) ->
485484

486485
return store
487486

487+
def _compute_lmd_ghost_head(
488+
self,
489+
start_root: Bytes32,
490+
attestations: Dict[ValidatorIndex, SignedAttestation],
491+
min_score: int = 0,
492+
) -> Bytes32:
493+
"""
494+
Internal implementation of LMD GHOST fork choice algorithm.
495+
496+
Navigates the block tree from `start_root` by choosing the heaviest child
497+
at each fork, based on the provided `attestations`.
498+
499+
This is the core fork choice logic. It walks down the tree from a given
500+
starting point (typically the latest justified checkpoint), choosing at
501+
each fork the child with the most attestation weight. When there is a tie,
502+
it breaks it lexicographically by hash.
503+
504+
Args:
505+
start_root: Starting point root (usually latest justified).
506+
attestations: Attestations to consider for fork choice weights.
507+
min_score: Minimum attestation count for block inclusion.
508+
509+
Returns:
510+
Hash of the chosen head block.
511+
"""
512+
# Start at genesis if root is zero hash
513+
if start_root == ZERO_HASH:
514+
start_root = min(
515+
self.blocks.keys(), key=lambda block_hash: self.blocks[block_hash].slot
516+
)
517+
518+
# Count attestations for each block (attestations for descendants count for ancestors)
519+
attestation_weights: Dict[Bytes32, int] = {}
520+
521+
for attestation in attestations.values():
522+
head = attestation.message.data.head
523+
if head.root in self.blocks:
524+
# Walk up from attestation target, incrementing ancestor weights
525+
block_hash = head.root
526+
while self.blocks[block_hash].slot > self.blocks[start_root].slot:
527+
attestation_weights[block_hash] = attestation_weights.get(block_hash, 0) + 1
528+
block_hash = self.blocks[block_hash].parent_root
529+
530+
# Build children mapping for ALL blocks (not just those above min_score)
531+
#
532+
# This ensures fork choice works even when there are no attestations
533+
children_map: Dict[Bytes32, list[Bytes32]] = {}
534+
for block_hash, block in self.blocks.items():
535+
if block.parent_root:
536+
# Only include blocks that have enough attestations OR when min_score is 0
537+
if min_score == 0 or attestation_weights.get(block_hash, 0) >= min_score:
538+
children_map.setdefault(block.parent_root, []).append(block_hash)
539+
540+
# Walk down tree, choosing child with most attestations (tiebreak by lexicographic hash)
541+
current = start_root
542+
while True:
543+
children = children_map.get(current, [])
544+
if not children:
545+
return current
546+
547+
# Choose best child: most attestations, then lexicographically highest hash
548+
current = max(children, key=lambda x: (attestation_weights.get(x, 0), x))
549+
488550
def update_head(self) -> "Store":
489551
"""
490552
Compute updated store with new canonical head.
@@ -532,10 +594,9 @@ def update_head(self) -> "Store":
532594
#
533595
# Selects canonical head by walking the tree from the justified root,
534596
# choosing the heaviest child at each fork based on attestation weights.
535-
new_head = get_fork_choice_head(
536-
self.blocks,
537-
latest_justified.root,
538-
self.latest_known_attestations,
597+
new_head = self._compute_lmd_ghost_head(
598+
start_root=latest_justified.root,
599+
attestations=self.latest_known_attestations,
539600
)
540601

541602
# Extract finalized checkpoint from head state
@@ -619,10 +680,9 @@ def update_safe_target(self) -> "Store":
619680
min_target_score = -(-num_validators * 2 // 3)
620681

621682
# Find head with minimum attestation threshold
622-
safe_target = get_fork_choice_head(
623-
self.blocks,
624-
self.latest_justified.root,
625-
self.latest_new_attestations,
683+
safe_target = self._compute_lmd_ghost_head(
684+
start_root=self.latest_justified.root,
685+
attestations=self.latest_new_attestations,
626686
min_score=min_target_score,
627687
)
628688

tests/lean_spec/subspecs/forkchoice/test_helpers.py

Lines changed: 0 additions & 143 deletions
This file was deleted.

0 commit comments

Comments
 (0)