diff --git a/pyproject.toml b/pyproject.toml index 405a772e..62067402 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,9 @@ classifiers = [ ] requires-python = ">=3.12" dependencies = [ + "ethereum-types<=0.2.4,<0.3", "pydantic>=2.9.2,<3", + "ssz>=0.5.2,<0.6", "typing-extensions>=4.4", ] diff --git a/src/lean_spec/client/__init__.py b/src/lean_spec/client/__init__.py new file mode 100644 index 00000000..604cd663 --- /dev/null +++ b/src/lean_spec/client/__init__.py @@ -0,0 +1,9 @@ +""" +Specification for pqdevnet-0 + +Key differences from 3SF-mini.py: +- Using `U64` instead of native `int` for all fields +- Using `Bytes32` instead of native `str` for all fields +- Combined `*_root` and `*_slot` pairs into a single `Checkpoint` field +- Removed optionals from `Block` +""" diff --git a/src/lean_spec/client/block.py b/src/lean_spec/client/block.py new file mode 100644 index 00000000..01200346 --- /dev/null +++ b/src/lean_spec/client/block.py @@ -0,0 +1,27 @@ +""" +A `Block` is a single link in the Lean Consensus chain. Each `Block` contains +associated metadata like the slot number, parent block hash and votes. + +Together, these blocks form a cryptographically secure journal recording the +history of all state transitions that have happened since the genesis of the +chain. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U64 +from pydantic import BaseModel, ConfigDict +from ssz.sedes.list import List + +from .preset import VALIDATOR_REGISTRY_LIMIT +from .vote import Vote + + +class Block(BaseModel): + """A single block in the Lean Consensus chain.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + slot: U64 + parent: Bytes32 + votes: List[Vote, VALIDATOR_REGISTRY_LIMIT] + state_root: Bytes32 diff --git a/src/lean_spec/client/checkpoint.py b/src/lean_spec/client/checkpoint.py new file mode 100644 index 00000000..6af0a8f5 --- /dev/null +++ b/src/lean_spec/client/checkpoint.py @@ -0,0 +1,17 @@ +""" +A `Checkpoint` is a single checkpoint for a block in the Lean Consensus chain. +Each `Checkpoint` contains its associated block root and slot. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U64 +from pydantic import BaseModel, ConfigDict + + +class Checkpoint(BaseModel): + """A single checkpoint in the Lean Consensus chain.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + root: Bytes32 + slot: U64 diff --git a/src/lean_spec/client/preset.py b/src/lean_spec/client/preset.py new file mode 100644 index 00000000..ed30399f --- /dev/null +++ b/src/lean_spec/client/preset.py @@ -0,0 +1,26 @@ +""" +The `preset` module contains the parameters that are used to configure the +Lean Consensus chain. +""" + +from ethereum_types.numeric import U64 + +# Time parameters +# --------------------------------------------------------------- + +# 4 seconds +SLOT_DURATION_MS: U64 = U64(4000) + +# Basis points (out of 10000) +PROPOSER_REORG_CUTOFF_BPS: U64 = U64(2500) +VOTE_DUE_BPS: U64 = U64(5000) +FAST_CONFIRM_DUE_BPS: U64 = U64(7500) +VIEW_FREEZE_CUTOFF_BPS: U64 = U64(7500) + +# Misc +# --------------------------------------------------------------- + +# 2^18, enough for 2^18 / (60 / 4) / 60 / 24 = 12.1 days +MAX_HISTORICAL_BLOCK_HASHES: U64 = U64(262144) + +VALIDATOR_REGISTRY_LIMIT: U64 = U64(4096) diff --git a/src/lean_spec/client/state.py b/src/lean_spec/client/state.py new file mode 100644 index 00000000..6618cfda --- /dev/null +++ b/src/lean_spec/client/state.py @@ -0,0 +1,37 @@ +""" +A `State` is a collection of metadata that describes the current state of the +Lean Consensus chain. It contains information about the latest justified and +finalized blocks, as well as the historical block hashes and justified slots. + +It is used to verify the integrity of the chain and to ensure that the chain is +progressing correctly. +""" + +from ethereum_types.bytes import Bytes32 +from pydantic import BaseModel, ConfigDict +from ssz.sedes.bitlist import Bitlist +from ssz.sedes.list import List + +from .checkpoint import Checkpoint +from .preset import MAX_HISTORICAL_BLOCK_HASHES, VALIDATOR_REGISTRY_LIMIT + + +class State(BaseModel): + """Represents the current state of the Lean Consensus chain.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Diverged from 3SF-mini.py: Removed `config: Config` from the state + + latest_justified: Checkpoint + latest_finalized: Checkpoint + + historical_block_hashes: List[Bytes32, MAX_HISTORICAL_BLOCK_HASHES] + justified_slots: List[bool, MAX_HISTORICAL_BLOCK_HASHES] + + # Diverged from 3SF-mini.py: + # Flattened `justifications: Dict[str, List[bool]]` for SSZ + justifications_roots: List[Bytes32, MAX_HISTORICAL_BLOCK_HASHES] + justifications_validators: Bitlist[ + MAX_HISTORICAL_BLOCK_HASHES * VALIDATOR_REGISTRY_LIMIT + ] diff --git a/src/lean_spec/client/vote.py b/src/lean_spec/client/vote.py new file mode 100644 index 00000000..061d29ae --- /dev/null +++ b/src/lean_spec/client/vote.py @@ -0,0 +1,22 @@ +""" +A `Vote` is a single vote for a block in the Lean Consensus chain. Each `Vote` +contains information about the validator that voted, the slot of the block they +voted for, and the block hash they voted for. +""" + +from ethereum_types.numeric import U64 +from pydantic import BaseModel, ConfigDict + +from .checkpoint import Checkpoint + + +class Vote(BaseModel): + """A single vote for a block in the Lean Consensus chain.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + validator_id: U64 + slot: U64 + head: Checkpoint + target: Checkpoint + source: Checkpoint diff --git a/tests/lean_spec/client/test_block.py b/tests/lean_spec/client/test_block.py new file mode 100644 index 00000000..913a8c98 --- /dev/null +++ b/tests/lean_spec/client/test_block.py @@ -0,0 +1,39 @@ +""" +Tests for the client's Block container. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U64 +from ssz.sedes.list import List + +from lean_spec.client.block import Block +from lean_spec.client.checkpoint import Checkpoint +from lean_spec.client.preset import VALIDATOR_REGISTRY_LIMIT +from lean_spec.client.vote import Vote + + +def test_block(): + Block( + slot=U64(1), + parent=Bytes32(b"\x02" * 32), + votes=List( + Vote( + validator_id=U64(1), + slot=U64(2), + head=Checkpoint( + root=Bytes32(b"\x04" * 32), + slot=U64(3), + ), + source=Checkpoint( + root=Bytes32(b"\x05" * 32), + slot=U64(5), + ), + target=Checkpoint( + root=Bytes32(b"\x06" * 32), + slot=U64(4), + ), + ), + int(VALIDATOR_REGISTRY_LIMIT), + ), + state_root=Bytes32(b"\x03" * 32), + ) diff --git a/tests/lean_spec/client/test_checkpoint.py b/tests/lean_spec/client/test_checkpoint.py new file mode 100644 index 00000000..e786bb3b --- /dev/null +++ b/tests/lean_spec/client/test_checkpoint.py @@ -0,0 +1,15 @@ +""" +Tests for the client's Checkpoint container. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U64 + +from lean_spec.client.checkpoint import Checkpoint + + +def test_checkpoint(): + Checkpoint( + root=Bytes32(b"\x42" * 32), + slot=U64(42), + ) diff --git a/tests/lean_spec/client/test_state.py b/tests/lean_spec/client/test_state.py new file mode 100644 index 00000000..fc2a4ba9 --- /dev/null +++ b/tests/lean_spec/client/test_state.py @@ -0,0 +1,52 @@ +""" +Tests for the client's State container. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U64 +from ssz.sedes.bitlist import Bitlist +from ssz.sedes.list import List + +from lean_spec.client.checkpoint import Checkpoint +from lean_spec.client.preset import ( + MAX_HISTORICAL_BLOCK_HASHES, + VALIDATOR_REGISTRY_LIMIT, +) +from lean_spec.client.state import State + + +def test_state(): + State( + latest_justified=Checkpoint( + root=Bytes32(b"\x00" * 32), + slot=U64(0), + ), + latest_finalized=Checkpoint( + root=Bytes32(b"\x00" * 32), + slot=U64(0), + ), + historical_block_hashes=List( + [ + Bytes32(b"\x00" * 32), + Bytes32(b"\x01" * 32), + ], + int(MAX_HISTORICAL_BLOCK_HASHES), + ), + justified_slots=List( + [ + True, + False, + ], + int(MAX_HISTORICAL_BLOCK_HASHES), + ), + justifications_roots=List( + [ + Bytes32(b"\x00" * 32), + Bytes32(b"\x01" * 32), + ], + int(MAX_HISTORICAL_BLOCK_HASHES), + ), + justifications_validators=Bitlist( + int(MAX_HISTORICAL_BLOCK_HASHES * VALIDATOR_REGISTRY_LIMIT) + ), + ) diff --git a/tests/lean_spec/client/test_vote.py b/tests/lean_spec/client/test_vote.py new file mode 100644 index 00000000..b857073e --- /dev/null +++ b/tests/lean_spec/client/test_vote.py @@ -0,0 +1,28 @@ +""" +Tests for the client's Vote container. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U64 + +from lean_spec.client.checkpoint import Checkpoint +from lean_spec.client.vote import Vote + + +def test_vote(): + Vote( + validator_id=U64(1), + slot=U64(2), + head=Checkpoint( + root=Bytes32(b"\x03" * 32), + slot=U64(3), + ), + source=Checkpoint( + root=Bytes32(b"\x05" * 32), + slot=U64(5), + ), + target=Checkpoint( + root=Bytes32(b"\x04" * 32), + slot=U64(4), + ), + )