Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/testing/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dependencies = [
"ckzg>=2.1.3,<3",
"tenacity>=9.0.0,<10",
"Jinja2>=3,<4",
"eth-remerkleable==0.1.31",
]

[project.urls]
Expand Down
311 changes: 311 additions & 0 deletions packages/testing/src/execution_testing/ssz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
"""
SSZ container types and helpers for the REST+SSZ Engine API.
"""

from typing import Any, Dict, Mapping, Sequence, Type, TypeVar

from remerkleable.complex import Container
from remerkleable.core import View

from .constants import (
MAX_BYTES_PER_EXECUTION_REQUEST,
MAX_BYTES_PER_TX,
MAX_EXECUTION_REQUESTS_PER_PAYLOAD,
MAX_EXTRA_DATA_BYTES,
MAX_TXS_PER_PAYLOAD,
MAX_WITHDRAWALS_PER_PAYLOAD,
)
from .containers import (
BlobAndProofV1,
BlobAndProofV2,
BlobCellsAndProofs,
BlobsBundleV1,
BlobsBundleV2,
BlobsV1Request,
BlobsV1Response,
BlobsV2Request,
BlobsV2Response,
BlobsV3Response,
BlobsV4Request,
BlobsV4Response,
BlobV1Entry,
BlobV2Entry,
BlobV4Entry,
BodiesByHashRequest,
BodiesResponse,
BodyEntry,
BuiltPayloadAmsterdam,
BuiltPayloadCancun,
BuiltPayloadOsaka,
BuiltPayloadParis,
BuiltPayloadPrague,
BuiltPayloadShanghai,
Comment on lines +37 to +42

@jtraglia jtraglia Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should the BuiltPayload containers really exist? Will clients have this?

CapabilitiesResponse,
ClientVersion,
ExecutionPayloadAmsterdam,
ExecutionPayloadBodyAmsterdam,
ExecutionPayloadBodyCancun,
ExecutionPayloadBodyOsaka,
ExecutionPayloadBodyParis,
ExecutionPayloadBodyPrague,
ExecutionPayloadBodyShanghai,
ExecutionPayloadCancun,
ExecutionPayloadEnvelopeAmsterdam,
ExecutionPayloadEnvelopeCancun,
ExecutionPayloadEnvelopeOsaka,
ExecutionPayloadEnvelopeParis,
ExecutionPayloadEnvelopePrague,
ExecutionPayloadEnvelopeShanghai,
ExecutionPayloadOsaka,
ExecutionPayloadParis,
ExecutionPayloadPrague,
ExecutionPayloadShanghai,
ForkchoiceState,
ForkchoiceUpdateAmsterdam,
ForkchoiceUpdateCancun,
ForkchoiceUpdateOsaka,
ForkchoiceUpdateParis,
ForkchoiceUpdatePrague,
ForkchoiceUpdateResponse,
ForkchoiceUpdateShanghai,
IdentityResponse,
PayloadAttributesAmsterdam,
PayloadAttributesCancun,
PayloadAttributesOsaka,
PayloadAttributesParis,
PayloadAttributesPrague,
PayloadAttributesShanghai,
PayloadStatus,
Withdrawal,
)
from .random_value import (
RandomizationMode,
deterministic_seed,
get_random_ssz_object,
)
from .ssz_types import (
Address,
Bloom,
Bytes4,
Bytes8,
Bytes32,
Bytes48,
Hash32,
Root,
VersionedHash,
)

ViewT = TypeVar("ViewT", bound=View)


def encode_bytes(value: View) -> bytes:
"""Serialize an SSZ value to its canonical byte encoding."""
return value.encode_bytes()


def decode_bytes(ssz_type: Type[ViewT], data: bytes) -> ViewT:
"""Deserialize ``data`` into an SSZ value of ``ssz_type``."""
return ssz_type.decode_bytes(data)


def hash_tree_root(value: View) -> bytes:
"""Return the 32-byte SSZ `hash_tree_root` of an SSZ value."""
return bytes(value.hash_tree_root())


EXECUTION_PAYLOAD_BY_FORK: Dict[str, Type[Container]] = {
"Paris": ExecutionPayloadParis,
"Shanghai": ExecutionPayloadShanghai,
"Cancun": ExecutionPayloadCancun,
"Prague": ExecutionPayloadPrague,
"Osaka": ExecutionPayloadOsaka,
"Amsterdam": ExecutionPayloadAmsterdam,
}


