Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/client/chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def process_slots(state: State, slot: Slot) -> None:
```python
def process_slot(state: BeaconState) -> None:
# Cache latest block header state root
if state.latest_block_header.state_root == Bytes32():
if state.latest_block_header.state_root == Bytes32.zero():
previous_state_root = hash_tree_root(state)
state.latest_block_header.state_root = previous_state_root
```
Expand All @@ -233,9 +233,9 @@ def process_block_header(state: State, block: Block) -> None:
assert block.parent_root == hash_tree_root(state.latest_block_header)

# If this was first block post genesis, 3sf mini special treatment is required
# to correctly set genesis block root as already justified and finalized.
# This is not possible at the time of genesis state generation and are set at
# zero bytes because genesis block is calculated using genesis state causing a
# to correctly set genesis block root as already justified and finalized.
# This is not possible at the time of genesis state generation and are set at
# zero bytes because genesis block is calculated using genesis state causing a
# circular dependency
if state.latest_block_header.slot == 0:
# block.parent_root is the genesis root
Expand All @@ -259,7 +259,7 @@ def process_block_header(state: State, block: Block) -> None:
slot=block.slot,
proposer_index=block.proposer_index,
parent_root=block.parent_root,
state_root=Bytes32(), # Overwritten in the next process_slot call
state_root=Bytes32.zero(), # Overwritten in the next process_slot call
body_root=hash_tree_root(block.body),
)
```
Expand Down Expand Up @@ -289,7 +289,7 @@ def process_attestations(state: State, attestations: List[SignedVote]) -> None:
state.justified_slots[vote.source.slot] is False
# This condition is missing in 3sf mini but has been added here because
# we don't want to re-introduce the target again for remaining votes if
# the slot is already justified and its tracking already cleared out
# the slot is already justified and its tracking already cleared out
# from justifications map
or state.justified_slots[vote.target.slot] is True
or vote.source.root != state.historical_block_hashes[vote.source.slot]
Expand Down
26 changes: 11 additions & 15 deletions src/lean_spec/subspecs/containers/block.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
"""Block Containers."""

from pydantic import Field
from typing_extensions import Annotated

from lean_spec.types import Bytes32, StrictBaseModel, Uint64
from lean_spec.subspecs.containers.slot import Slot
from lean_spec.types import Bytes32, List, StrictBaseModel, Uint64
from lean_spec.types.container import Container

from ..chain import config
from .vote import Vote
from .vote import SignedVote


class BlockBody(StrictBaseModel):
class BlockBody(Container):
"""The body of a block, containing payload data."""

votes: Annotated[
list[Vote],
Field(max_length=config.VALIDATOR_REGISTRY_LIMIT),
]
attestations: List[SignedVote, config.VALIDATOR_REGISTRY_LIMIT.as_int()] # type: ignore
"""
A list of votes included in the block.

Note: This will eventually be replaced by aggregated attestations.
"""


class BlockHeader(StrictBaseModel):
class BlockHeader(StrictBaseModel, Container):
"""The header of a block, containing metadata."""

slot: Uint64
slot: Slot
"""The slot in which the block was proposed."""

proposer_index: Uint64
Expand All @@ -42,10 +38,10 @@ class BlockHeader(StrictBaseModel):
"""The root of the block's body."""


class Block(StrictBaseModel):
class Block(StrictBaseModel, Container):
"""Represents a single block in the chain."""

slot: Uint64
slot: Slot
"""The slot in which the block was proposed."""

proposer_index: Uint64
Expand All @@ -61,7 +57,7 @@ class Block(StrictBaseModel):
"""The block's payload."""


class SignedBlock(StrictBaseModel):
class SignedBlock(StrictBaseModel, Container):
"""A container for a block and the proposer's signature."""

message: Block
Expand Down
8 changes: 5 additions & 3 deletions src/lean_spec/subspecs/containers/checkpoint.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Checkpoint Container."""

from lean_spec.types import Bytes32, StrictBaseModel, Uint64
from lean_spec.subspecs.containers.slot import Slot
from lean_spec.types import Bytes32, StrictBaseModel
from lean_spec.types.container import Container


class Checkpoint(StrictBaseModel):
class Checkpoint(StrictBaseModel, Container):
"""Represents a checkpoint in the chain's history."""

root: Bytes32
"""The root hash of the checkpoint's block."""

slot: Uint64
slot: Slot
"""The slot number of the checkpoint's block."""
3 changes: 2 additions & 1 deletion src/lean_spec/subspecs/containers/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Consensus Configuration Container."""

from lean_spec.types import StrictBaseModel, Uint64
from lean_spec.types.container import Container


class Config(StrictBaseModel):
class Config(StrictBaseModel, Container):
"""
Holds temporary configuration properties for simplified consensus.

Expand Down
48 changes: 48 additions & 0 deletions src/lean_spec/subspecs/containers/slot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Slot container."""

from __future__ import annotations

from functools import total_ordering

from lean_spec.types import Uint64


@total_ordering
class Slot(Uint64):
"""Represents a slot number as a 64-bit unsigned integer."""

def is_justifiable_after(self, finalized_slot: Slot) -> bool:
"""
Checks if this slot is a valid candidate for justification after a given finalized slot.

According to the 3SF-mini specification, a slot is justifiable if its
distance (`delta`) from the last finalized slot is:
1. Less than or equal to 5.
2. A perfect square (e.g., 9, 16, 25...).
3. A pronic number (of the form x^2 + x, e.g., 6, 12, 20...).

Args:
finalized_slot: The last slot that was finalized.

Returns:
True if the slot is justifiable, False otherwise.

Raises:
AssertionError: If this slot is earlier than the finalized slot.
"""
# Ensure the candidate slot is not before the finalized slot.
assert self >= finalized_slot, "Candidate slot must not be before finalized slot"

# Calculate the distance in slots from the last finalized slot.
delta = (self - finalized_slot).as_int()

return (
# Rule 1: The first few slots immediately following finalization are always justifiable.
delta <= 5
# Rule 2: Slots at perfect square distances are justifiable (e.g., sqrt(9) % 1 == 0).
or (delta**0.5) % 1 == 0
# Rule 3: Slots at pronic distances (x^2 + x) are justifiable.
#
# This is true if sqrt(delta + 0.25) has a fractional part of exactly 0.5.
or ((delta + 0.25) ** 0.5) % 1 == 0.5
)
Loading
Loading