diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index dd07fe97939..9eb286ae418 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/writing_tests/writing_a_new_test.md b/docs/writing_tests/writing_a_new_test.md index 9cb3f6123f1..eec3aa35aed 100644 --- a/docs/writing_tests/writing_a_new_test.md +++ b/docs/writing_tests/writing_a_new_test.md @@ -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. @@ -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` diff --git a/src/ethereum_clis/tests/test_execution_specs.py b/src/ethereum_clis/tests/test_execution_specs.py index 966355e5fbf..b94ff892a8a 100644 --- a/src/ethereum_clis/tests/test_execution_specs.py +++ b/src/ethereum_clis/tests/test_execution_specs.py @@ -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", @@ -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, diff --git a/src/ethereum_clis/types.py b/src/ethereum_clis/types.py index 46e62322f89..75325fd4d9a 100644 --- a/src/ethereum_clis/types.py +++ b/src/ethereum_clis/types.py @@ -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): diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 7a995ce3f3d..0c12c86273f 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -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 @@ -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 diff --git a/src/ethereum_test_forks/gas_costs.py b/src/ethereum_test_forks/gas_costs.py index 290ddd78376..b3c7429a721 100644 --- a/src/ethereum_test_forks/gas_costs.py +++ b/src/ethereum_test_forks/gas_costs.py @@ -58,3 +58,5 @@ class GasCosts: G_BLOCKHASH: int G_AUTHORIZATION: int + + R_AUTHORIZATION_EXISTING_AUTHORITY: int diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 9cdb9ed08c0..131794da04d 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -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: diff --git a/src/ethereum_test_specs/helpers.py b/src/ethereum_test_specs/helpers.py index 4a181949467..d0f91c6a222 100644 --- a/src/ethereum_test_specs/helpers.py +++ b/src/ethereum_test_specs/helpers.py @@ -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.""" @@ -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.""" @@ -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 @@ -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), @@ -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, @@ -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()) diff --git a/src/ethereum_test_specs/state.py b/src/ethereum_test_specs/state.py index 6a374f5e6a2..44e691898e3 100644 --- a/src/ethereum_test_specs/state.py +++ b/src/ethereum_test_specs/state.py @@ -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) diff --git a/src/ethereum_test_specs/tests/test_expect.py b/src/ethereum_test_specs/tests/test_expect.py index 6778fd6a502..a18a5f495d4 100644 --- a/src/ethereum_test_specs/tests/test_expect.py +++ b/src/ethereum_test_specs/tests/test_expect.py @@ -6,10 +6,17 @@ from ethereum_clis import ExecutionSpecsTransitionTool from ethereum_test_base_types import Account, Address, TestAddress, TestPrivateKey -from ethereum_test_fixtures import StateFixture +from ethereum_test_exceptions import TransactionException +from ethereum_test_fixtures import BlockchainFixture, FixtureFormat, StateFixture from ethereum_test_forks import Fork, get_deployed_forks -from ethereum_test_types import Alloc, Environment, Storage, Transaction +from ethereum_test_types import Alloc, Environment, Storage, Transaction, TransactionReceipt +from ..helpers import ( + TransactionExceptionMismatchError, + TransactionReceiptMismatchError, + TransactionUnexpectedFailError, + TransactionUnexpectedSuccessError, +) from ..state import StateTest ADDRESS_UNDER_TEST = Address(0x01) @@ -24,13 +31,19 @@ def tx() -> Transaction: @pytest.fixture def pre(request) -> Alloc: """Fixture set from the test's indirectly parametrized `pre` parameter.""" - return Alloc(request.param | {TestAddress: Account(balance=(10**18))}) + extra_accounts = {} + if hasattr(request, "param"): + extra_accounts = request.param + return Alloc(extra_accounts | {TestAddress: Account(balance=(10**18))}) @pytest.fixture def post(request) -> Alloc: # noqa: D103 """Fixture set from the test's indirectly parametrized `post` parameter.""" - return Alloc(request.param) + extra_accounts = {} + if hasattr(request, "param"): + extra_accounts = request.param + return Alloc(extra_accounts) @pytest.fixture @@ -249,3 +262,90 @@ def test_post_account_mismatch(state_test, t8n, fork, exception_type: Type[Excep return with pytest.raises(exception_type) as _: state_test.generate(request=None, t8n=t8n, fork=fork, fixture_format=StateFixture) + + +# Transaction result mismatch tests +@pytest.mark.run_in_serial +@pytest.mark.parametrize( + "tx,exception_type", + [ + pytest.param( + Transaction( + secret_key=TestPrivateKey, + expected_receipt=TransactionReceipt(gas_used=21_000), + ), + TransactionExceptionMismatchError, + id="TransactionExceptionMismatchError", + marks=pytest.mark.xfail( + reason="Exceptions need to be better described in the t8n tool." + ), + ), + pytest.param( + Transaction( + secret_key=TestPrivateKey, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + expected_receipt=TransactionReceipt(gas_used=21_000), + ), + TransactionUnexpectedSuccessError, + id="TransactionUnexpectedSuccessError", + ), + pytest.param( + Transaction( + secret_key=TestPrivateKey, + gas_limit=20_999, + expected_receipt=TransactionReceipt(gas_used=21_000), + ), + TransactionUnexpectedFailError, + id="TransactionUnexpectedFailError", + ), + pytest.param( + Transaction( + secret_key=TestPrivateKey, + expected_receipt=TransactionReceipt(gas_used=21_001), + ), + TransactionReceiptMismatchError, + id="TransactionReceiptMismatchError", + ), + pytest.param( + Transaction( + secret_key=TestPrivateKey, + gas_limit=20_999, + expected_receipt=TransactionReceipt(gas_used=21_001), + ), + TransactionUnexpectedFailError, + id="TransactionUnexpectedFailError+TransactionReceiptMismatchError", + ), + pytest.param( + Transaction( + secret_key=TestPrivateKey, + error=TransactionException.INTRINSIC_GAS_TOO_LOW, + expected_receipt=TransactionReceipt(gas_used=21_001), + ), + TransactionUnexpectedSuccessError, + id="TransactionUnexpectedSuccessError+TransactionReceiptMismatchError", + ), + ], +) +@pytest.mark.parametrize( + "fixture_format", + [ + StateFixture, + BlockchainFixture, + ], +) +def test_transaction_expectation( + state_test, + t8n, + fork, + exception_type: Type[Exception] | None, + fixture_format: FixtureFormat, +): + """ + Test a transaction that has an unexpected error, expected error, or expected a specific + value in its receipt. + """ + if exception_type is None: + state_test.generate(request=None, t8n=t8n, fork=fork, fixture_format=fixture_format) + else: + with pytest.raises(exception_type) as _: + state_test.generate(request=None, t8n=t8n, fork=fork, fixture_format=fixture_format) diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 7383377c192..1543208ccbc 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -51,6 +51,7 @@ Storage, TestParameterGroup, Transaction, + TransactionReceipt, Withdrawal, WithdrawalRequest, add_kzg_version, @@ -144,6 +145,7 @@ "TestPrivateKey2", "Transaction", "TransactionException", + "TransactionReceipt", "TransactionTest", "TransactionTestFiller", "Withdrawal", diff --git a/src/ethereum_test_types/__init__.py b/src/ethereum_test_types/__init__.py index 55d11d00a01..a146cf302dc 100644 --- a/src/ethereum_test_types/__init__.py +++ b/src/ethereum_test_types/__init__.py @@ -22,6 +22,7 @@ Storage, Transaction, TransactionDefaults, + TransactionReceipt, Withdrawal, WithdrawalRequest, keccak256, @@ -49,6 +50,7 @@ "TestPrivateKey2", "Transaction", "TransactionDefaults", + "TransactionReceipt", "Withdrawal", "WithdrawalRequest", "ZeroPaddedHexNumber", diff --git a/src/ethereum_test_types/helpers.py b/src/ethereum_test_types/helpers.py index f4c84b48d86..a4196641a64 100644 --- a/src/ethereum_test_types/helpers.py +++ b/src/ethereum_test_types/helpers.py @@ -78,7 +78,7 @@ def compute_eofcreate_address( def add_kzg_version( b_hashes: List[bytes | SupportsBytes | int | str], kzg_version: int -) -> List[bytes]: +) -> List[Hash]: """Add Kzg Version to each blob hash.""" kzg_version_hex = bytes([kzg_version]) kzg_versioned_hashes = [] @@ -86,11 +86,11 @@ def add_kzg_version( for b_hash in b_hashes: b_hash = bytes(Hash(b_hash)) if isinstance(b_hash, int) or isinstance(b_hash, str): - kzg_versioned_hashes.append(kzg_version_hex + b_hash[1:]) + kzg_versioned_hashes.append(Hash(kzg_version_hex + b_hash[1:])) elif isinstance(b_hash, bytes) or isinstance(b_hash, SupportsBytes): if isinstance(b_hash, SupportsBytes): b_hash = bytes(b_hash) - kzg_versioned_hashes.append(kzg_version_hex + b_hash[1:]) + kzg_versioned_hashes.append(Hash(kzg_version_hex + b_hash[1:])) else: raise TypeError("Blob hash must be either an integer, string or bytes") return kzg_versioned_hashes diff --git a/src/ethereum_test_types/types.py b/src/ethereum_test_types/types.py index 6dc36821748..e08e4c55452 100644 --- a/src/ethereum_test_types/types.py +++ b/src/ethereum_test_types/types.py @@ -27,6 +27,7 @@ AccessList, Account, Address, + Bloom, BLSPublicKey, BLSSignature, Bytes, @@ -558,6 +559,47 @@ def sign(self, private_key: Hash) -> None: self.s = HexNumber(signature[2]) +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 ReceiptDelegation(CamelModel): + """Transaction receipt set-code delegation.""" + + from_address: Address = Field(..., alias="from") + nonce: HexNumber + target: Address + + +class TransactionReceipt(CamelModel): + """Transaction receipt.""" + + transaction_hash: Hash | None = None + gas_used: HexNumber | None = None + 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[ReceiptDelegation] | None = None + + @dataclass class TransactionDefaults: """Default values for transactions.""" @@ -660,6 +702,8 @@ class Transaction(TransactionGeneric[HexNumber], TransactionTransitionToolConver protected: bool = Field(True, exclude=True) rlp_override: Bytes | None = Field(None, exclude=True) + expected_receipt: TransactionReceipt | None = Field(None, exclude=True) + wrapped_blob_transaction: bool = Field(False, exclude=True) blobs: Sequence[Bytes] | None = Field(None, exclude=True) blob_kzg_commitments: Sequence[Bytes] | None = Field(None, exclude=True) diff --git a/tests/cancun/eip4844_blobs/common.py b/tests/cancun/eip4844_blobs/common.py index 1f9a13b669a..78bbcc1ac60 100644 --- a/tests/cancun/eip4844_blobs/common.py +++ b/tests/cancun/eip4844_blobs/common.py @@ -5,6 +5,7 @@ from ethereum_test_tools import ( Address, + Hash, TestAddress, YulCompiler, add_kzg_version, @@ -263,7 +264,7 @@ class BlobhashScenario: """A utility class for generating blobhash calls.""" @staticmethod - def create_blob_hashes_list(length: int, max_blobs_per_block: int) -> list[list[bytes]]: + def create_blob_hashes_list(length: int, max_blobs_per_block: int) -> List[List[Hash]]: """ Create list of MAX_BLOBS_PER_BLOCK blob hashes using `random_blob_hashes`. diff --git a/tests/cancun/eip4844_blobs/test_blob_txs.py b/tests/cancun/eip4844_blobs/test_blob_txs.py index 14a080a4715..03c521271aa 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs.py @@ -101,7 +101,7 @@ def blobs_per_tx() -> List[int]: @pytest.fixture -def blob_hashes_per_tx(blobs_per_tx: List[int]) -> List[List[bytes]]: +def blob_hashes_per_tx(blobs_per_tx: List[int]) -> List[List[Hash]]: """ Produce the list of blob hashes that are sent during the test. diff --git a/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py b/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py index 4cc8570208e..577adb61ddc 100644 --- a/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py +++ b/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py @@ -42,7 +42,7 @@ def create_opcode_context(pre, tx, post): @pytest.fixture() def simple_blob_hashes( max_blobs_per_block: int, -) -> List[bytes]: +) -> List[Hash]: """Return a simple list of blob versioned hashes ranging from bytes32(1 to 4).""" return add_kzg_version( [(1 << x) for x in range(max_blobs_per_block)], diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index 9c571a0eb45..3cba5048ee4 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -50,6 +50,7 @@ StateTestFiller, Storage, Transaction, + TransactionReceipt, call_return_code, ) from ethereum_test_tools.vm.opcode import Opcodes as Op @@ -532,8 +533,7 @@ def test_tx_entry_point( - Using `gas_limit` with exact necessary gas, insufficient gas and extra gas. - Using correct and incorrect proofs """ - start_balance = 10**18 - sender = pre.fund_eoa(amount=start_balance) + sender = pre.fund_eoa() # Starting from EIP-7623, we need to use an access list to raise the intrinsic gas cost to be # above the floor data cost. @@ -557,7 +557,6 @@ def test_tx_entry_point( access_list=access_list, return_cost_deducted_prior_execution=True, ) - fee_per_gas = 7 tx = Transaction( sender=sender, @@ -565,13 +564,12 @@ def test_tx_entry_point( access_list=access_list, to=Address(Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS), gas_limit=call_gas + intrinsic_gas_cost, - gas_price=fee_per_gas, + expected_receipt=TransactionReceipt(gas_used=consumed_gas), ) post = { sender: Account( nonce=1, - balance=start_balance - (consumed_gas * fee_per_gas), ) } diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index 169339f005b..d20d08f2d8e 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -20,6 +20,7 @@ Environment, StateTestFiller, Transaction, + TransactionReceipt, ) from ethereum_test_tools import Opcodes as Op @@ -107,11 +108,6 @@ def call_exact_cost( ) -@pytest.fixture -def tx_max_fee_per_gas() -> int: # noqa: D103 - return 7 - - @pytest.fixture def block_gas_limit() -> int: # noqa: D103 return 100_000_000 @@ -139,8 +135,8 @@ def caller_address(pre: Alloc, callee_bytecode: bytes) -> Address: # noqa: D103 @pytest.fixture -def sender(pre: Alloc, tx_max_fee_per_gas: int, tx_gas_limit: int) -> Address: # noqa: D103 - return pre.fund_eoa(tx_max_fee_per_gas * tx_gas_limit) +def sender(pre: Alloc) -> Address: # noqa: D103 + return pre.fund_eoa() @pytest.fixture @@ -148,7 +144,6 @@ def tx( # noqa: D103 sender: Address, caller_address: Address, initial_memory: bytes, - tx_max_fee_per_gas: int, tx_gas_limit: int, tx_access_list: List[AccessList], ) -> Transaction: @@ -158,8 +153,7 @@ def tx( # noqa: D103 access_list=tx_access_list, data=initial_memory, gas_limit=tx_gas_limit, - max_fee_per_gas=tx_max_fee_per_gas, - max_priority_fee_per_gas=0, + expected_receipt=TransactionReceipt(gas_used=tx_gas_limit), ) diff --git a/tests/prague/eip7623_increase_calldata_cost/conftest.py b/tests/prague/eip7623_increase_calldata_cost/conftest.py index dde1bce6a52..dd1d25c5327 100644 --- a/tests/prague/eip7623_increase_calldata_cost/conftest.py +++ b/tests/prague/eip7623_increase_calldata_cost/conftest.py @@ -16,9 +16,11 @@ Hash, Transaction, TransactionException, + add_kzg_version, ) from ethereum_test_tools import Opcodes as Op +from ...cancun.eip4844_blobs.spec import Spec as EIP_4844_Spec from .helpers import DataTestType, find_floor_cost_threshold @@ -65,7 +67,7 @@ def access_list() -> List[AccessList] | None: @pytest.fixture -def authorization_existing_authority() -> bool: +def authorization_refund() -> bool: """Return whether the transaction has an existing authority in the authorization list.""" return False @@ -74,7 +76,7 @@ def authorization_existing_authority() -> bool: def authorization_list( request: pytest.FixtureRequest, pre: Alloc, - authorization_existing_authority: bool, + authorization_refund: bool, ) -> List[AuthorizationTuple] | None: """ Authorization-list for the transaction. @@ -88,17 +90,22 @@ def authorization_list( if request.param is None: return None return [ - AuthorizationTuple( - signer=pre.fund_eoa(1 if authorization_existing_authority else 0), address=address - ) + AuthorizationTuple(signer=pre.fund_eoa(1 if authorization_refund else 0), address=address) for address in request.param ] @pytest.fixture -def blob_versioned_hashes() -> Sequence[Hash] | None: +def blob_versioned_hashes(ty: int) -> Sequence[Hash] | None: """Versioned hashes for the transaction.""" - return None + return ( + add_kzg_version( + [Hash(1)], + EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, + ) + if ty == 3 + else None + ) @pytest.fixture @@ -224,34 +231,51 @@ def tx_gas_delta() -> int: @pytest.fixture -def tx_gas( +def tx_intrinsic_gas_cost( fork: Fork, tx_data: Bytes, access_list: List[AccessList] | None, authorization_list: List[AuthorizationTuple] | None, contract_creating_tx: bool, - tx_gas_delta: int, ) -> int: """ - Gas limit for the transaction. + Transaction intrinsic gas cost. The calculated value takes into account the normal intrinsic gas cost and the floor data gas cost. - - The gas delta is added to the intrinsic gas cost to generate different test scenarios. """ intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() - return ( - intrinsic_gas_cost_calculator( - calldata=tx_data, - contract_creation=contract_creating_tx, - access_list=access_list, - authorization_list_or_count=authorization_list, - ) - + tx_gas_delta + return intrinsic_gas_cost_calculator( + calldata=tx_data, + contract_creation=contract_creating_tx, + access_list=access_list, + authorization_list_or_count=authorization_list, ) +@pytest.fixture +def tx_floor_data_cost( + fork: Fork, + tx_data: Bytes, +) -> int: + """Floor data cost for the given transaction data.""" + fork_data_floor_cost_calculator = fork.transaction_data_floor_cost_calculator() + return fork_data_floor_cost_calculator(data=tx_data) + + +@pytest.fixture +def tx_gas_limit( + tx_intrinsic_gas_cost: int, + tx_gas_delta: int, +) -> int: + """ + Gas limit for the transaction. + + The gas delta is added to the intrinsic gas cost to generate different test scenarios. + """ + return tx_intrinsic_gas_cost + tx_gas_delta + + @pytest.fixture def tx_error(tx_gas_delta: int) -> TransactionException | None: """Transaction error, only expected if the gas delta is negative.""" @@ -268,7 +292,7 @@ def tx( access_list: List[AccessList] | None, authorization_list: List[AuthorizationTuple] | None, blob_versioned_hashes: Sequence[Hash] | None, - tx_gas: int, + tx_gas_limit: int, tx_error: TransactionException | None, ) -> Transaction: """Create the transaction used in each test.""" @@ -280,7 +304,7 @@ def tx( protected=protected, access_list=access_list, authorization_list=authorization_list, - gas_limit=tx_gas, + gas_limit=tx_gas_limit, blob_versioned_hashes=blob_versioned_hashes, error=tx_error, ) diff --git a/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py index c2544a48b13..c14fea638bd 100644 --- a/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py +++ b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py @@ -14,14 +14,12 @@ Alloc, AuthorizationTuple, Bytes, - Hash, StateTestFiller, Transaction, - add_kzg_version, + TransactionReceipt, ) from ethereum_test_tools import Opcodes as Op -from ...cancun.eip4844_blobs.spec import Spec as EIP_4844_Spec from .helpers import DataTestType from .spec import ref_spec_7623 @@ -39,7 +37,7 @@ def data_test_type() -> DataTestType: @pytest.fixture -def authorization_existing_authority() -> bool: +def authorization_refund() -> bool: """ Force the authority of the authorization tuple to be an existing authority in order to produce a refund. @@ -68,27 +66,31 @@ def to( """Return a contract that when executed results in refunds due to storage clearing.""" return pre.deploy_contract(Op.SSTORE(0, 0) + Op.STOP, storage={0: 1}) + @pytest.fixture + def refund(self, fork: Fork, ty: int) -> int: + """Return the refund gas of the transaction.""" + gas_costs = fork.gas_costs() + refund = gas_costs.R_STORAGE_CLEAR + if ty == 4: + refund += gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY + return refund + @pytest.mark.parametrize( - "ty,protected,blob_versioned_hashes,authorization_list", + "ty,protected,authorization_list", [ - pytest.param(0, False, None, None, id="type_0_unprotected"), - pytest.param(0, True, None, None, id="type_0_protected"), - pytest.param(1, True, None, None, id="type_1"), - pytest.param(2, True, None, None, id="type_2"), + pytest.param(0, False, None, id="type_0_unprotected"), + pytest.param(0, True, None, id="type_0_protected"), + pytest.param(1, True, None, id="type_1"), + pytest.param(2, True, None, id="type_2"), pytest.param( 3, True, - add_kzg_version( - [Hash(1)], - EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, - ), None, id="type_3", ), pytest.param( 4, True, - None, [Address(1)], id="type_4_with_authorization_refund", ), @@ -109,6 +111,8 @@ def test_gas_refunds_from_data_floor( state_test: StateTestFiller, pre: Alloc, tx: Transaction, + tx_floor_data_cost: int, + refund: int, ) -> None: """ Test gas refunds deducted from the data floor. @@ -116,6 +120,7 @@ def test_gas_refunds_from_data_floor( I.e. the used gas by the intrinsic gas cost plus the execution cost is less than the data floor, hence data floor is used, and then the gas refunds are applied to the data floor. """ + tx.expected_receipt = TransactionReceipt(gas_used=tx_floor_data_cost - refund) state_test( pre=pre, post={ @@ -143,27 +148,36 @@ def to( """Return a contract that consumes all gas when executed by calling an invalid opcode.""" return pre.deploy_contract(Op.INVALID) + @pytest.fixture + def refund( + self, + fork: Fork, + ty: int, + authorization_refund: bool, + ) -> int: + """Return the refund gas of the transaction.""" + gas_costs = fork.gas_costs() + refund = 0 + if ty == 4 and authorization_refund: + refund += gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY + return refund + @pytest.mark.parametrize( - "ty,protected,blob_versioned_hashes,authorization_list", + "ty,protected,authorization_list", [ - pytest.param(0, False, None, None, id="type_0_unprotected"), - pytest.param(0, True, None, None, id="type_0_protected"), - pytest.param(1, True, None, None, id="type_1"), - pytest.param(2, True, None, None, id="type_2"), + pytest.param(0, False, None, id="type_0_unprotected"), + pytest.param(0, True, None, id="type_0_protected"), + pytest.param(1, True, None, id="type_1"), + pytest.param(2, True, None, id="type_2"), pytest.param( 3, True, - add_kzg_version( - [Hash(1)], - EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, - ), None, id="type_3", ), pytest.param( 4, True, - None, [Address(1)], id="type_4_with_authorization_refund", ), @@ -184,8 +198,10 @@ def test_full_gas_consumption( state_test: StateTestFiller, pre: Alloc, tx: Transaction, + refund: int, ) -> None: """Test executing a transaction that fully consumes its execution gas allocation.""" + tx.expected_receipt = TransactionReceipt(gas_used=tx.gas_limit - refund) state_test( pre=pre, post={}, @@ -201,6 +217,20 @@ def contract_creating_tx(self) -> bool: """Use a constant in order to avoid circular fixture dependencies.""" return False + @pytest.fixture + def refund( + self, + fork: Fork, + ty: int, + authorization_refund: bool, + ) -> int: + """Return the refund gas of the transaction.""" + gas_costs = fork.gas_costs() + refund = 0 + if ty == 4 and authorization_refund: + refund += gas_costs.R_AUTHORIZATION_EXISTING_AUTHORITY + return refund + @pytest.fixture def to( self, @@ -209,19 +239,14 @@ def to( tx_data: Bytes, access_list: List[AccessList] | None, authorization_list: List[AuthorizationTuple] | None, + tx_floor_data_cost: int, ) -> Address | None: """ Return a contract that consumes almost all the gas before reaching the floor data cost. """ intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() - data_floor = intrinsic_gas_cost_calculator( - calldata=tx_data, - contract_creation=False, - access_list=access_list, - authorization_list_or_count=authorization_list, - ) - execution_gas = data_floor - intrinsic_gas_cost_calculator( + execution_gas = tx_floor_data_cost - intrinsic_gas_cost_calculator( calldata=tx_data, contract_creation=False, access_list=access_list, @@ -233,19 +258,15 @@ def to( return pre.deploy_contract((Op.JUMPDEST * (execution_gas - 1)) + Op.STOP) @pytest.mark.parametrize( - "ty,protected,blob_versioned_hashes,authorization_list,authorization_existing_authority", + "ty,protected,authorization_list,authorization_refund", [ - pytest.param(0, False, None, None, False, id="type_0_unprotected"), - pytest.param(0, True, None, None, False, id="type_0_protected"), - pytest.param(1, True, None, None, False, id="type_1"), - pytest.param(2, True, None, None, False, id="type_2"), + pytest.param(0, False, None, False, id="type_0_unprotected"), + pytest.param(0, True, None, False, id="type_0_protected"), + pytest.param(1, True, None, False, id="type_1"), + pytest.param(2, True, None, False, id="type_2"), pytest.param( 3, True, - add_kzg_version( - [Hash(1)], - EIP_4844_Spec.BLOB_COMMITMENT_VERSION_KZG, - ), None, False, id="type_3", @@ -253,7 +274,6 @@ def to( pytest.param( 4, True, - None, [Address(1)], False, id="type_4", @@ -261,7 +281,6 @@ def to( pytest.param( 4, True, - None, [Address(1)], True, id="type_4_with_authorization_refund", @@ -282,8 +301,11 @@ def test_gas_consumption_below_data_floor( state_test: StateTestFiller, pre: Alloc, tx: Transaction, + tx_floor_data_cost: int, + refund: int, ) -> None: """Test executing a transaction that almost consumes the floor data cost.""" + tx.expected_receipt = TransactionReceipt(gas_used=tx_floor_data_cost - refund) state_test( pre=pre, post={}, diff --git a/tests/prague/eip7702_set_code_tx/test_gas.py b/tests/prague/eip7702_set_code_tx/test_gas.py index 98b611209de..793eb9e1101 100644 --- a/tests/prague/eip7702_set_code_tx/test_gas.py +++ b/tests/prague/eip7702_set_code_tx/test_gas.py @@ -26,6 +26,7 @@ Storage, Transaction, TransactionException, + TransactionReceipt, extend_with_defaults, ) from ethereum_test_tools import Opcodes as Op @@ -688,12 +689,13 @@ def test_gas_cost( # We calculate the exact gas required to execute the test code. # We add SSTORE opcodes in order to make sure that the refund is less than one fifth (EIP-3529) # of the total gas used, so we can see the full discount being reflected in most of the tests. - gas_opcode_cost = 2 + gas_costs = fork.gas_costs() + gas_opcode_cost = gas_costs.G_BASE sstore_opcode_count = 10 push_opcode_count = (2 * (sstore_opcode_count)) - 1 - push_opcode_cost = 3 * push_opcode_count - sstore_opcode_cost = 20_000 * sstore_opcode_count - cold_storage_cost = 2_100 * sstore_opcode_count + push_opcode_cost = gas_costs.G_VERY_LOW * push_opcode_count + sstore_opcode_cost = gas_costs.G_STORAGE_SET * sstore_opcode_count + cold_storage_cost = gas_costs.G_COLD_SLOAD * sstore_opcode_count execution_gas = gas_opcode_cost + push_opcode_cost + sstore_opcode_cost + cold_storage_cost @@ -712,8 +714,6 @@ def test_gas_cost( test_code_address = pre.deploy_contract(test_code) tx_gas_limit = intrinsic_gas + execution_gas - tx_max_fee_per_gas = 1_000_000_000 - tx_exact_cost = tx_gas_limit * tx_max_fee_per_gas # EIP-3529 max_discount = tx_gas_limit // 5 @@ -722,20 +722,20 @@ def test_gas_cost( # Only one test hits this condition, but it's ok to also test this case. discount_gas = max_discount - discount_cost = discount_gas * tx_max_fee_per_gas + gas_used = tx_gas_limit - discount_gas sender_account = pre[sender] assert sender_account is not None tx = Transaction( gas_limit=tx_gas_limit, - max_fee_per_gas=tx_max_fee_per_gas, to=test_code_address, value=0, data=data, authorization_list=authorization_list, access_list=access_list, sender=sender, + expected_receipt=TransactionReceipt(gas_used=gas_used), ) state_test( @@ -744,7 +744,6 @@ def test_gas_cost( tx=tx, post={ test_code_address: Account(storage=test_code_storage), - sender: Account(balance=sender_account.balance - tx_exact_cost + discount_cost), }, ) diff --git a/tests/shanghai/eip3860_initcode/test_initcode.py b/tests/shanghai/eip3860_initcode/test_initcode.py index bcedb6f6cf4..eca5511f3d2 100644 --- a/tests/shanghai/eip3860_initcode/test_initcode.py +++ b/tests/shanghai/eip3860_initcode/test_initcode.py @@ -24,6 +24,7 @@ StateTestFiller, Transaction, TransactionException, + TransactionReceipt, ceiling_division, compute_create_address, ) @@ -291,6 +292,8 @@ def tx( gas_price=10, error=tx_error, sender=sender, + # The entire gas limit is expected to be consumed. + expected_receipt=TransactionReceipt(gas_used=gas_limit), ) @pytest.fixture