EXECUTION_PAYLOAD_ENVELOPE_BY_FORK: Dict[str, Type[Container]] = {
"Paris": ExecutionPayloadEnvelopeParis,
"Shanghai": ExecutionPayloadEnvelopeShanghai,
"Cancun": ExecutionPayloadEnvelopeCancun,
"Prague": ExecutionPayloadEnvelopePrague,
"Osaka": ExecutionPayloadEnvelopeOsaka,
"Amsterdam": ExecutionPayloadEnvelopeAmsterdam,
}


def _build(cls: Any, fork: str, candidates: Mapping[str, Any]) -> View:
kwargs = {}
for name in cls.fields():
value = candidates.get(name)
if value is None:
raise ValueError(
f"{cls.__name__} ({fork}) requires field {name!r}"
)
kwargs[name] = value
return cls(**kwargs)


def envelope_bytes(
fork: str,
*,
parent_hash: bytes,
fee_recipient: bytes,
state_root: bytes,
receipts_root: bytes,
logs_bloom: bytes,
Comment on lines +148 to +155

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shouldn't these be SSZ types?

@RazorClient RazorClient Jun 17, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

envelope_bytes is intentionally a plain-values builder: callers pass ordinary bytes/int, and it wraps them into the SSZ containers internally, so plain parameter types are the arguments

prev_randao: bytes,
block_number: int,
gas_limit: int,
gas_used: int,
timestamp: int,
extra_data: bytes,
base_fee_per_gas: int,
block_hash: bytes,
transactions: Sequence[bytes],
withdrawals: Sequence[Mapping[str, Any]] | None = None,
blob_gas_used: int | None = None,
excess_blob_gas: int | None = None,
block_access_list: bytes | None = None,
slot_number: int | None = None,
parent_beacon_block_root: bytes | None = None,
execution_requests: Sequence[bytes] | None = None,
) -> bytes:
"""
Build a fork's `newPayload` envelope from plain values and return its
canonical SSZ byte encoding.
"""
if fork not in EXECUTION_PAYLOAD_ENVELOPE_BY_FORK:
raise ValueError(f"unknown fork: {fork!r}")

def _opt_bytes(value: bytes | None) -> bytes | None:
return None if value is None else bytes(value)

payload_values: Mapping[str, Any] = {
"parent_hash": bytes(parent_hash),
"fee_recipient": bytes(fee_recipient),
"state_root": bytes(state_root),
"receipts_root": bytes(receipts_root),
"logs_bloom": bytes(logs_bloom),
"prev_randao": bytes(prev_randao),
"block_number": block_number,
"gas_limit": gas_limit,
"gas_used": gas_used,
"timestamp": timestamp,
"extra_data": bytes(extra_data),
"base_fee_per_gas": base_fee_per_gas,
"block_hash": bytes(block_hash),
"transactions": [bytes(tx) for tx in transactions],
"withdrawals": (
None
if withdrawals is None
else [Withdrawal(**w) for w in withdrawals]
),
"blob_gas_used": blob_gas_used,
"excess_blob_gas": excess_blob_gas,
"block_access_list": _opt_bytes(block_access_list),
"slot_number": slot_number,
}
payload = _build(EXECUTION_PAYLOAD_BY_FORK[fork], fork, payload_values)

envelope_values: Mapping[str, Any] = {
"payload": payload,
"parent_beacon_block_root": _opt_bytes(parent_beacon_block_root),
"execution_requests": (
None
if execution_requests is None
else [bytes(r) for r in execution_requests]
),
}
envelope = _build(
EXECUTION_PAYLOAD_ENVELOPE_BY_FORK[fork], fork, envelope_values
)
return encode_bytes(envelope)


