Skip to content

Commit 64fbb50

Browse files
authored
Merge pull request #1068 from ethereum/verify-transaction-output
feat(types,specs,tests): Verify resulting transaction receipt
2 parents 27534e1 + 09b3688 commit 64fbb50

File tree

23 files changed

+387
-162
lines changed

23 files changed

+387
-162
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Release tarball changes:
7777
- ✨ 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)).
7878
- 🐞 fix(consume): allow absolute paths with `--evm-bin` ([#1052](https://github.com/ethereum/execution-spec-tests/pull/1052)).
7979
- ✨ Disable EIP-7742 framework changes for Prague ([#1023](https://github.com/ethereum/execution-spec-tests/pull/1023)).
80+
- ✨ Allow verification of the transaction receipt on executed test transactions ([#1068](https://github.com/ethereum/execution-spec-tests/pull/1068)).
8081

8182
### 🔧 EVM Tools
8283

docs/writing_tests/writing_a_new_test.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ storage to be able to verify them in the post-state.
132132
## Test Transactions
133133

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

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

144+
They can also contain a `TransactionReceipt` object in field `expected_receipt`
145+
which allows checking for an exact `gas_used` value.
146+
144147
## Writing code for the accounts in the test
145148

146149
Account bytecode can be embedded in the test accounts by adding it to the `code`

src/ethereum_clis/tests/test_execution_specs.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ def monkeypatch_path_for_entry_points(monkeypatch):
3434
monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ['PATH']}")
3535

3636

37-
@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool()])
37+
@pytest.fixture
38+
def t8n(request):
39+
"""Fixture for the `t8n` argument."""
40+
return request.param()
41+
42+
43+
@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool], indirect=True)
3844
@pytest.mark.parametrize("fork", [London, Istanbul])
3945
@pytest.mark.parametrize(
4046
"alloc,base_fee,expected_hash",
@@ -150,7 +156,7 @@ def env(test_dir: str) -> Environment:
150156
return Environment.model_validate_json(f.read())
151157

152158

153-
@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool()])
159+
@pytest.mark.parametrize("t8n", [ExecutionSpecsTransitionTool], indirect=True)
154160
@pytest.mark.parametrize("test_dir", os.listdir(path=FIXTURES_ROOT))
155161
def test_evm_t8n(
156162
t8n: TransitionTool,

src/ethereum_clis/types.py

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,8 @@
44

55
from pydantic import Field
66

7-
from ethereum_test_base_types import Address, Bloom, Bytes, CamelModel, Hash, HexNumber
8-
from ethereum_test_types import Alloc, Environment, Transaction
9-
10-
11-
class TransactionLog(CamelModel):
12-
"""Transaction log."""
13-
14-
address: Address
15-
topics: List[Hash]
16-
data: Bytes
17-
block_number: HexNumber
18-
transaction_hash: Hash
19-
transaction_index: HexNumber
20-
block_hash: Hash
21-
log_index: HexNumber
22-
removed: bool
23-
24-
25-
class SetCodeDelegation(CamelModel):
26-
"""Set code delegation."""
27-
28-
from_address: Address = Field(..., alias="from")
29-
nonce: HexNumber
30-
target: Address
31-
32-
33-
class TransactionReceipt(CamelModel):
34-
"""Transaction receipt."""
35-
36-
transaction_hash: Hash
37-
gas_used: HexNumber
38-
root: Bytes | None = None
39-
status: HexNumber | None = None
40-
cumulative_gas_used: HexNumber | None = None
41-
logs_bloom: Bloom | None = None
42-
logs: List[TransactionLog] | None = None
43-
contract_address: Address | None = None
44-
effective_gas_price: HexNumber | None = None
45-
block_hash: Hash | None = None
46-
transaction_index: HexNumber | None = None
47-
blob_gas_used: HexNumber | None = None
48-
blob_gas_price: HexNumber | None = None
49-
delegations: List[SetCodeDelegation] | None = None
7+
from ethereum_test_base_types import Bloom, Bytes, CamelModel, Hash, HexNumber
8+
from ethereum_test_types import Alloc, Environment, Transaction, TransactionReceipt
509

5110

5211
class RejectedTransaction(CamelModel):

src/ethereum_test_forks/forks/forks.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts:
123123
G_KECCAK_256_WORD=6,
124124
G_COPY=3,
125125
G_BLOCKHASH=20,
126-
G_AUTHORIZATION=25_000,
126+
G_AUTHORIZATION=0,
127+
R_AUTHORIZATION_EXISTING_AUTHORITY=0,
127128
)
128129

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

10571060
@classmethod

src/ethereum_test_forks/gas_costs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,5 @@ class GasCosts:
5858
G_BLOCKHASH: int
5959

6060
G_AUTHORIZATION: int
61+
62+
R_AUTHORIZATION_EXISTING_AUTHORITY: int

