Skip to content

Commit

Permalink
Merge pull request #1068 from ethereum/verify-transaction-output
Browse files Browse the repository at this point in the history
feat(types,specs,tests): Verify resulting transaction receipt
  • Loading branch information
marioevz authored Jan 15, 2025
2 parents 27534e1 + 09b3688 commit 64fbb50
Show file tree
Hide file tree
Showing 23 changed files with 387 additions and 162 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Release tarball changes:
- ✨ Introduce [`pytest.mark.parametrize_by_fork`](https://ethereum.github.io/execution-spec-tests/main/writing_tests/test_markers/#pytestmarkfork_parametrize) helper marker ([#1019](https://github.com/ethereum/execution-spec-tests/pull/1019), [#1057](https://github.com/ethereum/execution-spec-tests/pull/1057)).
- 🐞 fix(consume): allow absolute paths with `--evm-bin` ([#1052](https://github.com/ethereum/execution-spec-tests/pull/1052)).
- ✨ Disable EIP-7742 framework changes for Prague ([#1023](https://github.com/ethereum/execution-spec-tests/pull/1023)).
- ✨ Allow verification of the transaction receipt on executed test transactions ([#1068](https://github.com/ethereum/execution-spec-tests/pull/1068)).

### 🔧 EVM Tools

Expand Down
5 changes: 4 additions & 1 deletion docs/writing_tests/writing_a_new_test.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ storage to be able to verify them in the post-state.
## Test Transactions

Transactions can be crafted by sending them with specific `data` or to a
specific account, which contains the code to be executed
specific account, which contains the code to be executed.

Transactions can also create more accounts, by setting the `to` field to an
empty string.
Expand All @@ -141,6 +141,9 @@ Transactions can be designed to fail, and a verification must be made that the
transaction fails with the specific error that matches what is expected by the
test.

They can also contain a `TransactionReceipt` object in field `expected_receipt`
which allows checking for an exact `gas_used` value.

## Writing code for the accounts in the test

Account bytecode can be embedded in the test accounts by adding it to the `code`
Expand Down
10 changes: 8 additions & 2 deletions src/ethereum_clis/tests/test_execution_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ def monkeypatch_path_for_entry_points(monkeypatch):
monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ['PATH']}")


@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool()])
@pytest.fixture
def t8n(request):
"""Fixture for the `t8n` argument."""
return request.param()


@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool], indirect=True)
@pytest.mark.parametrize("fork", [London, Istanbul])
@pytest.mark.parametrize(
"alloc,base_fee,expected_hash",
Expand Down Expand Up @@ -150,7 +156,7 @@ def env(test_dir: str) -> Environment:
return Environment.model_validate_json(f.read())


@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool()])
@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool], indirect=True)
@pytest.mark.parametrize("test_dir", os.listdir(path=FIXTURES_ROOT))
def test_evm_t8n(
t8n: TransitionTool,
Expand Down
45 changes: 2 additions & 43 deletions src/ethereum_clis/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,8 @@

from pydantic import Field

from ethereum_test_base_types import Address, Bloom, Bytes, CamelModel, Hash, HexNumber
from ethereum_test_types import Alloc, Environment, Transaction


class TransactionLog(CamelModel):
"""Transaction log."""

address: Address
topics: List[Hash]
data: Bytes
block_number: HexNumber
transaction_hash: Hash
transaction_index: HexNumber
block_hash: Hash
log_index: HexNumber
removed: bool


class SetCodeDelegation(CamelModel):
"""Set code delegation."""

from_address: Address = Field(..., alias="from")
nonce: HexNumber
target: Address


class TransactionReceipt(CamelModel):
"""Transaction receipt."""

transaction_hash: Hash
gas_used: HexNumber
root: Bytes | None = None
status: HexNumber | None = None
cumulative_gas_used: HexNumber | None = None
logs_bloom: Bloom | None = None
logs: List[TransactionLog] | None = None
contract_address: Address | None = None
effective_gas_price: HexNumber | None = None
block_hash: Hash | None = None
transaction_index: HexNumber | None = None
blob_gas_used: HexNumber | None = None
blob_gas_price: HexNumber | None = None
delegations: List[SetCodeDelegation] | None = None
from ethereum_test_base_types import Bloom, Bytes, CamelModel, Hash, HexNumber
from ethereum_test_types import Alloc, Environment, Transaction, TransactionReceipt


class RejectedTransaction(CamelModel):
Expand Down
5 changes: 4 additions & 1 deletion src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts:
G_KECCAK_256_WORD=6,
G_COPY=3,
G_BLOCKHASH=20,
G_AUTHORIZATION=25_000,
G_AUTHORIZATION=0,
R_AUTHORIZATION_EXISTING_AUTHORITY=0,
)

@classmethod
Expand Down Expand Up @@ -1052,6 +1053,8 @@ def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts:
super(Prague, cls).gas_costs(block_number, timestamp),
G_TX_DATA_STANDARD_TOKEN_COST=4, # https://eips.ethereum.org/EIPS/eip-7623
G_TX_DATA_FLOOR_TOKEN_COST=10,
G_AUTHORIZATION=25_000,
R_AUTHORIZATION_EXISTING_AUTHORITY=12_500,
)

@classmethod
Expand Down
2 changes: 2 additions & 0 deletions src/ethereum_test_forks/gas_costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ class GasCosts:
G_BLOCKHASH: int

G_AUTHORIZATION: int

