|
36 | 36 | from lean_spec.subspecs.containers.slot import Slot |
37 | 37 | from lean_spec.subspecs.ssz.hash import hash_tree_root |
38 | 38 | from lean_spec.types import ( |
| 39 | + ZERO_HASH, |
39 | 40 | Bytes32, |
40 | 41 | Uint64, |
41 | 42 | ValidatorIndex, |
42 | 43 | is_proposer, |
43 | 44 | ) |
44 | 45 | from lean_spec.types.container import Container |
45 | 46 |
|
46 | | -from .helpers import get_fork_choice_head |
47 | | - |
48 | 47 |
|
49 | 48 | class Store(Container): |
50 | 49 | """ |
@@ -485,6 +484,69 @@ def on_block(self, signed_block_with_attestation: SignedBlockWithAttestation) -> |
485 | 484 |
|
486 | 485 | return store |
487 | 486 |
|
| 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 | + |
488 | 550 | def update_head(self) -> "Store": |
489 | 551 | """ |
490 | 552 | Compute updated store with new canonical head. |
@@ -532,10 +594,9 @@ def update_head(self) -> "Store": |
532 | 594 | # |
533 | 595 | # Selects canonical head by walking the tree from the justified root, |
534 | 596 | # 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, |
539 | 600 | ) |
540 | 601 |
|
541 | 602 | # Extract finalized checkpoint from head state |
@@ -619,10 +680,9 @@ def update_safe_target(self) -> "Store": |
619 | 680 | min_target_score = -(-num_validators * 2 // 3) |
620 | 681 |
|
621 | 682 | # 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, |
626 | 686 | min_score=min_target_score, |
627 | 687 | ) |
628 | 688 |
|
|
0 commit comments