Skip to content
6 changes: 6 additions & 0 deletions packages/testing/src/execution_testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
DepositRequest,
Environment,
FeeSystemContractRequest,
Frame,
FrameReceipt,
FrameSignature,
NetworkWrappedTransaction,
Removable,
Requests,
Expand Down Expand Up @@ -175,6 +178,9 @@
"EIPChecklist",
"EngineAPIError",
"Environment",
"Frame",
"FrameReceipt",
"FrameSignature",
"EOA",
"FeeSystemContractRequest",
"FixedIterationsBytecode",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,15 @@ class ExecutionSpecsExceptionMapper(ExceptionMapper):
"Block access list exceeds gas limit"
),
TransactionException.LOG_MISMATCH: "LogMismatchError",
TransactionException.TYPE_6_INVALID_FRAME_FORMAT: (
"FrameTransactionFormatError"
),
TransactionException.TYPE_6_INVALID_SIGNATURE: (
"FrameTransactionSignatureError"
),
TransactionException.TYPE_6_INVALID_FRAME_EXECUTION: (
"FrameTransactionExecutionError"
),
}
mapping_regex: ClassVar[Dict[ExceptionBase, str]] = {
# Temporary solution for issue #1981.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ class GethExceptionMapper(ExceptionMapper):
TransactionException.TYPE_4_TX_PRE_FORK: (
"transaction type not supported"
),
TransactionException.TYPE_6_INVALID_FRAME_FORMAT: (
"invalid frame tx format"
),
TransactionException.TYPE_6_INVALID_SIGNATURE: (
"invalid frame tx signature"
),
TransactionException.TYPE_6_INVALID_FRAME_EXECUTION: (
"invalid frame execution"
),
TransactionException.INITCODE_SIZE_EXCEEDED: (
"max initcode size exceeded"
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,22 @@ class TransactionException(ExceptionBase):
"""
TYPE_4_TX_PRE_FORK = auto()
"""Transaction type 4 included before activation fork."""
TYPE_6_INVALID_FRAME_FORMAT = auto()
"""
Transaction is type 6, but violates a static frame transaction
constraint (frame count, mode, flags, value, signature entry
structure, expiry verifier frame shape, blob fields, etc.).
"""
TYPE_6_INVALID_SIGNATURE = auto()
"""
Transaction is type 6, but a signature entry failed protocol
validation.
"""
TYPE_6_INVALID_FRAME_EXECUTION = auto()
"""
Transaction is type 6, but frame execution invalidated it (a SENDER
frame ran before execution approval, a VERIFY frame reverted, or no
frame approved gas payment).
"""
LOG_MISMATCH = auto()
"""Transaction receipt logs do not match expected logs."""
4 changes: 4 additions & 0 deletions packages/testing/src/execution_testing/fixtures/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
from .common import (
FixtureAuthorizationTuple,
FixtureBlobSchedule,
FixtureFrame,
FixtureFrameSignature,
FixtureTransactionReceipt,
)

Expand Down Expand Up @@ -692,6 +694,8 @@ class FixtureTransaction(

authorization_list: List[FixtureAuthorizationTuple] | None = None
initcodes: List[Bytes] | None = None
frames: List[FixtureFrame] | None = None
signatures: List[FixtureFrameSignature] | None = None

@classmethod
def from_transaction(cls, tx: Transaction) -> Self:
Expand Down
46 changes: 46 additions & 0 deletions packages/testing/src/execution_testing/fixtures/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
)
from execution_testing.test_types.transaction_types import (
AuthorizationTupleGeneric,
FrameGeneric,
FrameSignatureGeneric,
Transaction,
)

Expand Down Expand Up @@ -104,6 +106,22 @@ def sign(self) -> None:
return


class FixtureFrame(FrameGeneric[ZeroPaddedHexNumber]):
"""Fixture variant of the EIP-8141 Frame type."""

# Allow extra fields: FixtureFrame is constructed from Frame via
# model_dump(), which may include extra fields.
model_config = CamelModel.model_config | {"extra": "ignore"}


class FixtureFrameSignature(FrameSignatureGeneric[ZeroPaddedHexNumber]):
"""Fixture variant of the EIP-8141 signature entry type."""

# Allow extra fields: FixtureFrameSignature is constructed from
# FrameSignature via model_dump(), which may include extra fields.
model_config = CamelModel.model_config | {"extra": "ignore"}


class FixtureTransactionLog(CamelModel, RLPSerializable):
"""Fixture variant of the TransactionLog type."""

Expand All @@ -126,6 +144,22 @@ class FixtureReceiptDelegation(ReceiptDelegation):
nonce: ZeroPaddedHexNumber


class FixtureFrameReceipt(CamelModel, RLPSerializable):
"""Fixture variant of the EIP-8141 FrameReceipt type."""

model_config = CamelModel.model_config | {"extra": "ignore"}

status: ZeroPaddedHexNumber
gas_used: ZeroPaddedHexNumber
logs: List[FixtureTransactionLog]

rlp_fields: ClassVar[List[str]] = [
"status",
"gas_used",
"logs",
]


class FixtureTransactionReceipt(CamelModel, RLPSerializable):
"""Fixture variant of the TransactionReceipt type."""

Expand All @@ -137,6 +171,9 @@ class FixtureTransactionReceipt(CamelModel, RLPSerializable):
post_state: Hash | None = None
status: bool | None = None

payer: Address | None = None
frame_receipts: List[FixtureFrameReceipt] | None = None

rlp_fields: ClassVar[List[str]] = [
"post_state",
"status",
Expand All @@ -146,6 +183,15 @@ class FixtureTransactionReceipt(CamelModel, RLPSerializable):
]
rlp_exclude_none: ClassVar[bool] = True

def get_rlp_fields(self) -> List[str]:
"""
Return the RLP field list, using the EIP-8141 frame receipt
payload for frame transactions.
"""
if self.payer is not None:
return ["cumulative_gas_used", "payer", "frame_receipts"]
return self.rlp_fields

@model_validator(mode="before")
@classmethod
def _drop_computed_fields(cls, data: Any) -> Any:
Expand Down
4 changes: 4 additions & 0 deletions packages/testing/src/execution_testing/fixtures/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from .common import (
FixtureAuthorizationTuple,
FixtureBlobSchedule,
FixtureFrame,
FixtureFrameSignature,
FixtureTransactionReceipt,
)

Expand Down Expand Up @@ -57,6 +59,8 @@ class FixtureTransaction(TransactionFixtureConverter):
access_lists: List[List[AccessList] | None] | None = None
authorization_list: List[FixtureAuthorizationTuple] | None = None
initcodes: List[Bytes] | None = None
frames: List[FixtureFrame] | None = None
signatures: List[FixtureFrameSignature] | None = None
max_fee_per_blob_gas: ZeroPaddedHexNumber | None = None
blob_versioned_hashes: Sequence[Hash] | None = None
sender: Address | None = None
Expand Down
4 changes: 4 additions & 0 deletions packages/testing/src/execution_testing/forks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Amsterdam,
ArrowGlacier,
Berlin,
Bogota,
Byzantium,
Cancun,
Constantinople,
Expand All @@ -28,6 +29,7 @@
TangerineWhistle,
)
from .forks.transition import (
AmsterdamToBogotaAtTime15k,
BerlinToLondonAt5,
BPO1ToBPO2AtTime15k,
BPO2ToAmsterdamAtTime15k,
Expand Down Expand Up @@ -90,8 +92,10 @@
"TransitionForkOrNoneAdapter",
"RefundTypes",
"Amsterdam",
"AmsterdamToBogotaAtTime15k",
"ArrowGlacier",
"Berlin",
"Bogota",
"BerlinToLondonAt5",
"Byzantium",
"Constantinople",
Expand Down
9 changes: 9 additions & 0 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1645,3 +1645,12 @@ class Amsterdam(
# live on mainnet.

pass


class Bogota(Amsterdam, deployed=False):
"""Bogota fork."""

@classmethod
def tx_types(cls) -> List[int]:
"""At Bogota, frame transactions (type 6) are introduced."""
return super(Bogota, cls).tx_types() + [6]
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
BPO4,
Amsterdam,
Berlin,
Bogota,
Cancun,
London,
Osaka,
Expand Down Expand Up @@ -78,6 +79,13 @@ class BPO2ToAmsterdamAtTime15k(TransitionBaseClass):
pass


@transition_fork(to_fork=Bogota, from_fork=Amsterdam, at_timestamp=15_000)
class AmsterdamToBogotaAtTime15k(TransitionBaseClass):
"""Amsterdam to Bogota transition at Timestamp 15k."""

pass


@transition_fork(to_fork=BPO3, from_fork=BPO2, at_timestamp=15_000)
class BPO2ToBPO3AtTime15k(TransitionBaseClass):
"""BPO2 to BPO3 transition at Timestamp 15k."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
BPO5,
Amsterdam,
Berlin,
Bogota,
Cancun,
Frontier,
Homestead,
Expand Down Expand Up @@ -57,8 +58,8 @@

FIRST_DEPLOYED = Frontier
LAST_DEPLOYED = Osaka
LAST_DEVELOPMENT = Amsterdam
DEVELOPMENT_FORKS = [Amsterdam]
LAST_DEVELOPMENT = Bogota
DEVELOPMENT_FORKS = [Amsterdam, Bogota]


def test_transition_forks() -> None:
Expand Down
98 changes: 98 additions & 0 deletions packages/testing/src/execution_testing/specs/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,100 @@ def verify_transaction_receipt(
# TODO: Add more fields as needed


def verify_frame_transaction_receipt(
transaction_index: int,
expected_receipt: TransactionReceipt | None,
actual_receipt: TransactionReceipt | None,
) -> None:
"""
Verify the frame-transaction-specific fields of the actual receipt
against the expected one: the `payer` and the per-frame receipt
entries defined by [EIP-8141].

Only called for frame transactions, on top of the generic
[`verify_transaction_receipt`][vtr] checks. If the expected receipt
is None, validation is skipped. Only non-None values in the
expected receipt are verified; within an expected frame receipt
entry, only its non-None fields are verified.

[vtr]: ref:execution_testing.specs.helpers.verify_transaction_receipt
[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141
"""
if expected_receipt is None:
return
assert actual_receipt is not None
if (
expected_receipt.payer is not None
and actual_receipt.payer != expected_receipt.payer
):
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name="payer",
expected_value=expected_receipt.payer,
actual_value=actual_receipt.payer,
)

if expected_receipt.frame_receipts is None:
return
actual_frame_receipts = actual_receipt.frame_receipts
if actual_frame_receipts is None:
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name="frame_receipts",
expected_value=expected_receipt.frame_receipts,
actual_value=None,
)
if len(expected_receipt.frame_receipts) != len(actual_frame_receipts):
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name="frame_receipt_count",
expected_value=len(expected_receipt.frame_receipts),
actual_value=len(actual_frame_receipts),
)
for frame_idx, (expected_frame, actual_frame) in enumerate(
zip(
expected_receipt.frame_receipts,
actual_frame_receipts,
strict=True,
)
):
if (
expected_frame.status is not None
and actual_frame.status != expected_frame.status
):
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name=f"frame_receipts[{frame_idx}].status",
expected_value=expected_frame.status,
actual_value=actual_frame.status,
)
if (
expected_frame.gas_used is not None
and actual_frame.gas_used != expected_frame.gas_used
):
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name=f"frame_receipts[{frame_idx}].gas_used",
expected_value=expected_frame.gas_used,
actual_value=actual_frame.gas_used,
)
if expected_frame.logs is not None:
actual_frame_logs = actual_frame.logs or []
if len(expected_frame.logs) != len(actual_frame_logs):
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name=f"frame_receipts[{frame_idx}].log_count",
expected_value=len(expected_frame.logs),
actual_value=len(actual_frame_logs),
)
for log_idx, (expected_log, actual_log) in enumerate(
zip(expected_frame.logs, actual_frame_logs, strict=True)
):
verify_log(
transaction_index, log_idx, expected_log, actual_log
)


def verify_transactions(
*,
txs: List[Transaction],
Expand Down Expand Up @@ -369,6 +463,10 @@ def verify_transactions(
verify_transaction_receipt(
i, tx.expected_receipt, result.receipts[receipt_index]
)
if tx.frames is not None:
verify_frame_transaction_receipt(
i, tx.expected_receipt, result.receipts[receipt_index]
)
receipt_index += 1

return list(rejected_txs.keys())
Expand Down
Loading
Loading