R_AUTHORIZATION_EXISTING_AUTHORITY: int
4 changes: 3 additions & 1 deletion src/ethereum_test_specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,9 @@ def generate_block_data(

try:
rejected_txs = verify_transactions(
t8n.exception_mapper, txs, transition_tool_output.result
txs=txs,
exception_mapper=t8n.exception_mapper,
result=transition_tool_output.result,
)
verify_result(transition_tool_output.result, env)
except Exception as e:
Expand Down
86 changes: 71 additions & 15 deletions src/ethereum_test_specs/helpers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"""Helper functions."""

from dataclasses import dataclass
from typing import Dict, List
from typing import Any, Dict, List

import pytest

from ethereum_clis import Result
from ethereum_test_exceptions import ExceptionBase, ExceptionMapper, UndefinedException
from ethereum_test_types import Transaction
from ethereum_test_types import Transaction, TransactionReceipt


class TransactionExpectedToFailSucceedError(Exception):
"""Exception used when the transaction expected to return an error, did succeed."""
class TransactionUnexpectedSuccessError(Exception):
"""Exception used when the transaction expected to fail succeeded instead."""

def __init__(self, index: int, nonce: int):
"""Initialize the exception with the transaction index and nonce."""
Expand All @@ -23,7 +23,7 @@ def __init__(self, index: int, nonce: int):


class TransactionUnexpectedFailError(Exception):
"""Exception used when the transaction expected to succeed, did fail."""
"""Exception used when the transaction expected to succeed failed instead."""

def __init__(self, index: int, nonce: int, message: str, exception: ExceptionBase):
"""Initialize the exception."""
Expand Down Expand Up @@ -70,12 +70,32 @@ def __init__(
super().__init__(message)


class TransactionReceiptMismatchError(Exception):
"""Exception used when the actual transaction receipt differs from the expected one."""

def __init__(
self,
index: int,
field_name: str,
expected_value: Any,
actual_value: Any,
):
"""Initialize the exception."""
message = (
f"\nTransactionReceiptMismatch (pos={index}):"
f"\n What: {field_name} mismatch!"
f"\n Want: {expected_value}"
f"\n Got: {actual_value}"
)
super().__init__(message)


@dataclass
class TransactionExceptionInfo:
"""Info to print transaction exception error messages."""

t8n_error_message: str | None
transaction_ind: int
transaction_index: int
tx: Transaction


Expand All @@ -89,12 +109,10 @@ def verify_transaction_exception(

# info.tx.error is expected error code defined in .py test
if expected_error and not info.t8n_error_message:
raise TransactionExpectedToFailSucceedError(
index=info.transaction_ind, nonce=info.tx.nonce
)
raise TransactionUnexpectedSuccessError(index=info.transaction_index, nonce=info.tx.nonce)
elif not expected_error and info.t8n_error_message:
raise TransactionUnexpectedFailError(
index=info.transaction_ind,
index=info.transaction_index,
nonce=info.tx.nonce,
message=info.t8n_error_message,
exception=exception_mapper.message_to_exception(info.t8n_error_message),
Expand Down Expand Up @@ -122,7 +140,7 @@ def verify_transaction_exception(

if expected_error_msg is None or expected_error_msg not in info.t8n_error_message:
raise TransactionExceptionMismatchError(
index=info.transaction_ind,
index=info.transaction_index,
nonce=info.tx.nonce,
expected_exception=expected_exception,
expected_message=expected_error_msg,
Expand All @@ -132,21 +150,59 @@ def verify_transaction_exception(
)


def verify_transaction_receipt(
transaction_index: int,
expected_receipt: TransactionReceipt | None,
actual_receipt: TransactionReceipt | None,
):
"""
Verify the actual receipt against the expected one.
If the expected receipt is None, validation is skipped.
Only verifies non-None values in the expected receipt if any.
"""
if expected_receipt is None:
return
assert actual_receipt is not None
if (
expected_receipt.gas_used is not None
and actual_receipt.gas_used != expected_receipt.gas_used
):
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name="gas_used",
expected_value=expected_receipt.gas_used,
actual_value=actual_receipt.gas_used,
)
# TODO: Add more fields as needed


def verify_transactions(
exception_mapper: ExceptionMapper, txs: List[Transaction], result: Result
*,
txs: List[Transaction],
exception_mapper: ExceptionMapper,
result: Result,
) -> List[int]:
"""
Verify rejected transactions (if any) against the expected outcome.
Raises exception on unexpected rejections or unexpected successful txs.
Verify accepted and rejected (if any) transactions against the expected outcome.
Raises exception on unexpected rejections, unexpected successful txs, or successful txs with
unexpected receipt values.
"""
rejected_txs: Dict[int, str] = {
rejected_tx.index: rejected_tx.error for rejected_tx in result.rejected_transactions
}

receipt_index = 0
for i, tx in enumerate(txs):
error_message = rejected_txs[i] if i in rejected_txs else None
info = TransactionExceptionInfo(t8n_error_message=error_message, transaction_ind=i, tx=tx)
info = TransactionExceptionInfo(
t8n_error_message=error_message, transaction_index=i, tx=tx
)
verify_transaction_exception(exception_mapper=exception_mapper, info=info)
if error_message is None:
verify_transaction_receipt(i, tx.expected_receipt, result.receipts[receipt_index])
receipt_index += 1

return list(rejected_txs.keys())

Expand Down
6 changes: 5 additions & 1 deletion src/ethereum_test_specs/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ def make_state_test_fixture(
raise e

try:
verify_transactions(t8n.exception_mapper, [tx], transition_tool_output.result)
verify_transactions(
txs=[tx],
exception_mapper=t8n.exception_mapper,
result=transition_tool_output.result,
)
except Exception as e:
print_traces(t8n.get_traces())
pprint(transition_tool_output.result)
Expand Down
Loading

0 comments on commit 64fbb50

Please sign in to comment.