__all__ = (
"EXECUTION_PAYLOAD_BY_FORK",
"EXECUTION_PAYLOAD_ENVELOPE_BY_FORK",
"Address",
"BlobAndProofV1",
"BlobAndProofV2",
"BlobCellsAndProofs",
"BlobsBundleV1",
"BlobsBundleV2",
"BlobsV1Request",
"BlobsV1Response",
"BlobsV2Request",
"BlobsV2Response",
"BlobsV3Response",
"BlobsV4Request",
"BlobsV4Response",
"BlobV1Entry",
"BlobV2Entry",
"BlobV4Entry",
"Bloom",
"BodiesByHashRequest",
"BodiesResponse",
"BodyEntry",
"BuiltPayloadAmsterdam",
"BuiltPayloadCancun",
"BuiltPayloadOsaka",
"BuiltPayloadParis",
"BuiltPayloadPrague",
"BuiltPayloadShanghai",
"Bytes32",
"Bytes4",
"Bytes48",
"Bytes8",
"CapabilitiesResponse",
"ClientVersion",
"ExecutionPayloadAmsterdam",
"ExecutionPayloadBodyAmsterdam",
"ExecutionPayloadBodyCancun",
"ExecutionPayloadBodyOsaka",
"ExecutionPayloadBodyParis",
"ExecutionPayloadBodyPrague",
"ExecutionPayloadBodyShanghai",
"ExecutionPayloadCancun",
"ExecutionPayloadEnvelopeAmsterdam",
"ExecutionPayloadEnvelopeCancun",
"ExecutionPayloadEnvelopeOsaka",
"ExecutionPayloadEnvelopeParis",
"ExecutionPayloadEnvelopePrague",
"ExecutionPayloadEnvelopeShanghai",
"ExecutionPayloadOsaka",
"ExecutionPayloadParis",
"ExecutionPayloadPrague",
"ExecutionPayloadShanghai",
"ForkchoiceState",
"ForkchoiceUpdateAmsterdam",
"ForkchoiceUpdateCancun",
"ForkchoiceUpdateOsaka",
"ForkchoiceUpdateParis",
"ForkchoiceUpdatePrague",
"ForkchoiceUpdateResponse",
"ForkchoiceUpdateShanghai",
"Hash32",
"IdentityResponse",
"MAX_BYTES_PER_EXECUTION_REQUEST",
"MAX_BYTES_PER_TX",
"MAX_EXECUTION_REQUESTS_PER_PAYLOAD",
"MAX_EXTRA_DATA_BYTES",
"MAX_TXS_PER_PAYLOAD",
"MAX_WITHDRAWALS_PER_PAYLOAD",
"PayloadAttributesAmsterdam",
"PayloadAttributesCancun",
"PayloadAttributesOsaka",
"PayloadAttributesParis",
"PayloadAttributesPrague",
"PayloadAttributesShanghai",
"PayloadStatus",
"RandomizationMode",
"Root",
"VersionedHash",
"Withdrawal",
"decode_bytes",
"deterministic_seed",
"encode_bytes",
"envelope_bytes",
"get_random_ssz_object",
"hash_tree_root",
)
36 changes: 36 additions & 0 deletions packages/testing/src/execution_testing/ssz/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""SSZ size limits for the REST+SSZ Engine API."""

# Payload / envelope limits.
MAX_BYTES_PER_TX = 2**30
MAX_TXS_PER_PAYLOAD = 2**20
MAX_WITHDRAWALS_PER_PAYLOAD = 2**4
BYTES_PER_LOGS_BLOOM = 256
MAX_EXTRA_DATA_BYTES = 2**5
MAX_BAL_BYTES = MAX_BYTES_PER_TX
MAX_EXECUTION_REQUESTS_PER_PAYLOAD = 2**8
MAX_BYTES_PER_EXECUTION_REQUEST = MAX_BYTES_PER_TX

# Blob / cell limits
MAX_BLOB_COMMITMENTS_PER_BLOCK = 2**12
FIELD_ELEMENTS_PER_BLOB = 4096
BYTES_PER_FIELD_ELEMENT = 32
BYTES_PER_BLOB = FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT
CELLS_PER_EXT_BLOB = 128
FIELD_ELEMENTS_PER_CELL = 64
BYTES_PER_CELL = FIELD_ELEMENTS_PER_CELL * BYTES_PER_FIELD_ELEMENT

# Blob-pool / bodies.
MAX_VERSIONED_HASHES_PER_REQUEST = 128
MAX_BLOBS_REQUEST = MAX_VERSIONED_HASHES_PER_REQUEST
MAX_BODIES_REQUEST = 2**5

# Status / error limits.
MAX_ERROR_BYTES = 1024

# Identity / capabilities limits.
MAX_CLIENT_CODE_LENGTH = 2
MAX_CLIENT_NAME_LENGTH = 64
MAX_CLIENT_VERSION_LENGTH = 64
MAX_CLIENT_VERSIONS = 4
MAX_CAPABILITY_NAME_LENGTH = 64
MAX_CAPABILITIES = 64
Loading
Loading