src/ethereum_test_specs/blockchain.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,9 @@ def generate_block_data(
402402

403403
try:
404404
rejected_txs = verify_transactions(
405-
t8n.exception_mapper, txs, transition_tool_output.result
405+
txs=txs,
406+
exception_mapper=t8n.exception_mapper,
407+
result=transition_tool_output.result,
406408
)
407409
verify_result(transition_tool_output.result, env)
408410
except Exception as e:

src/ethereum_test_specs/helpers.py

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
"""Helper functions."""
22

33
from dataclasses import dataclass
4-
from typing import Dict, List
4+
from typing import Any, Dict, List
55

66
import pytest
77

88
from ethereum_clis import Result
99
from ethereum_test_exceptions import ExceptionBase, ExceptionMapper, UndefinedException
10-
from ethereum_test_types import Transaction
10+
from ethereum_test_types import Transaction, TransactionReceipt
1111

1212

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

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

2424

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

2828
def __init__(self, index: int, nonce: int, message: str, exception: ExceptionBase):
2929
"""Initialize the exception."""
@@ -70,12 +70,32 @@ def __init__(
7070
super().__init__(message)
7171

7272

73+
class TransactionReceiptMismatchError(Exception):
74+
"""Exception used when the actual transaction receipt differs from the expected one."""
75+
76+
def __init__(
77+
self,
78+
index: int,
79+
field_name: str,
80+
expected_value: Any,
81+
actual_value: Any,
82+
):
83+
"""Initialize the exception."""
84+
message = (
85+
f"\nTransactionReceiptMismatch (pos={index}):"
86+
f"\n What: {field_name} mismatch!"
87+
f"\n Want: {expected_value}"
88+
f"\n Got: {actual_value}"
89+
)
90+
super().__init__(message)
91+
92+
7393
@dataclass
7494
class TransactionExceptionInfo:
7595
"""Info to print transaction exception error messages."""
7696

7797
t8n_error_message: str | None
78-
transaction_ind: int
98+
transaction_index: int
7999
tx: Transaction
80100

81101

@@ -89,12 +109,10 @@ def verify_transaction_exception(
89109

90110
# info.tx.error is expected error code defined in .py test
91111
if expected_error and not info.t8n_error_message:
92-
raise TransactionExpectedToFailSucceedError(
93-
index=info.transaction_ind, nonce=info.tx.nonce
94-
)
112+
raise TransactionUnexpectedSuccessError(index=info.transaction_index, nonce=info.tx.nonce)
95113
elif not expected_error and info.t8n_error_message:
96114
raise TransactionUnexpectedFailError(
97-
index=info.transaction_ind,
115+
index=info.transaction_index,
98116
nonce=info.tx.nonce,
99117
message=info.t8n_error_message,
100118
exception=exception_mapper.message_to_exception(info.t8n_error_message),
@@ -122,7 +140,7 @@ def verify_transaction_exception(
122140

123141
if expected_error_msg is None or expected_error_msg not in info.t8n_error_message:
124142
raise TransactionExceptionMismatchError(
125-
index=info.transaction_ind,
143+
index=info.transaction_index,
126144
nonce=info.tx.nonce,
127145
expected_exception=expected_exception,
128146
expected_message=expected_error_msg,
@@ -132,21 +150,59 @@ def verify_transaction_exception(
132150
)
133151

134152

153+
def verify_transaction_receipt(
154+
transaction_index: int,
155+
expected_receipt: TransactionReceipt | None,
156+
actual_receipt: TransactionReceipt | None,
157+
):
158+
"""
159+
Verify the actual receipt against the expected one.
160+
161+
If the expected receipt is None, validation is skipped.
162+
163+
Only verifies non-None values in the expected receipt if any.
164+
"""
165+
if expected_receipt is None:
166+
return
167+
assert actual_receipt is not None
168+
if (
169+
expected_receipt.gas_used is not None
170+
and actual_receipt.gas_used != expected_receipt.gas_used
171+
):
172+
raise TransactionReceiptMismatchError(
173+
index=transaction_index,
174+
field_name="gas_used",
175+
expected_value=expected_receipt.gas_used,
176+
actual_value=actual_receipt.gas_used,
177+
)
178+
# TODO: Add more fields as needed
179+
180+
135181
def verify_transactions(
136-
exception_mapper: ExceptionMapper, txs: List[Transaction], result: Result
182+
*,
183+
txs: List[Transaction],
184+
exception_mapper: ExceptionMapper,
185+
result: Result,
137186
) -> List[int]:
138187
"""
139-
Verify rejected transactions (if any) against the expected outcome.
140-
Raises exception on unexpected rejections or unexpected successful txs.
188+
Verify accepted and rejected (if any) transactions against the expected outcome.
189+
Raises exception on unexpected rejections, unexpected successful txs, or successful txs with
190+
unexpected receipt values.
141191
"""
142192
rejected_txs: Dict[int, str] = {
143193
rejected_tx.index: rejected_tx.error for rejected_tx in result.rejected_transactions
144194
}
145195

196+
receipt_index = 0
146197
for i, tx in enumerate(txs):
147198
error_message = rejected_txs[i] if i in rejected_txs else None
148-
info = TransactionExceptionInfo(t8n_error_message=error_message, transaction_ind=i, tx=tx)
199+
info = TransactionExceptionInfo(
200+
t8n_error_message=error_message, transaction_index=i, tx=tx
201+
)
149202
verify_transaction_exception(exception_mapper=exception_mapper, info=info)
203+
if error_message is None:
204+
verify_transaction_receipt(i, tx.expected_receipt, result.receipts[receipt_index])
205+
receipt_index += 1
150206

151207
return list(rejected_txs.keys())
152208

src/ethereum_test_specs/state.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ def make_state_test_fixture(
148148
raise e
149149

150150
try:
151-
verify_transactions(t8n.exception_mapper, [tx], transition_tool_output.result)
151+
verify_transactions(
152+
txs=[tx],
153+
exception_mapper=t8n.exception_mapper,
154+
result=transition_tool_output.result,
155+
)
152156
except Exception as e:
153157
print_traces(t8n.get_traces())
154158
pprint(transition_tool_output.result)

0 commit comments

Comments
 (0)