Skip to content

Commit 5f44e05

Browse files
committed
feat(specs): Check transaction receipts against expectation
docs: Add snipet about receipts changelog
1 parent d2de2d9 commit 5f44e05

File tree

5 files changed

+83
-14
lines changed

5 files changed

+83
-14
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Test fixtures for use by clients are available for each release on the [Github r
7373
- ✨ 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)).
7474
- 🐞 fix(consume): allow absolute paths with `--evm-bin` ([#1052](https://github.com/ethereum/execution-spec-tests/pull/1052)).
7575
- ✨ Disable EIP-7742 framework changes for Prague ([#1023](https://github.com/ethereum/execution-spec-tests/pull/1023)).
76+
- ✨ Allow verification of the transaction receipt on executed test transactions ([#1068](https://github.com/ethereum/execution-spec-tests/pull/1068)).
7677

7778
### 🔧 EVM Tools
7879

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_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: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
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

1313
class TransactionExpectedToFailSucceedError(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

@@ -90,11 +110,11 @@ def verify_transaction_exception(
90110
# info.tx.error is expected error code defined in .py test
91111
if expected_error and not info.t8n_error_message:
92112
raise TransactionExpectedToFailSucceedError(
93-
index=info.transaction_ind, nonce=info.tx.nonce
113+
index=info.transaction_index, nonce=info.tx.nonce
94114
)
95115
elif not expected_error and info.t8n_error_message:
96116
raise TransactionUnexpectedFailError(
97-
index=info.transaction_ind,
117+
index=info.transaction_index,
98118
nonce=info.tx.nonce,
99119
message=info.t8n_error_message,
100120
exception=exception_mapper.message_to_exception(info.t8n_error_message),
@@ -122,7 +142,7 @@ def verify_transaction_exception(
122142

123143
if expected_error_msg is None or expected_error_msg not in info.t8n_error_message:
124144
raise TransactionExceptionMismatchError(
125-
index=info.transaction_ind,
145+
index=info.transaction_index,
126146
nonce=info.tx.nonce,
127147
expected_exception=expected_exception,
128148
expected_message=expected_error_msg,
@@ -132,21 +152,60 @@ def verify_transaction_exception(
132152
)
133153

134154

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

198+
receipt_index = 0
146199
for i, tx in enumerate(txs):
147200
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)
149-
verify_transaction_exception(exception_mapper=exception_mapper, info=info)
201+
if error_message is None:
202+
verify_transaction_receipt(i, tx.expected_receipt, result.receipts[receipt_index])
203+
receipt_index += 1
204+
else:
205+
info = TransactionExceptionInfo(
206+
t8n_error_message=error_message, transaction_index=i, tx=tx
207+
)
208+
verify_transaction_exception(exception_mapper=exception_mapper, info=info)
150209

151210
return list(rejected_txs.keys())
152211

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)