From 2a1bc9231b090b5339172c4361f91981b3c73474 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Tue, 16 Jun 2026 18:39:03 +0530 Subject: [PATCH 1/6] feat(test-specs): add execution_testing.ssz package for the REST+SSZ Engine API --- packages/testing/pyproject.toml | 1 + .../src/execution_testing/ssz/__init__.py | 303 ++++++++ .../src/execution_testing/ssz/constants.py | 74 ++ .../src/execution_testing/ssz/containers.py | 700 ++++++++++++++++++ .../src/execution_testing/ssz/random_value.py | 183 +++++ .../execution_testing/ssz/tests/__init__.py | 1 + .../ssz/tests/test_containers.py | 123 +++ .../ssz/tests/test_engine_containers.py | 242 ++++++ .../ssz/tests/test_random_value.py | 155 ++++ pyproject.toml | 6 + uv.lock | 11 + 11 files changed, 1799 insertions(+) create mode 100644 packages/testing/src/execution_testing/ssz/__init__.py create mode 100644 packages/testing/src/execution_testing/ssz/constants.py create mode 100644 packages/testing/src/execution_testing/ssz/containers.py create mode 100644 packages/testing/src/execution_testing/ssz/random_value.py create mode 100644 packages/testing/src/execution_testing/ssz/tests/__init__.py create mode 100644 packages/testing/src/execution_testing/ssz/tests/test_containers.py create mode 100644 packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py create mode 100644 packages/testing/src/execution_testing/ssz/tests/test_random_value.py diff --git a/packages/testing/pyproject.toml b/packages/testing/pyproject.toml index ac0025be610..329e8b3c1c2 100644 --- a/packages/testing/pyproject.toml +++ b/packages/testing/pyproject.toml @@ -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] diff --git a/packages/testing/src/execution_testing/ssz/__init__.py b/packages/testing/src/execution_testing/ssz/__init__.py new file mode 100644 index 00000000000..facffe97fcf --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/__init__.py @@ -0,0 +1,303 @@ +""" +SSZ container types and helpers for the REST+SSZ Engine API. +""" + +from typing import Any, Mapping, Sequence, Type, TypeVar + +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, + Address, + Bloom, + Bytes4, + Bytes8, + Bytes32, + Bytes48, + Hash32, + Root, + VersionedHash, +) +from .containers import ( + BUILT_PAYLOAD_BY_FORK, + EXECUTION_PAYLOAD_BODY_BY_FORK, + EXECUTION_PAYLOAD_BY_FORK, + EXECUTION_PAYLOAD_ENVELOPE_BY_FORK, + FORKCHOICE_UPDATE_BY_FORK, + PAYLOAD_ATTRIBUTES_BY_FORK, + BlobAndProofV1, + BlobAndProofV2, + BlobCellsAndProofs, + BlobsBundleV1, + BlobsBundleV2, + BlobsV1Request, + BlobsV1Response, + BlobsV2Request, + BlobsV2Response, + BlobsV3Response, + BlobsV4Request, + BlobsV4Response, + BlobV1Entry, + BlobV2Entry, + BlobV4Entry, + BodiesByHashRequest, + BodiesResponse, + BodyEntry, + BuiltPayloadAmsterdam, + BuiltPayloadCancun, + BuiltPayloadOsaka, + BuiltPayloadParis, + BuiltPayloadPrague, + BuiltPayloadShanghai, + 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, +) + +ViewT = TypeVar("ViewT", bound=View) + +REFERENCE_SPEC_GIT_PATH = "src/engine/refactor-ssz.md" +REFERENCE_SPEC_VERSION = "4e0fed12d3ebc9d1ca8829331a82b97b1d1bd154" + + +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()) + + +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, + 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__ = ( + "BUILT_PAYLOAD_BY_FORK", + "EXECUTION_PAYLOAD_BODY_BY_FORK", + "EXECUTION_PAYLOAD_BY_FORK", + "EXECUTION_PAYLOAD_ENVELOPE_BY_FORK", + "FORKCHOICE_UPDATE_BY_FORK", + "PAYLOAD_ATTRIBUTES_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", + "REFERENCE_SPEC_GIT_PATH", + "REFERENCE_SPEC_VERSION", + "RandomizationMode", + "Root", + "VersionedHash", + "Withdrawal", + "decode_bytes", + "deterministic_seed", + "encode_bytes", + "envelope_bytes", + "get_random_ssz_object", + "hash_tree_root", +) diff --git a/packages/testing/src/execution_testing/ssz/constants.py b/packages/testing/src/execution_testing/ssz/constants.py new file mode 100644 index 00000000000..6ca67d9c3c7 --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/constants.py @@ -0,0 +1,74 @@ +"""SSZ size limits and byte-vector aliases for the REST+SSZ Engine API.""" + +from remerkleable.byte_arrays import ByteVector + +# 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 + + +class Hash32(ByteVector[32]): + """A 32-byte hash (`Hash32`, `Root` and `Bytes32` share this layout).""" + + +class Bytes32(ByteVector[32]): + """A 32-byte fixed vector.""" + + +class Root(ByteVector[32]): + """A 32-byte merkle root.""" + + +class Address(ByteVector[20]): + """A 20-byte execution-layer address.""" + + +class Bloom(ByteVector[BYTES_PER_LOGS_BLOOM]): + """A 256-byte logs bloom filter.""" + + +class VersionedHash(ByteVector[32]): + """An EIP-4844 versioned blob hash.""" + + +class Bytes8(ByteVector[8]): + """An 8-byte value (e.g. `payload_id`).""" + + +class Bytes4(ByteVector[4]): + """A 4-byte value (e.g. a client commit hash).""" + + +class Bytes48(ByteVector[48]): + """A 48-byte value (KZG commitments and proofs).""" diff --git a/packages/testing/src/execution_testing/ssz/containers.py b/packages/testing/src/execution_testing/ssz/containers.py new file mode 100644 index 00000000000..dca5e5321f5 --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/containers.py @@ -0,0 +1,700 @@ +""" +SSZ container definitions for the REST+SSZ Engine API. +""" + +from typing import Dict, Type + +from remerkleable.basic import boolean, uint8, uint64, uint256 +from remerkleable.bitfields import Bitvector +from remerkleable.byte_arrays import ByteList, ByteVector +from remerkleable.complex import Container, List + +from .constants import ( + BYTES_PER_BLOB, + BYTES_PER_CELL, + CELLS_PER_EXT_BLOB, + MAX_BAL_BYTES, + MAX_BLOB_COMMITMENTS_PER_BLOCK, + MAX_BLOBS_REQUEST, + MAX_BODIES_REQUEST, + MAX_BYTES_PER_EXECUTION_REQUEST, + MAX_BYTES_PER_TX, + MAX_CAPABILITIES, + MAX_CAPABILITY_NAME_LENGTH, + MAX_CLIENT_CODE_LENGTH, + MAX_CLIENT_NAME_LENGTH, + MAX_CLIENT_VERSION_LENGTH, + MAX_CLIENT_VERSIONS, + MAX_ERROR_BYTES, + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + MAX_EXTRA_DATA_BYTES, + MAX_TXS_PER_PAYLOAD, + MAX_WITHDRAWALS_PER_PAYLOAD, + Address, + Bloom, + Bytes4, + Bytes8, + Bytes32, + Bytes48, + Hash32, + Root, + VersionedHash, +) + + +class Withdrawal(Container): + """A validator withdrawal pushed into an execution payload.""" + + index: uint64 + validator_index: uint64 + address: Address + amount: uint64 + + +class ExecutionPayloadParis(Container): + """ + The Paris execution payload. + """ + + parent_hash: Hash32 + fee_recipient: Address + state_root: Hash32 + receipts_root: Hash32 + logs_bloom: Bloom + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + + +class ExecutionPayloadShanghai(Container): + """ + The Shanghai execution payload. + """ + + parent_hash: Hash32 + fee_recipient: Address + state_root: Hash32 + receipts_root: Hash32 + logs_bloom: Bloom + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +class ExecutionPayloadCancun(Container): + """ + The Cancun execution payload. + """ + + parent_hash: Hash32 + fee_recipient: Address + state_root: Hash32 + receipts_root: Hash32 + logs_bloom: Bloom + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + blob_gas_used: uint64 + excess_blob_gas: uint64 + + +class ExecutionPayloadPrague(Container): + """ + The Prague execution payload. + """ + + parent_hash: Hash32 + fee_recipient: Address + state_root: Hash32 + receipts_root: Hash32 + logs_bloom: Bloom + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + blob_gas_used: uint64 + excess_blob_gas: uint64 + + +class ExecutionPayloadOsaka(Container): + """ + The Osaka execution payload. + """ + + parent_hash: Hash32 + fee_recipient: Address + state_root: Hash32 + receipts_root: Hash32 + logs_bloom: Bloom + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + blob_gas_used: uint64 + excess_blob_gas: uint64 + + +class ExecutionPayloadAmsterdam(Container): + """ + The Amsterdam execution payload. + """ + + parent_hash: Hash32 + fee_recipient: Address + state_root: Hash32 + receipts_root: Hash32 + logs_bloom: Bloom + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + blob_gas_used: uint64 + excess_blob_gas: uint64 + block_access_list: ByteList[MAX_BAL_BYTES] + slot_number: uint64 + + +EXECUTION_PAYLOAD_BY_FORK: Dict[str, Type[Container]] = { + "Paris": ExecutionPayloadParis, + "Shanghai": ExecutionPayloadShanghai, + "Cancun": ExecutionPayloadCancun, + "Prague": ExecutionPayloadPrague, + "Osaka": ExecutionPayloadOsaka, + "Amsterdam": ExecutionPayloadAmsterdam, +} + + +class ExecutionPayloadEnvelopeParis(Container): + """ + The Paris `newPayload` envelope. + """ + + payload: ExecutionPayloadParis + + +class ExecutionPayloadEnvelopeShanghai(Container): + """ + The Shanghai `newPayload` envelope. + """ + + payload: ExecutionPayloadShanghai + + +class ExecutionPayloadEnvelopeCancun(Container): + """ + The Cancun `newPayload` envelope. + """ + + payload: ExecutionPayloadCancun + parent_beacon_block_root: Root + + +class ExecutionPayloadEnvelopePrague(Container): + """ + The Prague `newPayload` envelope. + """ + + payload: ExecutionPayloadPrague + parent_beacon_block_root: Root + execution_requests: List[ + ByteList[MAX_BYTES_PER_EXECUTION_REQUEST], + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + ] + + +class ExecutionPayloadEnvelopeOsaka(Container): + """ + The Osaka `newPayload` envelope. + """ + + payload: ExecutionPayloadOsaka + parent_beacon_block_root: Root + execution_requests: List[ + ByteList[MAX_BYTES_PER_EXECUTION_REQUEST], + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + ] + + +class ExecutionPayloadEnvelopeAmsterdam(Container): + """ + The Amsterdam `newPayload` envelope. + """ + + payload: ExecutionPayloadAmsterdam + parent_beacon_block_root: Root + execution_requests: List[ + ByteList[MAX_BYTES_PER_EXECUTION_REQUEST], + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + ] + + +EXECUTION_PAYLOAD_ENVELOPE_BY_FORK: Dict[str, Type[Container]] = { + "Paris": ExecutionPayloadEnvelopeParis, + "Shanghai": ExecutionPayloadEnvelopeShanghai, + "Cancun": ExecutionPayloadEnvelopeCancun, + "Prague": ExecutionPayloadEnvelopePrague, + "Osaka": ExecutionPayloadEnvelopeOsaka, + "Amsterdam": ExecutionPayloadEnvelopeAmsterdam, +} + + +class PayloadAttributesParis(Container): + """The Paris payload attributes.""" + + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: Address + + +class PayloadAttributesShanghai(Container): + """The Shanghai payload attributes.""" + + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: Address + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +class PayloadAttributesCancun(Container): + """The Cancun payload attributes.""" + + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: Address + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + parent_beacon_block_root: Root + + +class PayloadAttributesPrague(Container): + """The Prague payload attributes.""" + + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: Address + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + parent_beacon_block_root: Root + + +class PayloadAttributesOsaka(Container): + """The Osaka payload attributes.""" + + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: Address + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + parent_beacon_block_root: Root + + +class PayloadAttributesAmsterdam(Container): + """The Amsterdam payload attributes.""" + + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: Address + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + parent_beacon_block_root: Root + slot_number: uint64 + target_gas_limit: uint64 + + +PAYLOAD_ATTRIBUTES_BY_FORK: Dict[str, Type[Container]] = { + "Paris": PayloadAttributesParis, + "Shanghai": PayloadAttributesShanghai, + "Cancun": PayloadAttributesCancun, + "Prague": PayloadAttributesPrague, + "Osaka": PayloadAttributesOsaka, + "Amsterdam": PayloadAttributesAmsterdam, +} + + +class ForkchoiceState(Container): + """The forkchoice head/safe/finalized triple.""" + + head_block_hash: Hash32 + safe_block_hash: Hash32 + finalized_block_hash: Hash32 + + +class PayloadStatus(Container): + """ + A newPayload/forkchoice status. + + ``status`` is a uint8 enum (0=VALID, 1=INVALID, 2=SYNCING, 3=ACCEPTED). + ``latest_valid_hash`` is ``Optional[Hash32]`` and ``validation_error`` is + ``Optional[String]``. + """ + + status: uint8 + latest_valid_hash: List[Hash32, 1] + validation_error: List[ByteList[MAX_ERROR_BYTES], 1] + + +class ForkchoiceUpdateParis(Container): + """The Paris forkchoice update.""" + + forkchoice_state: ForkchoiceState + payload_attributes: List[PayloadAttributesParis, 1] + + +class ForkchoiceUpdateShanghai(Container): + """The Shanghai forkchoice update.""" + + forkchoice_state: ForkchoiceState + payload_attributes: List[PayloadAttributesShanghai, 1] + + +class ForkchoiceUpdateCancun(Container): + """The Cancun forkchoice update.""" + + forkchoice_state: ForkchoiceState + payload_attributes: List[PayloadAttributesCancun, 1] + + +class ForkchoiceUpdatePrague(Container): + """The Prague forkchoice update.""" + + forkchoice_state: ForkchoiceState + payload_attributes: List[PayloadAttributesPrague, 1] + + +class ForkchoiceUpdateOsaka(Container): + """The Osaka forkchoice update.""" + + forkchoice_state: ForkchoiceState + payload_attributes: List[PayloadAttributesOsaka, 1] + + +class ForkchoiceUpdateAmsterdam(Container): + """The Amsterdam forkchoice update.""" + + forkchoice_state: ForkchoiceState + payload_attributes: List[PayloadAttributesAmsterdam, 1] + custody_columns: List[Bitvector[CELLS_PER_EXT_BLOB], 1] + + +FORKCHOICE_UPDATE_BY_FORK: Dict[str, Type[Container]] = { + "Paris": ForkchoiceUpdateParis, + "Shanghai": ForkchoiceUpdateShanghai, + "Cancun": ForkchoiceUpdateCancun, + "Prague": ForkchoiceUpdatePrague, + "Osaka": ForkchoiceUpdateOsaka, + "Amsterdam": ForkchoiceUpdateAmsterdam, +} + + +class ForkchoiceUpdateResponse(Container): + """The forkchoice response: a status and an optional payload id.""" + + payload_status: PayloadStatus + payload_id: List[Bytes8, 1] + + +class ExecutionPayloadBodyParis(Container): + """The Paris execution payload body.""" + + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + + +class ExecutionPayloadBodyShanghai(Container): + """The Shanghai execution payload body.""" + + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +class ExecutionPayloadBodyCancun(Container): + """The Cancun execution payload body.""" + + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +class ExecutionPayloadBodyPrague(Container): + """The Prague execution payload body.""" + + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +class ExecutionPayloadBodyOsaka(Container): + """The Osaka execution payload body.""" + + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +class ExecutionPayloadBodyAmsterdam(Container): + """The Amsterdam execution payload body.""" + + transactions: List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + block_access_list: ByteList[MAX_BAL_BYTES] + + +EXECUTION_PAYLOAD_BODY_BY_FORK: Dict[str, Type[Container]] = { + "Paris": ExecutionPayloadBodyParis, + "Shanghai": ExecutionPayloadBodyShanghai, + "Cancun": ExecutionPayloadBodyCancun, + "Prague": ExecutionPayloadBodyPrague, + "Osaka": ExecutionPayloadBodyOsaka, + "Amsterdam": ExecutionPayloadBodyAmsterdam, +} + + +class BlobsBundleV1(Container): + """The Cancun blobs bundle.""" + + commitments: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK] + proofs: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK] + blobs: List[ByteVector[BYTES_PER_BLOB], MAX_BLOB_COMMITMENTS_PER_BLOCK] + + +class BlobsBundleV2(Container): + """The Osaka+ blobs bundle.""" + + commitments: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK] + proofs: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK * CELLS_PER_EXT_BLOB] + blobs: List[ByteVector[BYTES_PER_BLOB], MAX_BLOB_COMMITMENTS_PER_BLOCK] + + +class BuiltPayloadParis(Container): + """The Paris getPayload response.""" + + payload: ExecutionPayloadParis + block_value: uint256 + + +class BuiltPayloadShanghai(Container): + """The Shanghai getPayload response.""" + + payload: ExecutionPayloadShanghai + block_value: uint256 + should_override_builder: boolean + + +class BuiltPayloadCancun(Container): + """The Cancun getPayload response.""" + + payload: ExecutionPayloadCancun + block_value: uint256 + blobs_bundle: BlobsBundleV1 + should_override_builder: boolean + + +class BuiltPayloadPrague(Container): + """ + The Prague getPayload response. + """ + + payload: ExecutionPayloadPrague + block_value: uint256 + blobs_bundle: BlobsBundleV1 + execution_requests: List[ + ByteList[MAX_BYTES_PER_EXECUTION_REQUEST], + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + ] + should_override_builder: boolean + + +class BuiltPayloadOsaka(Container): + """The Osaka getPayload response.""" + + payload: ExecutionPayloadOsaka + block_value: uint256 + blobs_bundle: BlobsBundleV2 + execution_requests: List[ + ByteList[MAX_BYTES_PER_EXECUTION_REQUEST], + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + ] + should_override_builder: boolean + + +class BuiltPayloadAmsterdam(Container): + """The Amsterdam getPayload response.""" + + payload: ExecutionPayloadAmsterdam + block_value: uint256 + blobs_bundle: BlobsBundleV2 + execution_requests: List[ + ByteList[MAX_BYTES_PER_EXECUTION_REQUEST], + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + ] + should_override_builder: boolean + + +BUILT_PAYLOAD_BY_FORK: Dict[str, Type[Container]] = { + "Paris": BuiltPayloadParis, + "Shanghai": BuiltPayloadShanghai, + "Cancun": BuiltPayloadCancun, + "Prague": BuiltPayloadPrague, + "Osaka": BuiltPayloadOsaka, + "Amsterdam": BuiltPayloadAmsterdam, +} + + +class BlobAndProofV1(Container): + """A whole blob with a single proof.""" + + blob: ByteVector[BYTES_PER_BLOB] + proof: Bytes48 + + +class BlobAndProofV2(Container): + """A whole blob with cell proofs.""" + + blob: ByteVector[BYTES_PER_BLOB] + proofs: List[Bytes48, CELLS_PER_EXT_BLOB] + + +class BlobCellsAndProofs(Container): + """ + Cell-range blob contents (/blobs/v4, Amsterdam). + Each cell and proof is ``Optional``. + """ + + blob_cells: List[List[ByteVector[BYTES_PER_CELL], 1], CELLS_PER_EXT_BLOB] + proofs: List[List[Bytes48, 1], CELLS_PER_EXT_BLOB] + + +class BodiesByHashRequest(Container): + """The /bodies/hash request body.""" + + block_hashes: List[Hash32, MAX_BODIES_REQUEST] + + +class BodyEntry(Container): + """ + One bodies-response entry. + """ + + available: boolean + body: ExecutionPayloadBodyAmsterdam + + +class BodiesResponse(Container): + """The /bodies response.""" + + entries: List[BodyEntry, MAX_BODIES_REQUEST] + + +class BlobsV1Request(Container): + """The /blobs/v1 request.""" + + versioned_hashes: List[VersionedHash, MAX_BLOBS_REQUEST] + + +class BlobV1Entry(Container): + """One /blobs/v1 response entry.""" + + available: boolean + contents: BlobAndProofV1 + + +class BlobsV1Response(Container): + """The /blobs/v1 response.""" + + entries: List[BlobV1Entry, MAX_BLOBS_REQUEST] + + +class BlobsV2Request(Container): + """The /blobs/v2 request.""" + + versioned_hashes: List[VersionedHash, MAX_BLOBS_REQUEST] + + +class BlobV2Entry(Container): + """One /blobs/v2 response entry.""" + + available: boolean + contents: BlobAndProofV2 + + +class BlobsV2Response(Container): + """The /blobs/v2 response.""" + + entries: List[BlobV2Entry, MAX_BLOBS_REQUEST] + + +class BlobsV3Response(Container): + """The /blobs/v3 response.""" + + entries: List[BlobV2Entry, MAX_BLOBS_REQUEST] + + +class BlobsV4Request(Container): + """The /blobs/v4 request.""" + + versioned_hashes: List[VersionedHash, MAX_BLOBS_REQUEST] + indices_bitarray: Bitvector[CELLS_PER_EXT_BLOB] + + +class BlobV4Entry(Container): + """One /blobs/v4 response entry.""" + + available: boolean + contents: BlobCellsAndProofs + + +class BlobsV4Response(Container): + """The /blobs/v4 response.""" + + entries: List[BlobV4Entry, MAX_BLOBS_REQUEST] + + +class ClientVersion(Container): + """A client identity entry.""" + + code: ByteList[MAX_CLIENT_CODE_LENGTH] + name: ByteList[MAX_CLIENT_NAME_LENGTH] + version: ByteList[MAX_CLIENT_VERSION_LENGTH] + commit: Bytes4 + + +class IdentityResponse(Container): + """The /identity response.""" + + versions: List[ClientVersion, MAX_CLIENT_VERSIONS] + + +class CapabilitiesResponse(Container): + """The /capabilities response (capability-name list).""" + + capabilities: List[ByteList[MAX_CAPABILITY_NAME_LENGTH], MAX_CAPABILITIES] diff --git a/packages/testing/src/execution_testing/ssz/random_value.py b/packages/testing/src/execution_testing/ssz/random_value.py new file mode 100644 index 00000000000..2e8ed737e28 --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/random_value.py @@ -0,0 +1,183 @@ +""" +Deterministic random SSZ value generation for static test vectors. +""" + +import hashlib +from enum import Enum +from random import Random +from typing import Any + +from remerkleable.basic import boolean, uint +from remerkleable.byte_arrays import ByteList, ByteVector +from remerkleable.complex import Container, List +from remerkleable.core import View + +_MODE_NAMES = ( + "random", + "zero", + "max", + "nil_count", + "one_count", + "max_count", +) + + +class RandomizationMode(Enum): + """ + How a value's scalar and collection fields are filled. + Mirrors the consensus-specs ``RandomizationMode``. + """ + + mode_random = 0 + mode_zero = 1 + mode_max = 2 + mode_nil_count = 3 + mode_one_count = 4 + mode_max_count = 5 + + def is_changing(self) -> bool: + """ + Return whether the mode yields varying values across cases. + + True for ``random``, ``one_count`` and ``max_count`` -- those randomize + content, so several cases are worth generating; the rest are fully + determined by a single case. + """ + return self in ( + RandomizationMode.mode_random, + RandomizationMode.mode_one_count, + RandomizationMode.mode_max_count, + ) + + def to_name(self) -> str: + """Return the canonical short name for this mode.""" + return _MODE_NAMES[self.value] + + +def deterministic_seed(*parts: object) -> int: + """ + Return a stable integer seed derived from ``parts``. + Uses SHA-256 over the slash-joined string parts. + """ + joined = "/".join(str(part) for part in parts) + digest = hashlib.sha256(joined.encode("utf-8")).digest() + return int.from_bytes(digest, "big") + + +def get_random_ssz_object( + rng: Random, + typ: Any, + max_bytes_length: int, + max_list_length: int, + mode: RandomizationMode, + chaos: bool, +) -> View: + """ + Build a value of ``typ`` filled with random data per ``mode``. + """ + if chaos: + mode = rng.choice(list(RandomizationMode)) + if issubclass(typ, ByteList): + if mode == RandomizationMode.mode_nil_count: + return typ(b"") + elif mode == RandomizationMode.mode_max_count: + return typ(_random_bytes(rng, min(max_bytes_length, typ.limit()))) + elif mode == RandomizationMode.mode_one_count: + return typ(_random_bytes(rng, min(1, typ.limit()))) + elif mode == RandomizationMode.mode_zero: + return typ(b"\x00" * min(1, typ.limit())) + elif mode == RandomizationMode.mode_max: + return typ(b"\xff" * min(1, typ.limit())) + else: + return typ( + _random_bytes( + rng, rng.randint(0, min(max_bytes_length, typ.limit())) + ) + ) + if issubclass(typ, ByteVector): + # Byte vectors are fixed length; no max-bytes cap applies. + if mode == RandomizationMode.mode_zero: + return typ(b"\x00" * typ.type_byte_length()) + elif mode == RandomizationMode.mode_max: + return typ(b"\xff" * typ.type_byte_length()) + else: + return typ(_random_bytes(rng, typ.type_byte_length())) + elif issubclass(typ, (boolean, uint)): + if mode == RandomizationMode.mode_zero: + return _min_basic_value(typ) + elif mode == RandomizationMode.mode_max: + return _max_basic_value(typ) + else: + return _random_basic_value(rng, typ) + elif issubclass(typ, List): + limit = max_list_length + if typ.limit() < limit: + limit = typ.limit() + length = rng.randint(0, limit) + if mode == RandomizationMode.mode_one_count: + length = 1 + elif mode == RandomizationMode.mode_max_count: + length = limit + elif mode == RandomizationMode.mode_nil_count: + length = 0 + element_type = typ.element_cls() + max_list_length = 1 << (max_list_length.bit_length() >> 1) + return typ( + get_random_ssz_object( + rng, + element_type, + max_bytes_length, + max_list_length, + mode, + chaos, + ) + for _ in range(length) + ) + elif issubclass(typ, Container): + return typ( + **{ + field_name: get_random_ssz_object( + rng, + field_type, + max_bytes_length, + max_list_length, + mode, + chaos, + ) + for field_name, field_type in typ.fields().items() + } + ) + else: + raise TypeError(f"unsupported SSZ type: {typ!r}") + + +def _random_bytes(rng: Random, length: int) -> bytes: + """Return ``length`` random bytes.""" + return bytes(rng.getrandbits(8) for _ in range(length)) + + +def _random_basic_value(rng: Random, typ: Any) -> View: + """Return a random ``boolean`` or ``uint`` value.""" + if issubclass(typ, boolean): + return typ(rng.choice((True, False))) + if issubclass(typ, uint): + return typ(rng.randint(0, 2 ** (typ.type_byte_length() * 8) - 1)) + raise TypeError(f"not a basic type: {typ!r}") + + +def _min_basic_value(typ: Any) -> View: + """Return the minimum (zero / False) value of a basic type.""" + if issubclass(typ, boolean): + return typ(False) + if issubclass(typ, uint): + return typ(0) + raise TypeError(f"not a basic type: {typ!r}") + + +def _max_basic_value(typ: Any) -> View: + """Return the maximum (all-ones / True) value of a basic type.""" + if issubclass(typ, boolean): + return typ(True) + if issubclass(typ, uint): + return typ(2 ** (typ.type_byte_length() * 8) - 1) + raise TypeError(f"not a basic type: {typ!r}") diff --git a/packages/testing/src/execution_testing/ssz/tests/__init__.py b/packages/testing/src/execution_testing/ssz/tests/__init__.py new file mode 100644 index 00000000000..14f702a167d --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the `execution_testing.ssz` package.""" diff --git a/packages/testing/src/execution_testing/ssz/tests/test_containers.py b/packages/testing/src/execution_testing/ssz/tests/test_containers.py new file mode 100644 index 00000000000..61740fc17db --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/tests/test_containers.py @@ -0,0 +1,123 @@ +"""Round-trip and structural tests for the SSZ containers.""" + +from .. import decode_bytes, encode_bytes, hash_tree_root +from ..containers import ( + EXECUTION_PAYLOAD_BY_FORK, + EXECUTION_PAYLOAD_ENVELOPE_BY_FORK, + ExecutionPayloadAmsterdam, + ExecutionPayloadEnvelopeAmsterdam, + Withdrawal, +) + +TRANSACTIONS = [ + bytes.fromhex("02f86b01"), + bytes.fromhex("01" * 21), + bytes.fromhex("03" * 5), +] + + +def _withdrawal() -> Withdrawal: + return Withdrawal( + index=7, + validator_index=42, + address=bytes.fromhex("11" * 20), + amount=32_000_000_000, + ) + + +def _max_payload() -> ExecutionPayloadAmsterdam: + """The maximal (Amsterdam) payload: populated, carrying every field.""" + return ExecutionPayloadAmsterdam( + parent_hash=bytes.fromhex("aa" * 32), + fee_recipient=bytes.fromhex("bb" * 20), + state_root=bytes.fromhex("cc" * 32), + receipts_root=bytes.fromhex("dd" * 32), + logs_bloom=bytes.fromhex("00" * 256), + prev_randao=bytes.fromhex("ee" * 32), + block_number=21_000_000, + gas_limit=30_000_000, + gas_used=21_000, + timestamp=1_700_000_000, + extra_data=bytes.fromhex("dead"), + base_fee_per_gas=10**18, + block_hash=bytes.fromhex("ff" * 32), + transactions=list(TRANSACTIONS), + withdrawals=[_withdrawal()], + blob_gas_used=131_072, + excess_blob_gas=0, + block_access_list=bytes.fromhex("c0de"), + slot_number=9_999, + ) + + +def _max_envelope() -> ExecutionPayloadEnvelopeAmsterdam: + """The maximal (Amsterdam) envelope wrapping :func:`_max_payload`.""" + return ExecutionPayloadEnvelopeAmsterdam( + payload=_max_payload(), + parent_beacon_block_root=bytes.fromhex("12" * 32), + execution_requests=[bytes.fromhex("00aa"), bytes.fromhex("01bbcc")], + ) + + +def test_withdrawal_round_trip() -> None: + """A withdrawal survives encode -> decode unchanged.""" + value = _withdrawal() + raw = encode_bytes(value) + assert decode_bytes(Withdrawal, raw) == value + + +def test_payload_round_trip() -> None: + """An execution payload survives encode -> decode unchanged.""" + value = _max_payload() + raw = encode_bytes(value) + assert decode_bytes(ExecutionPayloadAmsterdam, raw) == value + + +def test_envelope_round_trip() -> None: + """An envelope survives encode -> decode unchanged.""" + value = _max_envelope() + raw = encode_bytes(value) + assert decode_bytes(ExecutionPayloadEnvelopeAmsterdam, raw) == value + + +def test_hash_tree_root_is_32_bytes_and_deterministic() -> None: + """`hash_tree_root` returns a stable 32-byte digest.""" + root = hash_tree_root(_max_envelope()) + assert isinstance(root, bytes) + assert len(root) == 32 + assert root == hash_tree_root(_max_envelope()) + + +def test_transactions_two_level_offsets() -> None: + """ + Transactions of differing lengths round-trip exactly, and reordering them + changes the root -- the inner offset table is order- and length-sensitive. + """ + value = _max_payload() + decoded = decode_bytes(ExecutionPayloadAmsterdam, encode_bytes(value)) + assert [bytes(tx) for tx in decoded.transactions] == TRANSACTIONS + + reordered = _max_payload() + reordered.transactions = list(reversed(TRANSACTIONS)) + assert hash_tree_root(reordered) != hash_tree_root(value) + + +def test_every_fork_payload_round_trips() -> None: + """Every modelled payload survives a zero-value encode -> decode.""" + for fork, cls in EXECUTION_PAYLOAD_BY_FORK.items(): + value = cls() + assert decode_bytes(cls, encode_bytes(value)) == value, fork + + +def test_every_fork_envelope_round_trips() -> None: + """Every modelled envelope survives a zero-value encode -> decode.""" + for fork, cls in EXECUTION_PAYLOAD_ENVELOPE_BY_FORK.items(): + value = cls() + assert decode_bytes(cls, encode_bytes(value)) == value, fork + + +def test_per_fork_envelope_wraps_matching_payload() -> None: + """Each envelope's ``payload`` field is its own fork's payload class.""" + for fork, cls in EXECUTION_PAYLOAD_ENVELOPE_BY_FORK.items(): + payload_type = dict(cls.fields())["payload"] + assert payload_type is EXECUTION_PAYLOAD_BY_FORK[fork], fork diff --git a/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py b/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py new file mode 100644 index 00000000000..9216ec87e27 --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py @@ -0,0 +1,242 @@ +""" +Structural tests for the broader Engine API SSZ containers from PR #793. +""" + +import pytest + +from .. import decode_bytes, encode_bytes, envelope_bytes +from ..containers import ( + BUILT_PAYLOAD_BY_FORK, + EXECUTION_PAYLOAD_BODY_BY_FORK, + FORKCHOICE_UPDATE_BY_FORK, + PAYLOAD_ATTRIBUTES_BY_FORK, + BlobAndProofV1, + BlobAndProofV2, + BlobCellsAndProofs, + BlobsBundleV1, + BlobsBundleV2, + BlobsV1Request, + BlobsV1Response, + BlobsV4Request, + BlobV1Entry, + BlobV4Entry, + BodiesByHashRequest, + BodiesResponse, + BodyEntry, + CapabilitiesResponse, + ClientVersion, + ExecutionPayloadEnvelopeAmsterdam, + ExecutionPayloadEnvelopeParis, + ForkchoiceState, + ForkchoiceUpdateResponse, + IdentityResponse, + PayloadStatus, +) + +_FORKS = ["Paris", "Shanghai", "Cancun", "Prague", "Osaka", "Amsterdam"] + + +def _fields(cls: object) -> list: + return list(cls.fields().keys()) + + +def test_new_registries_cover_forks_in_order() -> None: + """Every new per-fork registry spans Paris..Amsterdam.""" + for reg in ( + PAYLOAD_ATTRIBUTES_BY_FORK, + FORKCHOICE_UPDATE_BY_FORK, + EXECUTION_PAYLOAD_BODY_BY_FORK, + BUILT_PAYLOAD_BY_FORK, + ): + assert list(reg) == _FORKS + + +def test_payload_attributes_field_deltas() -> None: + """PayloadAttributes grows by the fields each fork added.""" + expected = ["timestamp", "prev_randao", "suggested_fee_recipient"] + additions = { + "Paris": [], + "Shanghai": ["withdrawals"], + "Cancun": ["parent_beacon_block_root"], + "Prague": [], + "Osaka": [], + "Amsterdam": ["slot_number", "target_gas_limit"], + } + for fork, cls in PAYLOAD_ATTRIBUTES_BY_FORK.items(): + expected = expected + additions[fork] + assert _fields(cls) == expected, fork + + +def test_execution_payload_body_field_deltas() -> None: + """ExecutionPayloadBody grows transactions -> withdrawals -> BAL.""" + expected = ["transactions"] + additions = { + "Paris": [], + "Shanghai": ["withdrawals"], + "Cancun": [], + "Prague": [], + "Osaka": [], + "Amsterdam": ["block_access_list"], + } + for fork, cls in EXECUTION_PAYLOAD_BODY_BY_FORK.items(): + expected = expected + additions[fork] + assert _fields(cls) == expected, fork + + +def test_built_payload_field_order() -> None: + """ + BuiltPayload field order per fork. + """ + expected = { + "Paris": ["payload", "block_value"], + "Shanghai": ["payload", "block_value", "should_override_builder"], + "Cancun": [ + "payload", + "block_value", + "blobs_bundle", + "should_override_builder", + ], + "Prague": [ + "payload", + "block_value", + "blobs_bundle", + "execution_requests", + "should_override_builder", + ], + } + expected["Osaka"] = expected["Prague"] + expected["Amsterdam"] = expected["Prague"] + for fork, cls in BUILT_PAYLOAD_BY_FORK.items(): + assert _fields(cls) == expected[fork], fork + + +def test_forkchoice_update_field_order() -> None: + """ForkchoiceUpdate gains custody_columns only at Amsterdam.""" + for fork, cls in FORKCHOICE_UPDATE_BY_FORK.items(): + base = ["forkchoice_state", "payload_attributes"] + if fork == "Amsterdam": + base = base + ["custody_columns"] + assert _fields(cls) == base, fork + + +def test_single_container_field_orders() -> None: + """Exact field order for the fork-independent containers.""" + assert _fields(ForkchoiceState) == [ + "head_block_hash", + "safe_block_hash", + "finalized_block_hash", + ] + assert _fields(PayloadStatus) == [ + "status", + "latest_valid_hash", + "validation_error", + ] + assert _fields(ForkchoiceUpdateResponse) == [ + "payload_status", + "payload_id", + ] + assert _fields(BlobsBundleV1) == ["commitments", "proofs", "blobs"] + assert _fields(BlobsBundleV2) == ["commitments", "proofs", "blobs"] + assert _fields(BlobAndProofV1) == ["blob", "proof"] + assert _fields(BlobAndProofV2) == ["blob", "proofs"] + assert _fields(BlobCellsAndProofs) == ["blob_cells", "proofs"] + assert _fields(BodiesByHashRequest) == ["block_hashes"] + assert _fields(BodyEntry) == ["available", "body"] + assert _fields(BodiesResponse) == ["entries"] + assert _fields(BlobsV1Request) == ["versioned_hashes"] + assert _fields(BlobV1Entry) == ["available", "contents"] + assert _fields(BlobsV1Response) == ["entries"] + assert _fields(BlobsV4Request) == [ + "versioned_hashes", + "indices_bitarray", + ] + assert _fields(BlobV4Entry) == ["available", "contents"] + assert _fields(ClientVersion) == ["code", "name", "version", "commit"] + assert _fields(IdentityResponse) == ["versions"] + assert _fields(CapabilitiesResponse) == ["capabilities"] + + +def test_payload_status_optional_encoding_matches_spec_examples() -> None: + """ + The ``Optional``/``String`` wire shape tests. + """ + valid = PayloadStatus( + status=0, latest_valid_hash=[b"\xaa" * 32], validation_error=[] + ) + assert len(encode_bytes(valid)) == 41 + assert decode_bytes(PayloadStatus, encode_bytes(valid)) == valid + + invalid = PayloadStatus( + status=1, + latest_valid_hash=[], + validation_error=[b"bad state root"], + ) + assert len(encode_bytes(invalid)) == 27 + assert decode_bytes(PayloadStatus, encode_bytes(invalid)) == invalid + + +def test_per_fork_engine_containers_round_trip() -> None: + """Zero-value encode -> decode for every per-fork engine container.""" + for reg in ( + PAYLOAD_ATTRIBUTES_BY_FORK, + FORKCHOICE_UPDATE_BY_FORK, + EXECUTION_PAYLOAD_BODY_BY_FORK, + BUILT_PAYLOAD_BY_FORK, + ): + for fork, cls in reg.items(): + value = cls() + assert decode_bytes(cls, encode_bytes(value)) == value, fork + + +def _base_payload_fields() -> dict: + """The fields every fork's payload carries, with valid dummy values.""" + return dict( + parent_hash=b"\x00" * 32, + fee_recipient=b"\x00" * 20, + state_root=b"\x00" * 32, + receipts_root=b"\x00" * 32, + logs_bloom=b"\x00" * 256, + prev_randao=b"\x00" * 32, + block_number=1, + gas_limit=1, + gas_used=1, + timestamp=1, + extra_data=b"", + base_fee_per_gas=1, + block_hash=b"\x00" * 32, + transactions=[], + ) + + +def test_envelope_bytes_dispatches_by_fork() -> None: + """``envelope_bytes`` builds each fork's envelope from the registries.""" + paris = envelope_bytes("Paris", **_base_payload_fields()) + assert ( + encode_bytes(decode_bytes(ExecutionPayloadEnvelopeParis, paris)) + == paris + ) + amsterdam = envelope_bytes( + "Amsterdam", + **_base_payload_fields(), + withdrawals=[], + blob_gas_used=0, + excess_blob_gas=0, + block_access_list=b"", + slot_number=7, + parent_beacon_block_root=b"\x00" * 32, + execution_requests=[], + ) + decoded = decode_bytes(ExecutionPayloadEnvelopeAmsterdam, amsterdam) + assert int(decoded.payload.slot_number) == 7 + + +def test_envelope_bytes_missing_required_field_raises() -> None: + """Omitting a field the fork's container needs raises ``ValueError``.""" + with pytest.raises(ValueError, match="requires field"): + envelope_bytes("Amsterdam", **_base_payload_fields()) + + +def test_envelope_bytes_unknown_fork_raises() -> None: + """An unknown fork name raises ``ValueError``.""" + with pytest.raises(ValueError, match="unknown fork"): + envelope_bytes("Bogus", **_base_payload_fields()) diff --git a/packages/testing/src/execution_testing/ssz/tests/test_random_value.py b/packages/testing/src/execution_testing/ssz/tests/test_random_value.py new file mode 100644 index 00000000000..2ab3a4dfe63 --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/tests/test_random_value.py @@ -0,0 +1,155 @@ +"""Property tests for the deterministic random SSZ value generator.""" + +from random import Random +from typing import Any + +import pytest + +from .. import decode_bytes, encode_bytes +from ..containers import ( + ExecutionPayloadAmsterdam, + ExecutionPayloadEnvelopeAmsterdam, + Withdrawal, +) +from ..random_value import ( + RandomizationMode, + deterministic_seed, + get_random_ssz_object, +) + +CONTAINERS = [ + Withdrawal, + ExecutionPayloadAmsterdam, + ExecutionPayloadEnvelopeAmsterdam, +] + +MAX_BYTES_LENGTH = 48 +MAX_LIST_LENGTH = 4 + + +def _value( + ssz_type: Any, + mode: RandomizationMode, + seed: int = 0, + chaos: bool = False, +) -> Any: + return get_random_ssz_object( + Random(seed), + ssz_type, + MAX_BYTES_LENGTH, + MAX_LIST_LENGTH, + mode, + chaos, + ) + + +def test_deterministic_seed_is_stable_and_not_pythons_hash() -> None: + """The seed depends only on its parts, not on process-salted hashing.""" + assert deterministic_seed("a", "b", 1) == deterministic_seed("a", "b", 1) + assert deterministic_seed("a", "b", 1) != deterministic_seed("a", "b", 2) + + +def test_is_changing_matches_upstream() -> None: + """``random``/``one_count``/``max_count`` change.""" + changing = { + RandomizationMode.mode_random, + RandomizationMode.mode_one_count, + RandomizationMode.mode_max_count, + } + for mode in RandomizationMode: + assert mode.is_changing() == (mode in changing), mode + + +@pytest.mark.parametrize("ssz_type", CONTAINERS) +@pytest.mark.parametrize("mode", list(RandomizationMode)) +def test_same_seed_yields_identical_value( + ssz_type: Any, mode: RandomizationMode +) -> None: + """A fixed seed reproduces the exact same value.""" + assert encode_bytes(_value(ssz_type, mode, seed=1234)) == encode_bytes( + _value(ssz_type, mode, seed=1234) + ) + + +@pytest.mark.parametrize("ssz_type", CONTAINERS) +@pytest.mark.parametrize("mode", list(RandomizationMode)) +def test_generated_value_round_trips( + ssz_type: Any, mode: RandomizationMode +) -> None: + """Every generated value survives encode -> decode unchanged.""" + value = _value(ssz_type, mode, seed=7) + assert decode_bytes(ssz_type, encode_bytes(value)) == value + + +def test_zero_mode_zeroes_scalars_and_byte_vectors() -> None: + """``zero`` mode zeroes scalars and byte vectors.""" + payload = _value(ExecutionPayloadAmsterdam, RandomizationMode.mode_zero) + assert int(payload.block_number) == 0 + assert bytes(payload.parent_hash) == b"\x00" * 32 + # A zero-mode ``ByteList`` is a single zero byte (``min(1, limit)``), not + # empty. + assert bytes(payload.extra_data) == b"\x00" + + +def test_max_mode_saturates_scalars_and_byte_vectors() -> None: + """``max`` mode maxes scalars and byte vectors.""" + payload = _value(ExecutionPayloadAmsterdam, RandomizationMode.mode_max) + assert int(payload.block_number) == 2**64 - 1 + assert bytes(payload.parent_hash) == b"\xff" * 32 + # A max-mode ``ByteList`` is a single ``0xff`` byte (``min(1, limit)``). + assert bytes(payload.extra_data) == b"\xff" + + +def test_nil_count_empties_collections() -> None: + """``nil_count`` empties every variable-length collection.""" + payload = _value( + ExecutionPayloadAmsterdam, RandomizationMode.mode_nil_count + ) + assert len(payload.transactions) == 0 + assert len(payload.withdrawals) == 0 + assert bytes(payload.extra_data) == b"" + + +def test_one_count_yields_single_element_collections() -> None: + """``one_count`` puts exactly one element in each collection.""" + payload = _value( + ExecutionPayloadAmsterdam, RandomizationMode.mode_one_count + ) + assert len(payload.transactions) == 1 + assert len(payload.withdrawals) == 1 + assert len(bytes(payload.extra_data)) == 1 + + +def test_max_count_fills_collections_to_cap() -> None: + """``max_count`` fills collections to the cap.""" + payload = _value( + ExecutionPayloadAmsterdam, RandomizationMode.mode_max_count + ) + assert len(payload.transactions) == MAX_LIST_LENGTH + assert len(payload.withdrawals) == MAX_LIST_LENGTH + + assert len(bytes(payload.extra_data)) == 32 + + +def test_random_mode_changes_with_seed() -> None: + """``random`` mode produces different values for different seeds.""" + a = encode_bytes( + _value(ExecutionPayloadAmsterdam, RandomizationMode.mode_random, 1) + ) + b = encode_bytes( + _value(ExecutionPayloadAmsterdam, RandomizationMode.mode_random, 2) + ) + assert a != b + + +def test_chaos_runs_and_round_trips() -> None: + """``chaos`` re-rolls the mode per node; the result still round-trips.""" + value = _value( + ExecutionPayloadAmsterdam, + RandomizationMode.mode_random, + seed=99, + chaos=True, + ) + assert ( + decode_bytes(ExecutionPayloadAmsterdam, encode_bytes(value)) == value + ) diff --git a/pyproject.toml b/pyproject.toml index a6e7a5870c1..a15989524cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -481,6 +481,12 @@ exclude = [ ] plugins = ["pydantic.mypy"] +# `eth-remerkleable` ships no type information (no py.typed); treat its imports +# as untyped instead of vendoring stubs. +[[tool.mypy.overrides]] +module = "remerkleable.*" +ignore_missing_imports = true + [tool.uv] required-version = ">=0.7.0" diff --git a/uv.lock b/uv.lock index e2429416d77..e1996198b43 100644 --- a/uv.lock +++ b/uv.lock @@ -765,6 +765,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/db/f8775490669d28aca24871c67dd56b3e72105cb3bcae9a4ec65dd70859b3/eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a", size = 8028, upload-time = "2025-01-13T21:29:19.365Z" }, ] +[[package]] +name = "eth-remerkleable" +version = "0.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/ac/40fde655f67fd02f07b28e3fe4b9bb4af521388ca8aa48462d622ce4fa03/eth_remerkleable-0.1.31.tar.gz", hash = "sha256:94df4b18a50dfc46f55b2e790cfece384e64032e9cb82d185cff09224682ad1d", size = 49525, upload-time = "2026-06-11T13:23:38.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/a1/87f7c996f344c5f213c2bca657e579c3696bb99737238c0cf9f04867386a/eth_remerkleable-0.1.31-py3-none-any.whl", hash = "sha256:7e48ea1b80935977effc02300f3f9b1db19153ab87ffbcea14a6e6429bbdc56b", size = 57192, upload-time = "2026-06-11T13:23:37.208Z" }, +] + [[package]] name = "eth-typing" version = "5.2.1" @@ -1044,6 +1053,7 @@ dependencies = [ { name = "coincurve" }, { name = "colorlog" }, { name = "eth-abi" }, + { name = "eth-remerkleable" }, { name = "ethereum-execution" }, { name = "ethereum-hive" }, { name = "ethereum-rlp" }, @@ -1097,6 +1107,7 @@ requires-dist = [ { name = "coincurve", specifier = ">=20.0.0,<21" }, { name = "colorlog", specifier = ">=6.7.0,<7" }, { name = "eth-abi", specifier = ">=5.2.0" }, + { name = "eth-remerkleable", specifier = "==0.1.31" }, { name = "ethereum-execution", editable = "." }, { name = "ethereum-hive", specifier = ">=0.1.0a1,<1.0.0" }, { name = "ethereum-rlp", specifier = ">=0.1.3,<0.2" }, From 351d6035a19142f2e1c318b3d9577b5be6ba375e Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Wed, 17 Jun 2026 11:19:49 +0530 Subject: [PATCH 2/6] spec deviates from source --- packages/testing/src/execution_testing/ssz/__init__.py | 3 --- packages/testing/src/execution_testing/ssz/containers.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/testing/src/execution_testing/ssz/__init__.py b/packages/testing/src/execution_testing/ssz/__init__.py index facffe97fcf..939ee32e030 100644 --- a/packages/testing/src/execution_testing/ssz/__init__.py +++ b/packages/testing/src/execution_testing/ssz/__init__.py @@ -100,9 +100,6 @@ ViewT = TypeVar("ViewT", bound=View) -REFERENCE_SPEC_GIT_PATH = "src/engine/refactor-ssz.md" -REFERENCE_SPEC_VERSION = "4e0fed12d3ebc9d1ca8829331a82b97b1d1bd154" - def encode_bytes(value: View) -> bytes: """Serialize an SSZ value to its canonical byte encoding.""" diff --git a/packages/testing/src/execution_testing/ssz/containers.py b/packages/testing/src/execution_testing/ssz/containers.py index dca5e5321f5..6fb1c66f84b 100644 --- a/packages/testing/src/execution_testing/ssz/containers.py +++ b/packages/testing/src/execution_testing/ssz/containers.py @@ -502,12 +502,12 @@ class BuiltPayloadParis(Container): block_value: uint256 +# Should not have the should_override_builder field,wrong in spec pr 793 class BuiltPayloadShanghai(Container): """The Shanghai getPayload response.""" payload: ExecutionPayloadShanghai block_value: uint256 - should_override_builder: boolean class BuiltPayloadCancun(Container): From 3bbb42a6880849d740598037b1b74adf389a5f78 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Wed, 17 Jun 2026 14:09:44 +0530 Subject: [PATCH 3/6] address reviews --- .../src/execution_testing/ssz/__init__.py | 22 +-- .../src/execution_testing/ssz/constants.py | 40 +----- .../src/execution_testing/ssz/containers.py | 2 + .../src/execution_testing/ssz/ssz_types.py | 41 ++++++ .../ssz/tests/test_containers.py | 57 ++++---- .../ssz/tests/test_engine_containers.py | 127 ------------------ pyproject.toml | 2 +- 7 files changed, 86 insertions(+), 205 deletions(-) create mode 100644 packages/testing/src/execution_testing/ssz/ssz_types.py diff --git a/packages/testing/src/execution_testing/ssz/__init__.py b/packages/testing/src/execution_testing/ssz/__init__.py index 939ee32e030..197cfd86e1b 100644 --- a/packages/testing/src/execution_testing/ssz/__init__.py +++ b/packages/testing/src/execution_testing/ssz/__init__.py @@ -13,15 +13,6 @@ MAX_EXTRA_DATA_BYTES, MAX_TXS_PER_PAYLOAD, MAX_WITHDRAWALS_PER_PAYLOAD, - Address, - Bloom, - Bytes4, - Bytes8, - Bytes32, - Bytes48, - Hash32, - Root, - VersionedHash, ) from .containers import ( BUILT_PAYLOAD_BY_FORK, @@ -97,6 +88,17 @@ deterministic_seed, get_random_ssz_object, ) +from .ssz_types import ( + Address, + Bloom, + Bytes4, + Bytes8, + Bytes32, + Bytes48, + Hash32, + Root, + VersionedHash, +) ViewT = TypeVar("ViewT", bound=View) @@ -285,8 +287,6 @@ def _opt_bytes(value: bytes | None) -> bytes | None: "PayloadAttributesPrague", "PayloadAttributesShanghai", "PayloadStatus", - "REFERENCE_SPEC_GIT_PATH", - "REFERENCE_SPEC_VERSION", "RandomizationMode", "Root", "VersionedHash", diff --git a/packages/testing/src/execution_testing/ssz/constants.py b/packages/testing/src/execution_testing/ssz/constants.py index 6ca67d9c3c7..2c84364252a 100644 --- a/packages/testing/src/execution_testing/ssz/constants.py +++ b/packages/testing/src/execution_testing/ssz/constants.py @@ -1,6 +1,4 @@ -"""SSZ size limits and byte-vector aliases for the REST+SSZ Engine API.""" - -from remerkleable.byte_arrays import ByteVector +"""SSZ size limits for the REST+SSZ Engine API.""" # Payload / envelope limits. MAX_BYTES_PER_TX = 2**30 @@ -36,39 +34,3 @@ MAX_CLIENT_VERSIONS = 4 MAX_CAPABILITY_NAME_LENGTH = 64 MAX_CAPABILITIES = 64 - - -class Hash32(ByteVector[32]): - """A 32-byte hash (`Hash32`, `Root` and `Bytes32` share this layout).""" - - -class Bytes32(ByteVector[32]): - """A 32-byte fixed vector.""" - - -class Root(ByteVector[32]): - """A 32-byte merkle root.""" - - -class Address(ByteVector[20]): - """A 20-byte execution-layer address.""" - - -class Bloom(ByteVector[BYTES_PER_LOGS_BLOOM]): - """A 256-byte logs bloom filter.""" - - -class VersionedHash(ByteVector[32]): - """An EIP-4844 versioned blob hash.""" - - -class Bytes8(ByteVector[8]): - """An 8-byte value (e.g. `payload_id`).""" - - -class Bytes4(ByteVector[4]): - """A 4-byte value (e.g. a client commit hash).""" - - -class Bytes48(ByteVector[48]): - """A 48-byte value (KZG commitments and proofs).""" diff --git a/packages/testing/src/execution_testing/ssz/containers.py b/packages/testing/src/execution_testing/ssz/containers.py index 6fb1c66f84b..e9f550ee452 100644 --- a/packages/testing/src/execution_testing/ssz/containers.py +++ b/packages/testing/src/execution_testing/ssz/containers.py @@ -30,6 +30,8 @@ MAX_EXTRA_DATA_BYTES, MAX_TXS_PER_PAYLOAD, MAX_WITHDRAWALS_PER_PAYLOAD, +) +from .ssz_types import ( Address, Bloom, Bytes4, diff --git a/packages/testing/src/execution_testing/ssz/ssz_types.py b/packages/testing/src/execution_testing/ssz/ssz_types.py new file mode 100644 index 00000000000..136b4a080ac --- /dev/null +++ b/packages/testing/src/execution_testing/ssz/ssz_types.py @@ -0,0 +1,41 @@ +"""Byte-vector type aliases for the REST+SSZ Engine API.""" + +from remerkleable.byte_arrays import ByteVector + +from .constants import BYTES_PER_LOGS_BLOOM + + +class Hash32(ByteVector[32]): + """A 32-byte hash (`Hash32`, `Root` and `Bytes32` share this layout).""" + + +class Bytes32(ByteVector[32]): + """A 32-byte fixed vector.""" + + +class Root(ByteVector[32]): + """A 32-byte merkle root.""" + + +class Address(ByteVector[20]): + """A 20-byte execution-layer address.""" + + +class Bloom(ByteVector[BYTES_PER_LOGS_BLOOM]): + """A 256-byte logs bloom filter.""" + + +class VersionedHash(ByteVector[32]): + """An EIP-4844 versioned blob hash.""" + + +class Bytes8(ByteVector[8]): + """An 8-byte value (e.g. `payload_id`).""" + + +class Bytes4(ByteVector[4]): + """A 4-byte value (e.g. a client commit hash).""" + + +class Bytes48(ByteVector[48]): + """A 48-byte value (KZG commitments and proofs).""" diff --git a/packages/testing/src/execution_testing/ssz/tests/test_containers.py b/packages/testing/src/execution_testing/ssz/tests/test_containers.py index 61740fc17db..12a15a3b19d 100644 --- a/packages/testing/src/execution_testing/ssz/tests/test_containers.py +++ b/packages/testing/src/execution_testing/ssz/tests/test_containers.py @@ -1,5 +1,7 @@ """Round-trip and structural tests for the SSZ containers.""" +from remerkleable.basic import uint64, uint256 + from .. import decode_bytes, encode_bytes, hash_tree_root from ..containers import ( EXECUTION_PAYLOAD_BY_FORK, @@ -8,6 +10,7 @@ ExecutionPayloadEnvelopeAmsterdam, Withdrawal, ) +from ..ssz_types import Address, Bloom, Bytes32, Hash32, Root TRANSACTIONS = [ bytes.fromhex("02f86b01"), @@ -18,43 +21,43 @@ def _withdrawal() -> Withdrawal: return Withdrawal( - index=7, - validator_index=42, - address=bytes.fromhex("11" * 20), - amount=32_000_000_000, + index=uint64(7), + validator_index=uint64(42), + address=Address(bytes.fromhex("11" * 20)), + amount=uint64(32_000_000_000), ) -def _max_payload() -> ExecutionPayloadAmsterdam: - """The maximal (Amsterdam) payload: populated, carrying every field.""" +def _random_payload() -> ExecutionPayloadAmsterdam: + """A fully-populated payload carrying every field of the latest fork.""" return ExecutionPayloadAmsterdam( - parent_hash=bytes.fromhex("aa" * 32), - fee_recipient=bytes.fromhex("bb" * 20), - state_root=bytes.fromhex("cc" * 32), - receipts_root=bytes.fromhex("dd" * 32), - logs_bloom=bytes.fromhex("00" * 256), - prev_randao=bytes.fromhex("ee" * 32), - block_number=21_000_000, - gas_limit=30_000_000, - gas_used=21_000, - timestamp=1_700_000_000, + parent_hash=Hash32(bytes.fromhex("aa" * 32)), + fee_recipient=Address(bytes.fromhex("bb" * 20)), + state_root=Hash32(bytes.fromhex("cc" * 32)), + receipts_root=Hash32(bytes.fromhex("dd" * 32)), + logs_bloom=Bloom(bytes.fromhex("00" * 256)), + prev_randao=Bytes32(bytes.fromhex("ee" * 32)), + block_number=uint64(21_000_000), + gas_limit=uint64(30_000_000), + gas_used=uint64(21_000), + timestamp=uint64(1_700_000_000), extra_data=bytes.fromhex("dead"), - base_fee_per_gas=10**18, - block_hash=bytes.fromhex("ff" * 32), + base_fee_per_gas=uint256(10**18), + block_hash=Hash32(bytes.fromhex("ff" * 32)), transactions=list(TRANSACTIONS), withdrawals=[_withdrawal()], - blob_gas_used=131_072, - excess_blob_gas=0, + blob_gas_used=uint64(131_072), + excess_blob_gas=uint64(0), block_access_list=bytes.fromhex("c0de"), - slot_number=9_999, + slot_number=uint64(9_999), ) def _max_envelope() -> ExecutionPayloadEnvelopeAmsterdam: - """The maximal (Amsterdam) envelope wrapping :func:`_max_payload`.""" + """A fully-populated envelope wrapping :func:`_random_payload`.""" return ExecutionPayloadEnvelopeAmsterdam( - payload=_max_payload(), - parent_beacon_block_root=bytes.fromhex("12" * 32), + payload=_random_payload(), + parent_beacon_block_root=Root(bytes.fromhex("12" * 32)), execution_requests=[bytes.fromhex("00aa"), bytes.fromhex("01bbcc")], ) @@ -68,7 +71,7 @@ def test_withdrawal_round_trip() -> None: def test_payload_round_trip() -> None: """An execution payload survives encode -> decode unchanged.""" - value = _max_payload() + value = _random_payload() raw = encode_bytes(value) assert decode_bytes(ExecutionPayloadAmsterdam, raw) == value @@ -93,11 +96,11 @@ def test_transactions_two_level_offsets() -> None: Transactions of differing lengths round-trip exactly, and reordering them changes the root -- the inner offset table is order- and length-sensitive. """ - value = _max_payload() + value = _random_payload() decoded = decode_bytes(ExecutionPayloadAmsterdam, encode_bytes(value)) assert [bytes(tx) for tx in decoded.transactions] == TRANSACTIONS - reordered = _max_payload() + reordered = _random_payload() reordered.transactions = list(reversed(TRANSACTIONS)) assert hash_tree_root(reordered) != hash_tree_root(value) diff --git a/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py b/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py index 9216ec87e27..2da888178bb 100644 --- a/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py +++ b/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py @@ -10,36 +10,14 @@ EXECUTION_PAYLOAD_BODY_BY_FORK, FORKCHOICE_UPDATE_BY_FORK, PAYLOAD_ATTRIBUTES_BY_FORK, - BlobAndProofV1, - BlobAndProofV2, - BlobCellsAndProofs, - BlobsBundleV1, - BlobsBundleV2, - BlobsV1Request, - BlobsV1Response, - BlobsV4Request, - BlobV1Entry, - BlobV4Entry, - BodiesByHashRequest, - BodiesResponse, - BodyEntry, - CapabilitiesResponse, - ClientVersion, ExecutionPayloadEnvelopeAmsterdam, ExecutionPayloadEnvelopeParis, - ForkchoiceState, - ForkchoiceUpdateResponse, - IdentityResponse, PayloadStatus, ) _FORKS = ["Paris", "Shanghai", "Cancun", "Prague", "Osaka", "Amsterdam"] -def _fields(cls: object) -> list: - return list(cls.fields().keys()) - - def test_new_registries_cover_forks_in_order() -> None: """Every new per-fork registry spans Paris..Amsterdam.""" for reg in ( @@ -51,111 +29,6 @@ def test_new_registries_cover_forks_in_order() -> None: assert list(reg) == _FORKS -def test_payload_attributes_field_deltas() -> None: - """PayloadAttributes grows by the fields each fork added.""" - expected = ["timestamp", "prev_randao", "suggested_fee_recipient"] - additions = { - "Paris": [], - "Shanghai": ["withdrawals"], - "Cancun": ["parent_beacon_block_root"], - "Prague": [], - "Osaka": [], - "Amsterdam": ["slot_number", "target_gas_limit"], - } - for fork, cls in PAYLOAD_ATTRIBUTES_BY_FORK.items(): - expected = expected + additions[fork] - assert _fields(cls) == expected, fork - - -def test_execution_payload_body_field_deltas() -> None: - """ExecutionPayloadBody grows transactions -> withdrawals -> BAL.""" - expected = ["transactions"] - additions = { - "Paris": [], - "Shanghai": ["withdrawals"], - "Cancun": [], - "Prague": [], - "Osaka": [], - "Amsterdam": ["block_access_list"], - } - for fork, cls in EXECUTION_PAYLOAD_BODY_BY_FORK.items(): - expected = expected + additions[fork] - assert _fields(cls) == expected, fork - - -def test_built_payload_field_order() -> None: - """ - BuiltPayload field order per fork. - """ - expected = { - "Paris": ["payload", "block_value"], - "Shanghai": ["payload", "block_value", "should_override_builder"], - "Cancun": [ - "payload", - "block_value", - "blobs_bundle", - "should_override_builder", - ], - "Prague": [ - "payload", - "block_value", - "blobs_bundle", - "execution_requests", - "should_override_builder", - ], - } - expected["Osaka"] = expected["Prague"] - expected["Amsterdam"] = expected["Prague"] - for fork, cls in BUILT_PAYLOAD_BY_FORK.items(): - assert _fields(cls) == expected[fork], fork - - -def test_forkchoice_update_field_order() -> None: - """ForkchoiceUpdate gains custody_columns only at Amsterdam.""" - for fork, cls in FORKCHOICE_UPDATE_BY_FORK.items(): - base = ["forkchoice_state", "payload_attributes"] - if fork == "Amsterdam": - base = base + ["custody_columns"] - assert _fields(cls) == base, fork - - -def test_single_container_field_orders() -> None: - """Exact field order for the fork-independent containers.""" - assert _fields(ForkchoiceState) == [ - "head_block_hash", - "safe_block_hash", - "finalized_block_hash", - ] - assert _fields(PayloadStatus) == [ - "status", - "latest_valid_hash", - "validation_error", - ] - assert _fields(ForkchoiceUpdateResponse) == [ - "payload_status", - "payload_id", - ] - assert _fields(BlobsBundleV1) == ["commitments", "proofs", "blobs"] - assert _fields(BlobsBundleV2) == ["commitments", "proofs", "blobs"] - assert _fields(BlobAndProofV1) == ["blob", "proof"] - assert _fields(BlobAndProofV2) == ["blob", "proofs"] - assert _fields(BlobCellsAndProofs) == ["blob_cells", "proofs"] - assert _fields(BodiesByHashRequest) == ["block_hashes"] - assert _fields(BodyEntry) == ["available", "body"] - assert _fields(BodiesResponse) == ["entries"] - assert _fields(BlobsV1Request) == ["versioned_hashes"] - assert _fields(BlobV1Entry) == ["available", "contents"] - assert _fields(BlobsV1Response) == ["entries"] - assert _fields(BlobsV4Request) == [ - "versioned_hashes", - "indices_bitarray", - ] - assert _fields(BlobV4Entry) == ["available", "contents"] - assert _fields(ClientVersion) == ["code", "name", "version", "commit"] - assert _fields(IdentityResponse) == ["versions"] - assert _fields(CapabilitiesResponse) == ["capabilities"] - - def test_payload_status_optional_encoding_matches_spec_examples() -> None: """ The ``Optional``/``String`` wire shape tests. diff --git a/pyproject.toml b/pyproject.toml index a15989524cd..3dbdf7a0cb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -481,7 +481,7 @@ exclude = [ ] plugins = ["pydantic.mypy"] -# `eth-remerkleable` ships no type information (no py.typed); treat its imports +# `eth-remerkleable` ships no type information so treat its imports # as untyped instead of vendoring stubs. [[tool.mypy.overrides]] module = "remerkleable.*" From d51e841d947cb2f1f0f10ccde2e31267ac5c96be Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Wed, 17 Jun 2026 14:19:37 +0530 Subject: [PATCH 4/6] resolve docstring reviews --- .../src/execution_testing/ssz/containers.py | 69 +++++-------------- .../src/execution_testing/ssz/ssz_types.py | 8 +-- 2 files changed, 20 insertions(+), 57 deletions(-) diff --git a/packages/testing/src/execution_testing/ssz/containers.py b/packages/testing/src/execution_testing/ssz/containers.py index e9f550ee452..c8023e687de 100644 --- a/packages/testing/src/execution_testing/ssz/containers.py +++ b/packages/testing/src/execution_testing/ssz/containers.py @@ -54,9 +54,7 @@ class Withdrawal(Container): class ExecutionPayloadParis(Container): - """ - The Paris execution payload. - """ + """The Paris execution payload.""" parent_hash: Hash32 fee_recipient: Address @@ -75,9 +73,7 @@ class ExecutionPayloadParis(Container): class ExecutionPayloadShanghai(Container): - """ - The Shanghai execution payload. - """ + """The Shanghai execution payload.""" parent_hash: Hash32 fee_recipient: Address @@ -97,9 +93,7 @@ class ExecutionPayloadShanghai(Container): class ExecutionPayloadCancun(Container): - """ - The Cancun execution payload. - """ + """The Cancun execution payload.""" parent_hash: Hash32 fee_recipient: Address @@ -121,9 +115,7 @@ class ExecutionPayloadCancun(Container): class ExecutionPayloadPrague(Container): - """ - The Prague execution payload. - """ + """The Prague execution payload.""" parent_hash: Hash32 fee_recipient: Address @@ -145,9 +137,7 @@ class ExecutionPayloadPrague(Container): class ExecutionPayloadOsaka(Container): - """ - The Osaka execution payload. - """ + """The Osaka execution payload.""" parent_hash: Hash32 fee_recipient: Address @@ -169,9 +159,7 @@ class ExecutionPayloadOsaka(Container): class ExecutionPayloadAmsterdam(Container): - """ - The Amsterdam execution payload. - """ + """The Amsterdam execution payload.""" parent_hash: Hash32 fee_recipient: Address @@ -205,34 +193,26 @@ class ExecutionPayloadAmsterdam(Container): class ExecutionPayloadEnvelopeParis(Container): - """ - The Paris `newPayload` envelope. - """ + """The Paris `newPayload` envelope.""" payload: ExecutionPayloadParis class ExecutionPayloadEnvelopeShanghai(Container): - """ - The Shanghai `newPayload` envelope. - """ + """The Shanghai `newPayload` envelope.""" payload: ExecutionPayloadShanghai class ExecutionPayloadEnvelopeCancun(Container): - """ - The Cancun `newPayload` envelope. - """ + """The Cancun `newPayload` envelope.""" payload: ExecutionPayloadCancun parent_beacon_block_root: Root class ExecutionPayloadEnvelopePrague(Container): - """ - The Prague `newPayload` envelope. - """ + """The Prague `newPayload` envelope.""" payload: ExecutionPayloadPrague parent_beacon_block_root: Root @@ -243,9 +223,7 @@ class ExecutionPayloadEnvelopePrague(Container): class ExecutionPayloadEnvelopeOsaka(Container): - """ - The Osaka `newPayload` envelope. - """ + """The Osaka `newPayload` envelope.""" payload: ExecutionPayloadOsaka parent_beacon_block_root: Root @@ -256,9 +234,7 @@ class ExecutionPayloadEnvelopeOsaka(Container): class ExecutionPayloadEnvelopeAmsterdam(Container): - """ - The Amsterdam `newPayload` envelope. - """ + """The Amsterdam `newPayload` envelope.""" payload: ExecutionPayloadAmsterdam parent_beacon_block_root: Root @@ -356,13 +332,7 @@ class ForkchoiceState(Container): class PayloadStatus(Container): - """ - A newPayload/forkchoice status. - - ``status`` is a uint8 enum (0=VALID, 1=INVALID, 2=SYNCING, 3=ACCEPTED). - ``latest_valid_hash`` is ``Optional[Hash32]`` and ``validation_error`` is - ``Optional[String]``. - """ + """A newPayload/forkchoice status.""" status: uint8 latest_valid_hash: List[Hash32, 1] @@ -522,9 +492,7 @@ class BuiltPayloadCancun(Container): class BuiltPayloadPrague(Container): - """ - The Prague getPayload response. - """ + """The Prague getPayload response.""" payload: ExecutionPayloadPrague block_value: uint256 @@ -587,10 +555,7 @@ class BlobAndProofV2(Container): class BlobCellsAndProofs(Container): - """ - Cell-range blob contents (/blobs/v4, Amsterdam). - Each cell and proof is ``Optional``. - """ + """Cell-range blob contents for /blobs/v4 (cells/proofs optional).""" blob_cells: List[List[ByteVector[BYTES_PER_CELL], 1], CELLS_PER_EXT_BLOB] proofs: List[List[Bytes48, 1], CELLS_PER_EXT_BLOB] @@ -603,9 +568,7 @@ class BodiesByHashRequest(Container): class BodyEntry(Container): - """ - One bodies-response entry. - """ + """One bodies-response entry.""" available: boolean body: ExecutionPayloadBodyAmsterdam diff --git a/packages/testing/src/execution_testing/ssz/ssz_types.py b/packages/testing/src/execution_testing/ssz/ssz_types.py index 136b4a080ac..54dfe6cef73 100644 --- a/packages/testing/src/execution_testing/ssz/ssz_types.py +++ b/packages/testing/src/execution_testing/ssz/ssz_types.py @@ -6,7 +6,7 @@ class Hash32(ByteVector[32]): - """A 32-byte hash (`Hash32`, `Root` and `Bytes32` share this layout).""" + """A 32-byte hash.""" class Bytes32(ByteVector[32]): @@ -30,12 +30,12 @@ class VersionedHash(ByteVector[32]): class Bytes8(ByteVector[8]): - """An 8-byte value (e.g. `payload_id`).""" + """An 8-byte value.""" class Bytes4(ByteVector[4]): - """A 4-byte value (e.g. a client commit hash).""" + """A 4-byte value.""" class Bytes48(ByteVector[48]): - """A 48-byte value (KZG commitments and proofs).""" + """A 48-byte value.""" From b81a65868ffb7dc81783acffc2caa2538520f554 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Wed, 17 Jun 2026 14:27:48 +0530 Subject: [PATCH 5/6] address *_by_fork testing infra comment --- .../src/execution_testing/ssz/__init__.py | 8 --- .../src/execution_testing/ssz/containers.py | 40 ------------ .../ssz/tests/test_engine_containers.py | 64 +++++++++++++++++-- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/packages/testing/src/execution_testing/ssz/__init__.py b/packages/testing/src/execution_testing/ssz/__init__.py index 197cfd86e1b..f52aef019c9 100644 --- a/packages/testing/src/execution_testing/ssz/__init__.py +++ b/packages/testing/src/execution_testing/ssz/__init__.py @@ -15,12 +15,8 @@ MAX_WITHDRAWALS_PER_PAYLOAD, ) from .containers import ( - BUILT_PAYLOAD_BY_FORK, - EXECUTION_PAYLOAD_BODY_BY_FORK, EXECUTION_PAYLOAD_BY_FORK, EXECUTION_PAYLOAD_ENVELOPE_BY_FORK, - FORKCHOICE_UPDATE_BY_FORK, - PAYLOAD_ATTRIBUTES_BY_FORK, BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofs, @@ -208,12 +204,8 @@ def _opt_bytes(value: bytes | None) -> bytes | None: __all__ = ( - "BUILT_PAYLOAD_BY_FORK", - "EXECUTION_PAYLOAD_BODY_BY_FORK", "EXECUTION_PAYLOAD_BY_FORK", "EXECUTION_PAYLOAD_ENVELOPE_BY_FORK", - "FORKCHOICE_UPDATE_BY_FORK", - "PAYLOAD_ATTRIBUTES_BY_FORK", "Address", "BlobAndProofV1", "BlobAndProofV2", diff --git a/packages/testing/src/execution_testing/ssz/containers.py b/packages/testing/src/execution_testing/ssz/containers.py index c8023e687de..1d020bb3ed5 100644 --- a/packages/testing/src/execution_testing/ssz/containers.py +++ b/packages/testing/src/execution_testing/ssz/containers.py @@ -313,16 +313,6 @@ class PayloadAttributesAmsterdam(Container): target_gas_limit: uint64 -PAYLOAD_ATTRIBUTES_BY_FORK: Dict[str, Type[Container]] = { - "Paris": PayloadAttributesParis, - "Shanghai": PayloadAttributesShanghai, - "Cancun": PayloadAttributesCancun, - "Prague": PayloadAttributesPrague, - "Osaka": PayloadAttributesOsaka, - "Amsterdam": PayloadAttributesAmsterdam, -} - - class ForkchoiceState(Container): """The forkchoice head/safe/finalized triple.""" @@ -382,16 +372,6 @@ class ForkchoiceUpdateAmsterdam(Container): custody_columns: List[Bitvector[CELLS_PER_EXT_BLOB], 1] -FORKCHOICE_UPDATE_BY_FORK: Dict[str, Type[Container]] = { - "Paris": ForkchoiceUpdateParis, - "Shanghai": ForkchoiceUpdateShanghai, - "Cancun": ForkchoiceUpdateCancun, - "Prague": ForkchoiceUpdatePrague, - "Osaka": ForkchoiceUpdateOsaka, - "Amsterdam": ForkchoiceUpdateAmsterdam, -} - - class ForkchoiceUpdateResponse(Container): """The forkchoice response: a status and an optional payload id.""" @@ -441,16 +421,6 @@ class ExecutionPayloadBodyAmsterdam(Container): block_access_list: ByteList[MAX_BAL_BYTES] -EXECUTION_PAYLOAD_BODY_BY_FORK: Dict[str, Type[Container]] = { - "Paris": ExecutionPayloadBodyParis, - "Shanghai": ExecutionPayloadBodyShanghai, - "Cancun": ExecutionPayloadBodyCancun, - "Prague": ExecutionPayloadBodyPrague, - "Osaka": ExecutionPayloadBodyOsaka, - "Amsterdam": ExecutionPayloadBodyAmsterdam, -} - - class BlobsBundleV1(Container): """The Cancun blobs bundle.""" @@ -530,16 +500,6 @@ class BuiltPayloadAmsterdam(Container): should_override_builder: boolean -BUILT_PAYLOAD_BY_FORK: Dict[str, Type[Container]] = { - "Paris": BuiltPayloadParis, - "Shanghai": BuiltPayloadShanghai, - "Cancun": BuiltPayloadCancun, - "Prague": BuiltPayloadPrague, - "Osaka": BuiltPayloadOsaka, - "Amsterdam": BuiltPayloadAmsterdam, -} - - class BlobAndProofV1(Container): """A whole blob with a single proof.""" diff --git a/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py b/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py index 2da888178bb..fd93ac417fa 100644 --- a/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py +++ b/packages/testing/src/execution_testing/ssz/tests/test_engine_containers.py @@ -6,17 +6,73 @@ from .. import decode_bytes, encode_bytes, envelope_bytes from ..containers import ( - BUILT_PAYLOAD_BY_FORK, - EXECUTION_PAYLOAD_BODY_BY_FORK, - FORKCHOICE_UPDATE_BY_FORK, - PAYLOAD_ATTRIBUTES_BY_FORK, + BuiltPayloadAmsterdam, + BuiltPayloadCancun, + BuiltPayloadOsaka, + BuiltPayloadParis, + BuiltPayloadPrague, + BuiltPayloadShanghai, + ExecutionPayloadBodyAmsterdam, + ExecutionPayloadBodyCancun, + ExecutionPayloadBodyOsaka, + ExecutionPayloadBodyParis, + ExecutionPayloadBodyPrague, + ExecutionPayloadBodyShanghai, ExecutionPayloadEnvelopeAmsterdam, ExecutionPayloadEnvelopeParis, + ForkchoiceUpdateAmsterdam, + ForkchoiceUpdateCancun, + ForkchoiceUpdateOsaka, + ForkchoiceUpdateParis, + ForkchoiceUpdatePrague, + ForkchoiceUpdateShanghai, + PayloadAttributesAmsterdam, + PayloadAttributesCancun, + PayloadAttributesOsaka, + PayloadAttributesParis, + PayloadAttributesPrague, + PayloadAttributesShanghai, PayloadStatus, ) _FORKS = ["Paris", "Shanghai", "Cancun", "Prague", "Osaka", "Amsterdam"] +PAYLOAD_ATTRIBUTES_BY_FORK = { + "Paris": PayloadAttributesParis, + "Shanghai": PayloadAttributesShanghai, + "Cancun": PayloadAttributesCancun, + "Prague": PayloadAttributesPrague, + "Osaka": PayloadAttributesOsaka, + "Amsterdam": PayloadAttributesAmsterdam, +} + +FORKCHOICE_UPDATE_BY_FORK = { + "Paris": ForkchoiceUpdateParis, + "Shanghai": ForkchoiceUpdateShanghai, + "Cancun": ForkchoiceUpdateCancun, + "Prague": ForkchoiceUpdatePrague, + "Osaka": ForkchoiceUpdateOsaka, + "Amsterdam": ForkchoiceUpdateAmsterdam, +} + +EXECUTION_PAYLOAD_BODY_BY_FORK = { + "Paris": ExecutionPayloadBodyParis, + "Shanghai": ExecutionPayloadBodyShanghai, + "Cancun": ExecutionPayloadBodyCancun, + "Prague": ExecutionPayloadBodyPrague, + "Osaka": ExecutionPayloadBodyOsaka, + "Amsterdam": ExecutionPayloadBodyAmsterdam, +} + +BUILT_PAYLOAD_BY_FORK = { + "Paris": BuiltPayloadParis, + "Shanghai": BuiltPayloadShanghai, + "Cancun": BuiltPayloadCancun, + "Prague": BuiltPayloadPrague, + "Osaka": BuiltPayloadOsaka, + "Amsterdam": BuiltPayloadAmsterdam, +} + def test_new_registries_cover_forks_in_order() -> None: """Every new per-fork registry spans Paris..Amsterdam.""" From 3c76516f0c074c4dde8762382c3f70f0bccce3c0 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Mon, 22 Jun 2026 16:56:59 +0530 Subject: [PATCH 6/6] address reviews --- .../src/execution_testing/ssz/__init__.py | 25 +++++++++++-- .../src/execution_testing/ssz/containers.py | 22 ------------ .../ssz/tests/test_containers.py | 36 +++++++++++++++---- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/packages/testing/src/execution_testing/ssz/__init__.py b/packages/testing/src/execution_testing/ssz/__init__.py index f52aef019c9..6a9d5e7f567 100644 --- a/packages/testing/src/execution_testing/ssz/__init__.py +++ b/packages/testing/src/execution_testing/ssz/__init__.py @@ -2,8 +2,9 @@ SSZ container types and helpers for the REST+SSZ Engine API. """ -from typing import Any, Mapping, Sequence, Type, TypeVar +from typing import Any, Dict, Mapping, Sequence, Type, TypeVar +from remerkleable.complex import Container from remerkleable.core import View from .constants import ( @@ -15,8 +16,6 @@ MAX_WITHDRAWALS_PER_PAYLOAD, ) from .containers import ( - EXECUTION_PAYLOAD_BY_FORK, - EXECUTION_PAYLOAD_ENVELOPE_BY_FORK, BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofs, @@ -114,6 +113,26 @@ def hash_tree_root(value: View) -> bytes: 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(): diff --git a/packages/testing/src/execution_testing/ssz/containers.py b/packages/testing/src/execution_testing/ssz/containers.py index 1d020bb3ed5..fb570cfd9c5 100644 --- a/packages/testing/src/execution_testing/ssz/containers.py +++ b/packages/testing/src/execution_testing/ssz/containers.py @@ -2,8 +2,6 @@ SSZ container definitions for the REST+SSZ Engine API. """ -from typing import Dict, Type - from remerkleable.basic import boolean, uint8, uint64, uint256 from remerkleable.bitfields import Bitvector from remerkleable.byte_arrays import ByteList, ByteVector @@ -182,16 +180,6 @@ class ExecutionPayloadAmsterdam(Container): slot_number: uint64 -EXECUTION_PAYLOAD_BY_FORK: Dict[str, Type[Container]] = { - "Paris": ExecutionPayloadParis, - "Shanghai": ExecutionPayloadShanghai, - "Cancun": ExecutionPayloadCancun, - "Prague": ExecutionPayloadPrague, - "Osaka": ExecutionPayloadOsaka, - "Amsterdam": ExecutionPayloadAmsterdam, -} - - class ExecutionPayloadEnvelopeParis(Container): """The Paris `newPayload` envelope.""" @@ -244,16 +232,6 @@ class ExecutionPayloadEnvelopeAmsterdam(Container): ] -EXECUTION_PAYLOAD_ENVELOPE_BY_FORK: Dict[str, Type[Container]] = { - "Paris": ExecutionPayloadEnvelopeParis, - "Shanghai": ExecutionPayloadEnvelopeShanghai, - "Cancun": ExecutionPayloadEnvelopeCancun, - "Prague": ExecutionPayloadEnvelopePrague, - "Osaka": ExecutionPayloadEnvelopeOsaka, - "Amsterdam": ExecutionPayloadEnvelopeAmsterdam, -} - - class PayloadAttributesParis(Container): """The Paris payload attributes.""" diff --git a/packages/testing/src/execution_testing/ssz/tests/test_containers.py b/packages/testing/src/execution_testing/ssz/tests/test_containers.py index 12a15a3b19d..a9faeae5866 100644 --- a/packages/testing/src/execution_testing/ssz/tests/test_containers.py +++ b/packages/testing/src/execution_testing/ssz/tests/test_containers.py @@ -1,17 +1,39 @@ """Round-trip and structural tests for the SSZ containers.""" from remerkleable.basic import uint64, uint256 +from remerkleable.byte_arrays import ByteList +from remerkleable.complex import List -from .. import decode_bytes, encode_bytes, hash_tree_root -from ..containers import ( +from .. import ( EXECUTION_PAYLOAD_BY_FORK, EXECUTION_PAYLOAD_ENVELOPE_BY_FORK, + decode_bytes, + encode_bytes, + hash_tree_root, +) +from ..constants import ( + MAX_BAL_BYTES, + MAX_BYTES_PER_EXECUTION_REQUEST, + MAX_BYTES_PER_TX, + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, + MAX_TXS_PER_PAYLOAD, +) +from ..containers import ( ExecutionPayloadAmsterdam, ExecutionPayloadEnvelopeAmsterdam, Withdrawal, ) from ..ssz_types import Address, Bloom, Bytes32, Hash32, Root + +Transactions = List[ByteList[MAX_BYTES_PER_TX], MAX_TXS_PER_PAYLOAD] +BlockAccessList = ByteList[MAX_BAL_BYTES] +ExecutionRequests = List[ + ByteList[MAX_BYTES_PER_EXECUTION_REQUEST], + MAX_EXECUTION_REQUESTS_PER_PAYLOAD, +] + + TRANSACTIONS = [ bytes.fromhex("02f86b01"), bytes.fromhex("01" * 21), @@ -44,11 +66,11 @@ def _random_payload() -> ExecutionPayloadAmsterdam: extra_data=bytes.fromhex("dead"), base_fee_per_gas=uint256(10**18), block_hash=Hash32(bytes.fromhex("ff" * 32)), - transactions=list(TRANSACTIONS), + transactions=Transactions(*TRANSACTIONS), withdrawals=[_withdrawal()], blob_gas_used=uint64(131_072), excess_blob_gas=uint64(0), - block_access_list=bytes.fromhex("c0de"), + block_access_list=BlockAccessList(bytes.fromhex("c0de")), slot_number=uint64(9_999), ) @@ -58,7 +80,9 @@ def _max_envelope() -> ExecutionPayloadEnvelopeAmsterdam: return ExecutionPayloadEnvelopeAmsterdam( payload=_random_payload(), parent_beacon_block_root=Root(bytes.fromhex("12" * 32)), - execution_requests=[bytes.fromhex("00aa"), bytes.fromhex("01bbcc")], + execution_requests=ExecutionRequests( + bytes.fromhex("00aa"), bytes.fromhex("01bbcc") + ), ) @@ -101,7 +125,7 @@ def test_transactions_two_level_offsets() -> None: assert [bytes(tx) for tx in decoded.transactions] == TRANSACTIONS reordered = _random_payload() - reordered.transactions = list(reversed(TRANSACTIONS)) + reordered.transactions = Transactions(*reversed(TRANSACTIONS)) assert hash_tree_root(reordered) != hash_tree_root(value)