Skip to content

Commit 23068cd

Browse files
tcoratgerunnawut
andauthored
lmd ghost: clarify documentation and simplify the method (#192)
* lmd ghost: clarify documentation and simplify the method * Update src/lean_spec/subspecs/forkchoice/store.py Co-authored-by: Unnawut Leepaisalsuwanna <921194+unnawut@users.noreply.github.com> * Update src/lean_spec/subspecs/forkchoice/store.py Co-authored-by: Unnawut Leepaisalsuwanna <921194+unnawut@users.noreply.github.com> --------- Co-authored-by: Unnawut Leepaisalsuwanna <921194+unnawut@users.noreply.github.com>
1 parent 98abd4a commit 23068cd

1 file changed

Lines changed: 64 additions & 34 deletions

File tree

  • src/lean_spec/subspecs/forkchoice

src/lean_spec/subspecs/forkchoice/store.py

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
]
1313

1414
import copy
15+
from collections import defaultdict
1516
from typing import Dict
1617

1718
from lean_spec.subspecs.chain.config import (
@@ -491,15 +492,23 @@ def _compute_lmd_ghost_head(
491492
min_score: int = 0,
492493
) -> Bytes32:
493494
"""
494-
Internal implementation of LMD GHOST fork choice algorithm.
495+
Walk the block tree according to the LMD GHOST rule.
495496
496-
Navigates the block tree from `start_root` by choosing the heaviest child
497-
at each fork, based on the provided `attestations`.
497+
The walk starts from a chosen root.
498+
At each fork, the child subtree with the highest weight is taken.
499+
The process stops when a leaf is reached.
500+
That leaf is the chosen head.
498501
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.
502+
Weights are derived from votes as follows:
503+
- Each validator contributes its full weight to its most recent head vote.
504+
- The weight of that vote also flows to every ancestor of the voted block.
505+
- The weight of a subtree is the sum of all such contributions inside it.
506+
507+
An optional threshold can be applied:
508+
- If a threshold is set, children below this threshold are ignored.
509+
510+
When two branches have equal weight, the one with the lexicographically
511+
larger hash is chosen to break ties.
503512
504513
Args:
505514
start_root: Starting point root (usually latest justified).
@@ -509,43 +518,64 @@ def _compute_lmd_ghost_head(
509518
Returns:
510519
Hash of the chosen head block.
511520
"""
512-
# Start at genesis if root is zero hash
521+
# If the starting point is not defined, choose the earliest known block.
522+
#
523+
# This ensures that the walk always has an anchor.
513524
if start_root == ZERO_HASH:
514525
start_root = min(
515526
self.blocks.keys(), key=lambda block_hash: self.blocks[block_hash].slot
516527
)
517528

518-
# Count attestations for each block (attestations for descendants count for ancestors)
519-
attestation_weights: Dict[Bytes32, int] = {}
529+
# Remember the slot of the anchor once and reuse it during the walk.
530+
#
531+
# This avoids repeated lookups inside the inner loop.
532+
start_slot = self.blocks[start_root].slot
520533

534+
# Prepare a table that will collect voting weight for each block.
535+
#
536+
# Each entry starts conceptually at zero and then accumulates contributions.
537+
weights: Dict[Bytes32, int] = defaultdict(int)
538+
539+
# For every vote, follow the chosen head upward through its ancestors.
540+
#
541+
# Each visited block accumulates one unit of weight from that validator.
521542
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)
543+
current_root = attestation.message.data.head.root
544+
545+
# Climb towards the anchor while staying inside the known tree.
546+
#
547+
# This naturally handles partial views and ongoing sync.
548+
while current_root in self.blocks and self.blocks[current_root].slot > start_slot:
549+
weights[current_root] += 1
550+
current_root = self.blocks[current_root].parent_root
551+
552+
# Build the adjacency tree (parent -> children).
531553
#
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
554+
# We use a defaultdict to avoid checking if keys exist.
555+
children_map: Dict[Bytes32, list[Bytes32]] = defaultdict(list)
556+
557+
for root, block in self.blocks.items():
558+
# 1. Structural check: skip blocks without parents (e.g., purely genesis/orphans)
559+
if not block.parent_root:
560+
continue
561+
562+
# 2. Heuristic check: prune branches early if they lack sufficient weight
563+
if min_score > 0 and weights[root] < min_score:
564+
continue
546565

566+
children_map[block.parent_root].append(root)
567+
568+
# Now perform the greedy walk.
569+
#
570+
# At each step, pick the child with the highest weight among the candidates.
571+
head = start_root
572+
573+
# Descend the tree, choosing the heaviest branch at every fork.
574+
while children := children_map.get(head):
547575
# Choose best child: most attestations, then lexicographically highest hash
548-
current = max(children, key=lambda x: (attestation_weights.get(x, 0), x))
576+
head = max(children, key=lambda x: (weights[x], x))
577+
578+
return head
549579

550580
def update_head(self) -> "Store":
551581
"""

0 commit comments

Comments
 (0)