Skip to content

Commit 173b313

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/inconsistent-json-camel-case
2 parents ce6c589 + 22f2e44 commit 173b313

17 files changed

Lines changed: 184 additions & 313 deletions

File tree

src/lean_spec/subspecs/containers/state/state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
justified and finalized.
77
"""
88

9-
from lean_spec.subspecs.ssz.constants import ZERO_HASH
109
from lean_spec.subspecs.ssz.hash import hash_tree_root
1110
from lean_spec.types import (
11+
ZERO_HASH,
1212
Boolean,
1313
Bytes32,
1414
Container,
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
"""Validator container for the Lean Ethereum consensus specification."""
22

3-
from lean_spec.types import Bytes52, Container
3+
from __future__ import annotations
4+
5+
from lean_spec.types import Bytes52, Container, ValidatorIndex
46

57
from ..xmss.containers import PublicKey
68
from ..xmss.interface import TEST_SIGNATURE_SCHEME, GeneralizedXmssScheme
9+
from .attestation import Attestation, AttestationData
710

811

912
class Validator(Container):
10-
"""Represents a validator's static metadata."""
13+
"""Represents a validator's static metadata and operational interface."""
1114

1215
pubkey: Bytes52
1316
"""XMSS one-time signature public key."""
1417

18+
index: ValidatorIndex = ValidatorIndex(0)
19+
"""Validator index in the registry."""
20+
1521
def get_pubkey(self, scheme: GeneralizedXmssScheme = TEST_SIGNATURE_SCHEME) -> PublicKey:
1622
"""Get the XMSS public key from this validator."""
1723
return PublicKey.from_bytes(bytes(self.pubkey), scheme.config)
24+
25+
def produce_attestation(self, data: AttestationData) -> Attestation:
26+
"""
27+
Produce an attestation from attestation data.
28+
29+
This method wraps AttestationData with the validator's identity to create
30+
a complete Attestation object ready for signing and broadcast.
31+
32+
Args:
33+
data: The attestation data containing slot, head, target, and source.
34+
35+
Returns:
36+
A fully constructed Attestation object with this validator's index.
37+
"""
38+
return Attestation(
39+
validator_id=self.index,
40+
data=data,
41+
)

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/constants.py

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

src/lean_spec/subspecs/forkchoice/helpers.py

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

src/lean_spec/subspecs/forkchoice/store.py

Lines changed: 109 additions & 63 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

@@ -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-
)

src/lean_spec/subspecs/ssz/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""SSZ (Simple Serialize) implementation."""
22

3-
from .constants import ZERO_HASH
3+
from lean_spec.types import ZERO_HASH
4+
45
from .hash import HashTreeRoot, hash_tree_root
56

67
__all__ = [
Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
"""Constants defined in the SSZ specification."""
22

3-
from lean_spec.types.byte_arrays import Bytes32
4-
53
BYTES_PER_CHUNK: int = 32
64
"""Number of bytes per Merkle chunk."""
75

86
BITS_PER_BYTE: int = 8
97
"""Number of bits per byte."""
10-
11-
ZERO_HASH: Bytes32 = Bytes32(b"\x00" * BYTES_PER_CHUNK)
12-
"""A zero hash, used for padding in Merkleization."""

0 commit comments

Comments
 (0)