diff --git a/src/ethereum_test_execution/transaction_post.py b/src/ethereum_test_execution/transaction_post.py index 5c97eb045bc..a94af44db6c 100644 --- a/src/ethereum_test_execution/transaction_post.py +++ b/src/ethereum_test_execution/transaction_post.py @@ -20,6 +20,9 @@ class TransactionPost(BaseExecute): blocks: List[List[Transaction]] post: Alloc + # Gas validation fields for benchmark tests + expected_benchmark_gas_used: int | None = None # Expected total gas to be consumed + skip_gas_used_validation: bool = False # Skip gas validation even if expected is set format_name: ClassVar[str] = "transaction_post_test" description: ClassVar[str] = ( @@ -33,6 +36,10 @@ def execute( assert not any(tx.ty == 3 for block in self.blocks for tx in block), ( "Transaction type 3 is not supported in execute mode." ) + + # Track transaction hashes for gas validation (benchmarking) + all_tx_hashes = [] + for block in self.blocks: signed_txs = [] for tx_index, tx in enumerate(block): @@ -51,11 +58,31 @@ def execute( for transaction in signed_txs: if transaction.error is None: eth_rpc.send_wait_transaction(transaction) + all_tx_hashes.append(transaction.hash) else: with pytest.raises(SendTransactionExceptionError): eth_rpc.send_transaction(transaction) else: eth_rpc.send_wait_transactions(signed_txs) + all_tx_hashes.extend([tx.hash for tx in signed_txs]) + + # Perform gas validation if required for benchmarking + # Ensures benchmark tests consume exactly the expected gas + if not self.skip_gas_used_validation and self.expected_benchmark_gas_used is not None: + total_gas_used = 0 + # Fetch transaction receipts to get actual gas used + for tx_hash in all_tx_hashes: + receipt = eth_rpc.get_transaction_receipt(tx_hash) + assert receipt is not None, f"Failed to get receipt for transaction {tx_hash}" + gas_used = int(receipt["gasUsed"], 16) + total_gas_used += gas_used + + # Verify that the total gas consumed matches expectations + assert total_gas_used == self.expected_benchmark_gas_used, ( + f"Total gas used ({total_gas_used}) does not match " + f"expected benchmark gas ({self.expected_benchmark_gas_used}), " + f"difference: {total_gas_used - self.expected_benchmark_gas_used}" + ) for address, account in self.post.root.items(): balance = eth_rpc.get_balance(address) diff --git a/src/ethereum_test_rpc/rpc.py b/src/ethereum_test_rpc/rpc.py index e654340c28f..9ea2d279eb5 100644 --- a/src/ethereum_test_rpc/rpc.py +++ b/src/ethereum_test_rpc/rpc.py @@ -292,6 +292,18 @@ def get_transaction_by_hash(self, transaction_hash: Hash) -> TransactionByHashRe pprint(e.errors()) raise e + def get_transaction_receipt(self, transaction_hash: Hash) -> dict | None: + """ + `eth_getTransactionReceipt`: Returns transaction receipt. + + Used to get the actual gas used by a transaction for gas validation + in benchmark tests. + """ + response = self.post_request( + method="getTransactionReceipt", params=[f"{transaction_hash}"] + ) + return response + def get_storage_at( self, address: Address, position: Hash, block_number: BlockNumberType = "latest" ) -> Hash: diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 6669ab2f274..3633e7ff286 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -899,9 +899,12 @@ def execute( blocks: List[List[Transaction]] = [] for block in self.blocks: blocks += [block.txs] + # Pass gas validation params for benchmark tests return TransactionPost( blocks=blocks, post=self.post, + expected_benchmark_gas_used=self.expected_benchmark_gas_used, + skip_gas_used_validation=self.skip_gas_used_validation, ) raise Exception(f"Unsupported execute format: {execute_format}") diff --git a/src/ethereum_test_specs/state.py b/src/ethereum_test_specs/state.py index 24b0cb08169..917ed8a28ee 100644 --- a/src/ethereum_test_specs/state.py +++ b/src/ethereum_test_specs/state.py @@ -444,9 +444,12 @@ def execute( ) -> BaseExecute: """Generate the list of test fixtures.""" if execute_format == TransactionPost: + # Pass gas validation params for benchmark tests return TransactionPost( blocks=[[self.tx]], post=self.post, + expected_benchmark_gas_used=self.expected_benchmark_gas_used, + skip_gas_used_validation=self.skip_gas_used_validation, ) raise Exception(f"Unsupported execute format: {execute_format}") diff --git a/src/pytest_plugins/execute/execute.py b/src/pytest_plugins/execute/execute.py index 42676a62113..62d5aa1cc6c 100644 --- a/src/pytest_plugins/execute/execute.py +++ b/src/pytest_plugins/execute/execute.py @@ -342,6 +342,11 @@ def __init__(self, *args, **kwargs): kwargs["pre"] = pre elif kwargs["pre"] != pre: raise ValueError("The pre-alloc object was modified by the test.") + # Set default for expected_benchmark_gas_used + if "expected_benchmark_gas_used" not in kwargs: + kwargs["expected_benchmark_gas_used"] = request.getfixturevalue( + "gas_benchmark_value" + ) kwargs |= { p: request.getfixturevalue(p) for p in cls_fixture_parameters