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
4 changes: 4 additions & 0 deletions packages/testing/src/consensus_testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .test_fixtures import (
BaseConsensusFixture,
ForkChoiceTest,
SSZTest,
StateTransitionTest,
VerifySignaturesTest,
)
Expand All @@ -27,6 +28,7 @@
StateTransitionTestFiller = Type[StateTransitionTest]
ForkChoiceTestFiller = Type[ForkChoiceTest]
VerifySignaturesTestFiller = Type[VerifySignaturesTest]
SSZTestFiller = Type[SSZTest]

__all__ = [
# Public API
Expand All @@ -40,6 +42,7 @@
"StateTransitionTest",
"ForkChoiceTest",
"VerifySignaturesTest",
"SSZTest",
# Test types
"BaseForkChoiceStep",
"TickStep",
Expand All @@ -54,4 +57,5 @@
"StateTransitionTestFiller",
"ForkChoiceTestFiller",
"VerifySignaturesTestFiller",
"SSZTestFiller",
]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .base import BaseConsensusFixture
from .fork_choice import ForkChoiceTest
from .ssz import SSZTest
from .state_transition import StateTransitionTest
from .verify_signatures import VerifySignaturesTest

Expand All @@ -10,4 +11,5 @@
"StateTransitionTest",
"ForkChoiceTest",
"VerifySignaturesTest",
"SSZTest",
]
71 changes: 71 additions & 0 deletions packages/testing/src/consensus_testing/test_fixtures/ssz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""SSZ test fixture format for serialization conformance testing."""

from typing import Any, ClassVar

from pydantic import field_serializer

from lean_spec.types.container import Container

from .base import BaseConsensusFixture


class SSZTest(BaseConsensusFixture):
"""
Test fixture for SSZ serialization/deserialization conformance.

Tests roundtrip serialization for SSZ containers.

Structure:
type_name: Name of the container class
value: The container instance
serialized: Hex-encoded SSZ bytes (computed)
"""

format_name: ClassVar[str] = "ssz"
description: ClassVar[str] = "Tests SSZ serialization roundtrip"

type_name: str
"""Name of the container class being tested."""

value: Container
"""The container instance to test."""

serialized: str = ""
"""Hex-encoded SSZ serialized bytes (computed during make_fixture)."""

@field_serializer("value", when_used="json")
def serialize_value(self, value: Container) -> dict[str, Any]:
"""Serialize the container value to JSON using its native serialization."""
return value.to_json()

def make_fixture(self) -> "SSZTest":
"""
Generate the fixture by testing SSZ roundtrip.

1. Serialize the value to SSZ bytes
2. Deserialize the bytes back to a container
3. Verify the roundtrip produces the same value

Returns:
SSZTest with computed serialized field.

Raises:
AssertionError: If roundtrip fails.
"""
# Serialize to SSZ bytes
ssz_bytes = self.value.encode_bytes()

# Deserialize back
container_cls = type(self.value)
decoded = container_cls.decode_bytes(ssz_bytes)

# Verify roundtrip
assert decoded == self.value, (
f"SSZ roundtrip failed for {self.type_name}: "
f"original != decoded\n"
f"Original: {self.value}\n"
f"Decoded: {decoded}"
)

# Return fixture with computed serialized field
return self.model_copy(update={"serialized": "0x" + ssz_bytes.hex()})
15 changes: 11 additions & 4 deletions src/lean_spec/subspecs/ssz/hash.py
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need Fp implementation here so that Fp in the Signature can be serialized and merkleized

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from functools import singledispatch
from math import ceil

from lean_spec.subspecs.koalabear import Fp
from lean_spec.subspecs.ssz.constants import BITS_PER_CHUNK, BYTES_PER_CHUNK
from lean_spec.types.bitfields import BaseBitlist, BaseBitvector
from lean_spec.types.boolean import Boolean
Expand Down Expand Up @@ -53,6 +54,12 @@ def _htr_boolean(value: Boolean) -> Bytes32:
return merkleize(pack_bytes(value.encode_bytes()))


@hash_tree_root.register
def _htr_fp(value: Fp) -> Bytes32:
"""KoalaBear field elements: pack bytes into chunks and merkleize."""
return merkleize(pack_bytes(value.encode_bytes()))


@hash_tree_root.register
def _htr_bytes(value: bytes) -> Bytes32:
"""Treat raw bytes like ByteVector[N]."""
Expand Down Expand Up @@ -104,9 +111,9 @@ def _htr_bitlist_base(value: BaseBitlist) -> Bytes32:
def _htr_vector(value: SSZVector) -> Bytes32:
elem_t, length = type(value).ELEMENT_TYPE, type(value).LENGTH

if issubclass(elem_t, (BaseUint, Boolean)):
if issubclass(elem_t, (BaseUint, Boolean, Fp)):
# BASIC elements: pack serialized bytes
elem_size = elem_t.get_byte_length() if issubclass(elem_t, BaseUint) else 1
elem_size = elem_t.get_byte_length()
# Compute limit in chunks: ceil((length * elem_size) / BYTES_PER_CHUNK)
limit_chunks = (length * elem_size + BYTES_PER_CHUNK - 1) // BYTES_PER_CHUNK
return merkleize(
Expand All @@ -122,9 +129,9 @@ def _htr_vector(value: SSZVector) -> Bytes32:
def _htr_list(value: SSZList) -> Bytes32:
elem_t, limit = type(value).ELEMENT_TYPE, type(value).LIMIT

if issubclass(elem_t, (BaseUint, Boolean)):
if issubclass(elem_t, (BaseUint, Boolean, Fp)):
# BASIC elements: pack serialized bytes
elem_size = elem_t.get_byte_length() if issubclass(elem_t, BaseUint) else 1
elem_size = elem_t.get_byte_length()
# Compute limit in chunks: ceil((limit * elem_size) / BYTES_PER_CHUNK)
limit_chunks = (limit * elem_size + BYTES_PER_CHUNK - 1) // BYTES_PER_CHUNK
root = merkleize(
Expand Down
7 changes: 7 additions & 0 deletions src/lean_spec/subspecs/xmss/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from typing import TYPE_CHECKING, Mapping, NamedTuple

from pydantic import model_serializer

from ...types import Bytes32, Uint64
from ...types.container import Container
from .subtree import HashSubTree
Expand Down Expand Up @@ -67,6 +69,11 @@ class Signature(Container):
hashes: HashDigestList
"""The one-time signature itself: a list of intermediate Winternitz chain hashes."""

@model_serializer(mode="plain", when_used="json")
def _serialize_as_bytes(self) -> str:
"""Serialize as hex-encoded SSZ bytes for JSON output."""
return "0x" + self.encode_bytes().hex()

def verify(
self,
public_key: PublicKey,
Expand Down
1 change: 1 addition & 0 deletions tests/consensus/devnet/ssz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""SSZ serialization conformance tests."""
Loading
Loading