22Base test class and helper functions for Ethereum state and blockchain tests.
33"""
44
5- from abc import abstractmethod
5+ from abc import ABC , abstractmethod
6+ from dataclasses import dataclass
67from enum import StrEnum , unique
78from functools import reduce
89from typing import (
3738from execution_testing .forks import Fork , TransitionFork
3839from execution_testing .forks .base_fork import BaseFork
3940from execution_testing .test_types import Environment , Withdrawal
40- from execution_testing .test_types .receipt_types import (
41- TransactionReceipt ,
42- )
4341
4442
4543class HashMismatchExceptionError (Exception ):
@@ -99,6 +97,130 @@ class FillResult(BaseModel):
9997 metadata : Dict [str , Any ] = Field (default_factory = dict )
10098
10199
100+ @dataclass
101+ class BlockVerification (ABC ):
102+ """
103+ Base class for block-level verification rules.
104+
105+ Each rule inspects the transition tool result for a
106+ single block and raises on failure. Add new rules by
107+ subclassing and implementing ``verify``.
108+ """
109+
110+ @abstractmethod
111+ def verify (
112+ self ,
113+ * ,
114+ result : Result ,
115+ block_number : int ,
116+ ) -> None :
117+ """Verify the block result, raise on failure."""
118+ ...
119+
120+
121+ @dataclass
122+ class NoTraceErrors (BlockVerification ):
123+ """
124+ Verify that no trace line contains an error.
125+
126+ Catches silent subcall failures, out of gas,
127+ invalid jumps, and stack errors.
128+ """
129+
130+ def verify (
131+ self ,
132+ * ,
133+ result : Result ,
134+ block_number : int ,
135+ ) -> None :
136+ """Raise if any trace line has an error."""
137+ if result .traces is None :
138+ return
139+ for tx_idx , tx in enumerate (result .traces .root ):
140+ for step , line in enumerate (tx .traces ):
141+ if line .error is not None :
142+ raise Exception (
143+ f"Trace error in block "
144+ f"{ block_number } , "
145+ f"tx { tx_idx } , "
146+ f"step { step } "
147+ f"(pc={ line .pc } , "
148+ f"op={ line .op_name } , "
149+ f"depth={ line .depth } ): "
150+ f"{ line .error } "
151+ )
152+
153+
154+ @dataclass
155+ class ReceiptStatusExpected (BlockVerification ):
156+ """
157+ Verify all transaction receipts have the expected
158+ status. Default expects success (status=1).
159+
160+ Catches silent OOG failures that roll back state
161+ and invalidate benchmarks.
162+ """
163+
164+ status : int = 1
165+
166+ def verify (
167+ self ,
168+ * ,
169+ result : Result ,
170+ block_number : int ,
171+ ) -> None :
172+ """Raise if any receipt status mismatches."""
173+ for i , receipt in enumerate (result .receipts ):
174+ if receipt .status is not None and (
175+ int (receipt .status ) != self .status
176+ ):
177+ raise Exception (
178+ f"Transaction { i } in block "
179+ f"{ block_number } has receipt "
180+ f"status { int (receipt .status )} , "
181+ f"expected { self .status } ."
182+ )
183+
184+
185+ @dataclass
186+ class ColdSloadExpected (BlockVerification ):
187+ """
188+ Verify every SLOAD in the trace is a cold access.
189+
190+ Checks that SLOAD gas cost meets the minimum for
191+ cold storage access (default 2100). Useful for
192+ benchmarks measuring cold storage performance.
193+ """
194+
195+ min_gas_cost : int = 2100
196+
197+ def verify (
198+ self ,
199+ * ,
200+ result : Result ,
201+ block_number : int ,
202+ ) -> None :
203+ """Raise if any SLOAD has warm-access gas cost."""
204+ if result .traces is None :
205+ return
206+ for tx_idx , tx in enumerate (result .traces .root ):
207+ for step , line in enumerate (tx .traces ):
208+ if (
209+ line .op_name == "SLOAD"
210+ and line .gas_cost is not None
211+ and int (line .gas_cost ) < self .min_gas_cost
212+ ):
213+ raise Exception (
214+ f"Warm SLOAD in block "
215+ f"{ block_number } , "
216+ f"tx { tx_idx } , step { step } "
217+ f"(pc={ line .pc } , "
218+ f"gas_cost={ line .gas_cost } , "
219+ f"expected >= "
220+ f"{ self .min_gas_cost } )"
221+ )
222+
223+
102224class BaseTest (BaseModel ):
103225 """
104226 Represents a base Ethereum test which must return a single test fixture.
@@ -115,7 +237,7 @@ class BaseTest(BaseModel):
115237 gas_optimization_max_gas_limit : int | None = None
116238 expected_benchmark_gas_used : int | None = None
117239 skip_gas_used_validation : bool = False
118- expected_receipt_status : int | None = None
240+ verifications : List [ BlockVerification ] = Field ( default_factory = list )
119241 is_tx_gas_heavy_test : bool = False
120242 is_exception_test : bool = False
121243
@@ -295,32 +417,23 @@ def validate_benchmark_gas(
295417 f"{ gas_benchmark_value } "
296418 )
297419
298- def validate_receipt_status (
420+ def run_block_verifications (
299421 self ,
300422 * ,
301- receipts : List [ TransactionReceipt ] ,
423+ result : Result ,
302424 block_number : int ,
303425 ) -> None :
304426 """
305- Validate receipt status for every transaction in a block .
427+ Run all block verification rules .
306428
307- When expected_receipt_status is set, verify that all
308- receipts match. Catches silent OOG failures that roll
309- back state and invalidate benchmarks.
429+ Dispatch every rule in ``self.verifications``
430+ against the transition tool result for a block.
310431 """
311- if "expected_receipt_status" not in self .model_fields_set :
312- return
313- for i , receipt in enumerate (receipts ):
314- if receipt .status is not None and (
315- int (receipt .status ) != self .expected_receipt_status
316- ):
317- raise Exception (
318- f"Transaction { i } in block "
319- f"{ block_number } has receipt "
320- f"status { int (receipt .status )} , "
321- f"expected "
322- f"{ self .expected_receipt_status } ."
323- )
432+ for v in self .verifications :
433+ v .verify (
434+ result = result ,
435+ block_number = block_number ,
436+ )
324437
325438
326439TestSpec = Callable [[Fork ], Generator [BaseTest , None , None ]]
0 commit comments