3636from lean_spec .subspecs .containers .slot import Slot
3737from lean_spec .subspecs .ssz .hash import hash_tree_root
3838from lean_spec .types import (
39+ ZERO_HASH ,
3940 Bytes32 ,
4041 Uint64 ,
4142 ValidatorIndex ,
4243 is_proposer ,
4344)
4445from lean_spec .types .container import Container
4546
46- from .helpers import get_fork_choice_head
47-
4847
4948class 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
@@ -791,6 +851,8 @@ def get_attestation_target(self) -> Checkpoint:
791851 for _ in range (JUSTIFICATION_LOOKBACK_SLOTS ):
792852 if self .blocks [target_block_root ].slot > self .blocks [self .safe_target ].slot :
793853 target_block_root = self .blocks [target_block_root ].parent_root
854+ else :
855+ break
794856
795857 # Ensure target is in justifiable slot range
796858 #
@@ -805,6 +867,43 @@ def get_attestation_target(self) -> Checkpoint:
805867 target_block = self .blocks [target_block_root ]
806868 return Checkpoint (root = hash_tree_root (target_block ), slot = target_block .slot )
807869
870+ def produce_attestation_data (self , slot : Slot ) -> AttestationData :
871+ """
872+ Produce attestation data for the given slot.
873+
874+ This method constructs an AttestationData object according to the lean protocol
875+ specification. The attestation data represents the chain state view including
876+ head, target, and source checkpoints.
877+
878+ The algorithm:
879+ 1. Get the current head block
880+ 2. Calculate the appropriate attestation target using current forkchoice state
881+ 3. Use the store's latest justified checkpoint as the attestation source
882+ 4. Construct and return the complete AttestationData object
883+
884+ Args:
885+ slot: The slot for which to produce the attestation data.
886+
887+ Returns:
888+ A fully constructed AttestationData object.
889+ """
890+ # Get the head block the validator sees for this slot
891+ head_checkpoint = Checkpoint (
892+ root = self .head ,
893+ slot = self .blocks [self .head ].slot ,
894+ )
895+
896+ # Calculate the target checkpoint for this attestation
897+ target_checkpoint = self .get_attestation_target ()
898+
899+ # Construct attestation data
900+ return AttestationData (
901+ slot = slot ,
902+ head = head_checkpoint ,
903+ target = target_checkpoint ,
904+ source = self .latest_justified ,
905+ )
906+
808907 def produce_block_with_signatures (
809908 self ,
810909 slot : Slot ,
@@ -934,56 +1033,3 @@ def produce_block_with_signatures(
9341033 )
9351034
9361035 return store , finalized_block , signatures
937-
938- def produce_attestation (
939- self ,
940- slot : Slot ,
941- validator_index : ValidatorIndex ,
942- ) -> Attestation :
943- """
944- Produce an attestation for the given slot and validator.
945-
946- This method constructs an Attestation object according to the lean protocol
947- specification for attestation. The attestation represents the
948- validator's view of the chain state and their choice for the
949- next justified checkpoint.
950-
951- The algorithm:
952- 1. Get the current head
953- 2. Calculate the appropriate attestation target using current forkchoice state
954- 3. Use the store's latest justified checkpoint as the attestation source
955- 4. Construct and return the complete Attestation object
956-
957- Args:
958- slot: The slot for which to produce the attestation.
959- validator_index: The validator index producing the attestation.
960-
961- Returns:
962- A fully constructed Attestation object ready for signing and broadcast.
963- """
964- # Get the head block the validator sees for this slot
965- head_checkpoint = Checkpoint (
966- root = self .head ,
967- slot = self .blocks [self .head ].slot ,
968- )
969-
970- # Calculate the target checkpoint for this attestation
971- #
972- # This uses the store's current forkchoice state to determine
973- # the appropriate attestation target, balancing between head
974- # advancement and safety guarantees.
975- target_checkpoint = self .get_attestation_target ()
976-
977- # Construct attestation data
978- attestation_data = AttestationData (
979- slot = slot ,
980- head = head_checkpoint ,
981- target = target_checkpoint ,
982- source = self .latest_justified ,
983- )
984-
985- # Create the attestation using current forkchoice state
986- return Attestation (
987- validator_id = validator_index ,
988- data = attestation_data ,
989- )
0 commit comments