diff --git a/.github/workflows/fixtures.yaml b/.github/workflows/fixtures.yaml index bf5a8030ac3..a68ed5dcb01 100644 --- a/.github/workflows/fixtures.yaml +++ b/.github/workflows/fixtures.yaml @@ -19,21 +19,11 @@ jobs: fill-params: '' solc: '0.8.21' python: '3.11' - - name: 'fixtures_hive' - evm-type: 'main' - fill-params: '--enable-hive --from=Merge' - solc: '0.8.21' - python: '3.11' - name: 'fixtures_develop' evm-type: 'develop' fill-params: '--until=Cancun' solc: '0.8.21' python: '3.11' - - name: 'fixtures_develop_hive' - evm-type: 'develop' - fill-params: '--enable-hive --from=Merge --until=Cancun' - solc: '0.8.21' - python: '3.11' steps: - uses: actions/checkout@v3 with: diff --git a/README.md b/README.md index 53208d459ff..ba543837087 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,11 @@ The following transition tools are supported by the framework: ### Upcoming EIP Development -Generally, specific `t8n` implementations and branches must be used when developing tests for upcoming EIPs (last updated 2023-10-19): +Generally, specific `t8n` implementations and branches must be used when developing tests for upcoming EIPs. -- Cancun related EIPs (4844, 4788, 1153, 6780) - [lightclient/go-ethereum@devnet-10](https://github.com/lightclient/go-ethereum/tree/devnet-10) -- EOF tests - [ethereum/evmone@master](https://github.com/ethereum/evmone) +We use named reference tags to point to the specific version of the `t8n` implementation that needs to be used fill the tests. + +All current tags, their t8n implementation and branch they point to, are listed in [evm-config.yaml](evm-config.yaml). ## Getting Started diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f37279af44f..3b641d1d45a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,21 +8,63 @@ Test fixtures for use by clients are available for each release on the [Github r ### ๐Ÿงช Test Cases +- โœจ Add tests for the MODEXP precompile ([#364](https://github.com/ethereum/execution-spec-tests/pull/364)) +- ๐Ÿ”€ Add reentrancy suicide revert test ([#372](https://github.com/ethereum/execution-spec-tests/pull/372)). +- ๐Ÿ”€ BlockchainTests converted to StateTest (also automatically generate BlockchainTest) ([#368](https://github.com/ethereum/execution-spec-tests/pull/368), [#370](https://github.com/ethereum/execution-spec-tests/pull/370)): + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_invalid_normal_gas + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_insufficient_balance_blob_tx + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_invalid_tx_blob_count + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_invalid_blob_hash_versioning_single_tx + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_blob_tx_attribute_opcodes + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_blob_tx_attribute_value_opcode + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_blob_tx_attribute_calldata_opcodes + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_blob_tx_attribute_gasprice_opcode + - tests/cancun/eip4844_blobs/test_blob_txs.py::test_blob_type_tx_pre_fork + - tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py::test_point_evaluation_precompile_gas_usage + - tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py::test_valid_precompile_calls + - tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py::test_invalid_precompile_calls + - tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py::test_point_evaluation_precompile_external_vectors + - tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py::test_point_evaluation_precompile_calls + - tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py::test_point_evaluation_precompile_gas_tx_to + - tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py::test_point_evaluation_precompile_before_fork + - tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py::test_blobbasefee_before_fork + - tests/shanghai/eip3860_initcode/test_initcode.py::test_contract_creating_tx + - tests/shanghai/eip3860_initcode/test_initcode.py::TestContractCreationGasUsage +- ๐Ÿž Fixed `tests/cancun/eip4844_blobs/test_blob_txs.py:test_invalid_tx_max_fee_per_blob_gas` to account for extra gas required in the case where the account is incorrectly deduced the balance as if it had the correct block blob gas fee ([#370](https://github.com/ethereum/execution-spec-tests/pull/370)). +- ๐Ÿž Fixed `tests/cancun/eip4844_blobs/test_blob_txs.py:test_insufficient_balance_blob_tx` to correctly calculate the minimum balance required for the accounts ([#379](https://github.com/ethereum/execution-spec-tests/pull/379)). +- โœจ Add `tests/cancun/eip4844_blobs/test_blob_txs.py::test_sufficient_balance_blob_tx` and `tests/cancun/eip4844_blobs/test_blob_txs.py::test_sufficient_balance_blob_tx_pre_fund_tx` ([#379](https://github.com/ethereum/execution-spec-tests/pull/379)). +- ๐Ÿž Fix and enable `tests/cancun/eip4844_blobs/test_blob_txs.py::test_invalid_blob_tx_contract_creation` ([#379](https://github.com/ethereum/execution-spec-tests/pull/379)). + ### ๐Ÿ› ๏ธ Framework - โœจ Add a `--single-fixture-per-file` flag to generate one fixture JSON file per test case ([#331](https://github.com/ethereum/execution-spec-tests/pull/331)). - ๐Ÿ”€ Rename test fixtures names to match the corresponding pytest node ID as generated using `fill` ([#342](https://github.com/ethereum/execution-spec-tests/pull/342)). - ๐Ÿ’ฅ Replace "=" with "_" in pytest node ids and test fixture names ([#342](https://github.com/ethereum/execution-spec-tests/pull/342)). +- โœจ Add `evm_bytes_to_python` command which converts EVM bytes to Python Opcodes ([#357](https://github.com/ethereum/execution-spec-tests/pull/357)) +- ๐Ÿ”€ Locally calculate the transactions list's root instead of using the one returned by t8n when producing BlockchainTests ([#353](https://github.com/ethereum/execution-spec-tests/pull/353)) +- โœจ Fork objects used to write tests can now be compared using the `>`, `>=`, `<`, `<=` operators, to check for a fork being newer than, newer than or equal, older than, older than or equal, respectively when compared against other fork ([#367](https://github.com/ethereum/execution-spec-tests/pull/367)) +- ๐Ÿž Storage type iterator is now fixed ([#369](https://github.com/ethereum/execution-spec-tests/pull/369)) +- ๐Ÿ’ฅ Removed `--enable-hive` parameter, now all test types are generated by default ([#358](https://github.com/ethereum/execution-spec-tests/pull/358)) +- ๐Ÿ’ฅ `StateTest`, spec format used to write tests, is now limited to a single transaction per test ([#361](https://github.com/ethereum/execution-spec-tests/pull/361)) +- โœจ `StateTestOnly`, spec format is now available and its only difference with `StateTest` is that it does not produce a `BlockchainTest` ([#368](https://github.com/ethereum/execution-spec-tests/pull/368)) +- โœจ Add a helper class `ethereum_test_tools.TestParameterGroup` to define test parameters as dataclasses and auto-generate test IDs ([#364](https://github.com/ethereum/execution-spec-tests/pull/364)). +- ๐Ÿ”€ Change custom exception classes to dataclasses to improve testability ([#386](https://github.com/ethereum/execution-spec-tests/pull/386)). ### ๐Ÿ”ง EVM Tools ### ๐Ÿ“‹ Misc - ๐Ÿ”€ Docs: Update `t8n` tool branch to fill tests for development features in the [readme](https://github.com/ethereum/execution-spec-test) ([#338](https://github.com/ethereum/execution-spec-tests/pull/338)). +- ๐Ÿ”€ Filling tool: Updated filling tool to go-ethereum@master ([#368](https://github.com/ethereum/execution-spec-tests/pull/368)) ## Breaking Changes -1. In this release the pytest node ID is now used for fixture names (previously only the test parameters were used), this should not be breaking. However, "=" in both node IDs and therefore fixture names, have been replaced with "_", which may break tooling that depends on the "=" character. +1. Fixture output, including release tarballs, now contain subdirectories for different test types: + - `blockchain_tests`: Contains BlockchainTest formatted tests + - `blockchain_tests_hive`: Contains BlockchainTest with Engine API call directives for use in hive + - `state_tests`: Contains StateTest formatted tests +2. `StateTest`, spec format used to write tests, is now limited to a single transaction per test. +3. In this release the pytest node ID is now used for fixture names (previously only the test parameters were used), this should not be breaking. However, "=" in both node IDs and therefore fixture names, have been replaced with "_", which may break tooling that depends on the "=" character. Pytest node ID example: diff --git a/docs/getting_started/debugging_t8n_tools.md b/docs/getting_started/debugging_t8n_tools.md index 531aeb18951..865d8c48f5c 100644 --- a/docs/getting_started/debugging_t8n_tools.md +++ b/docs/getting_started/debugging_t8n_tools.md @@ -22,38 +22,39 @@ will produce the directory structure: ```text ๐Ÿ“‚ /tmp/evm-dump -โ””โ”€โ”€ ๐Ÿ“‚ berlin__eip2930_access_list__test_acl__test_access_list - โ””โ”€โ”€ ๐Ÿ“‚ fork_Berlin - โ”œโ”€โ”€ ๐Ÿ“‚ 0 - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ args.py - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ input - โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json - โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ env.json - โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.json - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ output - โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json - โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ result.json - โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.rlp - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ returncode.txt - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ stderr.txt - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ stdin.txt - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ stdout.txt - โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ t8n.sh - โ””โ”€โ”€ ๐Ÿ“‚ 1 - โ”œโ”€โ”€ ๐Ÿ“„ args.py - โ”œโ”€โ”€ ๐Ÿ“‚ input - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ env.json - โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.json - โ”œโ”€โ”€ ๐Ÿ“‚ output - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ result.json - โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.rlp - โ”œโ”€โ”€ ๐Ÿ“„ returncode.txt - โ”œโ”€โ”€ ๐Ÿ“„ stderr.txt - โ”œโ”€โ”€ ๐Ÿ“„ stdin.txt - โ”œโ”€โ”€ ๐Ÿ“„ stdout.txt - โ””โ”€โ”€ ๐Ÿ“„ t8n.sh +โ””โ”€โ”€ ๐Ÿ“‚ blockchain_tests + โ””โ”€โ”€ ๐Ÿ“‚ berlin__eip2930_access_list__test_acl__test_access_list + โ””โ”€โ”€ ๐Ÿ“‚ fork_Berlin + โ”œโ”€โ”€ ๐Ÿ“‚ 0 + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ args.py + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ input + โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json + โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ env.json + โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.json + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ output + โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json + โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ result.json + โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.rlp + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ returncode.txt + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ stderr.txt + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ stdin.txt + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ stdout.txt + โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ t8n.sh + โ””โ”€โ”€ ๐Ÿ“‚ 1 + โ”œโ”€โ”€ ๐Ÿ“„ args.py + โ”œโ”€โ”€ ๐Ÿ“‚ input + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ env.json + โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.json + โ”œโ”€โ”€ ๐Ÿ“‚ output + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ result.json + โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.rlp + โ”œโ”€โ”€ ๐Ÿ“„ returncode.txt + โ”œโ”€โ”€ ๐Ÿ“„ stderr.txt + โ”œโ”€โ”€ ๐Ÿ“„ stdin.txt + โ”œโ”€โ”€ ๐Ÿ“„ stdout.txt + โ””โ”€โ”€ ๐Ÿ“„ t8n.sh ``` where the directories `0` and `1` correspond to the different calls made to the `t8n` tool executed during the test: @@ -120,24 +121,25 @@ will additionally run the `evm blocktest` command on every JSON fixture file and ```text ๐Ÿ“‚ /tmp/evm-dump -โ””โ”€โ”€ ๐Ÿ“‚ berlin__eip2930_access_list__test_acl__test_access_list - โ”œโ”€โ”€ ๐Ÿ“„ fixtures.json - โ”œโ”€โ”€ ๐Ÿ“‚ fork_Berlin - โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ 0 - โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ args.py - โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ input - โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json - โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ env.json - โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.json - โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ output - โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json - โ”‚ ... ... ... - โ”‚ - โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures_args.py - โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures_returncode.txt - โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures.sh - โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures_stderr.txt - โ””โ”€โ”€ ๐Ÿ“„ verify_fixtures_stdout.txt +โ””โ”€โ”€ ๐Ÿ“‚ blockchain_tests + โ””โ”€โ”€ ๐Ÿ“‚ berlin__eip2930_access_list__test_acl__test_access_list + โ”œโ”€โ”€ ๐Ÿ“„ fixtures.json + โ”œโ”€โ”€ ๐Ÿ“‚ fork_Berlin + โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ 0 + โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ args.py + โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ input + โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json + โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ env.json + โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ txs.json + โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“‚ output + โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ alloc.json + โ”‚ ... ... ... + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures_args.py + โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures_returncode.txt + โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures.sh + โ”œโ”€โ”€ ๐Ÿ“„ verify_fixtures_stderr.txt + โ””โ”€โ”€ ๐Ÿ“„ verify_fixtures_stdout.txt ``` where the `verify_fixtures.sh` script can be used to reproduce the `evm blocktest` command. diff --git a/docs/getting_started/executing_tests_command_line.md b/docs/getting_started/executing_tests_command_line.md index e49ef82443c..553c3b577c9 100644 --- a/docs/getting_started/executing_tests_command_line.md +++ b/docs/getting_started/executing_tests_command_line.md @@ -152,7 +152,6 @@ Arguments defining filler location and output: Don't group fixtures in JSON files by test function; write each fixture to its own file. This can be used to increase the granularity of --verify-fixtures. - --enable-hive Output test fixtures with the hive-specific properties. Arguments defining debug behavior: --evm-dump-dir EVM_DUMP_DIR, --t8n-dump-dir EVM_DUMP_DIR diff --git a/docs/getting_started/quick_start.md b/docs/getting_started/quick_start.md index c9b4825a7c7..62f175d6a5b 100644 --- a/docs/getting_started/quick_start.md +++ b/docs/getting_started/quick_start.md @@ -84,7 +84,7 @@ The following requires a Python 3.10, 3.11 or 3.12 installation. 2. The corresponding fixture file has been generated: ```console - head fixtures/berlin/eip2930_access_list/acl/access_list.json + head fixtures/blockchain_tests/berlin/eip2930_access_list/acl/access_list.json ``` ## Next Steps diff --git a/docs/getting_started/repository_overview.md b/docs/getting_started/repository_overview.md index de26dd190dc..ac571a73647 100644 --- a/docs/getting_started/repository_overview.md +++ b/docs/getting_started/repository_overview.md @@ -10,8 +10,9 @@ The most relevant folders and files in the repo are: โ”‚ โ”œโ”€โ”€ ๐Ÿ“ vm/ โ”‚ โ””โ”€โ”€ ๐Ÿ“ ... โ”œโ”€โ•ด๐Ÿ“ fixtures/ # default fixture output dir -โ”‚ โ”œโ”€โ”€ ๐Ÿ“ eips/ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“ vm/ +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ blockchain_tests/ +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ blockchain_tests_hive/ +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ state_tests/ โ”‚ โ””โ”€โ”€ ๐Ÿ“ ... โ”œโ”€โ•ด๐Ÿ“ src/ # library & framework packages โ”‚ โ”œโ”€โ”€ ๐Ÿ“ ethereum_test_fork/ diff --git a/docs/getting_started/using_fixtures.md b/docs/getting_started/using_fixtures.md index ff9af584521..a353091154c 100644 --- a/docs/getting_started/using_fixtures.md +++ b/docs/getting_started/using_fixtures.md @@ -37,10 +37,8 @@ The @ethereum/execution-spec-tests repository provides [releases](https://github | ------------------------------ | -------- | ------------------ | | `fixtures.tar.gz` | Clients | All tests until the last stable fork | "Must pass" | | `fixtures_develop.tar.gz` | Clients | All tests until the last development fork | -| `fixtures_hive.tar.gaz` | Hive | All tests until the last stable fork in hive format | -| `fixtures_develop_hive.tar.gz` | Hive | All tests until the last development fork in hive format | -The Hive format uses Engine API directives instead of the usual BlockchainTest format. +The Hive format tests are included in subdirectory `blockchain_tests_hive` and these use Engine API directives instead of the usual BlockchainTest format. ## Obtaining the Most Recent Release Artifacts diff --git a/docs/tutorials/state_transition.md b/docs/tutorials/state_transition.md index 001745016fd..14afa1f7863 100644 --- a/docs/tutorials/state_transition.md +++ b/docs/tutorials/state_transition.md @@ -1,6 +1,6 @@ # State Transition Tests -This tutorial teaches you to create a state transition execution specification test. These tests verify that a blockchain, starting from a defined pre-state, will reach a specified post-state after executing a set of specific transactions. +This tutorial teaches you to create a state transition execution specification test. These tests verify that a starting pre-state will reach a specified post-state after executing a single transaction. ## Pre-requisites @@ -13,7 +13,7 @@ Before proceeding with this tutorial, it is assumed that you have prior knowledg ## Example Tests -The most effective method of learning how to write tests is to study a couple of straightforward examples. In this tutorial we will go over the [Yul](https://github.com/ethereum/execution-spec-tests/blob/main/tests/example/test_yul_example.py#L17) state test. +The most effective method of learning how to write tests is to study a couple of straightforward examples. In this tutorial we will go over the [Yul](https://github.com/ethereum/execution-spec-tests/blob/main/tests/homestead/yul/test_yul_example.py#L19) state test. ### Yul Test @@ -83,7 +83,7 @@ The function definition ends when there is a line that is no longer indented. As env = Environment() ``` -This line specifies that `env` is an [`Environment`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/types.py#L445) object, and that we just use the default parameters. +This line specifies that `env` is an [`Environment`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/types.py#L878) object, and that we just use the default parameters. If necessary we can modify the environment to have different block gas limits, block numbers, etc. In most tests the defaults are good enough. @@ -102,7 +102,7 @@ It is a [dictionary](https://docs.python.org/3/tutorial/datastructures.html#dict "0x1000000000000000000000000000000000000000": Account( ``` -The keys of the dictionary are addresses (as strings), and the values are [`Account`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/types.py#L264) objects. +The keys of the dictionary are addresses (as strings), and the values are [`Account`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/types.py#L517) objects. You can read more about address fields [in the static test documentation](https://ethereum-tests.readthedocs.io/en/latest/test_filler/state_filler.html#address-fields). ```python @@ -145,7 +145,7 @@ Generally for execution spec tests the `sstore` instruction acts as a high-level } ``` -[`TestAddress`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/constants.py#L8) is an address for which the test filler has the private key. +[`TestAddress`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/constants.py#L7) is an address for which the test filler has the private key. This means that the test runner can issue a transaction as that contract. Of course, this address also needs a balance to be able to issue transactions. @@ -163,7 +163,7 @@ Of course, this address also needs a balance to be able to issue transactions. ) ``` -With the pre-state specified, we can add a description for the [`Transaction`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/types.py#L516). +With the pre-state specified, we can add a description for the [`Transaction`](https://github.com/ethereum/execution-spec-tests/blob/main/src/ethereum_test_tools/common/types.py#L1155). For more information, [see the static test documentation](https://ethereum-tests.readthedocs.io/en/latest/test_filler/state_filler.html#transaction) #### Post State @@ -185,10 +185,10 @@ In this case, we look at the storage of the contract we called and add to it wha #### State Test ```python - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) ``` -This line calls the wrapper to the `StateTest` object that provides all the objects required (for example, the fork parameter) in order to fill the test, generate the test fixtures and write them to file (by default, `./fixtures/example/yul_example/test_yul.json`). +This line calls the wrapper to the `StateTest` object that provides all the objects required (for example, the fork parameter) in order to fill the test, generate the test fixtures and write them to file (by default, `./fixtures/_tests/example/yul_example/test_yul.json`). ## Conclusion diff --git a/docs/tutorials/state_transition_bad_opcode.md b/docs/tutorials/state_transition_bad_opcode.md index 89a3e4b1285..5cb20b9eb9b 100644 --- a/docs/tutorials/state_transition_bad_opcode.md +++ b/docs/tutorials/state_transition_bad_opcode.md @@ -267,7 +267,7 @@ Over the entire for loop, it yields 255 different tests. yield StateTest( env=env, pre=pre, - txs=[tx], + tx=tx, post=(post_valid if opc_valid(opc) else post_invalid), ) ``` diff --git a/evm-config.yaml b/evm-config.yaml index 13653b0b690..5aef7c9e778 100644 --- a/evm-config.yaml +++ b/evm-config.yaml @@ -4,5 +4,5 @@ main: ref: master develop: impl: geth - repo: lightclient/go-ethereum - ref: devnet-10 \ No newline at end of file + repo: ethereum/go-ethereum + ref: master \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 91b27644896..1ba8bde67a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ console_scripts = pyspelling_soft_fail = entry_points.pyspelling_soft_fail:main markdownlintcli2_soft_fail = entry_points.markdownlintcli2_soft_fail:main create_whitelist_for_flake8_spelling = entry_points.create_whitelist_for_flake8_spelling:main + evm_bytes_to_python = entry_points.evm_bytes_to_python:main [options.extras_require] test = diff --git a/src/entry_points/evm_bytes_to_python.py b/src/entry_points/evm_bytes_to_python.py new file mode 100644 index 00000000000..930c1bc1df8 --- /dev/null +++ b/src/entry_points/evm_bytes_to_python.py @@ -0,0 +1,57 @@ +""" +Define an entry point wrapper for pytest. +""" + +import sys +from typing import Any, List, Optional + +from ethereum_test_tools import Opcodes as Op + + +def process_evm_bytes(evm_bytes_hex_string: Any) -> str: # noqa: D103 + if evm_bytes_hex_string.startswith("0x"): + evm_bytes_hex_string = evm_bytes_hex_string[2:] + + evm_bytes = bytearray(bytes.fromhex(evm_bytes_hex_string)) + + opcodes_strings: List[str] = [] + + while evm_bytes: + opcode_byte = evm_bytes.pop(0) + + opcode: Optional[Op] = None + for op in Op: + if op.int() == opcode_byte: + opcode = op + break + + if opcode is None: + raise ValueError(f"Unknown opcode: {opcode_byte}") + + if opcode.data_portion_length > 0: + data_portion = evm_bytes[: opcode.data_portion_length] + evm_bytes = evm_bytes[opcode.data_portion_length :] + opcodes_strings.append(f'Op.{opcode._name_}("0x{data_portion.hex()}")') + else: + opcodes_strings.append(f"Op.{opcode._name_}") + + return " + ".join(opcodes_strings) + + +def print_help(): # noqa: D103 + print("Usage: evm_bytes_to_python ") + + +def main(): # noqa: D103 + if len(sys.argv) != 2: + print_help() + sys.exit(1) + if sys.argv[1] in ["-h", "--help"]: + print_help() + sys.exit(0) + evm_bytes_hex_string = sys.argv[1] + print(process_evm_bytes(evm_bytes_hex_string)) + + +if __name__ == "__main__": + main() diff --git a/src/entry_points/tests/__init__.py b/src/entry_points/tests/__init__.py new file mode 100644 index 00000000000..6a7a6059f9d --- /dev/null +++ b/src/entry_points/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Basic pytest applications `entry_points` unit tests. +""" diff --git a/src/entry_points/tests/test_evm_bytes_to_python.py b/src/entry_points/tests/test_evm_bytes_to_python.py new file mode 100644 index 00000000000..df2dd7f2dc3 --- /dev/null +++ b/src/entry_points/tests/test_evm_bytes_to_python.py @@ -0,0 +1,55 @@ +""" +Test suite for `entry_points.evm_bytes_to_python` module. +""" + +import pytest +from evm_bytes_to_python import process_evm_bytes + +from ethereum_test_tools import Opcodes as Op + +basic_vector = [ + "0x60008080808061AAAA612d5ff1600055", + 'Op.PUSH1("0x00") + Op.DUP1 + Op.DUP1 + Op.DUP1 + Op.DUP1 + Op.PUSH2("0xaaaa") + Op.PUSH2("0x2d5f") + Op.CALL + Op.PUSH1("0x00") + Op.SSTORE', # noqa: E501 +] +complex_vector = [ + "0x7fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf5f527fc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf6020527fe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff60405260786040356020355f35608a565b5f515f55602051600155604051600255005b5e56", # noqa: E501 + 'Op.PUSH32("0xa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf") + Op.PUSH0 + Op.MSTORE + Op.PUSH32("0xc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf") + Op.PUSH1("0x20") + Op.MSTORE + Op.PUSH32("0xe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") + Op.PUSH1("0x40") + Op.MSTORE + Op.PUSH1("0x78") + Op.PUSH1("0x40") + Op.CALLDATALOAD + Op.PUSH1("0x20") + Op.CALLDATALOAD + Op.PUSH0 + Op.CALLDATALOAD + Op.PUSH1("0x8a") + Op.JUMP + Op.JUMPDEST + Op.PUSH0 + Op.MLOAD + Op.PUSH0 + Op.SSTORE + Op.PUSH1("0x20") + Op.MLOAD + Op.PUSH1("0x01") + Op.SSTORE + Op.PUSH1("0x40") + Op.MLOAD + Op.PUSH1("0x02") + Op.SSTORE + Op.STOP + Op.JUMPDEST + Op.MCOPY + Op.JUMP', # noqa: E501 +] + + +@pytest.mark.parametrize( + "evm_bytes, python_opcodes", + [ + (basic_vector[0], basic_vector[1]), + (basic_vector[0][2:], basic_vector[1]), # no "0x" prefix + (complex_vector[0], complex_vector[1]), + (complex_vector[0][2:], complex_vector[1]), # no "0x" prefix + ], +) +def test_evm_bytes_to_python(evm_bytes, python_opcodes): + """Test evm_bytes_to_python using the basic and complex vectors""" + assert process_evm_bytes(evm_bytes) == python_opcodes + + +@pytest.mark.parametrize("opcode", list(Op)) +def test_individual_opcodes(opcode): + """Test each opcode individually""" + if opcode.data_portion_length > 0: + expected_output = f'Op.{opcode._name_}("0x")' + else: + expected_output = f"Op.{opcode._name_}" + + bytecode = opcode.int().to_bytes(1, byteorder="big").hex() + assert process_evm_bytes("0x" + bytecode) == expected_output + + +def test_invalid_opcode(): + """Invalid hex string""" + with pytest.raises(ValueError): + process_evm_bytes("0xZZ") + + +def test_unknown_opcode(): + """Opcode not defined in Op""" + with pytest.raises(ValueError): + process_evm_bytes("0x0F") diff --git a/src/ethereum_test_forks/__init__.py b/src/ethereum_test_forks/__init__.py index 20831ca42c2..47300c273dd 100644 --- a/src/ethereum_test_forks/__init__.py +++ b/src/ethereum_test_forks/__init__.py @@ -32,7 +32,6 @@ get_development_forks, get_forks, get_transition_forks, - is_fork, transition_fork_from_to, transition_fork_to, ) @@ -64,7 +63,6 @@ "get_deployed_forks", "get_development_forks", "get_forks", - "is_fork", "transition_fork_from_to", "transition_fork_to", ] diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 2ddfbe13c2f..45b918ad83d 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -2,7 +2,7 @@ Abstract base class for Ethereum forks """ from abc import ABC, ABCMeta, abstractmethod -from typing import Any, List, Mapping, Optional, Protocol, Type +from typing import Any, ClassVar, List, Mapping, Optional, Protocol, Type from .base_decorators import prefer_transition_to_method @@ -36,6 +36,30 @@ def __repr__(cls) -> str: """ return cls.name() + def __gt__(cls, other: "BaseForkMeta") -> bool: + """ + Compare if a fork is newer than some other fork. + """ + return cls != other and other.__subclasscheck__(cls) + + def __ge__(cls, other: "BaseForkMeta") -> bool: + """ + Compare if a fork is newer than or equal to some other fork. + """ + return other.__subclasscheck__(cls) + + def __lt__(cls, other: "BaseForkMeta") -> bool: + """ + Compare if a fork is older than some other fork. + """ + return cls != other and cls.__subclasscheck__(other) + + def __le__(cls, other: "BaseForkMeta") -> bool: + """ + Compare if a fork is older than or equal to some other fork. + """ + return cls.__subclasscheck__(other) + class BaseFork(ABC, metaclass=BaseForkMeta): """ @@ -44,13 +68,23 @@ class BaseFork(ABC, metaclass=BaseForkMeta): Must contain all the methods used by every fork. """ - @classmethod - @abstractmethod - def fork(cls, block_number: int = 0, timestamp: int = 0) -> str: + _transition_tool_name: ClassVar[Optional[str]] = None + _blockchain_test_network_name: ClassVar[Optional[str]] = None + _solc_name: ClassVar[Optional[str]] = None + + def __init_subclass__( + cls, + *, + transition_tool_name: Optional[str] = None, + blockchain_test_network_name: Optional[str] = None, + solc_name: Optional[str] = None, + ) -> None: """ - Returns fork name as it's meant to be passed to the transition tool for execution. + Initializes the new fork with values that don't carry over to subclass forks. """ - pass + cls._transition_tool_name = transition_tool_name + cls._blockchain_test_network_name = blockchain_test_network_name + cls._solc_name = solc_name # Header information abstract methods @classmethod @@ -109,6 +143,14 @@ def header_beacon_root_required(cls, block_number: int, timestamp: int) -> bool: """ pass + @classmethod + @abstractmethod + def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: + """ + Returns the amount of blob gas used per blob for a given fork. + """ + pass + @classmethod @abstractmethod def get_reward(cls, block_number: int = 0, timestamp: int = 0) -> int: @@ -192,6 +234,31 @@ def name(cls) -> str: """ return cls.__name__ + @classmethod + @abstractmethod + def transition_tool_name(cls, block_number: int = 0, timestamp: int = 0) -> str: + """ + Returns fork name as it's meant to be passed to the transition tool for execution. + """ + pass + + @classmethod + @abstractmethod + def solc_name(cls, block_number: int = 0, timestamp: int = 0) -> str: + """ + Returns fork name as it's meant to be passed to the solc compiler. + """ + pass + + @classmethod + def blockchain_test_network_name(cls) -> str: + """ + Returns the network configuration name to be used in BlockchainTests for this fork. + """ + if cls._blockchain_test_network_name is not None: + return cls._blockchain_test_network_name + return cls.name() + @classmethod def is_deployed(cls) -> bool: """ diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index c13b41438a3..08abd09b73c 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -13,10 +13,21 @@ class Frontier(BaseFork): """ @classmethod - def fork(cls, block_number: int = 0, timestamp: int = 0) -> str: + def transition_tool_name(cls, block_number: int = 0, timestamp: int = 0) -> str: """ Returns fork name as it's meant to be passed to the transition tool for execution. """ + if cls._transition_tool_name is not None: + return cls._transition_tool_name + return cls.name() + + @classmethod + def solc_name(cls, block_number: int = 0, timestamp: int = 0) -> str: + """ + Returns fork name as it's meant to be passed to the solc compiler. + """ + if cls._solc_name is not None: + return cls._solc_name return cls.name() @classmethod @@ -61,6 +72,13 @@ def header_blob_gas_used_required(cls, block_number: int = 0, timestamp: int = 0 """ return False + @classmethod + def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: + """ + Returns the amount of blob gas used per blob for a given fork. + """ + return 0 + @classmethod def engine_new_payload_version( cls, block_number: int = 0, timestamp: int = 0 @@ -182,7 +200,7 @@ def get_reward(cls, block_number: int = 0, timestamp: int = 0) -> int: return 2_000_000_000_000_000_000 -class ConstantinopleFix(Constantinople): +class ConstantinopleFix(Constantinople, solc_name="Constantinople"): """ Constantinople Fix fork """ @@ -262,7 +280,12 @@ class GrayGlacier(ArrowGlacier): pass -class Merge(London): +class Merge( + London, + transition_tool_name="Merge", + blockchain_test_network_name="Merge", + solc_name="Paris", +): """ Merge fork """ @@ -354,6 +377,13 @@ def header_beacon_root_required(cls, block_number: int = 0, timestamp: int = 0) """ return True + @classmethod + def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: + """ + Blobs are enabled started from Cancun. + """ + return 2**17 + @classmethod def tx_types(cls, block_number: int = 0, timestamp: int = 0) -> List[int]: """ diff --git a/src/ethereum_test_forks/forks/transition.py b/src/ethereum_test_forks/forks/transition.py index 3180440233b..08b09091ff7 100644 --- a/src/ethereum_test_forks/forks/transition.py +++ b/src/ethereum_test_forks/forks/transition.py @@ -16,7 +16,7 @@ class BerlinToLondonAt5(Berlin): @transition_fork(to_fork=Shanghai, at_timestamp=15_000) -class MergeToShanghaiAtTime15k(Merge): +class MergeToShanghaiAtTime15k(Merge, blockchain_test_network_name="MergeToShanghaiAtTime15k"): """ Merge to Shanghai transition at Timestamp 15k """ diff --git a/src/ethereum_test_forks/helpers.py b/src/ethereum_test_forks/helpers.py index 6705bc95535..0b4d73689c3 100644 --- a/src/ethereum_test_forks/helpers.py +++ b/src/ethereum_test_forks/helpers.py @@ -135,18 +135,3 @@ def forks_from(fork: Fork, deployed_only: bool = True) -> List[Fork]: else: latest_fork = get_forks()[-1] return forks_from_until(fork, latest_fork) - - -def is_fork(fork: Fork, which: Fork) -> bool: - """ - Returns `True` if `fork` is `which` or beyond, `False otherwise. - """ - prev_fork = fork - - while prev_fork != BaseFork: - if prev_fork == which: - return True - - prev_fork = prev_fork.__base__ - - return False diff --git a/src/ethereum_test_forks/tests/test_forks.py b/src/ethereum_test_forks/tests/test_forks.py index 61cddd1ea17..0d37ef98b80 100644 --- a/src/ethereum_test_forks/tests/test_forks.py +++ b/src/ethereum_test_forks/tests/test_forks.py @@ -13,7 +13,6 @@ get_deployed_forks, get_development_forks, get_forks, - is_fork, transition_fork_from_to, transition_fork_to, ) @@ -37,14 +36,14 @@ def test_transition_forks(): assert BerlinToLondonAt5.transitions_to() == London assert BerlinToLondonAt5.transitions_from() == Berlin - assert BerlinToLondonAt5.fork(4, 0) == "Berlin" - assert BerlinToLondonAt5.fork(5, 0) == "London" + assert BerlinToLondonAt5.transition_tool_name(4, 0) == "Berlin" + assert BerlinToLondonAt5.transition_tool_name(5, 0) == "London" # Default values of transition forks is the transition block - assert BerlinToLondonAt5.fork() == "London" + assert BerlinToLondonAt5.transition_tool_name() == "London" - assert MergeToShanghaiAtTime15k.fork(0, 14_999) == "Merge" - assert MergeToShanghaiAtTime15k.fork(0, 15_000) == "Shanghai" - assert MergeToShanghaiAtTime15k.fork() == "Shanghai" + assert MergeToShanghaiAtTime15k.transition_tool_name(0, 14_999) == "Merge" + assert MergeToShanghaiAtTime15k.transition_tool_name(0, 15_000) == "Shanghai" + assert MergeToShanghaiAtTime15k.transition_tool_name() == "Shanghai" assert BerlinToLondonAt5.header_base_fee_required(4, 0) is False assert BerlinToLondonAt5.header_base_fee_required(5, 0) is True @@ -80,6 +79,14 @@ def test_forks(): assert f"{London}" == "London" assert f"{MergeToShanghaiAtTime15k}" == "MergeToShanghaiAtTime15k" + # Merge name will be changed to paris, but we need to check the inheriting fork name is still + # the default + assert Merge.transition_tool_name() == "Merge" + assert Shanghai.transition_tool_name() == "Shanghai" + assert Merge.blockchain_test_network_name() == "Merge" + assert Shanghai.blockchain_test_network_name() == "Shanghai" + assert MergeToShanghaiAtTime15k.blockchain_test_network_name() == "MergeToShanghaiAtTime15k" + # Test some fork properties assert Berlin.header_base_fee_required(0, 0) is False assert London.header_base_fee_required(0, 0) is True @@ -94,9 +101,38 @@ def test_forks(): assert cast(Fork, MergeToShanghaiAtTime15k).header_withdrawals_required(0, 15_000) is True assert cast(Fork, MergeToShanghaiAtTime15k).header_withdrawals_required() is True - assert is_fork(Berlin, Berlin) is True - assert is_fork(London, Berlin) is True - assert is_fork(Berlin, Merge) is False + # Test fork comparison + assert Merge > Berlin + assert not Berlin > Merge + assert Berlin < Merge + assert not Merge < Berlin + + assert Merge >= Berlin + assert not Berlin >= Merge + assert Berlin <= Merge + assert not Merge <= Berlin + + assert London > Berlin + assert not Berlin > London + assert Berlin < London + assert not London < Berlin + + assert London >= Berlin + assert not Berlin >= London + assert Berlin <= London + assert not London <= Berlin + + assert Berlin >= Berlin + assert Berlin <= Berlin + assert not Berlin > Berlin + assert not Berlin < Berlin + + fork = Berlin + assert fork >= Berlin + assert fork <= Berlin + assert not fork > Berlin + assert not fork < Berlin + assert fork == Berlin def test_get_forks(): # noqa: D103 diff --git a/src/ethereum_test_forks/transition_base_fork.py b/src/ethereum_test_forks/transition_base_fork.py index dddc72b0d7c..eb164540693 100644 --- a/src/ethereum_test_forks/transition_base_fork.py +++ b/src/ethereum_test_forks/transition_base_fork.py @@ -46,7 +46,14 @@ def decorator(cls) -> Type[TransitionBaseClass]: from_fork = cls.__bases__[0] assert issubclass(from_fork, BaseFork) - class NewTransitionClass(cls, TransitionBaseClass, BaseFork): # type: ignore + class NewTransitionClass( + cls, # type: ignore + TransitionBaseClass, + BaseFork, + transition_tool_name=cls._transition_tool_name, + blockchain_test_network_name=cls._blockchain_test_network_name, + solc_name=cls._solc_name, + ): pass NewTransitionClass.name = lambda: transition_name # type: ignore diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 23edbc7f9f8..55f8974a18b 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -18,19 +18,15 @@ AccessList, Account, Auto, - Block, EngineAPIError, Environment, - Fixture, - FixtureEngineNewPayload, - Header, HistoryStorageAddress, - HiveFixture, JSONEncoder, Removable, Storage, TestAddress, TestAddress2, + TestParameterGroup, TestPrivateKey, TestPrivateKey2, Transaction, @@ -45,25 +41,30 @@ to_address, to_hash, to_hash_bytes, + transaction_list_root, ) -from .filling.fill import fill_test from .reference_spec import ReferenceSpec, ReferenceSpecTypes from .spec import ( + SPEC_TYPES, + BaseFixture, BaseTest, - BaseTestConfig, BlockchainTest, BlockchainTestFiller, + FixtureCollector, StateTest, StateTestFiller, + TestInfo, ) +from .spec.blockchain.types import Block, Header from .vm import Opcode, OpcodeCallArg, Opcodes __all__ = ( + "SPEC_TYPES", "AccessList", "Account", "Auto", + "BaseFixture", "BaseTest", - "BaseTestConfig", "Block", "BlockchainTest", "BlockchainTestFiller", @@ -75,6 +76,7 @@ "EngineAPIError", "Environment", "Fixture", + "FixtureCollector", "FixtureEngineNewPayload", "Header", "HistoryStorageAddress", @@ -93,6 +95,8 @@ "Switch", "TestAddress", "TestAddress2", + "TestInfo", + "TestParameterGroup", "TestPrivateKey", "TestPrivateKey2", "Transaction", @@ -107,8 +111,8 @@ "cost_memory_bytes", "eip_2028_transaction_data_cost", "eip_2028_transaction_data_cost", - "fill_test", "to_address", "to_hash_bytes", "to_hash", + "transaction_list_root", ) diff --git a/src/ethereum_test_tools/code/yul.py b/src/ethereum_test_tools/code/yul.py index ca4fa3356b9..27639693bf5 100644 --- a/src/ethereum_test_tools/code/yul.py +++ b/src/ethereum_test_tools/code/yul.py @@ -7,7 +7,7 @@ from pathlib import Path from shutil import which from subprocess import PIPE, run -from typing import Mapping, Optional, Sized, SupportsBytes, Tuple, Type, Union +from typing import Optional, Sized, SupportsBytes, Tuple, Type, Union from semver import Version @@ -33,13 +33,7 @@ def get_evm_version_from_fork(fork: Fork | None): """ if not fork: return None - fork_to_evm_version_map: Mapping[str, str] = { - "Merge": "paris", - "ConstantinopleFix": "constantinople", - } - if fork.name() in fork_to_evm_version_map: - return fork_to_evm_version_map[fork.name()] - return fork.name().lower() + return fork.solc_name().lower() class Yul(SupportsBytes, Sized): diff --git a/src/ethereum_test_tools/common/__init__.py b/src/ethereum_test_tools/common/__init__.py index 395088726fb..6a09c4b59c1 100644 --- a/src/ethereum_test_tools/common/__init__.py +++ b/src/ethereum_test_tools/common/__init__.py @@ -13,6 +13,7 @@ TestPrivateKey2, ) from .helpers import ( + TestParameterGroup, add_kzg_version, ceiling_division, compute_create2_address, @@ -24,25 +25,18 @@ to_hash, to_hash_bytes, ) +from .json import to_json from .types import ( AccessList, Account, Address, Alloc, Auto, - Block, Bloom, Bytes, Environment, - Fixture, - FixtureBlock, - FixtureEngineNewPayload, - FixtureHeader, Hash, - Header, HeaderNonce, - HiveFixture, - InvalidFixtureBlock, JSONEncoder, Number, Removable, @@ -53,7 +47,7 @@ alloc_to_accounts, serialize_transactions, str_or_none, - to_json, + transaction_list_root, withdrawals_root, ) @@ -65,28 +59,21 @@ "AddrBB", "Alloc", "Auto", - "Block", "Bloom", "Bytes", "EngineAPIError", "EmptyTrieRoot", "Environment", - "Fixture", - "FixtureBlock", - "FixtureEngineNewPayload", - "FixtureHeader", "Hash", - "Header", "HeaderNonce", "HistoryStorageAddress", - "HiveFixture", - "InvalidFixtureBlock", "JSONEncoder", "Number", "Removable", "Storage", "TestAddress", "TestAddress2", + "TestParameterGroup", "TestPrivateKey", "TestPrivateKey2", "Transaction", @@ -106,5 +93,6 @@ "to_hash_bytes", "to_hash", "to_json", + "transaction_list_root", "withdrawals_root", ) diff --git a/src/ethereum_test_tools/common/helpers.py b/src/ethereum_test_tools/common/helpers.py index 65a5f89f2a9..71acecb920b 100644 --- a/src/ethereum_test_tools/common/helpers.py +++ b/src/ethereum_test_tools/common/helpers.py @@ -2,6 +2,7 @@ Helper functions/classes used to generate Ethereum tests. """ +from dataclasses import MISSING, dataclass, fields from typing import List, SupportsBytes from ethereum.crypto.hash import keccak256 @@ -124,3 +125,30 @@ def add_kzg_version( else: raise TypeError("Blob hash must be either an integer, string or bytes") return kzg_versioned_hashes + + +@dataclass(kw_only=True, frozen=True, repr=False) +class TestParameterGroup: + """ + Base class for grouping test parameters in a dataclass. Provides a generic + __repr__ method to generate clean test ids, including only non-default + optional fields. + """ + + __test__ = False # explicitly prevent pytest collecting this class + + def __repr__(self): + """ + Generates a repr string, intended to be used as a test id, based on the class + name and the values of the non-default optional fields. + """ + class_name = self.__class__.__name__ + field_strings = [] + + for field in fields(self): + value = getattr(self, field.name) + # Include the field only if it is not optional or not set to its default value + if field.default is MISSING or field.default != value: + field_strings.append(f"{field.name}_{value}") + + return f"{class_name}_{'-'.join(field_strings)}" diff --git a/src/ethereum_test_tools/common/types.py b/src/ethereum_test_tools/common/types.py index 344b9be37e1..e03bf563f14 100644 --- a/src/ethereum_test_tools/common/types.py +++ b/src/ethereum_test_tools/common/types.py @@ -2,11 +2,10 @@ Useful types for generating Ethereum tests. """ from copy import copy, deepcopy -from dataclasses import dataclass, fields, replace +from dataclasses import dataclass, fields from itertools import count from typing import ( Any, - Callable, ClassVar, Dict, Iterator, @@ -15,7 +14,6 @@ Optional, Sequence, SupportsBytes, - Tuple, Type, TypeAlias, TypeVar, @@ -28,10 +26,8 @@ from trie import HexaryTrie from ethereum_test_forks import Fork -from evm_transition_tool import TransitionTool -from ..reference_spec.reference_spec import ReferenceSpec -from .constants import AddrAA, EmptyOmmersRoot, EngineAPIError, TestPrivateKey +from .constants import AddrAA, TestPrivateKey from .conversions import ( BytesConvertible, FixedSizeBytesConvertible, @@ -42,7 +38,7 @@ to_fixed_size_bytes, to_number, ) -from .json import JSONEncoder, SupportsJSON, field, to_json +from .json import JSONEncoder, SupportsJSON, field # Sentinel classes @@ -267,6 +263,7 @@ class Storage(SupportsJSON): Dictionary type to be used when defining an input to initialize a storage. """ + @dataclass(kw_only=True) class InvalidType(Exception): """ Invalid type used when describing test's expected storage key or value. @@ -282,6 +279,7 @@ def __str__(self): """Print exception string""" return f"invalid type for key/value: {self.key_or_value}" + @dataclass(kw_only=True) class InvalidValue(Exception): """ Invalid value used when describing test's expected storage key or @@ -298,6 +296,7 @@ def __str__(self): """Print exception string""" return f"invalid value for key/value: {self.key_or_value}" + @dataclass(kw_only=True) class AmbiguousKeyValue(Exception): """ Key is represented twice in the storage. @@ -330,6 +329,7 @@ def __str__(self): s[{self.key_1}] = {self.val_1} and s[{self.key_2}] = {self.val_2} """ + @dataclass(kw_only=True) class MissingKey(Exception): """ Test expected to find a storage key set but key was missing. @@ -345,6 +345,7 @@ def __str__(self): """Print exception string""" return "key {0} not found in storage".format(Storage.key_value_to_string(self.key)) + @dataclass(kw_only=True) class KeyValueMismatch(Exception): """ Test expected a certain value in a storage key but value found @@ -384,10 +385,10 @@ def parse_key_value(input: str | int | bytes | SupportsBytes) -> int: elif isinstance(input, bytes) or isinstance(input, SupportsBytes): input = int.from_bytes(bytes(input), "big") else: - raise Storage.InvalidType(input) + raise Storage.InvalidType(key_or_value=input) if input > MAX_STORAGE_KEY_VALUE or input < MIN_STORAGE_KEY_VALUE: - raise Storage.InvalidValue(input) + raise Storage.InvalidValue(key_or_value=input) return input @staticmethod @@ -402,7 +403,7 @@ def key_value_to_string(value: int) -> str: hex_str = "0" + hex_str return "0x" + hex_str - def __init__(self, input: StorageDictType = {}, start_slot: int = 0): + def __init__(self, input: StorageDictType | "Storage" = {}, start_slot: int = 0): """ Initializes the storage using a given mapping which can have keys and values either as string or int. @@ -420,6 +421,10 @@ def __len__(self) -> int: """Returns number of elements in the storage""" return len(self.data) + def __iter__(self) -> Iterator[int]: + """Returns iterator of the storage""" + return iter(self.data) + def __contains__(self, key: str | int | bytes) -> bool: """Checks for an item in the storage""" key = Storage.parse_key_value(key) @@ -460,7 +465,9 @@ def __json__(self, encoder: JSONEncoder) -> Mapping[str, str]: key_repr = Storage.key_value_to_string(key) val_repr = Storage.key_value_to_string(self.data[key]) if key_repr in res and val_repr != res[key_repr]: - raise Storage.AmbiguousKeyValue(key_repr, res[key_repr], key, val_repr) + raise Storage.AmbiguousKeyValue( + key_1=key_repr, val_1=res[key_repr], key_2=key, val_2=val_repr + ) res[key_repr] = val_repr return res @@ -490,9 +497,11 @@ def must_contain(self, address: str, other: "Storage"): if key not in self.data: # storage[key]==0 is equal to missing storage if other[key] != 0: - raise Storage.MissingKey(key) + raise Storage.MissingKey(key=key) elif self.data[key] != other.data[key]: - raise Storage.KeyValueMismatch(address, key, self.data[key], other.data[key]) + raise Storage.KeyValueMismatch( + address=address, key=key, want=self.data[key], got=other.data[key] + ) def must_be_equal(self, address: str, other: "Storage"): """ @@ -501,16 +510,22 @@ def must_be_equal(self, address: str, other: "Storage"): # Test keys contained in both storage objects for key in self.data.keys() & other.data.keys(): if self.data[key] != other.data[key]: - raise Storage.KeyValueMismatch(address, key, self.data[key], other.data[key]) + raise Storage.KeyValueMismatch( + address=address, key=key, want=self.data[key], got=other.data[key] + ) # Test keys contained in either one of the storage objects for key in self.data.keys() ^ other.data.keys(): if key in self.data: if self.data[key] != 0: - raise Storage.KeyValueMismatch(address, key, self.data[key], 0) + raise Storage.KeyValueMismatch( + address=address, key=key, want=self.data[key], got=0 + ) elif other.data[key] != 0: - raise Storage.KeyValueMismatch(address, key, 0, other.data[key]) + raise Storage.KeyValueMismatch( + address=address, key=key, want=0, got=other.data[key] + ) @dataclass(kw_only=True) @@ -573,6 +588,7 @@ class Account: state. """ + @dataclass(kw_only=True) class NonceMismatch(Exception): """ Test expected a certain nonce value for an account but a different @@ -596,6 +612,7 @@ def __str__(self): + f"want {self.want}, got {self.got}" ) + @dataclass(kw_only=True) class BalanceMismatch(Exception): """ Test expected a certain balance for an account but a different @@ -619,6 +636,7 @@ def __str__(self): + f"want {self.want}, got {self.got}" ) + @dataclass(kw_only=True) class CodeMismatch(Exception): """ Test expected a certain bytecode for an account but a different @@ -1037,23 +1055,6 @@ class Environment: ), ) - @staticmethod - def from_parent_header(parent: "FixtureHeader") -> "Environment": - """ - Instantiates a new environment with the provided header as parent. - """ - return Environment( - parent_difficulty=parent.difficulty, - parent_timestamp=parent.timestamp, - parent_base_fee=parent.base_fee, - parent_blob_gas_used=parent.blob_gas_used, - parent_excess_blob_gas=parent.excess_blob_gas, - parent_gas_used=parent.gas_used, - parent_gas_limit=parent.gas_limit, - parent_ommers_hash=parent.ommers_hash, - block_hashes={parent.number: parent.hash if parent.hash is not None else 0}, - ) - def parent_hash(self) -> bytes: """ Obtains the latest hash according to the highest block number in @@ -1065,22 +1066,6 @@ def parent_hash(self) -> bytes: last_index = max([Number(k) for k in self.block_hashes.keys()]) return Hash(self.block_hashes[last_index]) - def apply_new_parent(self, new_parent: "FixtureHeader") -> "Environment": - """ - Applies a header as parent to a copy of this environment. - """ - env = copy(self) - env.parent_difficulty = new_parent.difficulty - env.parent_timestamp = new_parent.timestamp - env.parent_base_fee = new_parent.base_fee - env.parent_blob_gas_used = new_parent.blob_gas_used - env.parent_excess_blob_gas = new_parent.excess_blob_gas - env.parent_gas_used = new_parent.gas_used - env.parent_gas_limit = new_parent.gas_limit - env.parent_ommers_hash = new_parent.ommers_hash - env.block_hashes[new_parent.number] = new_parent.hash if new_parent.hash is not None else 0 - return env - def set_fork_requirements(self, fork: Fork, in_place: bool = False) -> "Environment": """ Fills the required fields in an environment depending on the fork. @@ -1103,6 +1088,8 @@ def set_fork_requirements(self, fork: Fork, in_place: bool = False) -> "Environm if fork.header_zero_difficulty_required(number, timestamp): res.difficulty = 0 + elif res.difficulty is None and res.parent_difficulty is None: + res.difficulty = 0x20000 if ( fork.header_excess_blob_gas_required(number, timestamp) @@ -1316,6 +1303,12 @@ class Transaction: skip=True, ), ) + rlp: Optional[bytes] = field( + default=None, + json_encoder=JSONEncoder.Field( + skip=True, + ), + ) class InvalidFeePayment(Exception): """ @@ -1546,6 +1539,9 @@ def serialized_bytes(self) -> bytes: Returns bytes of the serialized representation of the transaction, which is almost always RLP encoding. """ + if self.rlp is not None: + return self.rlp + if self.ty is None: raise ValueError("ty must be set for all tx types") @@ -1723,6 +1719,16 @@ def with_signature_and_sender(self) -> "Transaction": return tx +def transaction_list_root(input_txs: List[Transaction] | None) -> Hash: + """ + Returns the transactions root of a list of transactions. + """ + t = HexaryTrie(db={}) + for i, tx in enumerate(input_txs or []): + t.set(eth_rlp.encode(Uint(i)), tx.serialized_bytes()) + return Hash(t.root_hash) + + def transaction_list_to_serializable_list(input_txs: List[Transaction] | None) -> List[Any]: """ Returns the transaction list as a list of serializable objects. @@ -1876,974 +1882,3 @@ def from_transaction(cls, tx: Transaction) -> "FixtureTransaction": """ kwargs = {field.name: getattr(tx, field.name) for field in fields(tx)} return cls(**kwargs) - - -@dataclass(kw_only=True) -class Header: - """ - Header type used to describe block header properties in test specs. - """ - - parent_hash: Optional[FixedSizeBytesConvertible] = None - ommers_hash: Optional[FixedSizeBytesConvertible] = None - coinbase: Optional[FixedSizeBytesConvertible] = None - state_root: Optional[FixedSizeBytesConvertible] = None - transactions_root: Optional[FixedSizeBytesConvertible] = None - receipt_root: Optional[FixedSizeBytesConvertible] = None - bloom: Optional[FixedSizeBytesConvertible] = None - difficulty: Optional[NumberConvertible] = None - number: Optional[NumberConvertible] = None - gas_limit: Optional[NumberConvertible] = None - gas_used: Optional[NumberConvertible] = None - timestamp: Optional[NumberConvertible] = None - extra_data: Optional[BytesConvertible] = None - mix_digest: Optional[FixedSizeBytesConvertible] = None - nonce: Optional[FixedSizeBytesConvertible] = None - base_fee: Optional[NumberConvertible | Removable] = None - withdrawals_root: Optional[FixedSizeBytesConvertible | Removable] = None - blob_gas_used: Optional[NumberConvertible | Removable] = None - excess_blob_gas: Optional[NumberConvertible | Removable] = None - beacon_root: Optional[FixedSizeBytesConvertible | Removable] = None - hash: Optional[FixedSizeBytesConvertible] = None - - REMOVE_FIELD: ClassVar[Removable] = Removable() - """ - Sentinel object used to specify that a header field should be removed. - """ - EMPTY_FIELD: ClassVar[Removable] = Removable() - """ - Sentinel object used to specify that a header field must be empty during verification. - """ - - -@dataclass(kw_only=True) -class HeaderFieldSource: - """ - Block header field metadata specifying the source used to populate the field when collecting - the block header from different sources, and to validate it. - """ - - required: bool = True - """ - Whether the field is required or not, regardless of the fork. - """ - fork_requirement_check: Optional[str] = None - """ - Name of the method to call to check if the field is required for the current fork. - """ - default: Optional[Any] = None - """ - Default value for the field if no value was provided by either the transition tool or the - environment - """ - parse_type: Optional[Callable] = None - """ - The type or function to use to parse the field to before initializing the object. - """ - source_environment: Optional[str] = None - """ - Name of the field in the environment object, which can be a callable. - """ - source_transition_tool: Optional[str] = None - """ - Name of the field in the transition tool result dictionary. - """ - - def collect( - self, - *, - target: Dict[str, Any], - field_name: str, - fork: Fork, - number: int, - timestamp: int, - transition_tool_result: Dict[str, Any], - environment: Environment, - ) -> None: - """ - Collects the field from the different sources according to the - metadata description. - """ - value = None - required = self.required - if self.fork_requirement_check is not None: - required = getattr(fork, self.fork_requirement_check)(number, timestamp) - - if self.source_transition_tool is not None: - if self.source_transition_tool in transition_tool_result: - got_value = transition_tool_result.get(self.source_transition_tool) - if got_value is not None: - value = got_value - - if self.source_environment is not None: - got_value = getattr(environment, self.source_environment, None) - if callable(got_value): - got_value = got_value() - if got_value is not None: - value = got_value - - if required: - if value is None: - if self.default is not None: - value = self.default - else: - raise ValueError(f"missing required field '{field_name}'") - - if value is not None and self.parse_type is not None: - value = self.parse_type(value) - - target[field_name] = value - - -def header_field(*args, source: Optional[HeaderFieldSource] = None, **kwargs) -> Any: - """ - A wrapper around `dataclasses.field` that allows for json configuration info and header - metadata. - """ - if "metadata" in kwargs: - metadata = kwargs["metadata"] - else: - metadata = {} - assert isinstance(metadata, dict) - - if source is not None: - metadata["source"] = source - - kwargs["metadata"] = metadata - return field(*args, **kwargs) - - -@dataclass(kw_only=True) -class FixtureHeader: - """ - Representation of an Ethereum header within a test Fixture. - """ - - parent_hash: Hash = header_field( - source=HeaderFieldSource( - parse_type=Hash, - source_environment="parent_hash", - ), - json_encoder=JSONEncoder.Field(name="parentHash"), - ) - ommers_hash: Hash = header_field( - source=HeaderFieldSource( - parse_type=Hash, - source_transition_tool="sha3Uncles", - default=EmptyOmmersRoot, - ), - json_encoder=JSONEncoder.Field(name="uncleHash"), - ) - coinbase: Address = header_field( - source=HeaderFieldSource( - parse_type=Address, - source_environment="coinbase", - ), - json_encoder=JSONEncoder.Field(), - ) - state_root: Hash = header_field( - source=HeaderFieldSource( - parse_type=Hash, - source_transition_tool="stateRoot", - ), - json_encoder=JSONEncoder.Field(name="stateRoot"), - ) - transactions_root: Hash = header_field( - source=HeaderFieldSource( - parse_type=Hash, - source_transition_tool="txRoot", - ), - json_encoder=JSONEncoder.Field(name="transactionsTrie"), - ) - receipt_root: Hash = header_field( - source=HeaderFieldSource( - parse_type=Hash, - source_transition_tool="receiptsRoot", - ), - json_encoder=JSONEncoder.Field(name="receiptTrie"), - ) - bloom: Bloom = header_field( - source=HeaderFieldSource( - parse_type=Bloom, - source_transition_tool="logsBloom", - ), - json_encoder=JSONEncoder.Field(), - ) - difficulty: int = header_field( - source=HeaderFieldSource( - parse_type=Number, - source_transition_tool="currentDifficulty", - source_environment="difficulty", - default=0, - ), - json_encoder=JSONEncoder.Field(cast_type=ZeroPaddedHexNumber), - ) - number: int = header_field( - source=HeaderFieldSource( - parse_type=Number, - source_environment="number", - ), - json_encoder=JSONEncoder.Field(cast_type=ZeroPaddedHexNumber), - ) - gas_limit: int = header_field( - source=HeaderFieldSource( - parse_type=Number, - source_environment="gas_limit", - ), - json_encoder=JSONEncoder.Field(name="gasLimit", cast_type=ZeroPaddedHexNumber), - ) - gas_used: int = header_field( - source=HeaderFieldSource( - parse_type=Number, - source_transition_tool="gasUsed", - ), - json_encoder=JSONEncoder.Field(name="gasUsed", cast_type=ZeroPaddedHexNumber), - ) - timestamp: int = header_field( - source=HeaderFieldSource( - parse_type=Number, - source_environment="timestamp", - ), - json_encoder=JSONEncoder.Field(cast_type=ZeroPaddedHexNumber), - ) - extra_data: Bytes = header_field( - source=HeaderFieldSource( - parse_type=Bytes, - source_environment="extra_data", - default=b"", - ), - json_encoder=JSONEncoder.Field(name="extraData"), - ) - mix_digest: Hash = header_field( - source=HeaderFieldSource( - parse_type=Hash, - source_environment="prev_randao", - default=b"", - ), - json_encoder=JSONEncoder.Field(name="mixHash"), - ) - nonce: HeaderNonce = header_field( - source=HeaderFieldSource( - parse_type=HeaderNonce, - default=b"", - ), - json_encoder=JSONEncoder.Field(), - ) - base_fee: Optional[int] = header_field( - default=None, - source=HeaderFieldSource( - parse_type=Number, - fork_requirement_check="header_base_fee_required", - source_transition_tool="currentBaseFee", - source_environment="base_fee", - ), - json_encoder=JSONEncoder.Field(name="baseFeePerGas", cast_type=ZeroPaddedHexNumber), - ) - withdrawals_root: Optional[Hash] = header_field( - default=None, - source=HeaderFieldSource( - parse_type=Hash, - fork_requirement_check="header_withdrawals_required", - source_transition_tool="withdrawalsRoot", - ), - json_encoder=JSONEncoder.Field(name="withdrawalsRoot"), - ) - blob_gas_used: Optional[int] = header_field( - default=None, - source=HeaderFieldSource( - parse_type=Number, - fork_requirement_check="header_blob_gas_used_required", - source_transition_tool="blobGasUsed", - ), - json_encoder=JSONEncoder.Field(name="blobGasUsed", cast_type=ZeroPaddedHexNumber), - ) - excess_blob_gas: Optional[int] = header_field( - default=None, - source=HeaderFieldSource( - parse_type=Number, - fork_requirement_check="header_excess_blob_gas_required", - source_transition_tool="currentExcessBlobGas", - ), - json_encoder=JSONEncoder.Field(name="excessBlobGas", cast_type=ZeroPaddedHexNumber), - ) - beacon_root: Optional[Hash] = header_field( - default=None, - source=HeaderFieldSource( - parse_type=Hash, - fork_requirement_check="header_beacon_root_required", - source_environment="beacon_root", - ), - json_encoder=JSONEncoder.Field(name="parentBeaconBlockRoot"), - ) - hash: Optional[Hash] = header_field( - default=None, - source=HeaderFieldSource( - required=False, - ), - json_encoder=JSONEncoder.Field(), - ) - - @classmethod - def collect( - cls, - *, - fork: Fork, - transition_tool_result: Dict[str, Any], - environment: Environment, - ) -> "FixtureHeader": - """ - Collects a FixtureHeader object from multiple sources: - - The transition tool result - - The test's current environment - """ - # We depend on the environment to get the number and timestamp to check the fork - # requirements - number, timestamp = Number(environment.number), Number(environment.timestamp) - - # Collect the header fields - kwargs: Dict[str, Any] = {} - for header_field in fields(cls): - field_name = header_field.name - metadata = header_field.metadata - assert metadata is not None, f"Field {field_name} has no header field metadata" - field_metadata = metadata.get("source") - assert isinstance(field_metadata, HeaderFieldSource), ( - f"Field {field_name} has invalid header_field " f"metadata: {field_metadata}" - ) - field_metadata.collect( - target=kwargs, - field_name=field_name, - fork=fork, - number=number, - timestamp=timestamp, - transition_tool_result=transition_tool_result, - environment=environment, - ) - - # Pass the collected fields as keyword arguments to the constructor - return cls(**kwargs) - - def join(self, modifier: Header) -> "FixtureHeader": - """ - Produces a fixture header copy with the set values from the modifier. - """ - new_fixture_header = copy(self) - for header_field in self.__dataclass_fields__: - value = getattr(modifier, header_field) - if value is not None: - if value is Header.REMOVE_FIELD: - setattr(new_fixture_header, header_field, None) - else: - setattr(new_fixture_header, header_field, value) - return new_fixture_header - - def verify(self, baseline: Header): - """ - Verifies that the header fields from the baseline are as expected. - """ - for header_field in fields(self): - field_name = header_field.name - baseline_value = getattr(baseline, field_name) - if baseline_value is not None: - assert baseline_value is not Header.REMOVE_FIELD, "invalid baseline header" - value = getattr(self, field_name) - if baseline_value is Header.EMPTY_FIELD: - assert value is None, f"invalid header field {header_field}" - continue - metadata = header_field.metadata - field_metadata = metadata.get("source") - # type check is performed on collect() - if field_metadata.parse_type is not None: # type: ignore - baseline_value = field_metadata.parse_type(baseline_value) # type: ignore - assert value == baseline_value, f"invalid header field {header_field}" - - def build( - self, - *, - txs: List[Transaction], - ommers: List[Header], - withdrawals: List[Withdrawal] | None, - ) -> Tuple[Bytes, Hash]: - """ - Returns the serialized version of the block and its hash. - """ - header = [ - self.parent_hash, - self.ommers_hash, - self.coinbase, - self.state_root, - self.transactions_root, - self.receipt_root, - self.bloom, - Uint(int(self.difficulty)), - Uint(int(self.number)), - Uint(int(self.gas_limit)), - Uint(int(self.gas_used)), - Uint(int(self.timestamp)), - self.extra_data, - self.mix_digest, - self.nonce, - ] - if self.base_fee is not None: - header.append(Uint(int(self.base_fee))) - if self.withdrawals_root is not None: - header.append(self.withdrawals_root) - if self.blob_gas_used is not None: - header.append(Uint(int(self.blob_gas_used))) - if self.excess_blob_gas is not None: - header.append(Uint(self.excess_blob_gas)) - if self.beacon_root is not None: - header.append(self.beacon_root) - - block = [ - header, - transaction_list_to_serializable_list(txs), - ommers, # TODO: This is incorrect, and we probably need to serialize the ommers - ] - - if withdrawals is not None: - block.append([w.to_serializable_list() for w in withdrawals]) - - serialized_bytes = Bytes(eth_rlp.encode(block)) - - return serialized_bytes, Hash(keccak256(eth_rlp.encode(header))) - - -@dataclass(kw_only=True) -class Block(Header): - """ - Block type used to describe block properties in test specs - """ - - rlp: Optional[BytesConvertible] = None - """ - If set, blockchain test will skip generating the block and will pass this value directly to - the Fixture. - - Only meant to be used to simulate blocks with bad formats, and therefore - requires the block to produce an exception. - """ - header_verify: Optional[Header] = None - """ - If set, the block header will be verified against the specified values. - """ - rlp_modifier: Optional[Header] = None - """ - An RLP modifying header which values would be used to override the ones - returned by the `evm_transition_tool`. - """ - exception: Optional[str] = None - """ - If set, the block is expected to be rejected by the client. - """ - engine_api_error_code: Optional[EngineAPIError] = None - """ - If set, the block is expected to produce an error response from the Engine API. - """ - txs: Optional[List[Transaction]] = None - """ - List of transactions included in the block. - """ - ommers: Optional[List[Header]] = None - """ - List of ommer headers included in the block. - """ - withdrawals: Optional[List[Withdrawal]] = None - """ - List of withdrawals to perform for this block. - """ - - def set_environment(self, env: Environment) -> Environment: - """ - Creates a copy of the environment with the characteristics of this - specific block. - """ - new_env = copy(env) - - """ - Values that need to be set in the environment and are `None` for - this block need to be set to their defaults. - """ - environment_default = Environment() - new_env.difficulty = self.difficulty - new_env.coinbase = ( - self.coinbase if self.coinbase is not None else environment_default.coinbase - ) - new_env.gas_limit = ( - self.gas_limit if self.gas_limit is not None else environment_default.gas_limit - ) - if not isinstance(self.base_fee, Removable): - new_env.base_fee = self.base_fee - new_env.withdrawals = self.withdrawals - if not isinstance(self.excess_blob_gas, Removable): - new_env.excess_blob_gas = self.excess_blob_gas - if not isinstance(self.blob_gas_used, Removable): - new_env.blob_gas_used = self.blob_gas_used - if not isinstance(self.beacon_root, Removable): - new_env.beacon_root = self.beacon_root - """ - These values are required, but they depend on the previous environment, - so they can be calculated here. - """ - if self.number is not None: - new_env.number = self.number - else: - # calculate the next block number for the environment - if len(new_env.block_hashes) == 0: - new_env.number = 0 - else: - new_env.number = max([Number(n) for n in new_env.block_hashes.keys()]) + 1 - - if self.timestamp is not None: - new_env.timestamp = self.timestamp - else: - assert new_env.parent_timestamp is not None - new_env.timestamp = int(Number(new_env.parent_timestamp) + 12) - - return new_env - - def copy_with_rlp(self, rlp: Bytes | BytesConvertible | None) -> "Block": - """ - Creates a copy of the block and adds the specified RLP. - """ - new_block = deepcopy(self) - new_block.rlp = Bytes.or_none(rlp) - return new_block - - -@dataclass(kw_only=True) -class FixtureExecutionPayload(FixtureHeader): - """ - Representation of the execution payload of a block within a test fixture. - """ - - # Skipped fields in the Engine API - ommers_hash: Hash = field( - json_encoder=JSONEncoder.Field( - skip=True, - ), - ) - transactions_root: Hash = field( - json_encoder=JSONEncoder.Field( - skip=True, - ), - ) - difficulty: int = field( - json_encoder=JSONEncoder.Field( - skip=True, - ) - ) - nonce: HeaderNonce = field( - json_encoder=JSONEncoder.Field( - skip=True, - ) - ) - withdrawals_root: Optional[Hash] = field( - default=None, - json_encoder=JSONEncoder.Field( - skip=True, - ), - ) - - # Fields with different names - coinbase: Address = field( - json_encoder=JSONEncoder.Field( - name="feeRecipient", - ) - ) - receipt_root: Hash = field( - json_encoder=JSONEncoder.Field( - name="receiptsRoot", - ), - ) - bloom: Bloom = field( - json_encoder=JSONEncoder.Field( - name="logsBloom", - ) - ) - mix_digest: Hash = field( - json_encoder=JSONEncoder.Field( - name="prevRandao", - ), - ) - hash: Optional[Hash] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="blockHash", - ), - ) - - # Fields with different formatting - number: int = field( - json_encoder=JSONEncoder.Field( - name="blockNumber", - cast_type=HexNumber, - ) - ) - gas_limit: int = field(json_encoder=JSONEncoder.Field(name="gasLimit", cast_type=HexNumber)) - gas_used: int = field(json_encoder=JSONEncoder.Field(name="gasUsed", cast_type=HexNumber)) - timestamp: int = field(json_encoder=JSONEncoder.Field(cast_type=HexNumber)) - base_fee: Optional[int] = field( - default=None, - json_encoder=JSONEncoder.Field(name="baseFeePerGas", cast_type=HexNumber), - ) - blob_gas_used: Optional[int] = field( - default=None, - json_encoder=JSONEncoder.Field(name="blobGasUsed", cast_type=HexNumber), - ) - excess_blob_gas: Optional[int] = field( - default=None, - json_encoder=JSONEncoder.Field(name="excessBlobGas", cast_type=HexNumber), - ) - - # Fields only used in the Engine API - transactions: Optional[List[Transaction]] = field( - default=None, - json_encoder=JSONEncoder.Field( - cast_type=lambda txs: [Bytes(tx.serialized_bytes()) for tx in txs], - to_json=True, - ), - ) - withdrawals: Optional[List[Withdrawal]] = field( - default=None, - json_encoder=JSONEncoder.Field( - to_json=True, - ), - ) - - @classmethod - def from_fixture_header( - cls, - header: FixtureHeader, - transactions: Optional[List[Transaction]] = None, - withdrawals: Optional[List[Withdrawal]] = None, - ) -> "FixtureExecutionPayload": - """ - Returns a FixtureExecutionPayload from a FixtureHeader, a list - of transactions and a list of withdrawals. - """ - kwargs = {field.name: getattr(header, field.name) for field in fields(header)} - return cls(**kwargs, transactions=transactions, withdrawals=withdrawals) - - -@dataclass(kw_only=True) -class FixtureEngineNewPayload: - """ - Representation of the `engine_newPayloadVX` information to be - sent using the block information. - """ - - payload: FixtureExecutionPayload = field( - json_encoder=JSONEncoder.Field( - name="executionPayload", - to_json=True, - ) - ) - blob_versioned_hashes: Optional[List[FixedSizeBytesConvertible]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="expectedBlobVersionedHashes", - cast_type=lambda hashes: [Hash(hash) for hash in hashes], - to_json=True, - ), - ) - beacon_root: Optional[FixedSizeBytesConvertible] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="parentBeaconBlockRoot", - cast_type=Hash, - ), - ) - valid: bool = field( - json_encoder=JSONEncoder.Field( - skip_string_convert=True, - ), - ) - version: int = field( - json_encoder=JSONEncoder.Field(), - ) - error_code: Optional[EngineAPIError] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="errorCode", - cast_type=int, - ), - ) - - @classmethod - def from_fixture_header( - cls, - fork: Fork, - header: FixtureHeader, - transactions: List[Transaction], - withdrawals: Optional[List[Withdrawal]], - valid: bool, - error_code: Optional[EngineAPIError], - ) -> Optional["FixtureEngineNewPayload"]: - """ - Creates a `FixtureEngineNewPayload` from a `FixtureHeader`. - """ - new_payload_version = fork.engine_new_payload_version(header.number, header.timestamp) - - if new_payload_version is None: - return None - - new_payload = cls( - payload=FixtureExecutionPayload.from_fixture_header( - header=replace(header, beacon_root=None), - transactions=transactions, - withdrawals=withdrawals, - ), - version=new_payload_version, - valid=valid, - error_code=error_code, - ) - - if fork.engine_new_payload_blob_hashes(header.number, header.timestamp): - new_payload.blob_versioned_hashes = blob_versioned_hashes_from_transactions( - transactions - ) - - if fork.engine_new_payload_beacon_root(header.number, header.timestamp): - new_payload.beacon_root = header.beacon_root - - return new_payload - - -@dataclass(kw_only=True) -class FixtureBlock: - """ - Representation of an Ethereum block within a test Fixture. - """ - - @staticmethod - def _txs_encoder(txs: List[Transaction]) -> List[FixtureTransaction]: - return [FixtureTransaction.from_transaction(tx) for tx in txs] - - @staticmethod - def _withdrawals_encoder(withdrawals: List[Withdrawal]) -> List[FixtureWithdrawal]: - return [FixtureWithdrawal.from_withdrawal(w) for w in withdrawals] - - rlp: Bytes = field( - default=None, - json_encoder=JSONEncoder.Field(), - ) - block_header: Optional[FixtureHeader] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="blockHeader", - to_json=True, - ), - ) - expected_exception: Optional[str] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="expectException", - ), - ) - block_number: Optional[NumberConvertible] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="blocknumber", - cast_type=Number, - ), - ) - txs: Optional[List[Transaction]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="transactions", - cast_type=_txs_encoder, - to_json=True, - ), - ) - ommers: Optional[List[FixtureHeader]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="uncleHeaders", - to_json=True, - ), - ) - withdrawals: Optional[List[Withdrawal]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="withdrawals", - cast_type=_withdrawals_encoder, - to_json=True, - ), - ) - - -@dataclass(kw_only=True) -class InvalidFixtureBlock: - """ - Representation of an invalid Ethereum block within a test Fixture. - """ - - rlp: Bytes = field( - json_encoder=JSONEncoder.Field(), - ) - expected_exception: Optional[str] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="expectException", - ), - ) - rlp_decoded: FixtureBlock = field( - default=None, - json_encoder=JSONEncoder.Field( - name="rlp_decoded", - to_json=True, - ), - ) - - -@dataclass(kw_only=True) -class BaseFixture: - """ - Base Ethereum test fixture fields class. - """ - - info: Dict[str, str] = field( - default_factory=dict, - json_encoder=JSONEncoder.Field( - name="_info", - to_json=True, - ), - ) - name: str = field( - default="", - json_encoder=JSONEncoder.Field( - skip=True, - ), - ) - fork: str = field( - json_encoder=JSONEncoder.Field( - name="network", - ), - ) - _json: Dict[str, Any] | None = field( - default=None, - json_encoder=JSONEncoder.Field( - skip=True, - ), - ) - - def __post_init__(self): - """ - Post init hook to convert to JSON after instantiation. - """ - self._json = to_json(self) - - def to_json(self) -> Dict[str, Any]: - """ - Convert to JSON. - """ - assert self._json is not None, "Fixture not initialized" - self._json["_info"] = self.info - return self._json - - def fill_info( - self, - t8n: TransitionTool, - ref_spec: ReferenceSpec | None, - ): - """ - Fill the info field for this fixture - """ - self.info["filling-transition-tool"] = t8n.version() - if ref_spec is not None: - ref_spec.write_info(self.info) - - -@dataclass(kw_only=True) -class Fixture(BaseFixture): - """ - Cross-client specific test fixture information. - """ - - genesis_rlp: Bytes = field( - json_encoder=JSONEncoder.Field( - name="genesisRLP", - ), - ) - genesis: FixtureHeader = field( - json_encoder=JSONEncoder.Field( - name="genesisBlockHeader", - to_json=True, - ), - ) - blocks: Optional[List[FixtureBlock | InvalidFixtureBlock]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="blocks", - to_json=True, - ), - ) - last_block_hash: Hash = field( - json_encoder=JSONEncoder.Field( - name="lastblockhash", - ), - ) - pre_state: Mapping[str, Account] = field( - json_encoder=JSONEncoder.Field( - name="pre", - cast_type=Alloc, - to_json=True, - ), - ) - post_state: Optional[Mapping[str, Account]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="postState", - cast_type=Alloc, - to_json=True, - ), - ) - seal_engine: str = field( - default="NoProof", - json_encoder=JSONEncoder.Field( - name="sealEngine", - ), - ) - - -@dataclass(kw_only=True) -class HiveFixture(BaseFixture): - """ - Hive specific test fixture information. - """ - - genesis: FixtureHeader = field( - json_encoder=JSONEncoder.Field( - name="genesisBlockHeader", - to_json=True, - ), - ) - payloads: Optional[List[Optional[FixtureEngineNewPayload]]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="engineNewPayloads", - to_json=True, - ), - ) - fcu_version: Optional[int] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="engineFcuVersion", - ), - ) - pre_state: Mapping[str, Account] = field( - json_encoder=JSONEncoder.Field( - name="pre", - cast_type=Alloc, - to_json=True, - ), - ) - post_state: Optional[Mapping[str, Account]] = field( - default=None, - json_encoder=JSONEncoder.Field( - name="postState", - cast_type=Alloc, - to_json=True, - ), - ) diff --git a/src/ethereum_test_tools/filling/__init__.py b/src/ethereum_test_tools/filling/__init__.py deleted file mode 100644 index 60ae0519d73..00000000000 --- a/src/ethereum_test_tools/filling/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Test filling methods. -""" -from .fill import fill_test - -__all__ = ("fill_test",) diff --git a/src/ethereum_test_tools/filling/fill.py b/src/ethereum_test_tools/filling/fill.py deleted file mode 100644 index 742c87de83f..00000000000 --- a/src/ethereum_test_tools/filling/fill.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Test filler definitions. -""" -from typing import List, Optional, Union - -from ethereum_test_forks import Fork -from evm_transition_tool import TransitionTool - -from ..common import Fixture, HiveFixture -from ..reference_spec.reference_spec import ReferenceSpec -from ..spec import BaseTest - - -def fill_test( - t8n: TransitionTool, - test_spec: BaseTest, - fork: Fork, - spec: ReferenceSpec | None, - eips: Optional[List[int]] = None, -) -> Optional[Union[Fixture, HiveFixture]]: - """ - Fills default/hive fixture for the specified fork and test spec. - """ - fixture: Union[Fixture, HiveFixture] - t8n.reset_traces() - if test_spec.base_test_config.enable_hive: - if fork.engine_new_payload_version() is None: - return None # pre Merge tests are not supported in Hive - fixture = test_spec.make_hive_fixture(t8n, fork, eips) - else: - fixture = test_spec.make_fixture(t8n, fork, eips) - fixture.fill_info(t8n, spec) - return fixture diff --git a/src/ethereum_test_tools/spec/__init__.py b/src/ethereum_test_tools/spec/__init__.py index 8150f9b8c4f..9f32e30838a 100644 --- a/src/ethereum_test_tools/spec/__init__.py +++ b/src/ethereum_test_tools/spec/__init__.py @@ -1,19 +1,28 @@ """ Test spec definitions and utilities. """ -from .base_test import BaseTest, BaseTestConfig, TestSpec, verify_post_alloc -from .blockchain_test import BlockchainTest, BlockchainTestFiller, BlockchainTestSpec -from .state_test import StateTest, StateTestFiller, StateTestSpec +from typing import List, Type + +from .base.base_test import BaseFixture, BaseTest, TestSpec, verify_post_alloc +from .blockchain.blockchain_test import BlockchainTest, BlockchainTestFiller, BlockchainTestSpec +from .fixture_collector import FixtureCollector, TestInfo +from .state.state_test import StateTest, StateTestFiller, StateTestOnly, StateTestSpec + +SPEC_TYPES: List[Type[BaseTest]] = [BlockchainTest, StateTest, StateTestOnly] __all__ = ( + "SPEC_TYPES", + "BaseFixture", "BaseTest", - "BaseTestConfig", "BlockchainTest", "BlockchainTestFiller", "BlockchainTestSpec", + "FixtureCollector", "StateTest", "StateTestFiller", + "StateTestOnly", "StateTestSpec", + "TestInfo", "TestSpec", "verify_post_alloc", ) diff --git a/src/ethereum_test_tools/spec/base/__init__.py b/src/ethereum_test_tools/spec/base/__init__.py new file mode 100644 index 00000000000..0519b8f7a2c --- /dev/null +++ b/src/ethereum_test_tools/spec/base/__init__.py @@ -0,0 +1,3 @@ +""" +BaseTest spec class and utilities. +""" diff --git a/src/ethereum_test_tools/spec/base_test.py b/src/ethereum_test_tools/spec/base/base_test.py similarity index 58% rename from src/ethereum_test_tools/spec/base_test.py rename to src/ethereum_test_tools/spec/base/base_test.py index e5965b83614..98fee32e9ca 100644 --- a/src/ethereum_test_tools/spec/base_test.py +++ b/src/ethereum_test_tools/spec/base/base_test.py @@ -5,21 +5,17 @@ from dataclasses import dataclass, field from itertools import count from os import path -from typing import Any, Callable, Dict, Generator, Iterator, List, Mapping, Optional +from pathlib import Path +from typing import Any, Callable, Dict, Generator, Iterator, List, Mapping, Optional, TextIO from ethereum_test_forks import Fork -from evm_transition_tool import TransitionTool +from evm_transition_tool import FixtureFormats, TransitionTool -from ..common import ( - Account, - Address, - Environment, - Fixture, - HiveFixture, - Transaction, - withdrawals_root, -) -from ..common.conversions import to_hex +from ...common import Account, Address, Environment, Transaction, withdrawals_root +from ...common.conversions import to_hex +from ...common.json import JSONEncoder +from ...common.json import field as json_field +from ...reference_spec.reference_spec import ReferenceSpec def verify_transactions(txs: List[Transaction] | None, result) -> List[int]: @@ -78,65 +74,120 @@ def verify_result(result: Mapping, env: Environment): @dataclass(kw_only=True) -class BaseTestConfig: +class BaseFixture: """ - General configuration that all tests must support. + Represents a base Ethereum test fixture of any type. """ - enable_hive: bool = False - """ - Enable any hive-related properties that the output could contain. - """ + info: Dict[str, str] = json_field( + default_factory=dict, + json_encoder=JSONEncoder.Field( + name="_info", + to_json=True, + ), + ) + + def fill_info( + self, + t8n: TransitionTool, + ref_spec: ReferenceSpec | None, + ): + """ + Fill the info field for this fixture + """ + self.info["filling-transition-tool"] = t8n.version() + if ref_spec is not None: + ref_spec.write_info(self.info) + + @classmethod + @abstractmethod + def format(cls) -> FixtureFormats: + """ + Returns the fixture format which the evm tool can use to determine how to verify the + fixture. + """ + pass + + @classmethod + @abstractmethod + def collect_into_file(cls, fd: TextIO, fixtures: Dict[str, "BaseFixture"]): + """ + Returns the name of the subdirectory where this type of fixture should be dumped to. + """ + pass + + @classmethod + @abstractmethod + def output_base_dir_name(cls) -> Path: + """ + Returns the name of the subdirectory where this type of fixture should be dumped to. + """ + pass + + @classmethod + def output_file_extension(cls) -> str: + """ + Returns the file extension for this type of fixture. + + By default, fixtures are dumped as JSON files. + """ + return ".json" @dataclass(kw_only=True) class BaseTest: """ - Represents a base Ethereum test which must return a genesis and a - blockchain. + Represents a base Ethereum test which must return a single test fixture. """ pre: Mapping tag: str = "" - base_test_config: BaseTestConfig = field(default_factory=BaseTestConfig) + # Setting a default here is just for type checking, the correct value is automatically set + # by pytest. + fixture_format: FixtureFormats = FixtureFormats.UNSET_TEST_FORMAT # Transition tool specific fields t8n_dump_dir: Optional[str] = "" t8n_call_counter: Iterator[int] = field(init=False, default_factory=count) @abstractmethod - def make_fixture( + def generate( self, t8n: TransitionTool, fork: Fork, eips: Optional[List[int]] = None, - ) -> Fixture: + ) -> BaseFixture: """ - Generate blockchain that must be executed sequentially during test. + Generate the list of test fixtures. """ pass + @classmethod @abstractmethod - def make_hive_fixture( - self, - t8n: TransitionTool, - fork: Fork, - eips: Optional[List[int]] = None, - ) -> HiveFixture: + def pytest_parameter_name(cls) -> str: """ - Generate the blockchain that must be executed sequentially during test. + Must return the name of the parameter used in pytest to select this + spec type as filler for the test. """ pass @classmethod @abstractmethod - def pytest_parameter_name(cls) -> str: + def fixture_formats(cls) -> List[FixtureFormats]: """ - Must return the name of the parameter used in pytest to select this - spec type as filler for the test. + Returns a list of fixture formats that can be output to the test spec. """ pass + def __post_init__(self) -> None: + """ + Validate the fixture format. + """ + if self.fixture_format not in self.fixture_formats(): + raise ValueError( + f"Invalid fixture format {self.fixture_format} for {self.__class__.__name__}." + ) + def get_next_transition_tool_output_path(self) -> str: """ Returns the path to the next transition tool output file. diff --git a/src/ethereum_test_tools/spec/blockchain/__init__.py b/src/ethereum_test_tools/spec/blockchain/__init__.py new file mode 100644 index 00000000000..4a58da648e0 --- /dev/null +++ b/src/ethereum_test_tools/spec/blockchain/__init__.py @@ -0,0 +1,3 @@ +""" +BlockchainTest type definitions and logic +""" diff --git a/src/ethereum_test_tools/spec/blockchain_test.py b/src/ethereum_test_tools/spec/blockchain/blockchain_test.py similarity index 71% rename from src/ethereum_test_tools/spec/blockchain_test.py rename to src/ethereum_test_tools/spec/blockchain/blockchain_test.py index 0a4c32a509e..28051d46350 100644 --- a/src/ethereum_test_tools/spec/blockchain_test.py +++ b/src/ethereum_test_tools/spec/blockchain/blockchain_test.py @@ -1,40 +1,92 @@ """ Ethereum blockchain test spec definition and filler. """ - +from copy import copy from dataclasses import dataclass, field from pprint import pprint from typing import Any, Callable, Dict, Generator, List, Mapping, Optional, Tuple, Type from ethereum_test_forks import Fork -from evm_transition_tool import TransitionTool +from evm_transition_tool import FixtureFormats, TransitionTool -from ..common import ( +from ...common import ( Address, Alloc, - Block, Bloom, Bytes, EmptyTrieRoot, Environment, - Fixture, - FixtureBlock, - FixtureEngineNewPayload, - FixtureHeader, Hash, HeaderNonce, - HiveFixture, - InvalidFixtureBlock, Number, Transaction, ZeroPaddedHexNumber, alloc_to_accounts, - to_json, + transaction_list_root, withdrawals_root, ) -from ..common.constants import EmptyOmmersRoot -from .base_test import BaseTest, verify_post_alloc, verify_result, verify_transactions -from .debugging import print_traces +from ...common.constants import EmptyOmmersRoot +from ...common.json import to_json +from ..base.base_test import ( + BaseFixture, + BaseTest, + verify_post_alloc, + verify_result, + verify_transactions, +) +from ..debugging import print_traces +from .types import ( + Block, + Fixture, + FixtureBlock, + FixtureEngineNewPayload, + FixtureHeader, + HiveFixture, + InvalidFixtureBlock, +) + + +def environment_from_parent_header(parent: "FixtureHeader") -> "Environment": + """ + Instantiates a new environment with the provided header as parent. + """ + return Environment( + parent_difficulty=parent.difficulty, + parent_timestamp=parent.timestamp, + parent_base_fee=parent.base_fee, + parent_blob_gas_used=parent.blob_gas_used, + parent_excess_blob_gas=parent.excess_blob_gas, + parent_gas_used=parent.gas_used, + parent_gas_limit=parent.gas_limit, + parent_ommers_hash=parent.ommers_hash, + block_hashes={parent.number: parent.hash if parent.hash is not None else 0}, + ) + + +def apply_new_parent(env: Environment, new_parent: FixtureHeader) -> "Environment": + """ + Applies a header as parent to a copy of this environment. + """ + env = copy(env) + env.parent_difficulty = new_parent.difficulty + env.parent_timestamp = new_parent.timestamp + env.parent_base_fee = new_parent.base_fee + env.parent_blob_gas_used = new_parent.blob_gas_used + env.parent_excess_blob_gas = new_parent.excess_blob_gas + env.parent_gas_used = new_parent.gas_used + env.parent_gas_limit = new_parent.gas_limit + env.parent_ommers_hash = new_parent.ommers_hash + env.block_hashes[new_parent.number] = new_parent.hash if new_parent.hash is not None else 0 + return env + + +def count_blobs(txs: List[Transaction]) -> int: + """ + Returns the number of blobs in a list of transactions. + """ + return sum( + [len(tx.blob_versioned_hashes) for tx in txs if tx.blob_versioned_hashes is not None] + ) @dataclass(kw_only=True) @@ -57,12 +109,15 @@ def pytest_parameter_name(cls) -> str: """ return "blockchain_test" - @property - def hive_enabled(self) -> bool: + @classmethod + def fixture_formats(cls) -> List[FixtureFormats]: """ - Returns true if hive fixture generation is enabled, false otherwise. + Returns a list of fixture formats that can be output to the test spec. """ - return self.base_test_config.enable_hive + return [ + FixtureFormats.BLOCKCHAIN_TEST, + FixtureFormats.BLOCKCHAIN_TEST_HIVE, + ] def make_genesis( self, @@ -148,7 +203,9 @@ def generate_block_data( alloc=previous_alloc, txs=to_json(txs), env=to_json(env), - fork_name=fork.fork(block_number=Number(env.number), timestamp=Number(env.timestamp)), + fork_name=fork.transition_tool_name( + block_number=Number(env.number), timestamp=Number(env.timestamp) + ), chain_id=self.chain_id, reward=fork.get_reward(Number(env.number), Number(env.timestamp)), eips=eips, @@ -182,6 +239,18 @@ def generate_block_data( environment=env, ) + # Update the transactions root to the one calculated locally. + header.transactions_root = transaction_list_root(txs) + + # One special case of the invalid transactions is the blob gas used, since this value + # is not included in the transition tool result, but it is included in the block header, + # and some clients check it before executing the block by simply counting the type-3 txs, + # we need to set the correct value by default. + if ( + blob_gas_per_blob := fork.blob_gas_per_blob(Number(env.number), Number(env.timestamp)) + ) > 0: + header.blob_gas_used = blob_gas_per_blob * count_blobs(txs) + if block.header_verify is not None: # Verify the header after transition tool processing. header.verify(block.header_verify) @@ -199,11 +268,15 @@ def generate_block_data( return header, rlp, txs, next_alloc, env - def network_info(self, fork, eips=None): + def network_info(self, fork: Fork, eips: Optional[List[int]] = None): """ Returns fixture network information for the fork & EIP/s. """ - return "+".join([fork.name()] + [str(eip) for eip in eips]) if eips else fork.name() + return ( + "+".join([fork.blockchain_test_network_name()] + [str(eip) for eip in eips]) + if eips + else fork.blockchain_test_network_name() + ) def verify_post_state(self, t8n, alloc): """ @@ -229,7 +302,7 @@ def make_fixture( pre, genesis_rlp, genesis = self.make_genesis(t8n, fork) alloc = to_json(pre) - env = Environment.from_parent_header(genesis) + env = environment_from_parent_header(genesis) head = genesis.hash if genesis.hash is not None else Hash(0) for block in self.blocks: @@ -253,7 +326,7 @@ def make_fixture( ) # Update env, alloc and last block hash for the next block. alloc = new_alloc - env = new_env.apply_new_parent(header) + env = apply_new_parent(new_env, header) head = header.hash if header.hash is not None else Hash(0) else: fixture_blocks.append( @@ -301,7 +374,7 @@ def make_hive_fixture( pre, _, genesis = self.make_genesis(t8n, fork) alloc = to_json(pre) - env = Environment.from_parent_header(genesis) + env = environment_from_parent_header(genesis) for block in self.blocks: header, _, txs, new_alloc, new_env = self.generate_block_data( @@ -320,7 +393,7 @@ def make_hive_fixture( ) if block.exception is None: alloc = new_alloc - env = env.apply_new_parent(header) + env = apply_new_parent(env, header) fcu_version = fork.engine_forkchoice_updated_version(header.number, header.timestamp) self.verify_post_state(t8n, alloc) @@ -334,6 +407,28 @@ def make_hive_fixture( name=self.tag, ) + def generate( + self, + t8n: TransitionTool, + fork: Fork, + eips: Optional[List[int]] = None, + ) -> BaseFixture: + """ + Generate the BlockchainTest fixture. + """ + t8n.reset_traces() + if self.fixture_format == FixtureFormats.BLOCKCHAIN_TEST_HIVE: + if fork.engine_forkchoice_updated_version() is None: + raise Exception( + "A hive fixture was requested but no forkchoice update is defined. " + "The framework should never try to execute this test case." + ) + return self.make_hive_fixture(t8n, fork, eips) + elif self.fixture_format == FixtureFormats.BLOCKCHAIN_TEST: + return self.make_fixture(t8n, fork, eips) + + raise Exception(f"Unknown fixture format: {self.fixture_format}") + BlockchainTestSpec = Callable[[str], Generator[BlockchainTest, None, None]] BlockchainTestFiller = Type[BlockchainTest] diff --git a/src/ethereum_test_tools/spec/blockchain/types.py b/src/ethereum_test_tools/spec/blockchain/types.py new file mode 100644 index 00000000000..b9b8a369309 --- /dev/null +++ b/src/ethereum_test_tools/spec/blockchain/types.py @@ -0,0 +1,1038 @@ +""" +BlockchainTest types +""" +import json +from copy import copy, deepcopy +from dataclasses import dataclass, fields, replace +from pathlib import Path +from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, TextIO, Tuple + +from ethereum import rlp as eth_rlp +from ethereum.base_types import Uint +from ethereum.crypto.hash import keccak256 + +from ethereum_test_forks import Fork +from evm_transition_tool import FixtureFormats + +from ...common.constants import EmptyOmmersRoot, EngineAPIError +from ...common.conversions import BytesConvertible, FixedSizeBytesConvertible, NumberConvertible +from ...common.json import JSONEncoder, field, to_json +from ...common.types import ( + Account, + Address, + Alloc, + Bloom, + Bytes, + Environment, + FixtureTransaction, + FixtureWithdrawal, + Hash, + HeaderNonce, + HexNumber, + Number, + Removable, + Transaction, + Withdrawal, + ZeroPaddedHexNumber, + blob_versioned_hashes_from_transactions, + transaction_list_to_serializable_list, +) +from ..base.base_test import BaseFixture + + +@dataclass(kw_only=True) +class Header: + """ + Header type used to describe block header properties in test specs. + """ + + parent_hash: Optional[FixedSizeBytesConvertible] = None + ommers_hash: Optional[FixedSizeBytesConvertible] = None + coinbase: Optional[FixedSizeBytesConvertible] = None + state_root: Optional[FixedSizeBytesConvertible] = None + transactions_root: Optional[FixedSizeBytesConvertible] = None + receipt_root: Optional[FixedSizeBytesConvertible] = None + bloom: Optional[FixedSizeBytesConvertible] = None + difficulty: Optional[NumberConvertible] = None + number: Optional[NumberConvertible] = None + gas_limit: Optional[NumberConvertible] = None + gas_used: Optional[NumberConvertible] = None + timestamp: Optional[NumberConvertible] = None + extra_data: Optional[BytesConvertible] = None + mix_digest: Optional[FixedSizeBytesConvertible] = None + nonce: Optional[FixedSizeBytesConvertible] = None + base_fee: Optional[NumberConvertible | Removable] = None + withdrawals_root: Optional[FixedSizeBytesConvertible | Removable] = None + blob_gas_used: Optional[NumberConvertible | Removable] = None + excess_blob_gas: Optional[NumberConvertible | Removable] = None + beacon_root: Optional[FixedSizeBytesConvertible | Removable] = None + hash: Optional[FixedSizeBytesConvertible] = None + + REMOVE_FIELD: ClassVar[Removable] = Removable() + """ + Sentinel object used to specify that a header field should be removed. + """ + EMPTY_FIELD: ClassVar[Removable] = Removable() + """ + Sentinel object used to specify that a header field must be empty during verification. + """ + + +@dataclass(kw_only=True) +class HeaderFieldSource: + """ + Block header field metadata specifying the source used to populate the field when collecting + the block header from different sources, and to validate it. + """ + + required: bool = True + """ + Whether the field is required or not, regardless of the fork. + """ + fork_requirement_check: Optional[str] = None + """ + Name of the method to call to check if the field is required for the current fork. + """ + default: Optional[Any] = None + """ + Default value for the field if no value was provided by either the transition tool or the + environment + """ + parse_type: Optional[Callable] = None + """ + The type or function to use to parse the field to before initializing the object. + """ + source_environment: Optional[str] = None + """ + Name of the field in the environment object, which can be a callable. + """ + source_transition_tool: Optional[str] = None + """ + Name of the field in the transition tool result dictionary. + """ + + def collect( + self, + *, + target: Dict[str, Any], + field_name: str, + fork: Fork, + number: int, + timestamp: int, + transition_tool_result: Dict[str, Any], + environment: Environment, + ) -> None: + """ + Collects the field from the different sources according to the + metadata description. + """ + value = None + required = self.required + if self.fork_requirement_check is not None: + required = getattr(fork, self.fork_requirement_check)(number, timestamp) + + if self.source_transition_tool is not None: + if self.source_transition_tool in transition_tool_result: + got_value = transition_tool_result.get(self.source_transition_tool) + if got_value is not None: + value = got_value + + if self.source_environment is not None: + got_value = getattr(environment, self.source_environment, None) + if callable(got_value): + got_value = got_value() + if got_value is not None: + value = got_value + + if required: + if value is None: + if self.default is not None: + value = self.default + else: + raise ValueError(f"missing required field '{field_name}'") + + if value is not None and self.parse_type is not None: + value = self.parse_type(value) + + target[field_name] = value + + +def header_field(*args, source: Optional[HeaderFieldSource] = None, **kwargs) -> Any: + """ + A wrapper around `dataclasses.field` that allows for json configuration info and header + metadata. + """ + if "metadata" in kwargs: + metadata = kwargs["metadata"] + else: + metadata = {} + assert isinstance(metadata, dict) + + if source is not None: + metadata["source"] = source + + kwargs["metadata"] = metadata + return field(*args, **kwargs) + + +@dataclass(kw_only=True) +class FixtureHeader: + """ + Representation of an Ethereum header within a test Fixture. + """ + + parent_hash: Hash = header_field( + source=HeaderFieldSource( + parse_type=Hash, + source_environment="parent_hash", + ), + json_encoder=JSONEncoder.Field(name="parentHash"), + ) + ommers_hash: Hash = header_field( + source=HeaderFieldSource( + parse_type=Hash, + source_transition_tool="sha3Uncles", + default=EmptyOmmersRoot, + ), + json_encoder=JSONEncoder.Field(name="uncleHash"), + ) + coinbase: Address = header_field( + source=HeaderFieldSource( + parse_type=Address, + source_environment="coinbase", + ), + json_encoder=JSONEncoder.Field(), + ) + state_root: Hash = header_field( + source=HeaderFieldSource( + parse_type=Hash, + source_transition_tool="stateRoot", + ), + json_encoder=JSONEncoder.Field(name="stateRoot"), + ) + transactions_root: Hash = header_field( + source=HeaderFieldSource( + parse_type=Hash, + source_transition_tool="txRoot", + ), + json_encoder=JSONEncoder.Field(name="transactionsTrie"), + ) + receipt_root: Hash = header_field( + source=HeaderFieldSource( + parse_type=Hash, + source_transition_tool="receiptsRoot", + ), + json_encoder=JSONEncoder.Field(name="receiptTrie"), + ) + bloom: Bloom = header_field( + source=HeaderFieldSource( + parse_type=Bloom, + source_transition_tool="logsBloom", + ), + json_encoder=JSONEncoder.Field(), + ) + difficulty: int = header_field( + source=HeaderFieldSource( + parse_type=Number, + source_transition_tool="currentDifficulty", + source_environment="difficulty", + default=0, + ), + json_encoder=JSONEncoder.Field(cast_type=ZeroPaddedHexNumber), + ) + number: int = header_field( + source=HeaderFieldSource( + parse_type=Number, + source_environment="number", + ), + json_encoder=JSONEncoder.Field(cast_type=ZeroPaddedHexNumber), + ) + gas_limit: int = header_field( + source=HeaderFieldSource( + parse_type=Number, + source_environment="gas_limit", + ), + json_encoder=JSONEncoder.Field(name="gasLimit", cast_type=ZeroPaddedHexNumber), + ) + gas_used: int = header_field( + source=HeaderFieldSource( + parse_type=Number, + source_transition_tool="gasUsed", + ), + json_encoder=JSONEncoder.Field(name="gasUsed", cast_type=ZeroPaddedHexNumber), + ) + timestamp: int = header_field( + source=HeaderFieldSource( + parse_type=Number, + source_environment="timestamp", + ), + json_encoder=JSONEncoder.Field(cast_type=ZeroPaddedHexNumber), + ) + extra_data: Bytes = header_field( + source=HeaderFieldSource( + parse_type=Bytes, + source_environment="extra_data", + default=b"", + ), + json_encoder=JSONEncoder.Field(name="extraData"), + ) + mix_digest: Hash = header_field( + source=HeaderFieldSource( + parse_type=Hash, + source_environment="prev_randao", + default=b"", + ), + json_encoder=JSONEncoder.Field(name="mixHash"), + ) + nonce: HeaderNonce = header_field( + source=HeaderFieldSource( + parse_type=HeaderNonce, + default=b"", + ), + json_encoder=JSONEncoder.Field(), + ) + base_fee: Optional[int] = header_field( + default=None, + source=HeaderFieldSource( + parse_type=Number, + fork_requirement_check="header_base_fee_required", + source_transition_tool="currentBaseFee", + source_environment="base_fee", + ), + json_encoder=JSONEncoder.Field(name="baseFeePerGas", cast_type=ZeroPaddedHexNumber), + ) + withdrawals_root: Optional[Hash] = header_field( + default=None, + source=HeaderFieldSource( + parse_type=Hash, + fork_requirement_check="header_withdrawals_required", + source_transition_tool="withdrawalsRoot", + ), + json_encoder=JSONEncoder.Field(name="withdrawalsRoot"), + ) + blob_gas_used: Optional[int] = header_field( + default=None, + source=HeaderFieldSource( + parse_type=Number, + fork_requirement_check="header_blob_gas_used_required", + source_transition_tool="blobGasUsed", + ), + json_encoder=JSONEncoder.Field(name="blobGasUsed", cast_type=ZeroPaddedHexNumber), + ) + excess_blob_gas: Optional[int] = header_field( + default=None, + source=HeaderFieldSource( + parse_type=Number, + fork_requirement_check="header_excess_blob_gas_required", + source_transition_tool="currentExcessBlobGas", + ), + json_encoder=JSONEncoder.Field(name="excessBlobGas", cast_type=ZeroPaddedHexNumber), + ) + beacon_root: Optional[Hash] = header_field( + default=None, + source=HeaderFieldSource( + parse_type=Hash, + fork_requirement_check="header_beacon_root_required", + source_environment="beacon_root", + ), + json_encoder=JSONEncoder.Field(name="parentBeaconBlockRoot"), + ) + hash: Optional[Hash] = header_field( + default=None, + source=HeaderFieldSource( + required=False, + ), + json_encoder=JSONEncoder.Field(), + ) + + @classmethod + def collect( + cls, + *, + fork: Fork, + transition_tool_result: Dict[str, Any], + environment: Environment, + ) -> "FixtureHeader": + """ + Collects a FixtureHeader object from multiple sources: + - The transition tool result + - The test's current environment + """ + # We depend on the environment to get the number and timestamp to check the fork + # requirements + number, timestamp = Number(environment.number), Number(environment.timestamp) + + # Collect the header fields + kwargs: Dict[str, Any] = {} + for header_field in fields(cls): + field_name = header_field.name + metadata = header_field.metadata + assert metadata is not None, f"Field {field_name} has no header field metadata" + field_metadata = metadata.get("source") + assert isinstance(field_metadata, HeaderFieldSource), ( + f"Field {field_name} has invalid header_field " f"metadata: {field_metadata}" + ) + field_metadata.collect( + target=kwargs, + field_name=field_name, + fork=fork, + number=number, + timestamp=timestamp, + transition_tool_result=transition_tool_result, + environment=environment, + ) + + # Pass the collected fields as keyword arguments to the constructor + return cls(**kwargs) + + def join(self, modifier: Header) -> "FixtureHeader": + """ + Produces a fixture header copy with the set values from the modifier. + """ + new_fixture_header = copy(self) + for header_field in self.__dataclass_fields__: + value = getattr(modifier, header_field) + if value is not None: + if value is Header.REMOVE_FIELD: + setattr(new_fixture_header, header_field, None) + else: + setattr(new_fixture_header, header_field, value) + return new_fixture_header + + def verify(self, baseline: Header): + """ + Verifies that the header fields from the baseline are as expected. + """ + for header_field in fields(self): + field_name = header_field.name + baseline_value = getattr(baseline, field_name) + if baseline_value is not None: + assert baseline_value is not Header.REMOVE_FIELD, "invalid baseline header" + value = getattr(self, field_name) + if baseline_value is Header.EMPTY_FIELD: + assert ( + value is None + ), f"invalid header field {field_name}, got {value}, want None" + continue + metadata = header_field.metadata + field_metadata = metadata.get("source") + # type check is performed on collect() + if field_metadata.parse_type is not None: # type: ignore + baseline_value = field_metadata.parse_type(baseline_value) # type: ignore + assert value == baseline_value, ( + f"invalid header field ({field_name}) value, " + + f"got {value}, want {baseline_value}" + ) + + def build( + self, + *, + txs: List[Transaction], + ommers: List[Header], + withdrawals: List[Withdrawal] | None, + ) -> Tuple[Bytes, Hash]: + """ + Returns the serialized version of the block and its hash. + """ + header = [ + self.parent_hash, + self.ommers_hash, + self.coinbase, + self.state_root, + self.transactions_root, + self.receipt_root, + self.bloom, + Uint(int(self.difficulty)), + Uint(int(self.number)), + Uint(int(self.gas_limit)), + Uint(int(self.gas_used)), + Uint(int(self.timestamp)), + self.extra_data, + self.mix_digest, + self.nonce, + ] + if self.base_fee is not None: + header.append(Uint(int(self.base_fee))) + if self.withdrawals_root is not None: + header.append(self.withdrawals_root) + if self.blob_gas_used is not None: + header.append(Uint(int(self.blob_gas_used))) + if self.excess_blob_gas is not None: + header.append(Uint(self.excess_blob_gas)) + if self.beacon_root is not None: + header.append(self.beacon_root) + + block = [ + header, + transaction_list_to_serializable_list(txs), + ommers, # TODO: This is incorrect, and we probably need to serialize the ommers + ] + + if withdrawals is not None: + block.append([w.to_serializable_list() for w in withdrawals]) + + serialized_bytes = Bytes(eth_rlp.encode(block)) + + return serialized_bytes, Hash(keccak256(eth_rlp.encode(header))) + + +@dataclass(kw_only=True) +class Block(Header): + """ + Block type used to describe block properties in test specs + """ + + rlp: Optional[BytesConvertible] = None + """ + If set, blockchain test will skip generating the block and will pass this value directly to + the Fixture. + + Only meant to be used to simulate blocks with bad formats, and therefore + requires the block to produce an exception. + """ + header_verify: Optional[Header] = None + """ + If set, the block header will be verified against the specified values. + """ + rlp_modifier: Optional[Header] = None + """ + An RLP modifying header which values would be used to override the ones + returned by the `evm_transition_tool`. + """ + exception: Optional[str] = None + """ + If set, the block is expected to be rejected by the client. + """ + engine_api_error_code: Optional[EngineAPIError] = None + """ + If set, the block is expected to produce an error response from the Engine API. + """ + txs: Optional[List[Transaction]] = None + """ + List of transactions included in the block. + """ + ommers: Optional[List[Header]] = None + """ + List of ommer headers included in the block. + """ + withdrawals: Optional[List[Withdrawal]] = None + """ + List of withdrawals to perform for this block. + """ + + def set_environment(self, env: Environment) -> Environment: + """ + Creates a copy of the environment with the characteristics of this + specific block. + """ + new_env = copy(env) + + """ + Values that need to be set in the environment and are `None` for + this block need to be set to their defaults. + """ + environment_default = Environment() + new_env.difficulty = self.difficulty + new_env.coinbase = ( + self.coinbase if self.coinbase is not None else environment_default.coinbase + ) + new_env.gas_limit = ( + self.gas_limit if self.gas_limit is not None else environment_default.gas_limit + ) + if not isinstance(self.base_fee, Removable): + new_env.base_fee = self.base_fee + new_env.withdrawals = self.withdrawals + if not isinstance(self.excess_blob_gas, Removable): + new_env.excess_blob_gas = self.excess_blob_gas + if not isinstance(self.blob_gas_used, Removable): + new_env.blob_gas_used = self.blob_gas_used + if not isinstance(self.beacon_root, Removable): + new_env.beacon_root = self.beacon_root + """ + These values are required, but they depend on the previous environment, + so they can be calculated here. + """ + if self.number is not None: + new_env.number = self.number + else: + # calculate the next block number for the environment + if len(new_env.block_hashes) == 0: + new_env.number = 0 + else: + new_env.number = max([Number(n) for n in new_env.block_hashes.keys()]) + 1 + + if self.timestamp is not None: + new_env.timestamp = self.timestamp + else: + assert new_env.parent_timestamp is not None + new_env.timestamp = int(Number(new_env.parent_timestamp) + 12) + + return new_env + + def copy_with_rlp(self, rlp: Bytes | BytesConvertible | None) -> "Block": + """ + Creates a copy of the block and adds the specified RLP. + """ + new_block = deepcopy(self) + new_block.rlp = Bytes.or_none(rlp) + return new_block + + +@dataclass(kw_only=True) +class FixtureExecutionPayload(FixtureHeader): + """ + Representation of the execution payload of a block within a test fixture. + """ + + # Skipped fields in the Engine API + ommers_hash: Hash = field( + json_encoder=JSONEncoder.Field( + skip=True, + ), + ) + transactions_root: Hash = field( + json_encoder=JSONEncoder.Field( + skip=True, + ), + ) + difficulty: int = field( + json_encoder=JSONEncoder.Field( + skip=True, + ) + ) + nonce: HeaderNonce = field( + json_encoder=JSONEncoder.Field( + skip=True, + ) + ) + withdrawals_root: Optional[Hash] = field( + default=None, + json_encoder=JSONEncoder.Field( + skip=True, + ), + ) + + # Fields with different names + coinbase: Address = field( + json_encoder=JSONEncoder.Field( + name="feeRecipient", + ) + ) + receipt_root: Hash = field( + json_encoder=JSONEncoder.Field( + name="receiptsRoot", + ), + ) + bloom: Bloom = field( + json_encoder=JSONEncoder.Field( + name="logsBloom", + ) + ) + mix_digest: Hash = field( + json_encoder=JSONEncoder.Field( + name="prevRandao", + ), + ) + hash: Optional[Hash] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="blockHash", + ), + ) + + # Fields with different formatting + number: int = field( + json_encoder=JSONEncoder.Field( + name="blockNumber", + cast_type=HexNumber, + ) + ) + gas_limit: int = field(json_encoder=JSONEncoder.Field(name="gasLimit", cast_type=HexNumber)) + gas_used: int = field(json_encoder=JSONEncoder.Field(name="gasUsed", cast_type=HexNumber)) + timestamp: int = field(json_encoder=JSONEncoder.Field(cast_type=HexNumber)) + base_fee: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field(name="baseFeePerGas", cast_type=HexNumber), + ) + blob_gas_used: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field(name="blobGasUsed", cast_type=HexNumber), + ) + excess_blob_gas: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field(name="excessBlobGas", cast_type=HexNumber), + ) + + # Fields only used in the Engine API + transactions: Optional[List[Transaction]] = field( + default=None, + json_encoder=JSONEncoder.Field( + cast_type=lambda txs: [Bytes(tx.serialized_bytes()) for tx in txs], + to_json=True, + ), + ) + withdrawals: Optional[List[Withdrawal]] = field( + default=None, + json_encoder=JSONEncoder.Field( + to_json=True, + ), + ) + + @classmethod + def from_fixture_header( + cls, + header: FixtureHeader, + transactions: Optional[List[Transaction]] = None, + withdrawals: Optional[List[Withdrawal]] = None, + ) -> "FixtureExecutionPayload": + """ + Returns a FixtureExecutionPayload from a FixtureHeader, a list + of transactions and a list of withdrawals. + """ + kwargs = {field.name: getattr(header, field.name) for field in fields(header)} + return cls(**kwargs, transactions=transactions, withdrawals=withdrawals) + + +@dataclass(kw_only=True) +class FixtureEngineNewPayload: + """ + Representation of the `engine_newPayloadVX` information to be + sent using the block information. + """ + + payload: FixtureExecutionPayload = field( + json_encoder=JSONEncoder.Field( + name="executionPayload", + to_json=True, + ) + ) + blob_versioned_hashes: Optional[List[FixedSizeBytesConvertible]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="expectedBlobVersionedHashes", + cast_type=lambda hashes: [Hash(hash) for hash in hashes], + to_json=True, + ), + ) + beacon_root: Optional[FixedSizeBytesConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="parentBeaconBlockRoot", + cast_type=Hash, + ), + ) + valid: bool = field( + json_encoder=JSONEncoder.Field( + skip_string_convert=True, + ), + ) + version: int = field( + json_encoder=JSONEncoder.Field(), + ) + error_code: Optional[EngineAPIError] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="errorCode", + cast_type=int, + ), + ) + + @classmethod + def from_fixture_header( + cls, + fork: Fork, + header: FixtureHeader, + transactions: List[Transaction], + withdrawals: Optional[List[Withdrawal]], + valid: bool, + error_code: Optional[EngineAPIError], + ) -> Optional["FixtureEngineNewPayload"]: + """ + Creates a `FixtureEngineNewPayload` from a `FixtureHeader`. + """ + new_payload_version = fork.engine_new_payload_version(header.number, header.timestamp) + + if new_payload_version is None: + return None + + new_payload = cls( + payload=FixtureExecutionPayload.from_fixture_header( + header=replace(header, beacon_root=None), + transactions=transactions, + withdrawals=withdrawals, + ), + version=new_payload_version, + valid=valid, + error_code=error_code, + ) + + if fork.engine_new_payload_blob_hashes(header.number, header.timestamp): + new_payload.blob_versioned_hashes = blob_versioned_hashes_from_transactions( + transactions + ) + + if fork.engine_new_payload_beacon_root(header.number, header.timestamp): + new_payload.beacon_root = header.beacon_root + + return new_payload + + +@dataclass(kw_only=True) +class FixtureBlock: + """ + Representation of an Ethereum block within a test Fixture. + """ + + @staticmethod + def _txs_encoder(txs: List[Transaction]) -> List[FixtureTransaction]: + return [FixtureTransaction.from_transaction(tx) for tx in txs] + + @staticmethod + def _withdrawals_encoder(withdrawals: List[Withdrawal]) -> List[FixtureWithdrawal]: + return [FixtureWithdrawal.from_withdrawal(w) for w in withdrawals] + + rlp: Bytes = field( + default=None, + json_encoder=JSONEncoder.Field(), + ) + block_header: Optional[FixtureHeader] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="blockHeader", + to_json=True, + ), + ) + expected_exception: Optional[str] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="expectException", + ), + ) + block_number: Optional[NumberConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="blocknumber", + cast_type=Number, + ), + ) + txs: Optional[List[Transaction]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="transactions", + cast_type=_txs_encoder, + to_json=True, + ), + ) + ommers: Optional[List[FixtureHeader]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="uncleHeaders", + to_json=True, + ), + ) + withdrawals: Optional[List[Withdrawal]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="withdrawals", + cast_type=_withdrawals_encoder, + to_json=True, + ), + ) + + +@dataclass(kw_only=True) +class InvalidFixtureBlock: + """ + Representation of an invalid Ethereum block within a test Fixture. + """ + + rlp: Bytes = field( + json_encoder=JSONEncoder.Field(), + ) + expected_exception: Optional[str] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="expectException", + ), + ) + rlp_decoded: FixtureBlock = field( + default=None, + json_encoder=JSONEncoder.Field( + name="rlp_decoded", + to_json=True, + ), + ) + + +@dataclass(kw_only=True) +class FixtureCommon(BaseFixture): + """ + Base Ethereum test fixture fields class. + """ + + name: str = field( + default="", + json_encoder=JSONEncoder.Field( + skip=True, + ), + ) + fork: str = field( + json_encoder=JSONEncoder.Field( + name="network", + ), + ) + _json: Dict[str, Any] | None = field( + default=None, + json_encoder=JSONEncoder.Field( + skip=True, + ), + ) + + def __post_init__(self): + """ + Post init hook to convert to JSON after instantiation. + """ + self._json = to_json(self) + + def to_json(self) -> Dict[str, Any]: + """ + Convert to JSON. + """ + assert self._json is not None, "Fixture not initialized" + self._json["_info"] = self.info + return self._json + + @classmethod + def collect_into_file(cls, fd: TextIO, fixtures: Dict[str, "BaseFixture"]): + """ + For BlockchainTest format, we simply join the json fixtures into a single file. + """ + json_fixtures: Dict[str, Dict[str, Any]] = {} + for name, fixture in fixtures.items(): + assert isinstance(fixture, FixtureCommon), f"Invalid fixture type: {type(fixture)}" + json_fixtures[name] = fixture.to_json() + json.dump(json_fixtures, fd, indent=4) + + +@dataclass(kw_only=True) +class Fixture(FixtureCommon): + """ + Cross-client specific test fixture information. + """ + + genesis_rlp: Bytes = field( + json_encoder=JSONEncoder.Field( + name="genesisRLP", + ), + ) + genesis: FixtureHeader = field( + json_encoder=JSONEncoder.Field( + name="genesisBlockHeader", + to_json=True, + ), + ) + blocks: Optional[List[FixtureBlock | InvalidFixtureBlock]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="blocks", + to_json=True, + ), + ) + last_block_hash: Hash = field( + json_encoder=JSONEncoder.Field( + name="lastblockhash", + ), + ) + pre_state: Mapping[str, Account] = field( + json_encoder=JSONEncoder.Field( + name="pre", + cast_type=Alloc, + to_json=True, + ), + ) + post_state: Optional[Mapping[str, Account]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="postState", + cast_type=Alloc, + to_json=True, + ), + ) + seal_engine: str = field( + default="NoProof", + json_encoder=JSONEncoder.Field( + name="sealEngine", + ), + ) + + @classmethod + def output_base_dir_name(cls) -> Path: + """ + Returns the name of the subdirectory where this type of fixture should be dumped to. + """ + return Path("blockchain_tests") + + @classmethod + def format(cls) -> FixtureFormats: + """ + Returns the fixture format which the evm tool can use to determine how to verify the + fixture. + """ + return FixtureFormats.BLOCKCHAIN_TEST + + +@dataclass(kw_only=True) +class HiveFixture(FixtureCommon): + """ + Hive specific test fixture information. + """ + + genesis: FixtureHeader = field( + json_encoder=JSONEncoder.Field( + name="genesisBlockHeader", + to_json=True, + ), + ) + payloads: Optional[List[Optional[FixtureEngineNewPayload]]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="engineNewPayloads", + to_json=True, + ), + ) + fcu_version: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="engineFcuVersion", + ), + ) + pre_state: Mapping[str, Account] = field( + json_encoder=JSONEncoder.Field( + name="pre", + cast_type=Alloc, + to_json=True, + ), + ) + post_state: Optional[Mapping[str, Account]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="postState", + cast_type=Alloc, + to_json=True, + ), + ) + + @classmethod + def output_base_dir_name(cls) -> Path: + """ + Returns the name of the subdirectory where this type of fixture should be dumped to. + """ + return Path("blockchain_tests_hive") + + @classmethod + def format(cls) -> FixtureFormats: + """ + Returns the fixture format which the evm tool can use to determine how to verify the + fixture. + """ + return FixtureFormats.BLOCKCHAIN_TEST_HIVE diff --git a/src/ethereum_test_tools/spec/fixture_collector.py b/src/ethereum_test_tools/spec/fixture_collector.py new file mode 100644 index 00000000000..a9ea55e6159 --- /dev/null +++ b/src/ethereum_test_tools/spec/fixture_collector.py @@ -0,0 +1,199 @@ +""" +Fixture collector class used to collect, sort and combine the different types of generated +fixtures. +""" + +import os +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, Literal, Optional, Tuple + +from evm_transition_tool import FixtureFormats, TransitionTool + +from .base.base_test import BaseFixture + + +def strip_test_prefix(name: str) -> str: + """ + Removes the test prefix from a test case name. + """ + TEST_PREFIX = "test_" + if name.startswith(TEST_PREFIX): + return name[len(TEST_PREFIX) :] + return name + + +def get_module_relative_output_dir(test_module: Path, filler_path: Path) -> Path: + """ + Return a directory name for the provided test_module (relative to the + base ./tests directory) that can be used for output (within the + configured fixtures output path or the base_dump_dir directory). + + Example: + tests/shanghai/eip3855_push0/test_push0.py -> shanghai/eip3855_push0/test_push0 + """ + basename = test_module.with_suffix("").absolute() + basename_relative = basename.relative_to(filler_path.absolute()) + module_path = basename_relative.parent / basename_relative.stem + return module_path + + +@dataclass(kw_only=True) +class TestInfo: + """ + Contains test information from the current node. + """ + + name: str # pytest: Item.name + id: str # pytest: Item.nodeid + original_name: str # pytest: Item.originalname + path: Path # pytest: Item.path + + def get_name_and_parameters(self) -> Tuple[str, str]: + """ + Converts a test name to a tuple containing the test name and test parameters. + + Example: + test_push0_key_sstore[fork_Shanghai] -> test_push0_key_sstore, fork_Shanghai + """ + test_name, parameters = self.name.split("[") + return test_name, re.sub(r"[\[\-]", "_", parameters).replace("]", "") + + def get_single_test_name(self) -> str: + """ + Converts a test name to a single test name. + """ + test_name, test_parameters = self.get_name_and_parameters() + return f"{test_name}__{test_parameters}" + + def get_dump_dir_path( + self, + base_dump_dir: Optional[Path], + filler_path: Path, + level: Literal["test_module", "test_function", "test_parameter"] = "test_parameter", + ) -> Optional[Path]: + """ + The path to dump the debug output as defined by the level to dump at. + """ + if not base_dump_dir: + return None + test_module_relative_dir = get_module_relative_output_dir(self.path, filler_path) + if level == "test_module": + return Path(base_dump_dir) / Path(str(test_module_relative_dir).replace(os.sep, "__")) + test_name, test_parameter_string = self.get_name_and_parameters() + flat_path = f"{str(test_module_relative_dir).replace(os.sep, '__')}__{test_name}" + if level == "test_function": + return Path(base_dump_dir) / flat_path + elif level == "test_parameter": + return Path(base_dump_dir) / flat_path / test_parameter_string + raise Exception("Unexpected level.") + + +@dataclass(kw_only=True) +class FixtureCollector: + """ + Collects all fixtures generated by the test cases. + """ + + output_dir: str + flat_output: bool + single_fixture_per_file: bool + filler_path: Path + base_dump_dir: Optional[Path] = None + + # Internal state + all_fixtures: Dict[Path, Dict[str, BaseFixture]] = field(default_factory=dict) + json_path_to_fixture_type: Dict[Path, FixtureFormats] = field(default_factory=dict) + json_path_to_test_item: Dict[Path, TestInfo] = field(default_factory=dict) + + def get_fixture_basename(self, info: TestInfo) -> Path: + """ + Returns the basename of the fixture file for a given test case. + """ + if self.flat_output: + if self.single_fixture_per_file: + return Path(strip_test_prefix(info.get_single_test_name())) + return Path(strip_test_prefix(info.original_name)) + else: + relative_fixture_output_dir = Path(info.path).parent / strip_test_prefix( + Path(info.path).stem + ) + module_relative_output_dir = get_module_relative_output_dir( + relative_fixture_output_dir, self.filler_path + ) + + if self.single_fixture_per_file: + return module_relative_output_dir / strip_test_prefix(info.get_single_test_name()) + return module_relative_output_dir / strip_test_prefix(info.original_name) + + def add_fixture(self, info: TestInfo, fixture: BaseFixture) -> None: + """ + Adds a fixture to the list of fixtures of a given test case. + """ + fixture_basename = self.get_fixture_basename(info) + + fixture_path = ( + self.output_dir + / fixture.output_base_dir_name() + / fixture_basename.with_suffix(fixture.output_file_extension()) + ) + if fixture_path not in self.all_fixtures: # relevant when we group by test function + self.all_fixtures[fixture_path] = {} + if fixture_path in self.json_path_to_fixture_type: + if self.json_path_to_fixture_type[fixture_path] != fixture.format(): + raise Exception( + f"Fixture {fixture_path} has two different types: " + f"{self.json_path_to_fixture_type[fixture_path]} " + f"and {fixture.format()}" + ) + else: + self.json_path_to_fixture_type[fixture_path] = fixture.format() + self.json_path_to_test_item[fixture_path] = info + + self.all_fixtures[fixture_path][info.id] = fixture + + def dump_fixtures(self) -> None: + """ + Dumps all collected fixtures to their respective files. + """ + os.makedirs(self.output_dir, exist_ok=True) + for fixture_path, fixtures in self.all_fixtures.items(): + os.makedirs(fixture_path.parent, exist_ok=True) + + # Get the first fixture to dump to get its type + fixture = next(iter(fixtures.values())) + # Call class method to dump all the fixtures + with open(fixture_path, "w") as fd: + fixture.collect_into_file(fd, fixtures) + + def verify_fixture_files(self, evm_fixture_verification: TransitionTool) -> None: + """ + Runs `evm [state|block]test` on each fixture. + """ + for fixture_path, fixture_format in self.json_path_to_fixture_type.items(): + if FixtureFormats.is_verifiable(fixture_format): + info = self.json_path_to_test_item[fixture_path] + verify_fixtures_dump_dir = self._get_verify_fixtures_dump_dir(info) + evm_fixture_verification.verify_fixture( + fixture_format, fixture_path, verify_fixtures_dump_dir + ) + + def _get_verify_fixtures_dump_dir( + self, + info: TestInfo, + ): + """ + The directory to dump the current test function's fixture.json and fixture + verification debug output. + """ + if not self.base_dump_dir: + return None + if self.single_fixture_per_file: + return info.get_dump_dir_path( + self.base_dump_dir, self.filler_path, level="test_parameter" + ) + else: + return info.get_dump_dir_path( + self.base_dump_dir, self.filler_path, level="test_function" + ) diff --git a/src/ethereum_test_tools/spec/state/__init__.py b/src/ethereum_test_tools/spec/state/__init__.py new file mode 100644 index 00000000000..ed5114b881c --- /dev/null +++ b/src/ethereum_test_tools/spec/state/__init__.py @@ -0,0 +1,3 @@ +""" +StateTest type definitions and logic +""" diff --git a/src/ethereum_test_tools/spec/state/state_test.py b/src/ethereum_test_tools/spec/state/state_test.py new file mode 100644 index 00000000000..378c902a83e --- /dev/null +++ b/src/ethereum_test_tools/spec/state/state_test.py @@ -0,0 +1,227 @@ +""" +Ethereum state test spec definition and filler. +""" +from copy import copy +from dataclasses import dataclass +from typing import Callable, Generator, List, Mapping, Optional, Type + +from ethereum_test_forks import Cancun, Fork +from evm_transition_tool import FixtureFormats, TransitionTool + +from ...common import Account, Address, Alloc, Environment, Number, Transaction +from ...common.constants import EngineAPIError +from ...common.json import to_json +from ..base.base_test import BaseFixture, BaseTest, verify_post_alloc +from ..blockchain.blockchain_test import Block, BlockchainTest +from ..blockchain.types import Header +from ..debugging import print_traces +from .types import Fixture, FixtureForkPost + +BEACON_ROOTS_ADDRESS = Address(0x000F3DF6D732807EF1319FB7B8BB8522D0BEAC02) +TARGET_BLOB_GAS_PER_BLOCK = 393216 + + +@dataclass(kw_only=True) +class StateTest(BaseTest): + """ + Filler type that tests transactions over the period of a single block. + """ + + env: Environment + pre: Mapping + post: Mapping + tx: Transaction + engine_api_error_code: Optional[EngineAPIError] = None + blockchain_test_header_verify: Optional[Header] = None + blockchain_test_rlp_modifier: Optional[Header] = None + tag: str = "" + chain_id: int = 1 + + @classmethod + def pytest_parameter_name(cls) -> str: + """ + Returns the parameter name used to identify this filler in a test. + """ + return "state_test" + + @classmethod + def fixture_formats(cls) -> List[FixtureFormats]: + """ + Returns a list of fixture formats that can be output to the test spec. + """ + return [ + FixtureFormats.BLOCKCHAIN_TEST, + FixtureFormats.BLOCKCHAIN_TEST_HIVE, + FixtureFormats.STATE_TEST, + ] + + def _generate_blockchain_genesis_environment(self) -> Environment: + """ + Generate the genesis environment for the BlockchainTest formatted test. + """ + genesis_env = copy(self.env) + + # Modify values to the proper values for the genesis block + # TODO: All of this can be moved to a new method in `Fork` + genesis_env.withdrawals = None + genesis_env.beacon_root = None + genesis_env.number = Number(genesis_env.number) - 1 + assert ( + genesis_env.number >= 0 + ), "genesis block number cannot be negative, set state test env.number to 1" + if genesis_env.excess_blob_gas: + # The excess blob gas environment value means the value of the context (block header) + # where the transaction is executed. In a blockchain test, we need to indirectly + # set the excess blob gas by setting the excess blob gas of the genesis block + # to the expected value plus the TARGET_BLOB_GAS_PER_BLOCK, which is the value + # that will be subtracted from the excess blob gas when the first block is mined. + genesis_env.excess_blob_gas = ( + Number(genesis_env.excess_blob_gas) + TARGET_BLOB_GAS_PER_BLOCK + ) + + return genesis_env + + def _generate_blockchain_blocks(self) -> List[Block]: + """ + Generate the single block that represents this state test in a BlockchainTest format. + """ + return [ + Block( + number=self.env.number, + timestamp=self.env.timestamp, + coinbase=self.env.coinbase, + difficulty=self.env.difficulty, + gas_limit=self.env.gas_limit, + extra_data=self.env.extra_data, + withdrawals=self.env.withdrawals, + beacon_root=self.env.beacon_root, + txs=[self.tx], + ommers=[], + exception=self.tx.error, + header_verify=self.blockchain_test_header_verify, + rlp_modifier=self.blockchain_test_rlp_modifier, + ) + ] + + def generate_blockchain_test(self) -> BlockchainTest: + """ + Generate a BlockchainTest fixture from this StateTest fixture. + """ + return BlockchainTest( + genesis_environment=self._generate_blockchain_genesis_environment(), + pre=self.pre, + post=self.post, + blocks=self._generate_blockchain_blocks(), + fixture_format=self.fixture_format, + t8n_dump_dir=self.t8n_dump_dir, + ) + + def make_state_test_fixture( + self, + t8n: TransitionTool, + fork: Fork, + eips: Optional[List[int]] = None, + ) -> Fixture: + """ + Create a fixture from the state test definition. + """ + env = self.env.set_fork_requirements(fork) + tx = self.tx.with_signature_and_sender() + pre_alloc = Alloc.merge( + Alloc( + fork.pre_allocation(block_number=env.number, timestamp=Number(env.timestamp)), + ), + Alloc(self.pre), + ) + transition_tool_name = fork.transition_tool_name( + block_number=Number(self.env.number), + timestamp=Number(self.env.timestamp), + ) + fork_name = ( + "+".join([transition_tool_name] + [str(eip) for eip in eips]) + if eips + else transition_tool_name + ) + next_alloc, result = t8n.evaluate( + alloc=to_json(pre_alloc), + txs=to_json([tx]), + env=to_json(env), + fork_name=fork_name, + chain_id=self.chain_id, + reward=0, # Reward on state tests is always zero + eips=eips, + debug_output_path=self.get_next_transition_tool_output_path(), + ) + + try: + verify_post_alloc(self.post, next_alloc) + except Exception as e: + print_traces(t8n.get_traces()) + raise e + + # Perform post state processing required for some forks + if fork >= Cancun: + # StateTest does not execute any beacon root contract logic, but we still need to + # set the beacon root to the correct value, because most tests assume this happens, + # so we copy the beacon root contract storage from the post state into the pre state + # and the transaction is executed in isolation properly. + if beacon_roots_account := next_alloc.get(str(BEACON_ROOTS_ADDRESS)): + if beacon_roots_storage := beacon_roots_account.get("storage"): + pre_alloc = Alloc.merge( + pre_alloc, + Alloc({BEACON_ROOTS_ADDRESS: Account(storage=beacon_roots_storage)}), + ) + + return Fixture( + env=env, + pre_state=pre_alloc, + post={ + fork.blockchain_test_network_name(): [ + FixtureForkPost.collect( + transition_tool_result=result, + transaction=tx.with_signature_and_sender(), + ) + ] + }, + transaction=tx, + ) + + def generate( + self, + t8n: TransitionTool, + fork: Fork, + eips: Optional[List[int]] = None, + ) -> BaseFixture: + """ + Generate the BlockchainTest fixture. + """ + if self.fixture_format in BlockchainTest.fixture_formats(): + return self.generate_blockchain_test().generate(t8n, fork, eips) + elif self.fixture_format == FixtureFormats.STATE_TEST: + return self.make_state_test_fixture(t8n, fork, eips) + + raise Exception(f"Unknown fixture format: {self.fixture_format}") + + +class StateTestOnly(StateTest): + """ + StateTest filler that only generates a state test fixture. + """ + + @classmethod + def pytest_parameter_name(cls) -> str: + """ + Returns the parameter name used to identify this filler in a test. + """ + return "state_test_only" + + @classmethod + def fixture_formats(cls) -> List[FixtureFormats]: + """ + Returns a list of fixture formats that can be output to the test spec. + """ + return [FixtureFormats.STATE_TEST] + + +StateTestSpec = Callable[[str], Generator[StateTest, None, None]] +StateTestFiller = Type[StateTest] diff --git a/src/ethereum_test_tools/spec/state/types.py b/src/ethereum_test_tools/spec/state/types.py new file mode 100644 index 00000000000..1ce55bcf179 --- /dev/null +++ b/src/ethereum_test_tools/spec/state/types.py @@ -0,0 +1,361 @@ +""" +StateTest types +""" +import json +from dataclasses import dataclass, fields +from pathlib import Path +from typing import Any, Dict, List, Mapping, Optional, Sequence, TextIO + +from evm_transition_tool import FixtureFormats + +from ...common.conversions import BytesConvertible, FixedSizeBytesConvertible +from ...common.json import JSONEncoder, field, to_json +from ...common.types import ( + AccessList, + AddrAA, + Address, + Alloc, + Bytes, + Environment, + Hash, + HexNumber, + Number, + NumberConvertible, + Transaction, + ZeroPaddedHexNumber, +) +from ..base.base_test import BaseFixture + + +@dataclass(kw_only=True) +class FixtureEnvironment: + """ + Type used to describe the environment of a state test. + """ + + coinbase: FixedSizeBytesConvertible = field( + default="0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + json_encoder=JSONEncoder.Field( + name="currentCoinbase", + cast_type=Address, + ), + ) + gas_limit: NumberConvertible = field( + default=100000000000000000, + json_encoder=JSONEncoder.Field( + name="currentGasLimit", + cast_type=Number, + ), + ) + number: NumberConvertible = field( + default=1, + json_encoder=JSONEncoder.Field( + name="currentNumber", + cast_type=Number, + ), + ) + timestamp: NumberConvertible = field( + default=1000, + json_encoder=JSONEncoder.Field( + name="currentTimestamp", + cast_type=Number, + ), + ) + prev_randao: Optional[NumberConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="currentRandom", + cast_type=Number, + ), + ) + difficulty: Optional[NumberConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="currentDifficulty", + cast_type=Number, + ), + ) + base_fee: Optional[NumberConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="currentBaseFee", + cast_type=Number, + ), + ) + excess_blob_gas: Optional[NumberConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="currentExcessBlobGas", + cast_type=Number, + ), + ) + + @classmethod + def from_env(cls, env: Environment) -> "FixtureEnvironment": + """ + Returns a FixtureEnvironment from an Environment. + """ + kwargs = {field.name: getattr(env, field.name) for field in fields(cls)} + return cls(**kwargs) + + +@dataclass(kw_only=True) +class FixtureTransaction: + """ + Type used to describe a transaction in a state test. + """ + + nonce: int = field( + default=0, + json_encoder=JSONEncoder.Field( + cast_type=ZeroPaddedHexNumber, + ), + ) + gas_price: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="gasPrice", + cast_type=ZeroPaddedHexNumber, + ), + ) + max_priority_fee_per_gas: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="maxPriorityFeePerGas", + cast_type=HexNumber, + ), + ) + max_fee_per_gas: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="maxFeePerGas", + cast_type=HexNumber, + ), + ) + gas_limit: int = field( + default=21000, + json_encoder=JSONEncoder.Field( + name="gasLimit", + cast_type=lambda x: [ZeroPaddedHexNumber(x)], # Converted to list + to_json=True, + ), + ) + to: Optional[FixedSizeBytesConvertible] = field( + default=AddrAA, + json_encoder=JSONEncoder.Field( + cast_type=Address, + ), + ) + value: int = field( + default=0, + json_encoder=JSONEncoder.Field( + cast_type=lambda x: [ZeroPaddedHexNumber(x)], # Converted to list + to_json=True, + ), + ) + data: BytesConvertible = field( + default_factory=bytes, + json_encoder=JSONEncoder.Field( + cast_type=lambda x: [Bytes(x)], + to_json=True, + ), + ) + access_list: Optional[List[AccessList]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="accessLists", + cast_type=lambda x: [x], # Converted to list of lists + to_json=True, + ), + ) + max_fee_per_blob_gas: Optional[int] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="maxFeePerBlobGas", + cast_type=HexNumber, + ), + ) + blob_versioned_hashes: Optional[Sequence[FixedSizeBytesConvertible]] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="blobVersionedHashes", + cast_type=lambda x: [Hash(k) for k in x], + to_json=True, + ), + ) + + sender: Optional[FixedSizeBytesConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + cast_type=Address, + ), + ) + secret_key: Optional[FixedSizeBytesConvertible] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="secretKey", + cast_type=Hash, + ), + ) + + @classmethod + def from_transaction(cls, tx: Transaction) -> "FixtureTransaction": + """ + Returns a FixtureTransaction from a Transaction. + """ + kwargs = {field.name: getattr(tx, field.name) for field in fields(cls)} + return cls(**kwargs) + + +@dataclass(kw_only=True) +class FixtureForkPostIndexes: + """ + Type used to describe the indexes of a single post state of a single Fork. + """ + + data: int = field(default=0, json_encoder=JSONEncoder.Field(skip_string_convert=True)) + gas: int = field(default=0, json_encoder=JSONEncoder.Field(skip_string_convert=True)) + value: int = field(default=0, json_encoder=JSONEncoder.Field(skip_string_convert=True)) + + +@dataclass(kw_only=True) +class FixtureForkPost: + """ + Type used to describe the post state of a single Fork. + """ + + state_root: Hash = field( + json_encoder=JSONEncoder.Field( + name="hash", + ), + ) + logs_hash: Hash = field( + json_encoder=JSONEncoder.Field( + name="logs", + ), + ) + tx_bytes: BytesConvertible = field( + json_encoder=JSONEncoder.Field( + name="txbytes", + cast_type=Bytes, + ), + ) + expected_exception: Optional[str] = field( + default=None, + json_encoder=JSONEncoder.Field( + name="expectException", + ), + ) + indexes: FixtureForkPostIndexes = field( + json_encoder=JSONEncoder.Field( + to_json=True, + ), + ) + + @classmethod + def collect( + cls, + *, + transition_tool_result: Dict[str, Any], + transaction: Transaction, + ) -> "FixtureForkPost": + """ + Collects the post state of a single Fork from the transition tool result. + """ + state_root = Hash(transition_tool_result["stateRoot"]) + logs_hash = Hash(transition_tool_result["logsHash"]) + indexes = FixtureForkPostIndexes() + return cls( + state_root=state_root, + logs_hash=logs_hash, + tx_bytes=transaction.serialized_bytes(), + expected_exception=transaction.error, + indexes=indexes, + ) + + +@dataclass(kw_only=True) +class Fixture(BaseFixture): + """ + Fixture for a single StateTest. + """ + + env: Environment = field( + json_encoder=JSONEncoder.Field( + cast_type=FixtureEnvironment.from_env, + to_json=True, + ), + ) + + pre_state: Alloc = field( + json_encoder=JSONEncoder.Field( + name="pre", + cast_type=Alloc, + to_json=True, + ), + ) + + transaction: Transaction = field( + json_encoder=JSONEncoder.Field( + to_json=True, + cast_type=FixtureTransaction.from_transaction, + ), + ) + + post: Mapping[str, List[FixtureForkPost]] = field( + default_factory=dict, + json_encoder=JSONEncoder.Field( + name="post", + to_json=True, + ), + ) + + _json: Dict[str, Any] | None = field( + default=None, + json_encoder=JSONEncoder.Field( + skip=True, + ), + ) + + def __post_init__(self): + """ + Post init hook to convert to JSON after instantiation. + """ + self._json = to_json(self) + + def to_json(self) -> Dict[str, Any]: + """ + Convert to JSON. + """ + assert self._json is not None, "Fixture not initialized" + self._json["_info"] = self.info + return self._json + + @classmethod + def collect_into_file(cls, fd: TextIO, fixtures: Dict[str, "BaseFixture"]): + """ + For StateTest format, we simply join the json fixtures into a single file. + + We could do extra processing like combining tests that use the same pre-state, + and similar transaction, but this is not done for now. + """ + json_fixtures: Dict[str, Dict[str, Any]] = {} + for name, fixture in fixtures.items(): + assert isinstance(fixture, Fixture), f"Invalid fixture type: {type(fixture)}" + json_fixtures[name] = fixture.to_json() + json.dump(json_fixtures, fd, indent=4) + + @classmethod + def output_base_dir_name(cls) -> Path: + """ + Returns the name of the subdirectory where this type of fixture should be dumped to. + """ + return Path("state_tests") + + @classmethod + def format(cls) -> FixtureFormats: + """ + Returns the fixture format which the evm tool can use to determine how to verify the + fixture. + """ + return FixtureFormats.STATE_TEST diff --git a/src/ethereum_test_tools/spec/state_test.py b/src/ethereum_test_tools/spec/state_test.py deleted file mode 100644 index 74321496b4e..00000000000 --- a/src/ethereum_test_tools/spec/state_test.py +++ /dev/null @@ -1,246 +0,0 @@ -""" -Ethereum state test spec definition and filler. -""" -from copy import copy -from dataclasses import dataclass -from typing import Any, Callable, Dict, Generator, List, Mapping, Optional, Tuple, Type - -from ethereum_test_forks import Fork -from evm_transition_tool import TransitionTool - -from ..common import ( - Address, - Alloc, - Bloom, - Bytes, - EmptyTrieRoot, - Environment, - Fixture, - FixtureBlock, - FixtureEngineNewPayload, - FixtureHeader, - Hash, - HeaderNonce, - HiveFixture, - Number, - Transaction, - ZeroPaddedHexNumber, - alloc_to_accounts, - to_json, -) -from ..common.constants import EmptyOmmersRoot, EngineAPIError -from .base_test import BaseTest, verify_post_alloc, verify_result, verify_transactions -from .debugging import print_traces - - -@dataclass(kw_only=True) -class StateTest(BaseTest): - """ - Filler type that tests transactions over the period of a single block. - """ - - env: Environment - pre: Mapping - post: Mapping - txs: List[Transaction] - engine_api_error_code: Optional[EngineAPIError] = None - tag: str = "" - chain_id: int = 1 - - @classmethod - def pytest_parameter_name(cls) -> str: - """ - Returns the parameter name used to identify this filler in a test. - """ - return "state_test" - - def make_genesis( - self, - t8n: TransitionTool, - fork: Fork, - ) -> Tuple[Alloc, Bytes, FixtureHeader]: - """ - Create a genesis block from the state test definition. - """ - # Similar to the block 1 environment specified by the test - # with some slight differences, so make a copy here - genesis_env = copy(self.env) - - # Modify values to the proper values for the genesis block - genesis_env.withdrawals = None - genesis_env.beacon_root = None - genesis_env.number = Number(genesis_env.number) - 1 - assert ( - genesis_env.number >= 0 - ), "genesis block number cannot be negative, set state test env.number to 1" - - genesis_env.set_fork_requirements(fork, in_place=True) - pre_alloc = Alloc( - fork.pre_allocation( - block_number=genesis_env.number, timestamp=Number(genesis_env.timestamp) - ) - ) - new_alloc, state_root = t8n.calc_state_root( - alloc=to_json(Alloc.merge(pre_alloc, Alloc(self.pre))), - fork=fork, - debug_output_path=self.get_next_transition_tool_output_path(), - ) - genesis = FixtureHeader( - parent_hash=Hash(0), - ommers_hash=Hash(EmptyOmmersRoot), - coinbase=Address(0), - state_root=Hash(state_root), - transactions_root=Hash(EmptyTrieRoot), - receipt_root=Hash(EmptyTrieRoot), - bloom=Bloom(0), - difficulty=ZeroPaddedHexNumber( - 0x20000 if genesis_env.difficulty is None else genesis_env.difficulty - ), - number=ZeroPaddedHexNumber(genesis_env.number), - gas_limit=ZeroPaddedHexNumber(genesis_env.gas_limit), - gas_used=0, - timestamp=0, - extra_data=Bytes([0]), - mix_digest=Hash(0), - nonce=HeaderNonce(0), - base_fee=ZeroPaddedHexNumber.or_none(genesis_env.base_fee), - blob_gas_used=ZeroPaddedHexNumber.or_none(genesis_env.blob_gas_used), - excess_blob_gas=ZeroPaddedHexNumber.or_none(genesis_env.excess_blob_gas), - withdrawals_root=Hash.or_none( - EmptyTrieRoot if genesis_env.withdrawals is not None else None - ), - beacon_root=Hash.or_none(genesis_env.beacon_root), - ) - - genesis_rlp, genesis.hash = genesis.build( - txs=[], - ommers=[], - withdrawals=genesis_env.withdrawals, - ) - - return Alloc(new_alloc), genesis_rlp, genesis - - def generate_fixture_data( - self, t8n: TransitionTool, fork: Fork, eips: Optional[List[int]] = None - ) -> Tuple[FixtureHeader, Bytes, Alloc, List[Transaction], Dict, Dict[str, Any], str]: - """ - Generate common fixture data for both make_fixture and make_hive_fixture. - """ - pre, genesis_rlp, genesis = self.make_genesis(t8n, fork) - network_info = ( - "+".join([fork.name()] + [str(eip) for eip in eips]) if eips else fork.name() - ) - - self.env = self.env.apply_new_parent(genesis).set_fork_requirements(fork) - txs = [tx.with_signature_and_sender() for tx in self.txs] if self.txs else [] - - t8n_alloc, t8n_result = t8n.evaluate( - alloc=to_json(pre), - txs=to_json(txs), - env=to_json(self.env), - fork_name=network_info, - chain_id=self.chain_id, - reward=fork.get_reward(Number(self.env.number), Number(self.env.timestamp)), - eips=eips, - debug_output_path=self.get_next_transition_tool_output_path(), - ) - - rejected_txs = verify_transactions(txs, t8n_result) - if len(rejected_txs) > 0: - raise Exception( - "one or more transactions in `StateTest` are " - + "intrinsically invalid, which are not allowed. " - + "Use `BlockchainTest` to verify rejection of blocks " - + "that include invalid transactions." - ) - try: - verify_post_alloc(self.post, t8n_alloc) - verify_result(t8n_result, self.env) - except Exception as e: - print_traces(traces=t8n.get_traces()) - raise e - - return genesis, genesis_rlp, pre, txs, t8n_result, t8n_alloc, network_info - - def make_fixture( - self, t8n: TransitionTool, fork: Fork, eips: Optional[List[int]] = None - ) -> Fixture: - """ - Create a fixture from the state test definition. - """ - ( - genesis, - genesis_rlp, - pre, - txs, - t8n_result, - t8n_alloc, - network_info, - ) = self.generate_fixture_data(t8n, fork, eips) - header = FixtureHeader.collect( - fork=fork, transition_tool_result=t8n_result, environment=self.env - ) - block, header.hash = header.build(txs=txs, ommers=[], withdrawals=self.env.withdrawals) - - return Fixture( - fork=network_info, - genesis=genesis, - genesis_rlp=genesis_rlp, - blocks=[ - FixtureBlock( - rlp=block, - block_header=header, - txs=txs, - ommers=[], - withdrawals=self.env.withdrawals, - ) - ], - last_block_hash=header.hash, - pre_state=pre, - post_state=alloc_to_accounts(t8n_alloc), - name=self.tag, - ) - - def make_hive_fixture( - self, t8n: TransitionTool, fork: Fork, eips: Optional[List[int]] = None - ) -> HiveFixture: - """ - Create a hive fixture from the state test definition. - """ - ( - genesis, - _, - pre, - txs, - t8n_result, - t8n_alloc, - network_info, - ) = self.generate_fixture_data(t8n, fork, eips) - - header = FixtureHeader.collect( - fork=fork, transition_tool_result=t8n_result, environment=self.env - ) - _, header.hash = header.build(txs=txs, ommers=[], withdrawals=self.env.withdrawals) - fixture_payload = FixtureEngineNewPayload.from_fixture_header( - fork=fork, - header=header, - transactions=txs, - withdrawals=self.env.withdrawals, - valid=True, - error_code=None, - ) - fcu_version = fork.engine_forkchoice_updated_version(header.number, header.timestamp) - - return HiveFixture( - fork=network_info, - genesis=genesis, - payloads=[fixture_payload], - fcu_version=fcu_version, - pre_state=pre, - post_state=alloc_to_accounts(t8n_alloc), - name=self.tag, - ) - - -StateTestSpec = Callable[[str], Generator[StateTest, None, None]] -StateTestFiller = Type[StateTest] diff --git a/src/ethereum_test_tools/tests/test_code.py b/src/ethereum_test_tools/tests/test_code.py index 74019f83326..9ad1bfd4b3a 100644 --- a/src/ethereum_test_tools/tests/test_code.py +++ b/src/ethereum_test_tools/tests/test_code.py @@ -9,11 +9,10 @@ from semver import Version from ethereum_test_forks import Fork, Homestead, Shanghai, forks_from_until, get_deployed_forks -from evm_transition_tool import GethTransitionTool +from evm_transition_tool import FixtureFormats, GethTransitionTool from ..code import CalldataCase, Case, Code, Conditional, Initcode, Switch, Yul from ..common import Account, Environment, TestAddress, Transaction, to_hash_bytes -from ..filling import fill_test from ..spec import StateTest from ..vm.opcode import Opcodes as Op from .conftest import SOLC_PADDING_VERSION @@ -645,12 +644,17 @@ def test_switch(tx_data: bytes, switch_bytecode: bytes, expected_storage: Mappin TestAddress: Account(balance=10_000_000, nonce=0), code_address: Account(code=switch_bytecode), } - txs = [Transaction(to=code_address, data=tx_data, gas_limit=1_000_000)] + tx = Transaction(to=code_address, data=tx_data, gas_limit=1_000_000) post = {TestAddress: Account(nonce=1), code_address: Account(storage=expected_storage)} - state_test = StateTest(env=Environment(), pre=pre, txs=txs, post=post) - fill_test( + state_test = StateTest( + env=Environment(), + pre=pre, + tx=tx, + post=post, + fixture_format=FixtureFormats.BLOCKCHAIN_TEST, + ) + state_test.generate( t8n=GethTransitionTool(), - test_spec=state_test, fork=Shanghai, - spec=None, + eips=None, ) diff --git a/src/ethereum_test_tools/tests/test_filler_expect_account.py b/src/ethereum_test_tools/tests/test_filler_expect_account.py deleted file mode 100644 index d384e9feb80..00000000000 --- a/src/ethereum_test_tools/tests/test_filler_expect_account.py +++ /dev/null @@ -1,302 +0,0 @@ -""" -Tests for the fixture `post` (expect) section. -""" - -from collections import namedtuple -from typing import Any, Mapping, Optional - -import pytest - -from ethereum_test_forks import Fork, Shanghai -from evm_transition_tool import GethTransitionTool - -from ..common import Account, Environment, Transaction -from ..common.types import Storage -from ..filling import fill_test -from ..spec import BaseTestConfig, StateTest - -test_fork = Shanghai -sender_address = "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" -test_address = "0x1000000000000000000000000000000000000000" -test_transaction = Transaction( - ty=0x0, - chain_id=0x0, - nonce=0, - to=test_address, - gas_limit=100000000, - gas_price=10, - protected=False, -) - - -def run_test( - pre: Mapping[Any, Any], post: Mapping[Any, Any], tx: Transaction, fork: Fork, exception: Any -) -> Optional[pytest.ExceptionInfo]: - """ - Perform the test execution and post state verification given pre and post - """ - state_test = StateTest( - env=Environment(), - pre=pre, - post=post, - txs=[tx], - tag="post_storage_value_mismatch", - base_test_config=BaseTestConfig(enable_hive=False), - ) - - t8n = GethTransitionTool() - - e_info: pytest.ExceptionInfo - if exception is not None: - with pytest.raises(exception) as e_info: - fixture = { - f"000/my_chain_id_test/{fork}": fill_test( - t8n=t8n, - test_spec=state_test, - fork=fork, - spec=None, - ), - } - return e_info - else: - fixture = { - f"000/my_chain_id_test/{fork}": fill_test( - t8n=t8n, - test_spec=state_test, - fork=fork, - spec=None, - ), - } - return None - - -def test_post_account_mismatch_nonce(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1), - } - - post = { - test_address: Account(nonce=2), - } - - e_info = run_test(pre, post, test_transaction, test_fork, Account.NonceMismatch) - assert e_info.value.want == 2 - assert e_info.value.got == 1 - assert e_info.value.address == test_address - assert "unexpected nonce for account" in str(e_info.value) - - -def test_post_account_mismatch_nonce_a(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1), - } - - post = { - test_address: Account(), - } - - run_test(pre, post, test_transaction, test_fork, None) - - -def test_post_account_mismatch_nonce_b(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1), - } - - post = { - test_address: Account(nonce=0), - } - - e_info = run_test(pre, post, test_transaction, test_fork, Account.NonceMismatch) - assert e_info.value.want == 0 - assert e_info.value.got == 1 - assert e_info.value.address == test_address - assert "unexpected nonce for account" in str(e_info.value) - - -def test_post_account_mismatch_code(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(code="0x02"), - } - - post = { - test_address: Account(code="0x01"), - } - - e_info = run_test(pre, post, test_transaction, test_fork, Account.CodeMismatch) - assert e_info.value.want == "0x01" - assert e_info.value.got == "0x02" - assert e_info.value.address == test_address - assert "unexpected code for account" in str(e_info.value) - - -def test_post_account_mismatch_code_a(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(code="0x02"), - } - - post = { - test_address: Account(), - } - - run_test(pre, post, test_transaction, test_fork, None) - - -def test_post_account_mismatch_code_b(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(code="0x02"), - } - - post = { - test_address: Account(code=""), - } - - e_info = run_test(pre, post, test_transaction, test_fork, Account.CodeMismatch) - assert e_info.value.want == "0x" - assert e_info.value.got == "0x02" - assert e_info.value.address == test_address - assert "unexpected code for account" in str(e_info.value) - - -def test_post_account_mismatch_balance(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(balance=1), - } - - post = { - test_address: Account(balance=2), - } - - test_transaction.value = 0 - e_info = run_test(pre, post, test_transaction, test_fork, Account.BalanceMismatch) - assert e_info.value.want == 2 - assert e_info.value.got == 1 - assert e_info.value.address == test_address - assert "unexpected balance for account" in str(e_info.value) - - -def test_post_account_mismatch_balance_a(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(balance=1), - } - - post = { - test_address: Account(), - } - - test_transaction.value = 0 - run_test(pre, post, test_transaction, test_fork, None) - - -def test_post_account_mismatch_balance_b(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(balance=1), - } - - post = { - test_address: Account(balance=0), - } - - e_info = run_test(pre, post, test_transaction, test_fork, Account.BalanceMismatch) - assert e_info.value.want == 0 - assert e_info.value.got == 1 - assert e_info.value.address == test_address - assert "unexpected balance for account" in str(e_info.value) - - -def test_post_account_mismatch_account(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(balance=1), - } - - post = { - test_address: Account(), - } - - run_test(pre, post, test_transaction, test_fork, None) - - -def test_post_account_mismatch_account_a(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(balance=1), - } - - post = { - 0x1000000000000000000000000000000000000001: Account(), - } - - e_info = run_test(pre, post, test_transaction, test_fork, Exception) - assert "expected account not found" in str(e_info.value) - - -def test_post_account_mismatch_account_b(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(balance=1), - } - - post = {} - - run_test(pre, post, test_transaction, test_fork, None) - - -def test_post_account_mismatch_account_c(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(balance=1), - } - - post = {test_address: Account.NONEXISTENT} - - e_info = run_test(pre, post, test_transaction, test_fork, Exception) - assert "found unexpected account" in str(e_info.value) diff --git a/src/ethereum_test_tools/tests/test_filler_expect_storage.py b/src/ethereum_test_tools/tests/test_filler_expect_storage.py deleted file mode 100644 index 5d5d327abed..00000000000 --- a/src/ethereum_test_tools/tests/test_filler_expect_storage.py +++ /dev/null @@ -1,259 +0,0 @@ -""" -Tests for the fixture `post` (expect) section. -""" - -from typing import Any, Mapping - -import pytest - -from ethereum_test_forks import Fork, Shanghai -from evm_transition_tool import GethTransitionTool - -from ..common import Account, Environment, Transaction -from ..common.types import Storage -from ..filling import fill_test -from ..spec import BaseTestConfig, StateTest - -# Test vectors: -# mismatch_1: 1:1 vs 1:2 -# mismatch_2: 1:1 vs 2:1 -# mismatch_2_a: 1:1 vs 0:0 -# mismatch_2_b: 1:1 vs [] -# mismatch_3: 0:0 vs 1:2 -# mismatch_3_a: [] vs 1:2 -# mismatch_4: 0:3, 1:2 vs 1:2 -# mismatch_5: 1:2, 2:3 vs 1:2 -# mismatch_6: 1:2 vs 1:2, 2:3 -# mismatch_7: 1:2 vs 1:2, 2:0 - -test_fork = Shanghai -sender_address = "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" -test_address = "0x1000000000000000000000000000000000000000" -test_transaction = Transaction( - ty=0x0, - chain_id=0x0, - nonce=0, - to=test_address, - gas_limit=100000000, - gas_price=10, - protected=False, -) - - -def run_test( - pre: Mapping[Any, Any], post: Mapping[Any, Any], tx: Transaction, fork: Fork -) -> pytest.ExceptionInfo: - """ - Perform the test execution and post state verification given pre and post - """ - state_test = StateTest( - env=Environment(), - pre=pre, - post=post, - txs=[tx], - tag="post_storage_value_mismatch", - base_test_config=BaseTestConfig(enable_hive=False), - ) - - t8n = GethTransitionTool() - - e_info: pytest.ExceptionInfo - with pytest.raises(Storage.KeyValueMismatch) as e_info: - fixture = { - f"000/my_chain_id_test/{fork}": fill_test( - t8n=t8n, - test_spec=state_test, - fork=fork, - spec=None, - ), - } - return e_info - - -def test_post_storage_value_mismatch_1(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x01": "0x01"}), - } - - post = { - test_address: Account(storage={"0x01": "0x02"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 2 - assert e_info.value.got == 1 - assert e_info.value.key == 1 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_2(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x01": "0x01"}), - } - - post = { - test_address: Account(storage={"0x02": "0x01"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 0 - assert e_info.value.got == 1 - assert e_info.value.key == 1 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_2_a(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x01": "0x01"}), - } - - post = { - test_address: Account(storage={"0x00": "0x00"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 0 - assert e_info.value.got == 1 - assert e_info.value.key == 1 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_2_b(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x01": "0x01"}), - } - - post = { - test_address: Account(storage={}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 0 - assert e_info.value.got == 1 - assert e_info.value.key == 1 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_3(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x00": "0x00"}), - } - - post = { - test_address: Account(storage={"0x01": "0x02"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 2 - assert e_info.value.got == 0 - assert e_info.value.key == 1 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_3_a(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={}), - } - - post = { - test_address: Account(storage={"0x01": "0x02"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 2 - assert e_info.value.got == 0 - assert e_info.value.key == 1 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_4(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x00": "0x03", "0x01": "0x02"}), - } - - post = { - test_address: Account(storage={"0x01": "0x02"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 0 - assert e_info.value.got == 3 - assert e_info.value.key == 0 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_5(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x01": "0x02", "0x02": "0x03"}), - } - - post = { - test_address: Account(storage={"0x01": "0x02"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 0 - assert e_info.value.got == 3 - assert e_info.value.key == 2 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) - - -def test_post_storage_value_mismatch_6(): - """ - Test `ethereum_test.filler.fill_fixtures` with `StateTest` and post state verification. - """ - pre = { - sender_address: Account(balance=1000000000000000000000), - test_address: Account(nonce=1, storage={"0x01": "0x02"}), - } - - post = { - test_address: Account(storage={"0x01": "0x02", "0x02": "0x03"}), - } - - e_info = run_test(pre, post, test_transaction, test_fork) - assert e_info.value.want == 3 - assert e_info.value.got == 0 - assert e_info.value.key == 2 - assert e_info.value.address == test_address - assert "incorrect value in address" in str(e_info.value) diff --git a/src/ethereum_test_tools/tests/test_filling/__init__.py b/src/ethereum_test_tools/tests/test_filling/__init__.py new file mode 100644 index 00000000000..7ae47ad3a81 --- /dev/null +++ b/src/ethereum_test_tools/tests/test_filling/__init__.py @@ -0,0 +1,3 @@ +""" +`ethereum_test_tools.filling` verification tests. +""" diff --git a/src/ethereum_test_tools/tests/test_fixtures/blockchain_london_invalid_filled.json b/src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_london_invalid_filled.json similarity index 98% rename from src/ethereum_test_tools/tests/test_fixtures/blockchain_london_invalid_filled.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_london_invalid_filled.json index a5b152ef9f4..15bbc0690ba 100644 --- a/src/ethereum_test_tools/tests/test_fixtures/blockchain_london_invalid_filled.json +++ b/src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_london_invalid_filled.json @@ -140,7 +140,7 @@ "uncleHeaders": [] }, { - "rlp": "0xf902e1f901fea00e043cb2eb0339900f6199c0ab517e5be3a81d898fa58078ed8b866ddc60b010a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a069f3a735c7a7e1ea24a03a7107eba6a880d2d0251aaf24eaa7f109ece7969bf9a0ab28cd18f912c2177d3f787591ccc9ba7742c877cdeabe0098e7263ead8893c1a0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000388016345785d8a0000830155442480a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082029ff8ddb86c02f86901048203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820301c001a0720e2870881f8b0e285b7ec02c169f1165847bcb5f36ea5f33f3db6079854f63a04448266b715d7d99acd1e31dcab50d7119faa620d44c69b3f64f97d636634169b86d02f86a0105830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820302c080a06c7fb2be7e001a210d72480522b9ebecade52d721360ce5242e34a6c05a02715a01220e3cb7418cd6294443b38d05f5ed9f2967b182d25c784e11e7863454b8f9bc0", + "rlp": "0xf902e1f901fea00e043cb2eb0339900f6199c0ab517e5be3a81d898fa58078ed8b866ddc60b010a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a069f3a735c7a7e1ea24a03a7107eba6a880d2d0251aaf24eaa7f109ece7969bf9a07c6d7fe1d1734fca072880e563f763405dc362222d37487cb098a006f7db3b2ca0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000388016345785d8a0000830155442480a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082029ff8ddb86c02f86901048203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820301c001a0720e2870881f8b0e285b7ec02c169f1165847bcb5f36ea5f33f3db6079854f63a04448266b715d7d99acd1e31dcab50d7119faa620d44c69b3f64f97d636634169b86d02f86a0105830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820302c080a06c7fb2be7e001a210d72480522b9ebecade52d721360ce5242e34a6c05a02715a01220e3cb7418cd6294443b38d05f5ed9f2967b182d25c784e11e7863454b8f9bc0", "expectException": "invalid transaction", "rlp_decoded": { "blockHeader": { @@ -148,7 +148,7 @@ "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "coinbase": "0xba5e000000000000000000000000000000000000", "stateRoot": "0x69f3a735c7a7e1ea24a03a7107eba6a880d2d0251aaf24eaa7f109ece7969bf9", - "transactionsTrie": "0xab28cd18f912c2177d3f787591ccc9ba7742c877cdeabe0098e7263ead8893c1", + "transactionsTrie": "0x7c6d7fe1d1734fca072880e563f763405dc362222d37487cb098a006f7db3b2c", "receiptTrie": "0x976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4ef", "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "difficulty": "0x020000", @@ -160,7 +160,7 @@ "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "nonce": "0x0000000000000000", "baseFeePerGas": "0x029f", - "hash": "0x821a3c612a905a071d07151519a2ad225f0438b4b956c46edd12b6bf50e2239c" + "hash": "0x0cb9b60de1bb3893d7b7b806562a78aca5e9fbff47bf62893a5f6c0afcc73b48" }, "transactions": [ { @@ -274,7 +274,7 @@ "uncleHeaders": [] }, { - "rlp": "0xf902e1f901fea05c66e5b6d6513ec98e9d8ee88137f1a2418542550977ea02015439acd2bf8f8ea01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0e834ba6cd27f2702b0adf2ef6a85e2fbc340fb948c96e75b674e9a73a5dbc3d1a04ed2c2147e0a0d1c248330338f51778f350af8c209c528799278ac980786632ea0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000488016345785d8a0000830155443080a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082024cf8ddb86c02f86901078203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820401c001a0113c54f83e1b1e5c689ba86d288ec0ce2877f350b71821c4c7a3f7073b46602ca0548848e711b86ceeb657fd0a0bf44b792f6665ed18ec8a04f498471e811f8f97b86d02f86a0108830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820402c001a0ebc8ad530ec3d510998aa2485763fcd1c6958c900c8d8ae6eaf86e1eddde8b23a0341e4a021f7b77da28d853c07d11253b92331ab640ad3f28f5d7b2cdbc7ceca7c0", + "rlp": "0xf902e1f901fea05c66e5b6d6513ec98e9d8ee88137f1a2418542550977ea02015439acd2bf8f8ea01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0e834ba6cd27f2702b0adf2ef6a85e2fbc340fb948c96e75b674e9a73a5dbc3d1a04722f7b17f27aee5dfa0d92ba40e16de960374a98ec63e728acaa1564d8a54f3a0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000488016345785d8a0000830155443080a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082024cf8ddb86c02f86901078203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820401c001a0113c54f83e1b1e5c689ba86d288ec0ce2877f350b71821c4c7a3f7073b46602ca0548848e711b86ceeb657fd0a0bf44b792f6665ed18ec8a04f498471e811f8f97b86d02f86a0108830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820402c001a0ebc8ad530ec3d510998aa2485763fcd1c6958c900c8d8ae6eaf86e1eddde8b23a0341e4a021f7b77da28d853c07d11253b92331ab640ad3f28f5d7b2cdbc7ceca7c0", "expectException": "invalid transaction", "rlp_decoded": { "blockHeader": { @@ -282,7 +282,7 @@ "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "coinbase": "0xba5e000000000000000000000000000000000000", "stateRoot": "0xe834ba6cd27f2702b0adf2ef6a85e2fbc340fb948c96e75b674e9a73a5dbc3d1", - "transactionsTrie": "0x4ed2c2147e0a0d1c248330338f51778f350af8c209c528799278ac980786632e", + "transactionsTrie": "0x4722f7b17f27aee5dfa0d92ba40e16de960374a98ec63e728acaa1564d8a54f3", "receiptTrie": "0x976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4ef", "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "difficulty": "0x020000", @@ -294,7 +294,7 @@ "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "nonce": "0x0000000000000000", "baseFeePerGas": "0x024c", - "hash": "0xe0216dfed41475b9f321bcee40fca139957a9310454b868d2e5d3c9b1111e7bf" + "hash": "0x1f01f6d8ff3a461486c4c4334c94a05f114d161b1ac082c7374ad7ac51eea7f2" }, "transactions": [ { @@ -678,7 +678,7 @@ "uncleHeaders": [] }, { - "rlp": "0xf902e1f901fea015676cbd68ac93fede6f8192b19868145f17d2f89e231de456925dea93664e2da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0c12121517d65ac698ab8a67e75e208a9c11c3f02c1d380fc370375306e16971ea0ab28cd18f912c2177d3f787591ccc9ba7742c877cdeabe0098e7263ead8893c1a0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000388016345785d8a0000830155442480a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082029ff8ddb86c02f86901048203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820301c001a0720e2870881f8b0e285b7ec02c169f1165847bcb5f36ea5f33f3db6079854f63a04448266b715d7d99acd1e31dcab50d7119faa620d44c69b3f64f97d636634169b86d02f86a0105830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820302c080a06c7fb2be7e001a210d72480522b9ebecade52d721360ce5242e34a6c05a02715a01220e3cb7418cd6294443b38d05f5ed9f2967b182d25c784e11e7863454b8f9bc0", + "rlp": "0xf902e1f901fea015676cbd68ac93fede6f8192b19868145f17d2f89e231de456925dea93664e2da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0c12121517d65ac698ab8a67e75e208a9c11c3f02c1d380fc370375306e16971ea07c6d7fe1d1734fca072880e563f763405dc362222d37487cb098a006f7db3b2ca0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000388016345785d8a0000830155442480a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082029ff8ddb86c02f86901048203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820301c001a0720e2870881f8b0e285b7ec02c169f1165847bcb5f36ea5f33f3db6079854f63a04448266b715d7d99acd1e31dcab50d7119faa620d44c69b3f64f97d636634169b86d02f86a0105830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820302c080a06c7fb2be7e001a210d72480522b9ebecade52d721360ce5242e34a6c05a02715a01220e3cb7418cd6294443b38d05f5ed9f2967b182d25c784e11e7863454b8f9bc0", "expectException": "invalid transaction", "rlp_decoded": { "blockHeader": { @@ -686,7 +686,7 @@ "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "coinbase": "0xba5e000000000000000000000000000000000000", "stateRoot": "0xc12121517d65ac698ab8a67e75e208a9c11c3f02c1d380fc370375306e16971e", - "transactionsTrie": "0xab28cd18f912c2177d3f787591ccc9ba7742c877cdeabe0098e7263ead8893c1", + "transactionsTrie": "0x7c6d7fe1d1734fca072880e563f763405dc362222d37487cb098a006f7db3b2c", "receiptTrie": "0x976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4ef", "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "difficulty": "0x020000", @@ -698,7 +698,7 @@ "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "nonce": "0x0000000000000000", "baseFeePerGas": "0x029f", - "hash": "0x915ca54d6df004476300024f553c021e3fbbb69f6c81b9a1f74b1ec211209681" + "hash": "0xf3ad606edcdfb24e7b24e32328334b3ddf5149ecd6c45ccbd4d39628a4ef2a85" }, "transactions": [ { @@ -812,7 +812,7 @@ "uncleHeaders": [] }, { - "rlp": "0xf902e1f901fea00817157aaf7981caa63e995d4d45ee7e30c0b26e52fe668e1f8bcd2b457a79cea01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a04a631519f4a7675eb6edb98719287ab1d1896111acd02dde544386ef63445fdaa04ed2c2147e0a0d1c248330338f51778f350af8c209c528799278ac980786632ea0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000488016345785d8a0000830155443080a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082024cf8ddb86c02f86901078203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820401c001a0113c54f83e1b1e5c689ba86d288ec0ce2877f350b71821c4c7a3f7073b46602ca0548848e711b86ceeb657fd0a0bf44b792f6665ed18ec8a04f498471e811f8f97b86d02f86a0108830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820402c001a0ebc8ad530ec3d510998aa2485763fcd1c6958c900c8d8ae6eaf86e1eddde8b23a0341e4a021f7b77da28d853c07d11253b92331ab640ad3f28f5d7b2cdbc7ceca7c0", + "rlp": "0xf902e1f901fea00817157aaf7981caa63e995d4d45ee7e30c0b26e52fe668e1f8bcd2b457a79cea01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a04a631519f4a7675eb6edb98719287ab1d1896111acd02dde544386ef63445fdaa04722f7b17f27aee5dfa0d92ba40e16de960374a98ec63e728acaa1564d8a54f3a0976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4efb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000488016345785d8a0000830155443080a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082024cf8ddb86c02f86901078203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820401c001a0113c54f83e1b1e5c689ba86d288ec0ce2877f350b71821c4c7a3f7073b46602ca0548848e711b86ceeb657fd0a0bf44b792f6665ed18ec8a04f498471e811f8f97b86d02f86a0108830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820402c001a0ebc8ad530ec3d510998aa2485763fcd1c6958c900c8d8ae6eaf86e1eddde8b23a0341e4a021f7b77da28d853c07d11253b92331ab640ad3f28f5d7b2cdbc7ceca7c0", "expectException": "invalid transaction", "rlp_decoded": { "blockHeader": { @@ -820,7 +820,7 @@ "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "coinbase": "0xba5e000000000000000000000000000000000000", "stateRoot": "0x4a631519f4a7675eb6edb98719287ab1d1896111acd02dde544386ef63445fda", - "transactionsTrie": "0x4ed2c2147e0a0d1c248330338f51778f350af8c209c528799278ac980786632e", + "transactionsTrie": "0x4722f7b17f27aee5dfa0d92ba40e16de960374a98ec63e728acaa1564d8a54f3", "receiptTrie": "0x976beb67b634171d419ef326220dfdda98074e3495940240a105e17643f0a4ef", "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "difficulty": "0x020000", @@ -832,7 +832,7 @@ "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "nonce": "0x0000000000000000", "baseFeePerGas": "0x024c", - "hash": "0x6b86f7ac310b740894a89e718891fe3169d35e5e770493fe0f788c1fa2ee7d04" + "hash": "0xc80ae3f610a2adb971179fc1e1bc120f3b38c88ff388cf059809a579be6e5f2c" }, "transactions": [ { @@ -1075,4 +1075,4 @@ "sealEngine": "NoProof" } } -} +} \ No newline at end of file diff --git a/src/ethereum_test_tools/tests/test_fixtures/blockchain_london_valid_filled.json b/src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_london_valid_filled.json similarity index 100% rename from src/ethereum_test_tools/tests/test_fixtures/blockchain_london_valid_filled.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_london_valid_filled.json diff --git a/src/ethereum_test_tools/tests/test_fixtures/blockchain_shanghai_invalid_filled_hive.json b/src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_shanghai_invalid_filled_hive.json similarity index 98% rename from src/ethereum_test_tools/tests/test_fixtures/blockchain_shanghai_invalid_filled_hive.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_shanghai_invalid_filled_hive.json index fe81c071a08..315f488542c 100644 --- a/src/ethereum_test_tools/tests/test_fixtures/blockchain_shanghai_invalid_filled_hive.json +++ b/src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_shanghai_invalid_filled_hive.json @@ -85,7 +85,7 @@ "extraData": "0x", "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", "baseFeePerGas": "0x29f", - "blockHash": "0x6504f75aa7b88dd9e059088d2db4d911934a5c0e3d076a48f6aeef9129df1472", + "blockHash": "0xeaa67ef33964d925aabc53e217e3f5f143615723970bfa07b80c46ef946ca293", "transactions": [ "0x02f86901048203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820301c001a0720e2870881f8b0e285b7ec02c169f1165847bcb5f36ea5f33f3db6079854f63a04448266b715d7d99acd1e31dcab50d7119faa620d44c69b3f64f97d636634169", "0x02f86a0105830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820302c080a06c7fb2be7e001a210d72480522b9ebecade52d721360ce5242e34a6c05a02715a01220e3cb7418cd6294443b38d05f5ed9f2967b182d25c784e11e7863454b8f9b" @@ -134,7 +134,7 @@ "extraData": "0x", "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", "baseFeePerGas": "0x24c", - "blockHash": "0xa8eec4a7460bdc0d813ab931562ca3a3b4e25c4482b9039003fdb293c3b05c96", + "blockHash": "0xa8b4cee5dcb437faf9d815cbe99986f9000e32cf5ea86613b944ac285cac0187", "transactions": [ "0x02f86901078203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820401c001a0113c54f83e1b1e5c689ba86d288ec0ce2877f350b71821c4c7a3f7073b46602ca0548848e711b86ceeb657fd0a0bf44b792f6665ed18ec8a04f498471e811f8f97", "0x02f86a0108830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820402c001a0ebc8ad530ec3d510998aa2485763fcd1c6958c900c8d8ae6eaf86e1eddde8b23a0341e4a021f7b77da28d853c07d11253b92331ab640ad3f28f5d7b2cdbc7ceca7" @@ -384,7 +384,7 @@ "extraData": "0x", "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", "baseFeePerGas": "0x29f", - "blockHash": "0xec8258ae1312e560e07e7d0fd208237e515c3e7d709f92fdc7a5b7316da25bdc", + "blockHash": "0x5c16738a8a828e396bc356b54716694ac63cce50e27c4cb270727af80b6a6a8a", "transactions": [ "0x02f86901048203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820301c001a0720e2870881f8b0e285b7ec02c169f1165847bcb5f36ea5f33f3db6079854f63a04448266b715d7d99acd1e31dcab50d7119faa620d44c69b3f64f97d636634169", "0x02f86a0105830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820302c080a06c7fb2be7e001a210d72480522b9ebecade52d721360ce5242e34a6c05a02715a01220e3cb7418cd6294443b38d05f5ed9f2967b182d25c784e11e7863454b8f9b" @@ -433,7 +433,7 @@ "extraData": "0x", "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", "baseFeePerGas": "0x24c", - "blockHash": "0x30c73027954c5b8e8d633775e6cf4f1362fb15bf7d41be2424d757d2cc9d5219", + "blockHash": "0x1498909af4d98e8ad23020f9a8055ce5ec7cc6264ca84c710a8bc2a93cffeffc", "transactions": [ "0x02f86901078203e88203e8830f424094cccccccccccccccccccccccccccccccccccccccc80820401c001a0113c54f83e1b1e5c689ba86d288ec0ce2877f350b71821c4c7a3f7073b46602ca0548848e711b86ceeb657fd0a0bf44b792f6665ed18ec8a04f498471e811f8f97", "0x02f86a0108830186a08203e8830f424094cccccccccccccccccccccccccccccccccccccccd80820402c001a0ebc8ad530ec3d510998aa2485763fcd1c6958c900c8d8ae6eaf86e1eddde8b23a0341e4a021f7b77da28d853c07d11253b92331ab640ad3f28f5d7b2cdbc7ceca7" @@ -597,4 +597,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/ethereum_test_tools/tests/test_fixtures/blockchain_shanghai_valid_filled_hive.json b/src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_shanghai_valid_filled_hive.json similarity index 100% rename from src/ethereum_test_tools/tests/test_fixtures/blockchain_shanghai_valid_filled_hive.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/blockchain_shanghai_valid_filled_hive.json diff --git a/src/ethereum_test_tools/tests/test_fixtures/chainid_istanbul_filled.json b/src/ethereum_test_tools/tests/test_filling/fixtures/chainid_istanbul_filled.json similarity index 99% rename from src/ethereum_test_tools/tests/test_fixtures/chainid_istanbul_filled.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/chainid_istanbul_filled.json index 6967e813330..fdba9a80ab5 100644 --- a/src/ethereum_test_tools/tests/test_fixtures/chainid_istanbul_filled.json +++ b/src/ethereum_test_tools/tests/test_filling/fixtures/chainid_istanbul_filled.json @@ -41,6 +41,7 @@ "nonce": "0x0000000000000000", "hash": "0xc413245fffae8b7c6392bcd3dfbbdee24118e94d9a58722a7abd91a4e1d048b7" }, + "blocknumber": "1", "transactions": [ { "type": "0x00", diff --git a/src/ethereum_test_tools/tests/test_fixtures/chainid_london_filled.json b/src/ethereum_test_tools/tests/test_filling/fixtures/chainid_london_filled.json similarity index 99% rename from src/ethereum_test_tools/tests/test_fixtures/chainid_london_filled.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/chainid_london_filled.json index c2556c10485..9574fb926b8 100644 --- a/src/ethereum_test_tools/tests/test_fixtures/chainid_london_filled.json +++ b/src/ethereum_test_tools/tests/test_filling/fixtures/chainid_london_filled.json @@ -43,6 +43,7 @@ "baseFeePerGas": "0x07", "hash": "0xe05293fe6050385e463d93c310bc52f87715f509aeb036455bbe4597cf36706a" }, + "blocknumber": "1", "transactions": [ { "type": "0x00", diff --git a/src/ethereum_test_tools/tests/test_fixtures/chainid_merge_filled_hive.json b/src/ethereum_test_tools/tests/test_filling/fixtures/chainid_merge_filled_hive.json similarity index 100% rename from src/ethereum_test_tools/tests/test_fixtures/chainid_merge_filled_hive.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/chainid_merge_filled_hive.json diff --git a/src/ethereum_test_tools/tests/test_fixtures/chainid_shanghai_filled_hive.json b/src/ethereum_test_tools/tests/test_filling/fixtures/chainid_shanghai_filled_hive.json similarity index 100% rename from src/ethereum_test_tools/tests/test_fixtures/chainid_shanghai_filled_hive.json rename to src/ethereum_test_tools/tests/test_filling/fixtures/chainid_shanghai_filled_hive.json diff --git a/src/ethereum_test_tools/tests/test_filling/test_expect.py b/src/ethereum_test_tools/tests/test_filling/test_expect.py new file mode 100644 index 00000000000..513d7932275 --- /dev/null +++ b/src/ethereum_test_tools/tests/test_filling/test_expect.py @@ -0,0 +1,234 @@ +""" +Test suite for `ethereum_test_tools.filling` fixture post (expect) section. +""" +from typing import Any, Mapping, Optional + +import pytest + +from ethereum_test_forks import Shanghai +from evm_transition_tool import FixtureFormats, GethTransitionTool + +from ...common import Account, Environment, TestAddress, Transaction, to_address +from ...common.types import Storage +from ...spec import StateTest + +# from ...spec.blockchain.types import FixtureCommon as BlockchainFixtureCommon + +ADDRESS_UNDER_TEST = to_address(0x01) +pre = { + TestAddress: Account(balance=1000000000000000000000), +} +post = {} + + +def run_post_state_mismatch_test( + pre: Mapping[Any, Any], post: Mapping[Any, Any], exception: Any +) -> Optional[pytest.ExceptionInfo]: + """ + Performs test filling and post state verification. + """ + fork = Shanghai + t8n = GethTransitionTool() + fixture_format = FixtureFormats.BLOCKCHAIN_TEST + state_test = StateTest( + env=Environment(), + pre=pre, + post=post, + tx=Transaction(), + tag="post_value_mismatch", + fixture_format=fixture_format, + ) + + if exception: + e_info: pytest.ExceptionInfo + with pytest.raises(exception) as e_info: + state_test.generate(t8n=t8n, fork=fork) + return e_info + else: + return None + + +# Storage value mismatch tests +@pytest.mark.parametrize( + "pre_storage,post_storage,expected_exception", + [ + ( # mismatch_1: 1:1 vs 1:2 + {"0x01": "0x01"}, + {"0x01": "0x02"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=1, want=2, got=1), + ), + ( # mismatch_2: 1:1 vs 2:1 + {"0x01": "0x01"}, + {"0x02": "0x01"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=1, want=0, got=1), + ), + ( # mismatch_2_a: 1:1 vs 0:0 + {"0x01": "0x01"}, + {"0x00": "0x00"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=1, want=0, got=1), + ), + ( # mismatch_2_b: 1:1 vs empty + {"0x01": "0x01"}, + {}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=1, want=0, got=1), + ), + ( # mismatch_3: 0:0 vs 1:2 + {"0x00": "0x00"}, + {"0x01": "0x02"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=1, want=2, got=0), + ), + ( # mismatch_3_a: empty vs 1:2 + {}, + {"0x01": "0x02"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=1, want=2, got=0), + ), + ( # mismatch_4: 0:3, 1:2 vs 1:2 + {"0x00": "0x03", "0x01": "0x02"}, + {"0x01": "0x02"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=0, want=0, got=3), + ), + ( # mismatch_5: 1:2, 2:3 vs 1:2 + {"0x01": "0x02", "0x02": "0x03"}, + {"0x01": "0x02"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=2, want=0, got=3), + ), + ( # mismatch_6: 1:2 vs 1:2, 2:3 + {"0x01": "0x02"}, + {"0x01": "0x02", "0x02": "0x03"}, + Storage.KeyValueMismatch(address=ADDRESS_UNDER_TEST, key=2, want=3, got=0), + ), + ], +) +def test_post_storage_value_mismatch(pre_storage, post_storage, expected_exception): + """ + Test `ethereum_test.filler.fill_test` post state storage verification. + """ + pre[ADDRESS_UNDER_TEST] = Account(nonce=1, storage=pre_storage) + post[ADDRESS_UNDER_TEST] = Account(storage=post_storage) + e_info = run_post_state_mismatch_test(pre, post, Storage.KeyValueMismatch) + assert e_info.value == expected_exception + + +# Nonce value mismatch tests +@pytest.mark.parametrize( + "pre_nonce,post_nonce", + [(1, 2), (1, 0), (1, None)], +) +def test_post_nonce_value_mismatch(pre_nonce, post_nonce): + """ + Test `ethereum_test.filler.fill_test` post state nonce verification. + """ + pre[ADDRESS_UNDER_TEST] = Account(nonce=pre_nonce) + post[ADDRESS_UNDER_TEST] = Account(nonce=post_nonce) + if post_nonce is None: + run_post_state_mismatch_test(pre, post, None) + else: + e_info = run_post_state_mismatch_test(pre, post, Account.NonceMismatch) + assert e_info.value == Account.NonceMismatch( + address=ADDRESS_UNDER_TEST, want=post_nonce, got=pre_nonce + ) + + +# Code value mismatch tests +@pytest.mark.parametrize( + "pre_code,post_code", + [ + ( + "0x02", + "0x01", + ), + ( + "0x02", + "0x", + ), + ( + "0x02", + None, + ), + ], +) +def test_post_code_value_mismatch(pre_code, post_code): + """ + Test `ethereum_test.filler.fill_test` post state code verification. + """ + pre[ADDRESS_UNDER_TEST] = Account(code=pre_code) + post[ADDRESS_UNDER_TEST] = Account(code=post_code) + if post_code is None: + run_post_state_mismatch_test(pre, post, None) + else: + e_info = run_post_state_mismatch_test(pre, post, Account.CodeMismatch) + assert e_info.value == Account.CodeMismatch( + address=ADDRESS_UNDER_TEST, want=post_code, got=pre_code + ) + + +# Balance value mismatch tests +@pytest.mark.parametrize( + "pre_balance,post_balance", + [ + ( + 1, + 2, + ), + ( + 1, + 0, + ), + ( + 1, + None, + ), + ], +) +def test_post_balance_value_mismatch(pre_balance, post_balance): + """ + Test `ethereum_test.filler.fill_test` post state balance verification. + """ + pre[ADDRESS_UNDER_TEST] = Account(balance=pre_balance) + post[ADDRESS_UNDER_TEST] = Account(balance=post_balance) + if post_balance is None: + run_post_state_mismatch_test(pre, post, None) + else: + e_info = run_post_state_mismatch_test(pre, post, Account.BalanceMismatch) + assert e_info.value == Account.BalanceMismatch( + address=ADDRESS_UNDER_TEST, want=post_balance, got=pre_balance + ) + + +# Account mismatch tests +@pytest.mark.parametrize( + "pre_accounts,post_accounts,error_str", + [ + ( + {ADDRESS_UNDER_TEST: Account(balance=1)}, + {ADDRESS_UNDER_TEST: Account()}, + None, + ), + ( + {ADDRESS_UNDER_TEST: Account(balance=1)}, + {ADDRESS_UNDER_TEST: Account(balance=1), to_address(0x02): Account(balance=1)}, + "expected account not found", + ), + ( + {ADDRESS_UNDER_TEST: Account(balance=1)}, + {}, + None, + ), + ( + {ADDRESS_UNDER_TEST: Account(balance=1)}, + {ADDRESS_UNDER_TEST: Account.NONEXISTENT}, + "found unexpected account", + ), + ], +) +def test_post_account_mismatch(pre_accounts, post_accounts, error_str): + """ + Test `ethereum_test.filler.fill_test` post state account verification. + """ + pre.update(pre_accounts) + post.update(post_accounts) + if error_str is None: + run_post_state_mismatch_test(pre, post, None) + else: + e_info = run_post_state_mismatch_test(pre, post, Exception) + assert error_str in str(e_info.value) diff --git a/src/ethereum_test_tools/tests/test_filler.py b/src/ethereum_test_tools/tests/test_filling/test_fixtures.py similarity index 89% rename from src/ethereum_test_tools/tests/test_filler.py rename to src/ethereum_test_tools/tests/test_filling/test_fixtures.py index f98a18ef5e3..24a8250c567 100644 --- a/src/ethereum_test_tools/tests/test_filler.py +++ b/src/ethereum_test_tools/tests/test_filling/test_fixtures.py @@ -1,5 +1,5 @@ """ -Test suite for `ethereum_test` module. +Test suite for `ethereum_test_tools.filling` fixture generation. """ import json @@ -10,13 +10,15 @@ from semver import Version from ethereum_test_forks import Berlin, Fork, Istanbul, London, Merge, Shanghai -from evm_transition_tool import GethTransitionTool +from evm_transition_tool import FixtureFormats, GethTransitionTool -from ..code import Yul -from ..common import Account, Block, Environment, TestAddress, Transaction, to_json -from ..filling import fill_test -from ..spec import BaseTestConfig, BlockchainTest, StateTest -from .conftest import SOLC_PADDING_VERSION +from ...code import Yul +from ...common import Account, Environment, TestAddress, Transaction, to_json +from ...spec import BlockchainTest, StateTest +from ...spec.blockchain.types import Block +from ...spec.blockchain.types import Fixture as BlockchainFixture +from ...spec.blockchain.types import FixtureCommon as BlockchainFixtureCommon +from ..conftest import SOLC_PADDING_VERSION def remove_info(fixture_json: Dict[str, Any]): # noqa: D103 @@ -74,19 +76,23 @@ def test_make_genesis(fork: Fork, hash: bytes): # noqa: D103 } t8n = GethTransitionTool() + fixture = BlockchainTest( + genesis_environment=env, + pre=pre, + post={}, + blocks=[], + tag="some_state_test", + fixture_format=FixtureFormats.BLOCKCHAIN_TEST, + ).generate(t8n, fork) + assert isinstance(fixture, BlockchainFixture) + assert fixture.genesis is not None - _, _, genesis = StateTest( - env=env, pre=pre, post={}, txs=[], tag="some_state_test" - ).make_genesis( - t8n, - fork, - ) - assert genesis.hash is not None - assert genesis.hash.startswith(hash) + assert fixture.genesis.hash is not None + assert fixture.genesis.hash.startswith(hash) @pytest.mark.parametrize( - "fork,enable_hive,expected_json_file", + "fork,check_hive,expected_json_file", [ (Istanbul, False, "chainid_istanbul_filled.json"), (London, False, "chainid_london_filled.json"), @@ -94,7 +100,7 @@ def test_make_genesis(fork: Fork, hash: bytes): # noqa: D103 (Shanghai, True, "chainid_shanghai_filled_hive.json"), ], ) -def test_fill_state_test(fork: Fork, expected_json_file: str, enable_hive: bool): +def test_fill_state_test(fork: Fork, expected_json_file: str, check_hive: bool): """ Test `ethereum_test.filler.fill_fixtures` with `StateTest`. """ @@ -127,24 +133,25 @@ def test_fill_state_test(fork: Fork, expected_json_file: str, enable_hive: bool) ), } - state_test = StateTest( + t8n = GethTransitionTool() + fixture_format = ( + FixtureFormats.BLOCKCHAIN_TEST_HIVE if check_hive else FixtureFormats.BLOCKCHAIN_TEST + ) + generated_fixture = StateTest( env=env, pre=pre, post=post, - txs=[tx], + tx=tx, tag="my_chain_id_test", - base_test_config=BaseTestConfig(enable_hive=enable_hive), + fixture_format=fixture_format, + ).generate( + t8n=t8n, + fork=fork, ) - - t8n = GethTransitionTool() - + assert generated_fixture.format() == fixture_format + assert isinstance(generated_fixture, BlockchainFixtureCommon) fixture = { - f"000/my_chain_id_test/{fork}": fill_test( - t8n=t8n, - test_spec=state_test, - fork=fork, - spec=None, - ), + f"000/my_chain_id_test/{fork}": generated_fixture.to_json(), } with open( @@ -152,7 +159,8 @@ def test_fill_state_test(fork: Fork, expected_json_file: str, enable_hive: bool) "src", "ethereum_test_tools", "tests", - "test_fixtures", + "test_filling", + "fixtures", expected_json_file, ) ) as f: @@ -164,14 +172,14 @@ def test_fill_state_test(fork: Fork, expected_json_file: str, enable_hive: bool) @pytest.mark.parametrize( - "fork,enable_hive,expected_json_file", + "fork,check_hive,expected_json_file", [ (London, False, "blockchain_london_valid_filled.json"), (Shanghai, True, "blockchain_shanghai_valid_filled_hive.json"), ], ) def test_fill_blockchain_valid_txs( - fork: Fork, solc_version: str, enable_hive: bool, expected_json_file: str + fork: Fork, solc_version: str, check_hive: bool, expected_json_file: str ): """ Test `ethereum_test.filler.fill_fixtures` with `BlockchainTest`. @@ -424,24 +432,27 @@ def test_fill_blockchain_valid_txs( coinbase="0xba5e000000000000000000000000000000000000", ) - blockchain_test = BlockchainTest( + t8n = GethTransitionTool() + fixture_format = ( + FixtureFormats.BLOCKCHAIN_TEST_HIVE if check_hive else FixtureFormats.BLOCKCHAIN_TEST + ) + generated_fixture = BlockchainTest( pre=pre, post=post, blocks=blocks, genesis_environment=genesis_environment, tag="my_blockchain_test_valid_txs", - base_test_config=BaseTestConfig(enable_hive=enable_hive), + fixture_format=fixture_format, + ).generate( + t8n=t8n, + fork=fork, ) - t8n = GethTransitionTool() + assert generated_fixture.format() == fixture_format + assert isinstance(generated_fixture, BlockchainFixtureCommon) fixture = { - f"000/my_blockchain_test/{fork.name()}": fill_test( - t8n=t8n, - test_spec=blockchain_test, - fork=fork, - spec=None, - ) + f"000/my_blockchain_test/{fork.name()}": generated_fixture.to_json(), } with open( @@ -449,7 +460,8 @@ def test_fill_blockchain_valid_txs( "src", "ethereum_test_tools", "tests", - "test_fixtures", + "test_filling", + "fixtures", expected_json_file, ) ) as f: @@ -467,14 +479,14 @@ def test_fill_blockchain_valid_txs( @pytest.mark.parametrize( - "fork,enable_hive,expected_json_file", + "fork,check_hive,expected_json_file", [ (London, False, "blockchain_london_invalid_filled.json"), (Shanghai, True, "blockchain_shanghai_invalid_filled_hive.json"), ], ) def test_fill_blockchain_invalid_txs( - fork: Fork, solc_version: str, enable_hive: bool, expected_json_file: str + fork: Fork, solc_version: str, check_hive: bool, expected_json_file: str ): """ Test `ethereum_test.filler.fill_fixtures` with `BlockchainTest`. @@ -773,23 +785,24 @@ def test_fill_blockchain_invalid_txs( coinbase="0xba5e000000000000000000000000000000000000", ) - blockchain_test = BlockchainTest( + t8n = GethTransitionTool() + fixture_format = ( + FixtureFormats.BLOCKCHAIN_TEST_HIVE if check_hive else FixtureFormats.BLOCKCHAIN_TEST + ) + generated_fixture = BlockchainTest( pre=pre, post=post, blocks=blocks, genesis_environment=genesis_environment, - base_test_config=BaseTestConfig(enable_hive=enable_hive), + fixture_format=fixture_format, + ).generate( + t8n=t8n, + fork=fork, ) - - t8n = GethTransitionTool() - + assert generated_fixture.format() == fixture_format + assert isinstance(generated_fixture, BlockchainFixtureCommon) fixture = { - f"000/my_blockchain_test/{fork.name()}": fill_test( - t8n=t8n, - test_spec=blockchain_test, - fork=fork, - spec=None, - ) + f"000/my_blockchain_test/{fork.name()}": generated_fixture.to_json(), } with open( @@ -797,7 +810,8 @@ def test_fill_blockchain_invalid_txs( "src", "ethereum_test_tools", "tests", - "test_fixtures", + "test_filling", + "fixtures", expected_json_file, ) ) as f: diff --git a/src/ethereum_test_tools/tests/test_types.py b/src/ethereum_test_tools/tests/test_types.py index d725354d8d8..d227d7ab283 100644 --- a/src/ethereum_test_tools/tests/test_types.py +++ b/src/ethereum_test_tools/tests/test_types.py @@ -23,14 +23,12 @@ Alloc, Bloom, Bytes, - FixtureEngineNewPayload, - FixtureExecutionPayload, - FixtureHeader, FixtureTransaction, Hash, HeaderNonce, ZeroPaddedHexNumber, ) +from ..spec.blockchain.types import FixtureEngineNewPayload, FixtureExecutionPayload, FixtureHeader def test_storage(): @@ -52,6 +50,10 @@ def test_storage(): assert 10 in s.data assert s.data[10] == 10 + iter_s = iter(Storage({10: 20, "11": "21"})) + assert next(iter_s) == 10 + assert next(iter_s) == 11 + s["10"] = "0x10" s["0x10"] = "10" assert s.data[10] == 16 diff --git a/src/evm_transition_tool/besu.py b/src/evm_transition_tool/besu.py index fef93743722..a7759286546 100644 --- a/src/evm_transition_tool/besu.py +++ b/src/evm_transition_tool/besu.py @@ -181,4 +181,4 @@ def is_fork_supported(self, fork: Fork) -> bool: """ Returns True if the fork is supported by the tool """ - return fork.fork() in self.help_string + return fork.transition_tool_name() in self.help_string diff --git a/src/evm_transition_tool/geth.py b/src/evm_transition_tool/geth.py index 06dfe3fe23e..0ad7efd32f2 100644 --- a/src/evm_transition_tool/geth.py +++ b/src/evm_transition_tool/geth.py @@ -2,6 +2,7 @@ Go-ethereum Transition tool interface. """ +import json import shutil import subprocess import textwrap @@ -9,6 +10,8 @@ from re import compile from typing import Optional +import pytest + from ethereum_test_forks import Fork from .transition_tool import FixtureFormats, TransitionTool, dump_files_to_directory @@ -51,7 +54,18 @@ def is_fork_supported(self, fork: Fork) -> bool: If the fork is a transition fork, we want to check the fork it transitions to. """ - return fork.fork() in self.help_string + return fork.transition_tool_name() in self.help_string + + def process_statetest_result(self, result: str): + """ + Process the result of a `evm statetest` to parse as JSON and raise if any test failed. + """ + result_json = json.loads(result) + if not isinstance(result_json, list): + raise Exception(f"Unexpected result from evm statetest: {result_json}") + for test_result in result_json: + if not test_result["pass"]: + pytest.fail(f"Test failed: {test_result['name']}. Error: {test_result['error']}") def verify_fixture( self, fixture_format: FixtureFormats, fixture_path: Path, debug_output_path: Optional[Path] @@ -81,6 +95,9 @@ def verify_fixture( stderr=subprocess.PIPE, ) + if FixtureFormats.is_state_test(fixture_format): + self.process_statetest_result(result.stdout.decode()) + if debug_output_path: debug_fixture_path = debug_output_path / "fixtures.json" shutil.copyfile(fixture_path, debug_fixture_path) diff --git a/src/evm_transition_tool/nimbus.py b/src/evm_transition_tool/nimbus.py index 4762f04f242..bb360709297 100644 --- a/src/evm_transition_tool/nimbus.py +++ b/src/evm_transition_tool/nimbus.py @@ -57,4 +57,4 @@ def is_fork_supported(self, fork: Fork) -> bool: If the fork is a transition fork, we want to check the fork it transitions to. """ - return fork.fork() in self.help_string + return fork.transition_tool_name() in self.help_string diff --git a/src/evm_transition_tool/tests/test_evaluate.py b/src/evm_transition_tool/tests/test_evaluate.py index 171fd5e5455..57e15060cff 100644 --- a/src/evm_transition_tool/tests/test_evaluate.py +++ b/src/evm_transition_tool/tests/test_evaluate.py @@ -119,7 +119,7 @@ def test_evm_t8n(t8n: TransitionTool, test_dir: str) -> None: # noqa: D103 alloc=alloc, txs=txs, env=env_json, - fork_name=Berlin.fork( + fork_name=Berlin.transition_tool_name( block_number=int(env_json["currentNumber"], 0), timestamp=int(env_json["currentTimestamp"], 0), ), diff --git a/src/evm_transition_tool/transition_tool.py b/src/evm_transition_tool/transition_tool.py index e5458f55616..bbd8f5f44cb 100644 --- a/src/evm_transition_tool/transition_tool.py +++ b/src/evm_transition_tool/transition_tool.py @@ -40,14 +40,14 @@ class FixtureFormats(Enum): Helper class to define fixture formats. """ + UNSET_TEST_FORMAT = "unset_test_format" STATE_TEST = "state_test" - STATE_TEST_HIVE = "state_test_hive" BLOCKCHAIN_TEST = "blockchain_test" BLOCKCHAIN_TEST_HIVE = "blockchain_test_hive" @classmethod def is_state_test(cls, format): # noqa: D102 - return format in (cls.STATE_TEST, cls.STATE_TEST_HIVE) + return format == cls.STATE_TEST @classmethod def is_blockchain_test(cls, format): # noqa: D102 @@ -55,12 +55,33 @@ def is_blockchain_test(cls, format): # noqa: D102 @classmethod def is_hive_format(cls, format): # noqa: D102 - return format in (cls.STATE_TEST_HIVE, cls.BLOCKCHAIN_TEST_HIVE) + return format == cls.BLOCKCHAIN_TEST_HIVE @classmethod def is_standard_format(cls, format): # noqa: D102 return format in (cls.STATE_TEST, cls.BLOCKCHAIN_TEST) + @classmethod + def is_verifiable(cls, format): # noqa: D102 + return format in (cls.STATE_TEST, cls.BLOCKCHAIN_TEST) + + @classmethod + def get_format_description(cls, format): + """ + Returns a description of the fixture format. + + Used to add a description to the generated pytest marks. + """ + if format == cls.UNSET_TEST_FORMAT: + return "Unknown fixture format; it has not been set." + elif format == cls.STATE_TEST: + return "Tests that generate a state test fixture." + elif format == cls.BLOCKCHAIN_TEST: + return "Tests that generate a blockchain test fixture." + elif format == cls.BLOCKCHAIN_TEST_HIVE: + return "Tests that generate a blockchain test fixture in hive format." + raise Exception(f"Unknown fixture format: {format}.") + class TransitionTool: """ @@ -576,7 +597,7 @@ def calc_state_root( alloc=alloc, txs=[], env=env, - fork_name=fork.fork(block_number=0, timestamp=0), + fork_name=fork.transition_tool_name(block_number=0, timestamp=0), debug_output_path=debug_output_path, ) state_root = result.get("stateRoot") diff --git a/src/pytest_plugins/forks/tests/test_forks.py b/src/pytest_plugins/forks/tests/test_forks.py index 1c534ccb8c3..1d9ddbcc671 100644 --- a/src/pytest_plugins/forks/tests/test_forks.py +++ b/src/pytest_plugins/forks/tests/test_forks.py @@ -4,7 +4,14 @@ import pytest -from ethereum_test_forks import ArrowGlacier, forks_from_until, get_deployed_forks, get_forks +from ethereum_test_forks import ( + ArrowGlacier, + Merge, + forks_from_until, + get_deployed_forks, + get_forks, +) +from ethereum_test_tools import StateTest @pytest.fixture @@ -22,10 +29,10 @@ def test_no_options_no_validity_marker(pytester): - no fork validity marker. """ pytester.makepyfile( - """ + f""" import pytest - def test_all_forks(state_test): + def test_all_forks({StateTest.pytest_parameter_name()}): pass """ ) @@ -33,10 +40,18 @@ def test_all_forks(state_test): result = pytester.runpytest("-v") all_forks = get_deployed_forks() forks_under_test = forks_from_until(all_forks[0], all_forks[-1]) + expected_passed = len(forks_under_test) * len(StateTest.fixture_formats()) + stdout = "\n".join(result.stdout.lines) for fork in forks_under_test: - assert f":test_all_forks[fork_{fork}]" in "\n".join(result.stdout.lines) + for fixture_format in StateTest.fixture_formats(): + if fixture_format.name.endswith("HIVE") and fork < Merge: + expected_passed -= 1 + assert f":test_all_forks[fork_{fork}-{fixture_format.name.lower()}]" not in stdout + continue + assert f":test_all_forks[fork_{fork}-{fixture_format.name.lower()}]" in stdout + result.assert_outcomes( - passed=len(forks_under_test), + passed=expected_passed, failed=0, skipped=0, errors=0, @@ -52,10 +67,10 @@ def test_from_london_option_no_validity_marker(pytester, fork_map, fork): - no fork validity marker. """ pytester.makepyfile( - """ + f""" import pytest - def test_all_forks(state_test): + def test_all_forks({StateTest.pytest_parameter_name()}): pass """ ) @@ -63,10 +78,17 @@ def test_all_forks(state_test): result = pytester.runpytest("-v", "--from", fork) all_forks = get_deployed_forks() forks_under_test = forks_from_until(fork_map[fork], all_forks[-1]) - for fork_under_test in forks_under_test: - assert f":test_all_forks[fork_{fork_under_test}]" in "\n".join(result.stdout.lines) + expected_passed = len(forks_under_test) * len(StateTest.fixture_formats()) + stdout = "\n".join(result.stdout.lines) + for fork in forks_under_test: + for fixture_format in StateTest.fixture_formats(): + if fixture_format.name.endswith("HIVE") and fork < Merge: + expected_passed -= 1 + assert f":test_all_forks[fork_{fork}-{fixture_format.name.lower()}]" not in stdout + continue + assert f":test_all_forks[fork_{fork}-{fixture_format.name.lower()}]" in stdout result.assert_outcomes( - passed=len(forks_under_test), + passed=expected_passed, failed=0, skipped=0, errors=0, @@ -81,22 +103,30 @@ def test_from_london_until_shanghai_option_no_validity_marker(pytester, fork_map - no fork validity marker. """ pytester.makepyfile( - """ + f""" import pytest - def test_all_forks(state_test): + def test_all_forks({StateTest.pytest_parameter_name()}): pass """ ) pytester.copy_example(name="pytest.ini") result = pytester.runpytest("-v", "--from", "London", "--until", "Shanghai") forks_under_test = forks_from_until(fork_map["London"], fork_map["Shanghai"]) + expected_passed = len(forks_under_test) * len(StateTest.fixture_formats()) + stdout = "\n".join(result.stdout.lines) if ArrowGlacier in forks_under_test: forks_under_test.remove(ArrowGlacier) - for fork_under_test in forks_under_test: - assert f":test_all_forks[fork_{fork_under_test}]" in "\n".join(result.stdout.lines) + expected_passed -= len(StateTest.fixture_formats()) + for fork in forks_under_test: + for fixture_format in StateTest.fixture_formats(): + if fixture_format.name.endswith("HIVE") and fork < Merge: + expected_passed -= 1 + assert f":test_all_forks[fork_{fork}-{fixture_format.name.lower()}]" not in stdout + continue + assert f":test_all_forks[fork_{fork}-{fixture_format.name.lower()}]" in stdout result.assert_outcomes( - passed=len(forks_under_test), + passed=expected_passed, failed=0, skipped=0, errors=0, diff --git a/src/pytest_plugins/test_filler/test_filler.py b/src/pytest_plugins/test_filler/test_filler.py index 7337ddce13b..d96d64fb888 100644 --- a/src/pytest_plugins/test_filler/test_filler.py +++ b/src/pytest_plugins/test_filler/test_filler.py @@ -5,28 +5,14 @@ and that modifies pytest hooks in order to fill test specs for all tests and writes the generated fixtures to file. """ -import json -import os -import re import warnings from pathlib import Path -from typing import Any, Dict, Generator, List, Literal, Optional, Tuple, Type, Union +from typing import Generator, List, Optional, Type import pytest -from ethereum_test_forks import Fork, get_development_forks -from ethereum_test_tools import ( - BaseTest, - BaseTestConfig, - BlockchainTest, - BlockchainTestFiller, - Fixture, - HiveFixture, - StateTest, - StateTestFiller, - Yul, - fill_test, -) +from ethereum_test_forks import Fork, Merge, get_development_forks +from ethereum_test_tools import SPEC_TYPES, BaseTest, FixtureCollector, TestInfo, Yul from evm_transition_tool import FixtureFormats, TransitionTool from pytest_plugins.spec_version_checker.spec_version_checker import EIPSpecTestItem @@ -122,13 +108,6 @@ def pytest_addoption(parser): "file. This can be used to increase the granularity of --verify-fixtures." ), ) - test_group.addoption( - "--enable-hive", - action="store_true", - dest="enable_hive", - default=False, - help="Output test fixtures with the hive-specific properties.", - ) debug_group = parser.getgroup("debug", "Arguments defining debug behavior") debug_group.addoption( @@ -149,14 +128,15 @@ def pytest_configure(config): Custom marker registration: https://docs.pytest.org/en/7.1.x/how-to/writing_plugins.html#registering-custom-markers """ - config.addinivalue_line( - "markers", - "state_test: a test case that implement a single state transition test.", - ) - config.addinivalue_line( - "markers", - "blockchain_test: a test case that implements a block transition test.", - ) + for fixture_format in FixtureFormats: + config.addinivalue_line( + "markers", + ( + f"{fixture_format.name.lower()}: " + f"{FixtureFormats.get_format_description(fixture_format)}" + ), + ) + config.addinivalue_line( "markers", "yul_test: a test case that compiles Yul code.", @@ -242,12 +222,6 @@ def do_fixture_verification(request, t8n) -> bool: do_fixture_verification = True if request.config.getoption("verify_fixtures"): do_fixture_verification = True - if do_fixture_verification and request.config.getoption("enable_hive"): - pytest.exit( - "Hive fixtures can not be verify using geth's evm tool: " - "Remove --enable-hive to verify test fixtures.", - returncode=pytest.ExitCode.USAGE_ERROR, - ) return do_fixture_verification @@ -275,85 +249,21 @@ def evm_fixture_verification( evm_fixture_verification.shutdown() -@pytest.fixture(autouse=True, scope="session") -def base_test_config(request) -> BaseTestConfig: - """ - Returns the base test configuration that all tests must use. - """ - config = BaseTestConfig() - config.enable_hive = request.config.getoption("enable_hive") - return config - - -def strip_test_prefix(name: str) -> str: - """ - Removes the test prefix from a test case name. - """ - TEST_PREFIX = "test_" - if name.startswith(TEST_PREFIX): - return name[len(TEST_PREFIX) :] - return name - - -def convert_test_id_to_test_name_and_parameters(name: str) -> Tuple[str, str]: - """ - Converts a test name to a tuple containing the test name and test parameters. - - Example: - test_push0_key_sstore[fork_Shanghai] -> test_push0_key_sstore, fork_Shanghai - """ - test_name, parameters = name.split("[") - return test_name, re.sub(r"[\[\-]", "_", parameters).replace("]", "") - - -def get_module_relative_output_dir(test_module: Path, filler_path: Path) -> Path: - """ - Return a directory name for the provided test_module (relative to the - base ./tests directory) that can be used for output (within the - configured fixtures output path or the base_dump_dir directory). - - Example: - tests/shanghai/eip3855_push0/test_push0.py -> shanghai/eip3855_push0/test_push0 - """ - basename = test_module.with_suffix("").absolute() - basename_relative = basename.relative_to(filler_path.absolute()) - module_path = basename_relative.parent / basename_relative.stem - return module_path - - -def get_dump_dir_path( - base_dump_dir: Path, - filler_path: Path, - node: pytest.Item, - level: Literal["test_module", "test_function", "test_parameter"] = "test_parameter", -) -> Optional[Path]: - """ - The path to dump the debug output as defined by the level to dump at. - """ - if not base_dump_dir: - return None - test_module_relative_dir = get_module_relative_output_dir(Path(node.path), filler_path) - if level == "test_module": - return Path(base_dump_dir) / Path(str(test_module_relative_dir).replace(os.sep, "__")) - test_name, test_parameter_string = convert_test_id_to_test_name_and_parameters(node.name) - flat_path = f"{str(test_module_relative_dir).replace(os.sep, '__')}__{test_name}" - if level == "test_function": - return Path(base_dump_dir) / flat_path - elif level == "test_parameter": - return Path(base_dump_dir) / flat_path / test_parameter_string - raise Exception("Unexpected level.") - - @pytest.fixture(scope="session") -def base_dump_dir(request) -> Path: +def base_dump_dir(request) -> Optional[Path]: """ The base directory to dump the evm debug output. """ - return request.config.getoption("base_dump_dir") + base_dump_dir_str = request.config.getoption("base_dump_dir") + if base_dump_dir_str: + return Path(base_dump_dir_str) + return None @pytest.fixture(scope="function") -def dump_dir_parameter_level(request, base_dump_dir: Path, filler_path: Path) -> Optional[Path]: +def dump_dir_parameter_level( + request, base_dump_dir: Optional[Path], filler_path: Path +) -> Optional[Path]: """ The directory to dump evm transition tool debug output on a test parameter level. @@ -361,7 +271,11 @@ def dump_dir_parameter_level(request, base_dump_dir: Path, filler_path: Path) -> Example with --evm-dump-dir=/tmp/evm: -> /tmp/evm/shanghai__eip3855_push0__test_push0__test_push0_key_sstore/fork_shanghai/ """ - return get_dump_dir_path(base_dump_dir, filler_path, request.node, level="test_parameter") + return node_to_test_info(request.node).get_dump_dir_path( + base_dump_dir, + filler_path, + level="test_parameter", + ) def get_fixture_collection_scope(fixture_name, config): @@ -375,118 +289,13 @@ def get_fixture_collection_scope(fixture_name, config): return "module" -class FixtureCollector: - """ - Collects all fixtures generated by the test cases. - """ - - all_fixtures: Dict[Path, Dict[str, Any]] - output_dir: str - flat_output: bool - json_path_to_fixture_type: Dict[Path, FixtureFormats] - json_path_to_test_item: Dict[Path, pytest.Item] - - def __init__( - self, - output_dir: str, - flat_output: bool, - ) -> None: - self.all_fixtures = {} - self.output_dir = output_dir - self.flat_output = flat_output - self.json_path_to_fixture_type = {} - self.json_path_to_test_item = {} - - def add_fixture( - self, item, fixture: Optional[Union[Fixture, HiveFixture]], fixture_format: FixtureFormats - ) -> None: - """ - Adds a fixture to the list of fixtures of a given test case. - """ - # TODO: remove this logic. if hive enabled set --from to Merge - if fixture is None: - return - - def get_single_test_name(item): - test_name, test_parameters = convert_test_id_to_test_name_and_parameters(item.name) - return f"{test_name}__{test_parameters}" - - def get_fixture_basename_for_flat_output(self, item): - if item.config.getoption("single_fixture_per_file"): - return Path(strip_test_prefix(get_single_test_name(item))) - return Path(strip_test_prefix(item.originalname)) - - def get_fixture_basename_for_nested_output(self, item): - relative_fixture_output_dir = Path(item.path).parent / strip_test_prefix( - Path(item.path).stem - ) - module_relative_output_dir = get_module_relative_output_dir( - relative_fixture_output_dir, item.config.getoption("filler_path") - ) - - if item.config.getoption("single_fixture_per_file"): - return module_relative_output_dir / strip_test_prefix(get_single_test_name(item)) - return module_relative_output_dir / strip_test_prefix(item.originalname) - - fixture_basename: Path - if self.flat_output: - fixture_basename = get_fixture_basename_for_flat_output(self, item) - else: - fixture_basename = get_fixture_basename_for_nested_output(self, item) - - fixture_path = self.output_dir / fixture_basename.with_suffix(".json") - if fixture_path not in self.all_fixtures: # relevant when we group by test function - self.all_fixtures[fixture_path] = {} - self.json_path_to_fixture_type[fixture_path] = fixture_format - self.json_path_to_test_item[fixture_path] = item - - self.all_fixtures[fixture_path][item.nodeid] = fixture.to_json() - - def dump_fixtures(self) -> None: - """ - Dumps all collected fixtures to their respective files. - """ - os.makedirs(self.output_dir, exist_ok=True) - for fixture_path, fixtures in self.all_fixtures.items(): - if not self.flat_output: - os.makedirs(fixture_path.parent, exist_ok=True) - with open(fixture_path, "w") as f: - json.dump(fixtures, f, indent=4) - - def verify_fixture_files(self, evm_fixture_verification: TransitionTool) -> None: - """ - Runs `evm [state|block]test` on each fixture. - """ - for fixture_path, fixture_format in self.json_path_to_fixture_type.items(): - item = self.json_path_to_test_item[fixture_path] - verify_fixtures_dump_dir = self._get_verify_fixtures_dump_dir(item) - evm_fixture_verification.verify_fixture( - fixture_format, fixture_path, verify_fixtures_dump_dir - ) - - def _get_verify_fixtures_dump_dir( - self, - item: pytest.Item, - ): - """ - The directory to dump the current test function's fixture.json and fixture - verification debug output. - """ - base_dump_dir = item.config.getoption("base_dump_dir") - if not base_dump_dir: - return None - filler_path = item.config.getoption("filler_path") - if item.config.getoption("single_fixture_per_file"): - return get_dump_dir_path(base_dump_dir, filler_path, item, level="test_parameter") - else: - return get_dump_dir_path(base_dump_dir, filler_path, item, level="test_function") - - @pytest.fixture(scope=get_fixture_collection_scope) def fixture_collector( request, do_fixture_verification: bool, evm_fixture_verification: TransitionTool, + filler_path: Path, + base_dump_dir: Optional[Path], ): """ Returns the configured fixture collector instance used for all tests @@ -495,6 +304,9 @@ def fixture_collector( fixture_collector = FixtureCollector( output_dir=request.config.getoption("output"), flat_output=request.config.getoption("flat_output"), + single_fixture_per_file=request.config.getoption("single_fixture_per_file"), + filler_path=filler_path, + base_dump_dir=base_dump_dir, ) yield fixture_collector fixture_collector.dump_fixtures() @@ -551,122 +363,130 @@ def __init__(self, *args, **kwargs): return YulWrapper -SPEC_TYPES: List[Type[BaseTest]] = [StateTest, BlockchainTest] SPEC_TYPES_PARAMETERS: List[str] = [s.pytest_parameter_name() for s in SPEC_TYPES] -@pytest.fixture(scope="function") -def fixture_format(request) -> FixtureFormats: +def node_to_test_info(node) -> TestInfo: """ - Returns the test format of the current test case. + Returns the test info of the current node item. """ - enable_hive = request.config.getoption("enable_hive") - has_blockchain_test_format = set(["state_test", "blockchain_test"]) & set(request.fixturenames) - if has_blockchain_test_format and enable_hive: - return FixtureFormats.BLOCKCHAIN_TEST_HIVE - elif has_blockchain_test_format and not enable_hive: - return FixtureFormats.BLOCKCHAIN_TEST - raise Exception("Unknown fixture format.") + return TestInfo( + name=node.name, + id=node.nodeid, + original_name=node.originalname, + path=Path(node.path), + ) -@pytest.fixture(scope="function") -def state_test( - request, - t8n, - fork, - reference_spec, - eips, - dump_dir_parameter_level, - fixture_collector, - fixture_format, - base_test_config, -) -> StateTestFiller: - """ - Fixture used to instantiate an auto-fillable StateTest object from within - a test function. - - Every test that defines a StateTest filler must explicitly specify this - fixture in its function arguments. - - Implementation detail: It must be scoped on test function level to avoid +def base_test_parametrizer(cls: Type[BaseTest]): + """ + Generates a pytest.fixture for a given BaseTest subclass. + + Implementation detail: All spec fixtures must be scoped on test function level to avoid leakage between tests. """ - class StateTestWrapper(StateTest): - def __init__(self, *args, **kwargs): - kwargs["base_test_config"] = base_test_config - kwargs["t8n_dump_dir"] = dump_dir_parameter_level - super(StateTestWrapper, self).__init__(*args, **kwargs) - fixture_collector.add_fixture( - request.node, - fill_test( + @pytest.fixture( + scope="function", + name=cls.pytest_parameter_name(), + ) + def base_test_parametrizer_func( + request, + t8n, + fork, + reference_spec, + eips, + dump_dir_parameter_level, + fixture_collector, + ): + """ + Fixture used to instantiate an auto-fillable BaseTest object from within + a test function. + + Every test that defines a test filler must explicitly specify its parameter name + (see `pytest_parameter_name` in each implementation of BaseTest) in its function + arguments. + + When parametrizing, indirect must be used along with the fixture format as value. + """ + fixture_format = request.param + assert isinstance(fixture_format, FixtureFormats) + + class BaseTestWrapper(cls): + def __init__(self, *args, **kwargs): + kwargs["fixture_format"] = fixture_format + kwargs["t8n_dump_dir"] = dump_dir_parameter_level + super(BaseTestWrapper, self).__init__(*args, **kwargs) + fixture = self.generate( t8n, - self, fork, - reference_spec, eips=eips, - ), - fixture_format, - ) + ) + fixture.fill_info(t8n, reference_spec) - return StateTestWrapper + fixture_collector.add_fixture( + node_to_test_info(request.node), + fixture, + ) + return BaseTestWrapper + + return base_test_parametrizer_func -@pytest.fixture(scope="function") -def blockchain_test( - request, - t8n, - fork, - reference_spec, - eips, - dump_dir_parameter_level, - fixture_collector, - fixture_format, - base_test_config, -) -> BlockchainTestFiller: - """ - Fixture used to define an auto-fillable BlockchainTest analogous to the - state_test fixture for StateTests. - See the state_test fixture docstring for details. - """ - - class BlockchainTestWrapper(BlockchainTest): - def __init__(self, *args, **kwargs): - kwargs["base_test_config"] = base_test_config - kwargs["t8n_dump_dir"] = dump_dir_parameter_level - super(BlockchainTestWrapper, self).__init__(*args, **kwargs) - fixture_collector.add_fixture( - request.node, - fill_test( - t8n, - self, - fork, - reference_spec, - eips=eips, - ), - fixture_format, - ) - return BlockchainTestWrapper +# Dynamically generate a pytest fixture for each test spec type. +for cls in SPEC_TYPES: + # Fixture needs to be defined in the global scope so pytest can detect it. + globals()[cls.pytest_parameter_name()] = base_test_parametrizer(cls) -def pytest_collection_modifyitems(items, config): +def pytest_generate_tests(metafunc): + """ + Pytest hook used to dynamically generate test cases for each fixture format a given + test spec supports. + """ + for test_type in SPEC_TYPES: + if test_type.pytest_parameter_name() in metafunc.fixturenames: + metafunc.parametrize( + [test_type.pytest_parameter_name()], + [ + pytest.param( + fixture_format, + id=fixture_format.name.lower(), + marks=[getattr(pytest.mark, fixture_format.name.lower())], + ) + for fixture_format in test_type.fixture_formats() + ], + scope="function", + indirect=True, + ) + + +@pytest.hookimpl(trylast=True) +def pytest_collection_modifyitems(config, items): """ - A pytest hook called during collection, after all items have been - collected. + Remove pre-Merge tests parametrized to generate hive type fixtures; these + can't be used in the Hive Pyspec Simulator. - Here we dynamically apply "state_test" or "blockchain_test" markers - to a test if the test function uses the corresponding fixture. + This can't be handled in this plugins pytest_generate_tests() as the fork + parametrization occurs in the forks plugin. """ - for item in items: + for item in items[:]: # use a copy of the list, as we'll be modifying it if isinstance(item, EIPSpecTestItem): continue - if "state_test" in item.fixturenames: - marker = pytest.mark.state_test() - item.add_marker(marker) - elif "blockchain_test" in item.fixturenames: - marker = pytest.mark.blockchain_test() - item.add_marker(marker) + if item.callspec.params["fork"] < Merge: + # Even though the `state_test` test spec does not produce a hive STATE_TEST, it does + # produce a BLOCKCHAIN_TEST_HIVE, so we need to remove it here. + # TODO: Ideally, the logic could be contained in the `FixtureFormat` class, we create + # a `fork_supported` method that returns True if the fork is supported. + if ("state_test" in item.callspec.params) and item.callspec.params[ + "state_test" + ].name.endswith("HIVE"): + items.remove(item) + if ("blockchain_test" in item.callspec.params) and item.callspec.params[ + "blockchain_test" + ].name.endswith("HIVE"): + items.remove(item) def pytest_runtest_setup(item): diff --git a/src/pytest_plugins/test_filler/tests/test_test_filler.py b/src/pytest_plugins/test_filler/tests/test_test_filler.py index 256982d8bd4..d8db47539df 100644 --- a/src/pytest_plugins/test_filler/tests/test_test_filler.py +++ b/src/pytest_plugins/test_filler/tests/test_test_filler.py @@ -9,6 +9,7 @@ import pytest +# flake8: noqa def get_all_files_in_directory(base_dir): # noqa: D103 base_path = Path(base_dir) return [f.relative_to(os.getcwd()) for f in base_path.rglob("*") if f.is_file()] @@ -28,19 +29,19 @@ def count_keys_in_fixture(file_path): # noqa: D103 """\ import pytest - from ethereum_test_tools import Account, Environment, TestAddress + from ethereum_test_tools import Account, Environment, TestAddress, Transaction @pytest.mark.valid_from("Merge") @pytest.mark.valid_until("Shanghai") def test_merge_one(state_test): state_test(env=Environment(), - pre={TestAddress: Account(balance=1_000_000)}, post={}, txs=[]) + pre={TestAddress: Account(balance=1_000_000)}, post={}, tx=Transaction()) @pytest.mark.valid_from("Merge") @pytest.mark.valid_until("Shanghai") def test_merge_two(state_test): state_test(env=Environment(), - pre={TestAddress: Account(balance=1_000_000)}, post={}, txs=[]) + pre={TestAddress: Account(balance=1_000_000)}, post={}, tx=Transaction()) """ ) test_count_merge = 4 @@ -49,25 +50,26 @@ def test_merge_two(state_test): """\ import pytest - from ethereum_test_tools import Account, Environment, TestAddress + from ethereum_test_tools import Account, Environment, TestAddress, Transaction @pytest.mark.valid_from("Merge") @pytest.mark.valid_until("Shanghai") def test_shanghai_one(state_test): state_test(env=Environment(), - pre={TestAddress: Account(balance=1_000_000)}, post={}, txs=[]) + pre={TestAddress: Account(balance=1_000_000)}, post={}, tx=Transaction()) @pytest.mark.parametrize("x", [1, 2, 3]) @pytest.mark.valid_from("Merge") @pytest.mark.valid_until("Shanghai") def test_shanghai_two(state_test, x): state_test(env=Environment(), - pre={TestAddress: Account(balance=1_000_000)}, post={}, txs=[]) + pre={TestAddress: Account(balance=1_000_000)}, post={}, tx=Transaction()) """ ) test_count_shanghai = 8 -test_count = test_count_merge + test_count_shanghai + +total_test_count = test_count_merge + test_count_shanghai @pytest.mark.parametrize( @@ -76,133 +78,367 @@ def test_shanghai_two(state_test, x): pytest.param( [], [ - Path("fixtures/merge/module_merge/merge_one.json"), - Path("fixtures/merge/module_merge/merge_two.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_one.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_two.json"), + Path("fixtures/blockchain_tests/merge/module_merge/merge_one.json"), + Path("fixtures/blockchain_tests_hive/merge/module_merge/merge_one.json"), + Path("fixtures/state_tests/merge/module_merge/merge_one.json"), + Path("fixtures/blockchain_tests/merge/module_merge/merge_two.json"), + Path("fixtures/blockchain_tests_hive/merge/module_merge/merge_two.json"), + Path("fixtures/state_tests/merge/module_merge/merge_two.json"), + Path("fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_one.json"), + Path("fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_one.json"), + Path("fixtures/state_tests/shanghai/module_shanghai/shanghai_one.json"), + Path("fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two.json"), + Path("fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two.json"), + Path("fixtures/state_tests/shanghai/module_shanghai/shanghai_two.json"), ], - [2, 2, 2, 6], + [2, 2, 2, 2, 2, 2, 2, 2, 2, 6, 6, 6], id="default-args", ), pytest.param( ["--flat-output"], [ - Path("fixtures/merge_one.json"), - Path("fixtures/merge_two.json"), - Path("fixtures/shanghai_one.json"), - Path("fixtures/shanghai_two.json"), + Path("fixtures/blockchain_tests/merge_one.json"), + Path("fixtures/blockchain_tests_hive/merge_one.json"), + Path("fixtures/state_tests/merge_one.json"), + Path("fixtures/blockchain_tests/merge_two.json"), + Path("fixtures/blockchain_tests_hive/merge_two.json"), + Path("fixtures/state_tests/merge_two.json"), + Path("fixtures/blockchain_tests/shanghai_one.json"), + Path("fixtures/blockchain_tests_hive/shanghai_one.json"), + Path("fixtures/state_tests/shanghai_one.json"), + Path("fixtures/blockchain_tests/shanghai_two.json"), + Path("fixtures/blockchain_tests_hive/shanghai_two.json"), + Path("fixtures/state_tests/shanghai_two.json"), ], - [2, 2, 2, 6], + [2, 2, 2, 2, 2, 2, 2, 2, 2, 6, 6, 6], id="flat-output", ), pytest.param( ["--flat-output", "--output", "other_fixtures"], [ - Path("other_fixtures/merge_one.json"), - Path("other_fixtures/merge_two.json"), - Path("other_fixtures/shanghai_one.json"), - Path("other_fixtures/shanghai_two.json"), + Path("other_fixtures/blockchain_tests/merge_one.json"), + Path("other_fixtures/blockchain_tests_hive/merge_one.json"), + Path("other_fixtures/state_tests/merge_one.json"), + Path("other_fixtures/blockchain_tests/merge_two.json"), + Path("other_fixtures/blockchain_tests_hive/merge_two.json"), + Path("other_fixtures/state_tests/merge_two.json"), + Path("other_fixtures/blockchain_tests/shanghai_one.json"), + Path("other_fixtures/blockchain_tests_hive/shanghai_one.json"), + Path("other_fixtures/state_tests/shanghai_one.json"), + Path("other_fixtures/blockchain_tests/shanghai_two.json"), + Path("other_fixtures/blockchain_tests_hive/shanghai_two.json"), + Path("other_fixtures/state_tests/shanghai_two.json"), ], - [2, 2, 2, 6], + [2, 2, 2, 2, 2, 2, 2, 2, 2, 6, 6, 6], id="flat-output_custom-output-dir", ), - pytest.param( - ["--flat-output", "--output", "other_fixtures", "--enable-hive"], - [ - Path("other_fixtures/merge_one.json"), - Path("other_fixtures/merge_two.json"), - Path("other_fixtures/shanghai_one.json"), - Path("other_fixtures/shanghai_two.json"), - ], - [2, 2, 2, 6], - id="flat-output_custom-output-dir_enable-hive", - ), pytest.param( ["--single-fixture-per-file"], [ - Path("fixtures/merge/module_merge/merge_one__fork_Merge.json"), - Path("fixtures/merge/module_merge/merge_one__fork_Shanghai.json"), - Path("fixtures/merge/module_merge/merge_two__fork_Merge.json"), - Path("fixtures/merge/module_merge/merge_two__fork_Shanghai.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_one__fork_Merge.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_one__fork_Shanghai.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_1.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_2.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_3.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_1.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_2.json"), - Path("fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_3.json"), + Path( + "fixtures/blockchain_tests/merge/module_merge/merge_one__fork_Merge_blockchain_test.json" + ), + Path( + "fixtures/state_tests/merge/module_merge/merge_one__fork_Merge_state_test.json" + ), + Path( + "fixtures/blockchain_tests_hive/merge/module_merge/merge_one__fork_Merge_blockchain_test_hive.json" + ), + Path( + "fixtures/blockchain_tests/merge/module_merge/merge_one__fork_Shanghai_blockchain_test.json" + ), + Path( + "fixtures/state_tests/merge/module_merge/merge_one__fork_Shanghai_state_test.json" + ), + Path( + "fixtures/blockchain_tests_hive/merge/module_merge/merge_one__fork_Shanghai_blockchain_test_hive.json" + ), + Path( + "fixtures/blockchain_tests/merge/module_merge/merge_two__fork_Merge_blockchain_test.json" + ), + Path( + "fixtures/state_tests/merge/module_merge/merge_two__fork_Merge_state_test.json" + ), + Path( + "fixtures/blockchain_tests_hive/merge/module_merge/merge_two__fork_Merge_blockchain_test_hive.json" + ), + Path( + "fixtures/blockchain_tests/merge/module_merge/merge_two__fork_Shanghai_blockchain_test.json" + ), + Path( + "fixtures/state_tests/merge/module_merge/merge_two__fork_Shanghai_state_test.json" + ), + Path( + "fixtures/blockchain_tests_hive/merge/module_merge/merge_two__fork_Shanghai_blockchain_test_hive.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_one__fork_Merge_blockchain_test.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_one__fork_Merge_state_test.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_one__fork_Merge_blockchain_test_hive.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_one__fork_Shanghai_blockchain_test.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_one__fork_Shanghai_state_test.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_one__fork_Shanghai_blockchain_test_hive.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_x_1.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_state_test_x_1.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_hive_x_1.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_x_2.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_state_test_x_2.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_hive_x_2.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_x_3.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_state_test_x_3.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_hive_x_3.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_x_1.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_state_test_x_1.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_hive_x_1.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_x_2.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_state_test_x_2.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_hive_x_2.json" + ), + Path( + "fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_x_3.json" + ), + Path( + "fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_state_test_x_3.json" + ), + Path( + "fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_hive_x_3.json" + ), ], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1] * 36, id="single-fixture-per-file", ), pytest.param( ["--single-fixture-per-file", "--output", "other_fixtures"], [ - Path("other_fixtures/merge/module_merge/merge_one__fork_Merge.json"), - Path("other_fixtures/merge/module_merge/merge_one__fork_Shanghai.json"), - Path("other_fixtures/merge/module_merge/merge_two__fork_Merge.json"), - Path("other_fixtures/merge/module_merge/merge_two__fork_Shanghai.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_one__fork_Merge.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_one__fork_Shanghai.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_1.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_2.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_3.json"), Path( - "other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_1.json" + "other_fixtures/blockchain_tests/merge/module_merge/merge_one__fork_Merge_blockchain_test.json" ), Path( - "other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_2.json" + "other_fixtures/state_tests/merge/module_merge/merge_one__fork_Merge_state_test.json" ), Path( - "other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_3.json" + "other_fixtures/blockchain_tests_hive/merge/module_merge/merge_one__fork_Merge_blockchain_test_hive.json" + ), + Path( + "other_fixtures/blockchain_tests/merge/module_merge/merge_one__fork_Shanghai_blockchain_test.json" + ), + Path( + "other_fixtures/state_tests/merge/module_merge/merge_one__fork_Shanghai_state_test.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/merge/module_merge/merge_one__fork_Shanghai_blockchain_test_hive.json" + ), + Path( + "other_fixtures/blockchain_tests/merge/module_merge/merge_two__fork_Merge_blockchain_test.json" ), - ], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - id="single-fixture-per-file_custom_output_dir", - ), - pytest.param( - ["--single-fixture-per-file", "--output", "other_fixtures", "--enable-hive"], - [ - Path("other_fixtures/merge/module_merge/merge_one__fork_Merge.json"), - Path("other_fixtures/merge/module_merge/merge_one__fork_Shanghai.json"), - Path("other_fixtures/merge/module_merge/merge_two__fork_Merge.json"), - Path("other_fixtures/merge/module_merge/merge_two__fork_Shanghai.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_one__fork_Merge.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_one__fork_Shanghai.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_1.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_2.json"), - Path("other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Merge_x_3.json"), Path( - "other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_1.json" + "other_fixtures/state_tests/merge/module_merge/merge_two__fork_Merge_state_test.json" ), Path( - "other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_2.json" + "other_fixtures/blockchain_tests_hive/merge/module_merge/merge_two__fork_Merge_blockchain_test_hive.json" ), Path( - "other_fixtures/shanghai/module_shanghai/shanghai_two__fork_Shanghai_x_3.json" + "other_fixtures/blockchain_tests/merge/module_merge/merge_two__fork_Shanghai_blockchain_test.json" + ), + Path( + "other_fixtures/state_tests/merge/module_merge/merge_two__fork_Shanghai_state_test.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/merge/module_merge/merge_two__fork_Shanghai_blockchain_test_hive.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_one__fork_Merge_blockchain_test.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_one__fork_Merge_state_test.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_one__fork_Merge_blockchain_test_hive.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_one__fork_Shanghai_blockchain_test.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_one__fork_Shanghai_state_test.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_one__fork_Shanghai_blockchain_test_hive.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_x_1.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_state_test_x_1.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_hive_x_1.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_x_2.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_state_test_x_2.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_hive_x_2.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_x_3.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Merge_state_test_x_3.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Merge_blockchain_test_hive_x_3.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_x_1.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_state_test_x_1.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_hive_x_1.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_x_2.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_state_test_x_2.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_hive_x_2.json" + ), + Path( + "other_fixtures/blockchain_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_x_3.json" + ), + Path( + "other_fixtures/state_tests/shanghai/module_shanghai/shanghai_two__fork_Shanghai_state_test_x_3.json" + ), + Path( + "other_fixtures/blockchain_tests_hive/shanghai/module_shanghai/shanghai_two__fork_Shanghai_blockchain_test_hive_x_3.json" ), ], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - id="single-fixture-per-file_custom_output_dir_enable-hive", + [1] * 36, + id="single-fixture-per-file_custom_output_dir", ), pytest.param( ["--flat-output", "--single-fixture-per-file"], [ - Path("fixtures/merge_one__fork_Merge.json"), - Path("fixtures/merge_one__fork_Shanghai.json"), - Path("fixtures/merge_two__fork_Merge.json"), - Path("fixtures/merge_two__fork_Shanghai.json"), - Path("fixtures/shanghai_one__fork_Merge.json"), - Path("fixtures/shanghai_one__fork_Shanghai.json"), - Path("fixtures/shanghai_two__fork_Merge_x_1.json"), - Path("fixtures/shanghai_two__fork_Merge_x_2.json"), - Path("fixtures/shanghai_two__fork_Merge_x_3.json"), - Path("fixtures/shanghai_two__fork_Shanghai_x_1.json"), - Path("fixtures/shanghai_two__fork_Shanghai_x_2.json"), - Path("fixtures/shanghai_two__fork_Shanghai_x_3.json"), + Path("fixtures/blockchain_tests/merge_one__fork_Merge_blockchain_test.json"), + Path("fixtures/state_tests/merge_one__fork_Merge_state_test.json"), + Path( + "fixtures/blockchain_tests_hive/merge_one__fork_Merge_blockchain_test_hive.json" + ), + Path("fixtures/blockchain_tests/merge_one__fork_Shanghai_blockchain_test.json"), + Path("fixtures/state_tests/merge_one__fork_Shanghai_state_test.json"), + Path( + "fixtures/blockchain_tests_hive/merge_one__fork_Shanghai_blockchain_test_hive.json" + ), + Path("fixtures/blockchain_tests/merge_two__fork_Merge_blockchain_test.json"), + Path("fixtures/state_tests/merge_two__fork_Merge_state_test.json"), + Path( + "fixtures/blockchain_tests_hive/merge_two__fork_Merge_blockchain_test_hive.json" + ), + Path("fixtures/blockchain_tests/merge_two__fork_Shanghai_blockchain_test.json"), + Path("fixtures/state_tests/merge_two__fork_Shanghai_state_test.json"), + Path( + "fixtures/blockchain_tests_hive/merge_two__fork_Shanghai_blockchain_test_hive.json" + ), + Path("fixtures/blockchain_tests/shanghai_one__fork_Merge_blockchain_test.json"), + Path("fixtures/state_tests/shanghai_one__fork_Merge_state_test.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_one__fork_Merge_blockchain_test_hive.json" + ), + Path("fixtures/blockchain_tests/shanghai_one__fork_Shanghai_blockchain_test.json"), + Path("fixtures/state_tests/shanghai_one__fork_Shanghai_state_test.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_one__fork_Shanghai_blockchain_test_hive.json" + ), + Path( + "fixtures/blockchain_tests/shanghai_two__fork_Merge_blockchain_test_x_1.json" + ), + Path("fixtures/state_tests/shanghai_two__fork_Merge_state_test_x_1.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_two__fork_Merge_blockchain_test_hive_x_1.json" + ), + Path( + "fixtures/blockchain_tests/shanghai_two__fork_Merge_blockchain_test_x_2.json" + ), + Path("fixtures/state_tests/shanghai_two__fork_Merge_state_test_x_2.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_two__fork_Merge_blockchain_test_hive_x_2.json" + ), + Path( + "fixtures/blockchain_tests/shanghai_two__fork_Merge_blockchain_test_x_3.json" + ), + Path("fixtures/state_tests/shanghai_two__fork_Merge_state_test_x_3.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_two__fork_Merge_blockchain_test_hive_x_3.json" + ), + Path( + "fixtures/blockchain_tests/shanghai_two__fork_Shanghai_blockchain_test_x_1.json" + ), + Path("fixtures/state_tests/shanghai_two__fork_Shanghai_state_test_x_1.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_two__fork_Shanghai_blockchain_test_hive_x_1.json" + ), + Path( + "fixtures/blockchain_tests/shanghai_two__fork_Shanghai_blockchain_test_x_2.json" + ), + Path("fixtures/state_tests/shanghai_two__fork_Shanghai_state_test_x_2.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_two__fork_Shanghai_blockchain_test_hive_x_2.json" + ), + Path( + "fixtures/blockchain_tests/shanghai_two__fork_Shanghai_blockchain_test_x_3.json" + ), + Path("fixtures/state_tests/shanghai_two__fork_Shanghai_state_test_x_3.json"), + Path( + "fixtures/blockchain_tests_hive/shanghai_two__fork_Shanghai_blockchain_test_hive_x_3.json" + ), ], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1] * 36, id="flat-single-per-file_flat-output", ), ], @@ -244,7 +480,7 @@ def test_fixture_output_based_on_command_line_args( args.append("-v") result = testdir.runpytest(*args) result.assert_outcomes( - passed=test_count, + passed=total_test_count * 3, failed=0, skipped=0, errors=0, diff --git a/tests/berlin/eip2930_access_list/test_acl.py b/tests/berlin/eip2930_access_list/test_acl.py index 6b29ef959ab..b69a5ada7e1 100644 --- a/tests/berlin/eip2930_access_list/test_acl.py +++ b/tests/berlin/eip2930_access_list/test_acl.py @@ -4,7 +4,6 @@ import pytest -from ethereum_test_forks import Fork, London, is_fork from ethereum_test_tools import AccessList, Account, Environment from ethereum_test_tools import Opcodes as Op from ethereum_test_tools import StateTestFiller, Transaction @@ -14,8 +13,7 @@ @pytest.mark.valid_from("Berlin") -@pytest.mark.valid_until("London") -def test_access_list(state_test: StateTestFiller, fork: Fork): +def test_access_list(state_test: StateTestFiller): """ Test type 1 transaction. """ @@ -59,12 +57,9 @@ def test_access_list(state_test: StateTestFiller, fork: Fork): balance=4, nonce=1, ), - "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba": Account( - balance=0x1BC16D674EC80000 if is_fork(fork, London) else 0x1BC16D674ECB26CE, - ), "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": Account( balance=0x2CD931, nonce=1, ), } - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/byzantium/__init__.py b/tests/byzantium/__init__.py new file mode 100644 index 00000000000..6792ecb2cc1 --- /dev/null +++ b/tests/byzantium/__init__.py @@ -0,0 +1,3 @@ +""" +Test cases for EVM functionality introduced in Byzantium. +""" diff --git a/tests/byzantium/eip198_modexp_precompile/__init__.py b/tests/byzantium/eip198_modexp_precompile/__init__.py new file mode 100644 index 00000000000..d46d4fe5a75 --- /dev/null +++ b/tests/byzantium/eip198_modexp_precompile/__init__.py @@ -0,0 +1,3 @@ +""" +Test for precompiles introduced in Byzantium. +""" diff --git a/tests/byzantium/eip198_modexp_precompile/test_modexp.py b/tests/byzantium/eip198_modexp_precompile/test_modexp.py new file mode 100644 index 00000000000..526e8160b75 --- /dev/null +++ b/tests/byzantium/eip198_modexp_precompile/test_modexp.py @@ -0,0 +1,272 @@ +""" +abstract: Test [EIP-198: MODEXP Precompile](https://eips.ethereum.org/EIPS/eip-198) + + Tests the MODEXP precompile, located at address 0x0000..0005. Test cases from the EIP are + labelled with `EIP-198-caseX` in the test id. +""" +from dataclasses import dataclass + +import pytest + +from ethereum_test_tools import ( + Account, + Environment, + StateTestFiller, + TestAddress, + TestParameterGroup, + Transaction, + compute_create_address, + to_address, +) +from ethereum_test_tools.vm.opcode import Opcodes as Op + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-198.md" +REFERENCE_SPEC_VERSION = "9e393a79d9937f579acbdcb234a67869259d5a96" + + +@dataclass(kw_only=True, frozen=True, repr=False) +class ModExpInput(TestParameterGroup): + """ + Helper class that defines the MODEXP precompile inputs and creates the + call data from them. + + Attributes: + base (str): The base value for the MODEXP precompile. + exponent (str): The exponent value for the MODEXP precompile. + modulus (str): The modulus value for the MODEXP precompile. + extra_data (str): Defines extra padded data to be added at the end of the calldata + to the precompile. Defaults to an empty string. + """ + + base: str + exponent: str + modulus: str + extra_data: str = "" + + def create_modexp_tx_data(self): + """ + Generates input for the MODEXP precompile. + """ + return ( + "0x" + + f"{int(len(self.base)/2):x}".zfill(64) + + f"{int(len(self.exponent)/2):x}".zfill(64) + + f"{int(len(self.modulus)/2):x}".zfill(64) + + self.base + + self.exponent + + self.modulus + + self.extra_data + ) + + +@dataclass(kw_only=True, frozen=True, repr=False) +class ModExpRawInput(TestParameterGroup): + """ + Helper class to directly define a raw input to the MODEXP precompile. + """ + + raw_input: str + + def create_modexp_tx_data(self): + """ + The raw input is already the MODEXP precompile input. + """ + return self.raw_input + + +@dataclass(kw_only=True, frozen=True, repr=False) +class ExpectedOutput(TestParameterGroup): + """ + Expected test result. + + Attributes: + call_return_code (str): The return_code from CALL, 0 indicates unsuccessful call + (out-of-gas), 1 indicates call succeeded. + returned_data (str): The output returnData is the expected output of the call + """ + + call_return_code: str + returned_data: str + + +@pytest.mark.valid_from("Byzantium") +@pytest.mark.parametrize( + ["input", "output"], + [ + ( + ModExpInput(base="", exponent="", modulus="02"), + ExpectedOutput(call_return_code="0x01", returned_data="0x01"), + ), + ( + ModExpInput(base="", exponent="", modulus="0002"), + ExpectedOutput(call_return_code="0x01", returned_data="0x0001"), + ), + ( + ModExpInput(base="00", exponent="00", modulus="02"), + ExpectedOutput(call_return_code="0x01", returned_data="0x01"), + ), + ( + ModExpInput(base="", exponent="01", modulus="02"), + ExpectedOutput(call_return_code="0x01", returned_data="0x00"), + ), + ( + ModExpInput(base="01", exponent="01", modulus="02"), + ExpectedOutput(call_return_code="0x01", returned_data="0x01"), + ), + ( + ModExpInput(base="02", exponent="01", modulus="03"), + ExpectedOutput(call_return_code="0x01", returned_data="0x02"), + ), + ( + ModExpInput(base="02", exponent="02", modulus="05"), + ExpectedOutput(call_return_code="0x01", returned_data="0x04"), + ), + ( + ModExpInput(base="", exponent="", modulus=""), + ExpectedOutput(call_return_code="0x01", returned_data="0x"), + ), + ( + ModExpInput(base="", exponent="", modulus="00"), + ExpectedOutput(call_return_code="0x01", returned_data="0x00"), + ), + ( + ModExpInput(base="", exponent="", modulus="01"), + ExpectedOutput(call_return_code="0x01", returned_data="0x00"), + ), + ( + ModExpInput(base="", exponent="", modulus="0001"), + ExpectedOutput(call_return_code="0x01", returned_data="0x0000"), + ), + # Test cases from EIP 198. + pytest.param( + ModExpInput( + base="03", + exponent="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e", + modulus="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + ), + ExpectedOutput( + call_return_code="0x01", + returned_data="0000000000000000000000000000000000000000000000000000000000000001", + ), + id="EIP-198-case1", + ), + pytest.param( + ModExpInput( + base="", + exponent="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e", + modulus="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + ), + ExpectedOutput( + call_return_code="0x01", + returned_data="0000000000000000000000000000000000000000000000000000000000000000", + ), + id="EIP-198-case2", + ), + pytest.param( # Note: This is the only test case which goes out-of-gas. + ModExpRawInput( + raw_input="0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000020" + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd" + ), + ExpectedOutput( + call_return_code="0x00", + returned_data="0000000000000000000000000000000000000000000000000000000000000000", + ), + id="EIP-198-case3-raw-input-out-of-gas", + ), + pytest.param( + ModExpInput( + base="03", + exponent="ffff", + modulus="8000000000000000000000000000000000000000000000000000000000000000", + extra_data="07", + ), + ExpectedOutput( + call_return_code="0x01", + returned_data="0x3b01b01ac41f2d6e917c6d6a221ce793802469026d9ab7578fa2e79e4da6aaab", + ), + id="EIP-198-case4-extra-data_07", + ), + pytest.param( + ModExpRawInput( + raw_input="0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000002" + "0000000000000000000000000000000000000000000000000000000000000020" + "03" + "ffff" + "80" + ), + ExpectedOutput( + call_return_code="0x01", + returned_data="0x3b01b01ac41f2d6e917c6d6a221ce793802469026d9ab7578fa2e79e4da6aaab", + ), + id="EIP-198-case5-raw-input", + ), + ], + ids=lambda param: param.__repr__(), # only required to remove parameter names (input/output) +) +def test_modexp(state_test: StateTestFiller, input: ModExpInput, output: ExpectedOutput): + """ + Test the MODEXP precompile + """ + env = Environment() + pre = {TestAddress: Account(balance=1000000000000000000000)} + + account = to_address(0x100) + + pre[account] = Account( + code=( + # Store all CALLDATA into memory (offset 0) + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE()) + # Store the returned CALL status (success = 1, fail = 0) into slot 0: + + Op.SSTORE( + 0, + # Setup stack to CALL into ModExp with the CALLDATA and CALL into it (+ pop value) + Op.CALL(Op.GAS(), 0x05, 0, 0, Op.CALLDATASIZE(), 0, 0), + ) + # Store contract deployment code to deploy the returned data from ModExp as + # contract code (16 bytes) + + Op.MSTORE( + 0, + ( + ( + # Need to `ljust` this PUSH32 in order to ensure the code starts + # in memory at offset 0 (memory right-aligns stack items which are not + # 32 bytes) + Op.PUSH32( + ( + Op.CODECOPY(0, 16, Op.SUB(Op.CODESIZE(), 16)) + + Op.RETURN(0, Op.SUB(Op.CODESIZE, 16)) + ).ljust(32, bytes(1)) + ) + ) + ), + ) + # RETURNDATACOPY the returned data from ModExp into memory (offset 16 bytes) + + Op.RETURNDATACOPY(16, 0, Op.RETURNDATASIZE()) + # CREATE contract with the deployment code + the returned data from ModExp + + Op.CREATE(0, 0, Op.ADD(16, Op.RETURNDATASIZE())) + # STOP (handy for tracing) + + Op.STOP() + ) + ) + + tx = Transaction( + ty=0x0, + nonce=0, + to=account, + data=input.create_modexp_tx_data(), + gas_limit=500000, + gas_price=10, + protected=True, + ) + + post = {} + if output.call_return_code != "0x00": + contract_address = compute_create_address(account, tx.nonce) + post[contract_address] = Account(code=output.returned_data) + post[account] = Account(storage={0: output.call_return_code}) + + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip1153_tstore/test_tstorage.py b/tests/cancun/eip1153_tstore/test_tstorage.py index de8efe4c2e7..5cf655f4254 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage.py +++ b/tests/cancun/eip1153_tstore/test_tstorage.py @@ -42,13 +42,11 @@ def test_transient_storage_unset_values(state_test: StateTestFiller): code_address: Account(code=code, storage={slot: 1 for slot in slots_under_test}), } - txs = [ - Transaction( - to=code_address, - data=b"", - gas_limit=1_000_000, - ) - ] + tx = Transaction( + to=code_address, + data=b"", + gas_limit=1_000_000, + ) post = {code_address: Account(storage={slot: 0 for slot in slots_under_test})} @@ -56,7 +54,7 @@ def test_transient_storage_unset_values(state_test: StateTestFiller): env=env, pre=pre, post=post, - txs=txs, + tx=tx, ) @@ -79,13 +77,11 @@ def test_tload_after_tstore(state_test: StateTestFiller): code_address: Account(code=code, storage={slot: 0 for slot in slots_under_test}), } - txs = [ - Transaction( - to=code_address, - data=b"", - gas_limit=1_000_000, - ) - ] + tx = Transaction( + to=code_address, + data=b"", + gas_limit=1_000_000, + ) post = {code_address: Account(storage={slot: slot for slot in slots_under_test})} @@ -93,7 +89,7 @@ def test_tload_after_tstore(state_test: StateTestFiller): env=env, pre=pre, post=post, - txs=txs, + tx=tx, ) @@ -119,13 +115,11 @@ def test_tload_after_sstore(state_test: StateTestFiller): code_address: Account(code=code, storage={slot: 1 for slot in slots_under_test}), } - txs = [ - Transaction( - to=code_address, - data=b"", - gas_limit=1_000_000, - ) - ] + tx = Transaction( + to=code_address, + data=b"", + gas_limit=1_000_000, + ) post = { code_address: Account( @@ -139,7 +133,7 @@ def test_tload_after_sstore(state_test: StateTestFiller): env=env, pre=pre, post=post, - txs=txs, + tx=tx, ) @@ -166,13 +160,11 @@ def test_tload_after_tstore_is_zero(state_test: StateTestFiller): ), } - txs = [ - Transaction( - to=code_address, - data=b"", - gas_limit=1_000_000, - ) - ] + tx = Transaction( + to=code_address, + data=b"", + gas_limit=1_000_000, + ) post = { code_address: Account( @@ -184,7 +176,7 @@ def test_tload_after_tstore_is_zero(state_test: StateTestFiller): env=env, pre=pre, post=post, - txs=txs, + tx=tx, ) @@ -244,15 +236,13 @@ def test_gas_usage( TestAddress: Account(balance=10_000_000, nonce=0), code_address: Account(code=gas_measure_bytecode), } - txs = [ - Transaction( - to=code_address, - data=b"", - gas_limit=1_000_000, - ) - ] + tx = Transaction( + to=code_address, + data=b"", + gas_limit=1_000_000, + ) post = { code_address: Account(code=gas_measure_bytecode, storage={0: expected_gas}), TestAddress: Account(nonce=1), } - state_test(env=env, pre=pre, txs=txs, post=post) + state_test(env=env, pre=pre, tx=tx, post=post) diff --git a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py index 32d6ca31747..d2d033ef1ce 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py @@ -237,5 +237,5 @@ def test_contract_creation( env=Environment(), pre=pre, post=post, - txs=[tx], + tx=tx, ) diff --git a/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py index 5f22bb8dc2c..a3173095a88 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py @@ -5,7 +5,7 @@ """ # noqa: E501 from enum import EnumMeta, unique -from typing import List, Mapping +from typing import Mapping import pytest @@ -314,12 +314,10 @@ def __init__(self, value): caller_address: Account(code=value["caller_bytecode"]), callee_address: Account(code=value["callee_bytecode"]), }, - "txs": [ - Transaction( - to=caller_address, - gas_limit=1_000_000, - ) - ], + "tx": Transaction( + to=caller_address, + gas_limit=1_000_000, + ), "post": { caller_address: Account(storage=value["expected_caller_storage"]), callee_address: Account(storage=value["expected_callee_storage"]), @@ -333,7 +331,7 @@ def test_subcall( state_test: StateTestFiller, env: Environment, pre: Mapping, - txs: List[Transaction], + tx: Transaction, post: Mapping, ): """ @@ -344,4 +342,4 @@ def test_subcall( - `DELEGATECALL` - `STATICCALL` """ - state_test(env=env, pre=pre, post=post, txs=txs) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py index 5ad4c3c2447..0e26304b869 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_reentrancy_contexts.py @@ -279,4 +279,4 @@ def test_reentrant_call(state_test: StateTestFiller, bytecode, expected_storage) post = {callee_address: Account(code=bytecode, storage=expected_storage)} - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip1153_tstore/test_tstorage_selfdestruct.py b/tests/cancun/eip1153_tstore/test_tstorage_selfdestruct.py index 8225c67f0e7..73a8074d97b 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_selfdestruct.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_selfdestruct.py @@ -249,4 +249,4 @@ def test_reentrant_selfdestructing_call( else: post[callee_address] = Account.NONEXISTENT - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py b/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py index 02aa434500b..1b5ef3e8007 100644 --- a/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py +++ b/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py @@ -92,7 +92,7 @@ def test_beacon_root_contract_calls( state_test( env=env, pre=pre, - txs=[tx], + tx=tx, post=post, ) @@ -138,7 +138,7 @@ def test_beacon_root_contract_timestamps( state_test( env=env, pre=pre, - txs=[tx], + tx=tx, post=post, ) @@ -169,7 +169,7 @@ def test_calldata_lengths( state_test( env=env, pre=pre, - txs=[tx], + tx=tx, post=post, ) @@ -202,7 +202,7 @@ def test_beacon_root_equal_to_timestamp( state_test( env=env, pre=pre, - txs=[tx], + tx=tx, post=post, ) @@ -224,7 +224,7 @@ def test_tx_to_beacon_root_contract( state_test( env=env, pre=pre, - txs=[tx], + tx=tx, post=post, ) @@ -254,7 +254,7 @@ def test_invalid_beacon_root_calldata_value( state_test( env=env, pre=pre, - txs=[tx], + tx=tx, post=post, ) @@ -281,12 +281,14 @@ def test_beacon_root_selfdestruct( code=Op.CALL(100000, Op.PUSH20(to_address(0x1337)), 0, 0, 0, 0, 0) + Op.SSTORE(0, Op.BALANCE(Spec.BEACON_ROOTS_ADDRESS)), ) - post[to_address(0xCC)] = Account( - storage=Storage({0: 0xBA1}), - ) + post = { + to_address(0xCC): Account( + storage=Storage({0: 0xBA1}), + ) + } state_test( env=env, pre=pre, - txs=[tx, Transaction(nonce=1, to=to_address(0xCC), gas_limit=100000, gas_price=10)], + tx=Transaction(nonce=0, to=to_address(0xCC), gas_limit=100000, gas_price=10), post=post, ) diff --git a/tests/cancun/eip4844_blobs/test_blob_txs.py b/tests/cancun/eip4844_blobs/test_blob_txs.py index 5ae5db4f339..7ed9bee443a 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs.py @@ -8,21 +8,23 @@ Add a function that is named `test_` and takes at least the following arguments: - - blockchain_test + - blockchain_test or state_test - pre - env - - blocks + - block or txs All other `pytest.fixture` fixtures can be parametrized to generate new combinations and test cases. """ # noqa: E501 import itertools +from dataclasses import replace from typing import Dict, List, Optional, Tuple import pytest from ethereum_test_forks import Fork from ethereum_test_tools import ( + AccessList, Account, Block, BlockchainTestFiller, @@ -33,6 +35,7 @@ from ethereum_test_tools import Opcodes as Op from ethereum_test_tools import ( Removable, + StateTestFiller, Storage, TestAddress, TestAddress2, @@ -48,6 +51,9 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_4844.git_path REFERENCE_SPEC_VERSION = ref_spec_4844.version +TestPreFundingKey = "0x0b2986cc45bd8a8d028c3fcf6f7a11a52f1df61f3ea5d63f05ca109dd73a3fa0" +TestPreFundingAddress = "0x97a7cb1de3cc7d556d0aa32433b035067709e1fc" + @pytest.fixture def destination_account() -> str: @@ -66,9 +72,21 @@ def tx_value() -> int: @pytest.fixture -def tx_gas() -> int: +def tx_gas( + tx_calldata: bytes, + tx_access_list: List[AccessList], +) -> int: """Default gas allocated to transactions sent during test.""" - return 21000 + access_list_gas = 0 + if tx_access_list: + ACCESS_LIST_ADDRESS_COST = 2400 + ACCESS_LIST_STORAGE_KEY_COST = 1900 + + for address in tx_access_list: + access_list_gas += ACCESS_LIST_ADDRESS_COST + access_list_gas += len(address.storage_keys) * ACCESS_LIST_STORAGE_KEY_COST + + return 21000 + eip_2028_transaction_data_cost(tx_calldata) + access_list_gas @pytest.fixture @@ -181,9 +199,7 @@ def blob_hashes_per_tx(blobs_per_tx: List[int]) -> List[List[bytes]]: def total_account_minimum_balance( # noqa: D103 tx_gas: int, tx_value: int, - tx_calldata: bytes, tx_max_fee_per_gas: int, - tx_max_priority_fee_per_gas: int, tx_max_fee_per_blob_gas: int, blob_hashes_per_tx: List[List[bytes]], ) -> int: @@ -194,12 +210,7 @@ def total_account_minimum_balance( # noqa: D103 total_cost = 0 for tx_blob_count in [len(x) for x in blob_hashes_per_tx]: data_cost = tx_max_fee_per_blob_gas * Spec.GAS_PER_BLOB * tx_blob_count - total_cost += ( - (tx_gas * (tx_max_fee_per_gas + tx_max_priority_fee_per_gas)) - + tx_value - + eip_2028_transaction_data_cost(tx_calldata) - + data_cost - ) + total_cost += (tx_gas * tx_max_fee_per_gas) + tx_value + data_cost return total_cost @@ -236,6 +247,16 @@ def tx_max_fee_per_blob_gas( # noqa: D103 return blob_gasprice +@pytest.fixture +def tx_access_list() -> List[AccessList]: + """ + Default access list for transactions sent during test. + + Can be overloaded by a test case to provide a custom access list. + """ + return [] + + @pytest.fixture def tx_error() -> Optional[str]: """ @@ -256,6 +277,7 @@ def txs( # noqa: D103 tx_max_fee_per_gas: int, tx_max_fee_per_blob_gas: int, tx_max_priority_fee_per_gas: int, + tx_access_list: List[AccessList], blob_hashes_per_tx: List[List[bytes]], tx_error: Optional[str], ) -> List[Transaction]: @@ -273,7 +295,7 @@ def txs( # noqa: D103 max_fee_per_gas=tx_max_fee_per_gas, max_priority_fee_per_gas=tx_max_priority_fee_per_gas, max_fee_per_blob_gas=tx_max_fee_per_blob_gas, - access_list=[], + access_list=tx_access_list, blob_versioned_hashes=blob_hashes, error=tx_error if tx_i == (len(blob_hashes_per_tx) - 1) else None, ) @@ -311,16 +333,43 @@ def pre( # noqa: D103 @pytest.fixture def env( parent_excess_blob_gas: Optional[int], + parent_blobs: int, ) -> Environment: """ - Prepare the environment for all test cases. + Prepare the environment of the genesis block for all blockchain tests. """ + excess_blob_gas = parent_excess_blob_gas if parent_excess_blob_gas else 0 + if parent_blobs: + # We increase the excess blob gas of the genesis because + # we cannot include blobs in the genesis, so the + # test blobs are actually in block 1. + excess_blob_gas += Spec.TARGET_BLOB_GAS_PER_BLOCK return Environment( - excess_blob_gas=parent_excess_blob_gas, + excess_blob_gas=excess_blob_gas, blob_gas_used=0, ) +@pytest.fixture +def state_env( + parent_excess_blob_gas: Optional[int], + parent_blobs: int, +) -> Environment: + """ + Prepare the environment for all state test cases. + + Main difference is that the excess blob gas is not increased by the target, as + there is no genesis block -> block 1 transition, and therefore the excess blob gas + is not decreased by the target. + """ + return Environment( + excess_blob_gas=SpecHelpers.calc_excess_blob_gas_from_blob_count( + parent_excess_blob_gas=parent_excess_blob_gas if parent_excess_blob_gas else 0, + parent_blob_count=parent_blobs, + ), + ) + + @pytest.fixture def engine_api_error_code() -> Optional[EngineAPIError]: """ @@ -396,27 +445,71 @@ def expected_excess_blob_gas( @pytest.fixture -def blocks( +def header_verify( + txs: List[Transaction], expected_blob_gas_used: Optional[int | Removable], expected_excess_blob_gas: Optional[int | Removable], +) -> Header: + """ + Header fields to verify from the transition tool. + """ + header_verify = Header() + header_verify.blob_gas_used = expected_blob_gas_used + header_verify.excess_blob_gas = expected_excess_blob_gas + if len([tx for tx in txs if not tx.error]) == 0: + header_verify.gas_used = 0 + return header_verify + + +@pytest.fixture +def all_blob_gas_used( + fork: Fork, txs: List[Transaction], - block_error: Optional[str], - engine_api_error_code: Optional[EngineAPIError], -) -> List[Block]: + block_number: int, + block_timestamp: int, +) -> Optional[int | Removable]: """ - Prepare the list of blocks for all test cases. + Calculates the blob gas used by the test block taking into account failed transactions. """ - return [ - Block( - txs=txs, - exception=block_error, - engine_api_error_code=engine_api_error_code, - header_verify=Header( - blob_gas_used=expected_blob_gas_used, - excess_blob_gas=expected_excess_blob_gas, - ), - ) - ] + if not fork.header_blob_gas_used_required( + block_number=block_number, timestamp=block_timestamp + ): + return Header.EMPTY_FIELD + return sum([Spec.get_total_blob_gas(tx) for tx in txs]) + + +@pytest.fixture +def rlp_modifier( + all_blob_gas_used: Optional[int | Removable], +) -> Optional[Header]: + """ + Header fields to modify on the output block in the BlockchainTest. + """ + if all_blob_gas_used == Header.EMPTY_FIELD: + return None + return Header( + blob_gas_used=all_blob_gas_used, + ) + + +@pytest.fixture +def block( + txs: List[Transaction], + block_error: Optional[str], + engine_api_error_code: Optional[EngineAPIError], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], +) -> Block: + """ + Test block for all blockchain test cases. + """ + return Block( + txs=txs, + exception=block_error, + engine_api_error_code=engine_api_error_code, + header_verify=header_verify, + rlp_modifier=rlp_modifier, + ) def all_valid_blob_combinations() -> List[Tuple[int, ...]]: @@ -475,7 +568,7 @@ def test_valid_blob_tx_combinations( blockchain_test: BlockchainTestFiller, pre: Dict, env: Environment, - blocks: List[Block], + block: Block, ): """ Test all valid blob combinations in a single block, assuming a given value of @@ -491,7 +584,7 @@ def test_valid_blob_tx_combinations( blockchain_test( pre=pre, post={}, - blocks=blocks, + blocks=[block], genesis_environment=env, ) @@ -500,30 +593,34 @@ def test_valid_blob_tx_combinations( "parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", [ # tx max_blob_gas_cost of the transaction is not enough - ( + pytest.param( SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) - 1, # blob gas price is 1 SpecHelpers.target_blobs_per_block() + 1, # blob gas cost increases to 2 1, # tx max_blob_gas_cost is 1 "insufficient max fee per blob gas", + id="insufficient_max_fee_per_blob_gas", ), # tx max_blob_gas_cost of the transaction is zero, which is invalid - ( + pytest.param( 0, # blob gas price is 1 0, # blob gas cost stays put at 1 0, # tx max_blob_gas_cost is 0 "invalid max fee per blob gas", + id="invalid_max_fee_per_blob_gas", ), ], - ids=["insufficient_max_fee_per_blob_gas", "invalid_max_fee_per_blob_gas"], ) +@pytest.mark.parametrize( + "account_balance_modifier", + [1_000_000_000], +) # Extra balance to cover block blob gas cost @pytest.mark.valid_from("Cancun") def test_invalid_tx_max_fee_per_blob_gas( blockchain_test: BlockchainTestFiller, pre: Dict, env: Environment, - blocks: List[Block], - parent_blobs: int, - non_zero_blob_gas_used_genesis_block: Block, + block: Block, + non_zero_blob_gas_used_genesis_block: Optional[Block], ): """ Reject blocks with invalid blob txs due to: @@ -531,12 +628,10 @@ def test_invalid_tx_max_fee_per_blob_gas( - tx max_fee_per_blob_gas is barely not enough - tx max_fee_per_blob_gas is zero """ - if parent_blobs: + blocks = [block] + if non_zero_blob_gas_used_genesis_block is not None: pre[TestAddress2] = Account(balance=10**9) - blocks.insert(0, non_zero_blob_gas_used_genesis_block) - if env.excess_blob_gas is not None: - assert isinstance(env.excess_blob_gas, int) - env.excess_blob_gas += Spec.TARGET_BLOB_GAS_PER_BLOCK + blocks = [non_zero_blob_gas_used_genesis_block, block] blockchain_test( pre=pre, post={}, @@ -545,6 +640,49 @@ def test_invalid_tx_max_fee_per_blob_gas( ) +@pytest.mark.parametrize( + "parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", + [ + # tx max_blob_gas_cost of the transaction is not enough + pytest.param( + SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) - 1, # blob gas price is 1 + SpecHelpers.target_blobs_per_block() + 1, # blob gas cost increases to 2 + 1, # tx max_blob_gas_cost is 1 + "insufficient max fee per blob gas", + id="insufficient_max_fee_per_blob_gas", + ), + # tx max_blob_gas_cost of the transaction is zero, which is invalid + pytest.param( + 0, # blob gas price is 1 + 0, # blob gas cost stays put at 1 + 0, # tx max_blob_gas_cost is 0 + "invalid max fee per blob gas", + id="invalid_max_fee_per_blob_gas", + ), + ], +) +@pytest.mark.valid_from("Cancun") +def test_invalid_tx_max_fee_per_blob_gas_state( + state_test_only: StateTestFiller, + state_env: Environment, + pre: Dict, + txs: List[Transaction], +): + """ + Reject an invalid blob transaction due to: + + - tx max_fee_per_blob_gas is barely not enough + - tx max_fee_per_blob_gas is zero + """ + assert len(txs) == 1 + state_test_only( + pre=pre, + post={}, + tx=txs[0], + env=state_env, + ) + + @pytest.mark.parametrize( "tx_max_fee_per_gas,tx_error", [ @@ -558,21 +696,26 @@ def test_invalid_tx_max_fee_per_blob_gas( ) @pytest.mark.valid_from("Cancun") def test_invalid_normal_gas( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, + state_env: Environment, pre: Dict, - env: Environment, - blocks: List[Block], + txs: List[Transaction], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], ): """ - Reject blocks with invalid blob txs due to: + Reject an invalid blob transaction due to: - Sufficient max fee per blob gas, but insufficient max fee per gas """ - blockchain_test( + assert len(txs) == 1 + state_test( pre=pre, post={}, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, + blockchain_test_header_verify=header_verify, + blockchain_test_rlp_modifier=rlp_modifier, ) @@ -580,13 +723,13 @@ def test_invalid_normal_gas( "blobs_per_tx", invalid_blob_combinations(), ) -@pytest.mark.parametrize("block_error", ["invalid_blob_count"]) +@pytest.mark.parametrize("tx_error", ["maximum blob gas allowance exceeded"]) @pytest.mark.valid_from("Cancun") def test_invalid_block_blob_count( blockchain_test: BlockchainTestFiller, pre: Dict, env: Environment, - blocks: List[Block], + block: Block, ): """ Test all invalid blob combinations in a single block, where the sum of all blobs in a block is @@ -599,12 +742,18 @@ def test_invalid_block_blob_count( blockchain_test( pre=pre, post={}, - blocks=blocks, + blocks=[block], genesis_environment=env, ) -@pytest.mark.parametrize("tx_max_priority_fee_per_gas", [0, 8]) +@pytest.mark.parametrize( + "tx_access_list", + [[], [AccessList(address=100, storage_keys=[100, 200])]], + ids=["no_access_list", "access_list"], +) +@pytest.mark.parametrize("tx_max_fee_per_gas", [7, 14]) +@pytest.mark.parametrize("tx_max_priority_fee_per_gas", [0, 7]) @pytest.mark.parametrize("tx_value", [0, 1]) @pytest.mark.parametrize( "tx_calldata", @@ -616,24 +765,129 @@ def test_invalid_block_blob_count( @pytest.mark.parametrize("tx_error", ["insufficient_account_balance"], ids=[""]) @pytest.mark.valid_from("Cancun") def test_insufficient_balance_blob_tx( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, + state_env: Environment, pre: Dict, - env: Environment, - blocks: List[Block], + txs: List[Transaction], ): """ Reject blocks where user cannot afford the blob gas specified (but max_fee_per_gas would be enough for current block), including: + - Transactions with max fee equal or higher than current block base fee - Transactions with and without priority fee - Transactions with and without value - Transactions with and without calldata - Transactions with max fee per blob gas lower or higher than the priority fee """ + assert len(txs) == 1 + state_test( + pre=pre, + post={}, + tx=txs[0], + env=state_env, + ) + + +@pytest.mark.parametrize( + "tx_access_list", + [[], [AccessList(address=100, storage_keys=[100, 200])]], + ids=["no_access_list", "access_list"], +) +@pytest.mark.parametrize("tx_max_fee_per_gas", [7, 14]) +@pytest.mark.parametrize("tx_max_priority_fee_per_gas", [0, 7]) +@pytest.mark.parametrize("tx_value", [0, 1]) +@pytest.mark.parametrize( + "tx_calldata", + [b"", b"\x00", b"\x01"], + ids=["no_calldata", "single_zero_calldata", "single_one_calldata"], +) +@pytest.mark.parametrize("tx_max_fee_per_blob_gas", [1, 100, 10000]) +@pytest.mark.valid_from("Cancun") +def test_sufficient_balance_blob_tx( + state_test: StateTestFiller, + state_env: Environment, + pre: Dict, + txs: List[Transaction], +): + """ + Check that transaction is accepted when user can exactly afford the blob gas specified (and + max_fee_per_gas would be enough for current block), including: + + - Transactions with max fee equal or higher than current block base fee + - Transactions with and without priority fee + - Transactions with and without value + - Transactions with and without calldata + - Transactions with max fee per blob gas lower or higher than the priority fee + """ + assert len(txs) == 1 + state_test( + pre=pre, + post={}, + tx=txs[0], + env=state_env, + ) + + +@pytest.mark.parametrize( + "tx_access_list", + [[], [AccessList(address=100, storage_keys=[100, 200])]], + ids=["no_access_list", "access_list"], +) +@pytest.mark.parametrize("tx_max_fee_per_gas", [7, 14]) +@pytest.mark.parametrize("tx_max_priority_fee_per_gas", [0, 7]) +@pytest.mark.parametrize("tx_value", [0, 1]) +@pytest.mark.parametrize( + "tx_calldata", + [b"", b"\x00", b"\x01"], + ids=["no_calldata", "single_zero_calldata", "single_one_calldata"], +) +@pytest.mark.parametrize("tx_max_fee_per_blob_gas", [1, 100, 10000]) +@pytest.mark.valid_from("Cancun") +def test_sufficient_balance_blob_tx_pre_fund_tx( + blockchain_test: BlockchainTestFiller, + total_account_minimum_balance: int, + env: Environment, + pre: Dict, + txs: List[Transaction], + header_verify: Optional[Header], +): + """ + Check that transaction is accepted when user can exactly afford the blob gas specified (and + max_fee_per_gas would be enough for current block) because a funding transaction is + prepended in the same block, including: + + - Transactions with max fee equal or higher than current block base fee + - Transactions with and without priority fee + - Transactions with and without value + - Transactions with and without calldata + - Transactions with max fee per blob gas lower or higher than the priority fee + """ + pre = { + TestPreFundingAddress: Account(balance=(21_000 * 100) + total_account_minimum_balance), + } + txs = [ + Transaction( + ty=2, + nonce=0, + to=TestAddress, + value=total_account_minimum_balance, + gas_limit=21_000, + max_fee_per_gas=100, + max_priority_fee_per_gas=0, + access_list=[], + secret_key=TestPreFundingKey, + ) + ] + txs blockchain_test( pre=pre, post={}, - blocks=blocks, + blocks=[ + Block( + txs=txs, + header_verify=header_verify, + ) + ], genesis_environment=env, ) @@ -649,7 +903,7 @@ def test_insufficient_balance_blob_tx_combinations( blockchain_test: BlockchainTestFiller, pre: Dict, env: Environment, - blocks: List[Block], + block: Block, ): """ Reject all valid blob transaction combinations in a block, but block is invalid due to: @@ -660,25 +914,27 @@ def test_insufficient_balance_blob_tx_combinations( blockchain_test( pre=pre, post={}, - blocks=blocks, + blocks=[block], genesis_environment=env, ) @pytest.mark.parametrize( - "blobs_per_tx,tx_error,block_error", + "blobs_per_tx,tx_error", [ - ([0], "zero blob tx", "zero blob tx"), - ([SpecHelpers.max_blobs_per_block() + 1], None, "too many blobs"), + ([0], "zero blob tx"), + ([SpecHelpers.max_blobs_per_block() + 1], "too many blobs"), ], ids=["too_few_blobs", "too_many_blobs"], ) @pytest.mark.valid_from("Cancun") def test_invalid_tx_blob_count( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, + state_env: Environment, pre: Dict, - env: Environment, - blocks: List[Block], + txs: List[Transaction], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], ): """ Reject blocks that include blob transactions with invalid blob counts: @@ -686,11 +942,14 @@ def test_invalid_tx_blob_count( - `blob count == 0` in type 3 transaction - `blob count > MAX_BLOBS_PER_BLOCK` in type 3 transaction """ - blockchain_test( + assert len(txs) == 1 + state_test( pre=pre, post={}, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, + blockchain_test_header_verify=header_verify, + blockchain_test_rlp_modifier=rlp_modifier, ) @@ -707,6 +966,45 @@ def test_invalid_tx_blob_count( [to_hash_bytes(1)] + add_kzg_version([to_hash_bytes(2)], Spec.BLOB_COMMITMENT_VERSION_KZG) ], + ], + ids=[ + "single_blob", + "multiple_blobs", + "multiple_blobs_single_bad_hash_1", + "multiple_blobs_single_bad_hash_2", + ], +) +@pytest.mark.parametrize("tx_error", ["invalid blob versioned hash"], ids=[""]) +@pytest.mark.valid_from("Cancun") +def test_invalid_blob_hash_versioning_single_tx( + state_test: StateTestFiller, + state_env: Environment, + pre: Dict, + txs: List[Transaction], + header_verify: Optional[Header], + rlp_modifier: Optional[Header], +): + """ + Reject blob transactions with invalid blob hash version, including: + + - Transaction with single blob with invalid version + - Transaction with multiple blobs all with invalid version + - Transaction with multiple blobs either with invalid version + """ + assert len(txs) == 1 + state_test( + pre=pre, + post={}, + tx=txs[0], + env=state_env, + blockchain_test_header_verify=header_verify, + blockchain_test_rlp_modifier=rlp_modifier, + ) + + +@pytest.mark.parametrize( + "blob_hashes_per_tx", + [ [ add_kzg_version([to_hash_bytes(1)], Spec.BLOB_COMMITMENT_VERSION_KZG), [to_hash_bytes(2)], @@ -727,31 +1025,24 @@ def test_invalid_tx_blob_count( ], ], ids=[ - "single_tx_single_blob", - "single_tx_multiple_blobs", - "single_tx_multiple_blobs_single_bad_hash_1", - "single_tx_multiple_blobs_single_bad_hash_2", - "multiple_txs_single_blob", - "multiple_txs_multiple_blobs", - "multiple_txs_multiple_blobs_single_bad_hash_1", - "multiple_txs_multiple_blobs_single_bad_hash_2", + "single_blob", + "multiple_blobs", + "multiple_blobs_single_bad_hash_1", + "multiple_blobs_single_bad_hash_2", ], ) @pytest.mark.parametrize("tx_error", ["invalid blob versioned hash"], ids=[""]) @pytest.mark.valid_from("Cancun") -def test_invalid_blob_hash_versioning( +def test_invalid_blob_hash_versioning_multiple_txs( blockchain_test: BlockchainTestFiller, pre: Dict, env: Environment, - blocks: List[Block], + block: Block, ): """ Reject blocks that include blob transactions with invalid blob hash version, including: - - Single blob transaction with single blob with invalid version - - Single blob transaction with multiple blobs all with invalid version - - Single blob transaction with multiple blobs either with invalid version - Multiple blob transactions with single blob all with invalid version - Multiple blob transactions with multiple blobs all with invalid version - Multiple blob transactions with multiple blobs only one with invalid version @@ -759,30 +1050,40 @@ def test_invalid_blob_hash_versioning( blockchain_test( pre=pre, post={}, - blocks=blocks, + blocks=[block], genesis_environment=env, ) @pytest.mark.parametrize( - "destination_account,tx_error", [(None, "no_contract_creating_blob_txs")], ids=[""] -) -# TODO: Uncomment after #242 -> https://github.com/ethereum/execution-spec-tests/issues/242 -@pytest.mark.skip(reason="Unable to fill due to invalid field in transaction") + "tx_gas", [500_000], ids=[""] +) # Increase gas to account for contract creation @pytest.mark.valid_from("Cancun") def test_invalid_blob_tx_contract_creation( blockchain_test: BlockchainTestFiller, pre: Dict, env: Environment, - blocks: List[Block], + txs: List[Transaction], + header_verify: Optional[Header], ): """ Reject blocks that include blob transactions that have nil to value (contract creating). """ + assert len(txs) == 1 + assert txs[0].blob_versioned_hashes is not None and len(txs[0].blob_versioned_hashes) == 1 + # Replace the transaction with a contract creating one, only in the RLP version + contract_creating_tx = replace(txs[0], to=None).with_signature_and_sender() + txs[0] = replace(txs[0], rlp=contract_creating_tx.serialized_bytes()) blockchain_test( pre=pre, post={}, - blocks=blocks, + blocks=[ + Block( + txs=txs, + exception="no_contract_creating_blob_txs", + header_verify=header_verify, + ) + ], genesis_environment=env, ) @@ -854,11 +1155,11 @@ def opcode( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_opcodes( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], destination_account: str, ): """ @@ -867,6 +1168,7 @@ def test_blob_tx_attribute_opcodes( - ORIGIN - CALLER """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -874,11 +1176,11 @@ def test_blob_tx_attribute_opcodes( storage=storage, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -887,17 +1189,18 @@ def test_blob_tx_attribute_opcodes( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_value_opcode( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], tx_value: int, destination_account: str, ): """ Test the VALUE opcode with different blob type transaction value amounts. """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -906,11 +1209,11 @@ def test_blob_tx_attribute_value_opcode( balance=tx_value, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -935,11 +1238,11 @@ def test_blob_tx_attribute_value_opcode( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_calldata_opcodes( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], destination_account: str, ): """ @@ -949,6 +1252,7 @@ def test_blob_tx_attribute_calldata_opcodes( - CALLDATASIZE - CALLDATACOPY """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -956,11 +1260,11 @@ def test_blob_tx_attribute_calldata_opcodes( storage=storage, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -971,11 +1275,11 @@ def test_blob_tx_attribute_calldata_opcodes( @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") def test_blob_tx_attribute_gasprice_opcode( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, opcode: Tuple[bytes, Storage.StorageDictType], - env: Environment, - blocks: List[Block], + state_env: Environment, + txs: List[Transaction], destination_account: str, ): """ @@ -986,6 +1290,7 @@ def test_blob_tx_attribute_gasprice_opcode( - Priority fee below data fee - Priority fee above data fee """ + assert len(txs) == 1 code, storage = opcode pre[destination_account] = Account(code=code) post = { @@ -993,11 +1298,11 @@ def test_blob_tx_attribute_gasprice_opcode( storage=storage, ) } - blockchain_test( + state_test( pre=pre, post=post, - blocks=blocks, - genesis_environment=env, + tx=txs[0], + env=state_env, ) @@ -1016,9 +1321,9 @@ def test_blob_tx_attribute_gasprice_opcode( ) @pytest.mark.valid_at_transition_to("Cancun") def test_blob_type_tx_pre_fork( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, - blocks: List[Block], + txs: List[Transaction], ): """ Reject blocks with blob type transactions before Cancun fork. @@ -1026,9 +1331,10 @@ def test_blob_type_tx_pre_fork( Blocks sent by NewPayloadV2 (Shanghai) that contain blob type transactions, furthermore blobs field within NewPayloadV2 method must be computed as INVALID, due to an invalid block hash. """ - blockchain_test( + assert len(txs) == 1 + state_test( pre=pre, post={}, - blocks=blocks, - genesis_environment=Environment(), # `env` fixture has blob fields + tx=txs[0], + env=Environment(), # `env` fixture has blob fields ) diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index 4da73cc9e98..4076113b3ff 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -7,7 +7,7 @@ Add a function that is named `test_` and takes at least the following arguments: - - blockchain_test + - blockchain_test | state_test - pre - tx - post @@ -31,15 +31,16 @@ import glob import json import os -from typing import Dict, Iterator, List +from typing import Dict, Iterator, List, Optional import pytest from ethereum_test_tools import ( Account, - Auto, Block, BlockchainTestFiller, + Environment, + StateTestFiller, Storage, TestAddress, Transaction, @@ -54,12 +55,10 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_4844.git_path REFERENCE_SPEC_VERSION = ref_spec_4844.version -auto = Auto() - @pytest.fixture def precompile_input( - versioned_hash: bytes | int | Auto, + versioned_hash: Optional[bytes | int], kzg_commitment: bytes | int, z: bytes | int, y: bytes | int, @@ -76,7 +75,7 @@ def precompile_input( kzg_commitment = kzg_commitment.to_bytes(48, "big") if isinstance(kzg_proof, int): kzg_proof = kzg_proof.to_bytes(48, "big") - if isinstance(versioned_hash, Auto): + if versioned_hash is None: versioned_hash = Spec.kzg_to_versioned_hash(kzg_commitment) elif isinstance(versioned_hash, int): versioned_hash = versioned_hash.to_bytes(32, "big") @@ -245,13 +244,13 @@ def post( @pytest.mark.parametrize( "z,y,kzg_commitment,kzg_proof,versioned_hash", [ - pytest.param(Spec.BLS_MODULUS - 1, 0, INF_POINT, INF_POINT, auto, id="in_bounds_z"), + pytest.param(Spec.BLS_MODULUS - 1, 0, INF_POINT, INF_POINT, None, id="in_bounds_z"), ], ) @pytest.mark.parametrize("success", [True]) @pytest.mark.valid_from("Cancun") def test_valid_precompile_calls( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, tx: Transaction, post: Dict, @@ -262,25 +261,26 @@ def test_valid_precompile_calls( - `kzg_commitment` and `kzg_proof` are set to values such that `p(z)==0` for all values of `z`, hence `y` is tested to be zero, and call to be successful. """ - blockchain_test( + state_test( + env=Environment(), pre=pre, post=post, - blocks=[Block(txs=[tx])], + tx=tx, ) @pytest.mark.parametrize( "z,y,kzg_commitment,kzg_proof,versioned_hash", [ - (Spec.BLS_MODULUS, 0, INF_POINT, INF_POINT, auto), - (0, Spec.BLS_MODULUS, INF_POINT, INF_POINT, auto), - (Z, 0, INF_POINT, INF_POINT[:-1], auto), - (Z, 0, INF_POINT, INF_POINT[0:1], auto), - (Z, 0, INF_POINT, INF_POINT + bytes([0]), auto), - (Z, 0, INF_POINT, INF_POINT + bytes([0] * 1023), auto), + (Spec.BLS_MODULUS, 0, INF_POINT, INF_POINT, None), + (0, Spec.BLS_MODULUS, INF_POINT, INF_POINT, None), + (Z, 0, INF_POINT, INF_POINT[:-1], None), + (Z, 0, INF_POINT, INF_POINT[0:1], None), + (Z, 0, INF_POINT, INF_POINT + bytes([0]), None), + (Z, 0, INF_POINT, INF_POINT + bytes([0] * 1023), None), (bytes(), bytes(), bytes(), bytes(), bytes()), (0, 0, 0, 0, 0), - (0, 0, 0, 0, auto), + (0, 0, 0, 0, None), (Z, 0, INF_POINT, INF_POINT, Spec.kzg_to_versioned_hash(0xC0 << 376, 0x00)), (Z, 0, INF_POINT, INF_POINT, Spec.kzg_to_versioned_hash(0xC0 << 376, 0x02)), (Z, 0, INF_POINT, INF_POINT, Spec.kzg_to_versioned_hash(0xC0 << 376, 0xFF)), @@ -303,7 +303,7 @@ def test_valid_precompile_calls( @pytest.mark.parametrize("success", [False]) @pytest.mark.valid_from("Cancun") def test_invalid_precompile_calls( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, tx: Transaction, post: Dict, @@ -317,10 +317,11 @@ def test_invalid_precompile_calls( - Zero inputs - Correct proof, commitment, z and y, but incorrect version versioned hash """ - blockchain_test( + state_test( + env=Environment(), pre=pre, post=post, - blocks=[Block(txs=[tx])], + tx=tx, ) @@ -417,10 +418,10 @@ def all_external_vectors() -> List: "z,y,kzg_commitment,kzg_proof,success", all_external_vectors(), ) -@pytest.mark.parametrize("versioned_hash", [auto]) +@pytest.mark.parametrize("versioned_hash", [None]) @pytest.mark.valid_from("Cancun") def test_point_evaluation_precompile_external_vectors( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, tx: Transaction, post: Dict, @@ -431,10 +432,11 @@ def test_point_evaluation_precompile_external_vectors( - `go_kzg_4844_verify_kzg_proof.json`: test vectors from the [go-kzg-4844](https://github.com/crate-crypto/go-kzg-4844) repository. """ - blockchain_test( + state_test( + env=Environment(), pre=pre, post=post, - blocks=[Block(txs=[tx])], + tx=tx, ) @@ -458,12 +460,12 @@ def test_point_evaluation_precompile_external_vectors( ) @pytest.mark.parametrize( "z,kzg_commitment,kzg_proof,versioned_hash", - [[Z, INF_POINT, INF_POINT, auto]], + [[Z, INF_POINT, INF_POINT, None]], ids=[""], ) @pytest.mark.valid_from("Cancun") def test_point_evaluation_precompile_calls( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, tx: Transaction, post: Dict, @@ -476,10 +478,11 @@ def test_point_evaluation_precompile_calls( - Using correct and incorrect proofs - Using barely insufficient gas """ - blockchain_test( + state_test( + env=Environment(), pre=pre, post=post, - blocks=[Block(txs=[tx])], + tx=tx, ) @@ -495,14 +498,14 @@ def test_point_evaluation_precompile_calls( @pytest.mark.parametrize( "z,y,kzg_commitment,kzg_proof,versioned_hash,proof_correct", [ - [Z, 0, INF_POINT, INF_POINT, auto, True], - [Z, 1, INF_POINT, INF_POINT, auto, False], + [Z, 0, INF_POINT, INF_POINT, None, True], + [Z, 1, INF_POINT, INF_POINT, None, False], ], ids=["correct_proof", "incorrect_proof"], ) @pytest.mark.valid_from("Cancun") def test_point_evaluation_precompile_gas_tx_to( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, precompile_input: bytes, call_gas: int, proof_correct: bool, @@ -554,20 +557,80 @@ def test_point_evaluation_precompile_gas_tx_to( ) } - blockchain_test( + state_test( + env=Environment(), pre=pre, post=post, - blocks=[Block(txs=[tx])], + tx=tx, ) @pytest.mark.parametrize( "z,y,kzg_commitment,kzg_proof,versioned_hash", - [[Z, 0, INF_POINT, INF_POINT, auto]], + [[Z, 0, INF_POINT, INF_POINT, None]], ids=["correct_proof"], ) @pytest.mark.valid_at_transition_to("Cancun") def test_point_evaluation_precompile_before_fork( + state_test: StateTestFiller, + pre: Dict, + tx: Transaction, +): + """ + Test calling the Point Evaluation Precompile before the appropriate fork. + """ + precompile_caller_code = Op.SSTORE( + Op.NUMBER, + Op.CALL( + Op.GAS, + Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, + 1, # Value + 0, # Zero-length calldata + 0, + 0, # Zero-length return + 0, + ), + ) + precompile_caller_address = to_address(0x100) + + pre = { + TestAddress: Account( + nonce=0, + balance=0x10**18, + ), + precompile_caller_address: Account( + nonce=0, + code=precompile_caller_code, + balance=0x10**18, + ), + } + + post = { + precompile_caller_address: Account( + storage={1: 1}, + # The call succeeds because precompile is not there yet + ), + to_address(Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS): Account( + balance=1, + ), + } + + state_test( + tag="point_evaluation_precompile_before_fork", + pre=pre, + env=Environment(timestamp=7_500), + post=post, + tx=tx, + ) + + +@pytest.mark.parametrize( + "z,y,kzg_commitment,kzg_proof,versioned_hash", + [[Z, 0, INF_POINT, INF_POINT, None]], + ids=["correct_proof"], +) +@pytest.mark.valid_at_transition_to("Cancun") +def test_point_evaluation_precompile_during_fork( blockchain_test: BlockchainTestFiller, pre: Dict, tx: Transaction, @@ -620,7 +683,7 @@ def tx_generator() -> Iterator[Transaction]: post = { precompile_caller_address: Account( storage={b: 1 for b in range(1, len(PRE_FORK_BLOCK_RANGE) + 1)}, - # The tx in last block succeeds; storage 0 by default. + # Only the call in the last block's tx fails; storage 0 by default. ), to_address(Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS): Account( balance=len(PRE_FORK_BLOCK_RANGE), diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py index efd324b3ff4..bcb9ff6ace0 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py @@ -10,9 +10,9 @@ from ethereum_test_tools import ( Account, - Block, - BlockchainTestFiller, CodeGasMeasure, + Environment, + StateTestFiller, TestAddress, Transaction, copy_opcode_cost, @@ -213,7 +213,7 @@ def post( @pytest.mark.parametrize("proof", ["correct", "incorrect"]) @pytest.mark.valid_from("Cancun") def test_point_evaluation_precompile_gas_usage( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, tx: Transaction, post: Dict, @@ -225,8 +225,9 @@ def test_point_evaluation_precompile_gas_usage( - Test using different gas limits (exact gas, insufficient gas, extra gas) - Test using correct and incorrect proofs """ - blockchain_test( + state_test( + env=Environment(), pre=pre, post=post, - blocks=[Block(txs=[tx])], + tx=tx, ) diff --git a/tests/cancun/eip5656_mcopy/test_mcopy.py b/tests/cancun/eip5656_mcopy/test_mcopy.py index 52187ccb5d5..2d9af995550 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy.py @@ -197,7 +197,7 @@ def test_valid_mcopy_operations( env=Environment(), pre=pre, post=post, - txs=[tx], + tx=tx, ) @@ -219,5 +219,5 @@ def test_mcopy_on_empty_memory( env=Environment(), pre=pre, post=post, - txs=[tx], + tx=tx, ) diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_contexts.py b/tests/cancun/eip5656_mcopy/test_mcopy_contexts.py index ac69389986e..504dfa78401 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_contexts.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_contexts.py @@ -214,5 +214,5 @@ def test_no_memory_corruption_on_upper_call_stack_levels( env=Environment(), pre=pre, post=post, - txs=[tx], + tx=tx, ) diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index 8de58d9f5d6..fd2456d902f 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -215,7 +215,7 @@ def test_mcopy_memory_expansion( env=Environment(), pre=pre, post=post, - txs=[tx], + tx=tx, ) @@ -276,5 +276,5 @@ def test_mcopy_huge_memory_expansion( env=Environment(), pre=pre, post=post, - txs=[tx], + tx=tx, ) diff --git a/tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py b/tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py new file mode 100644 index 00000000000..891ab6ffd98 --- /dev/null +++ b/tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py @@ -0,0 +1,157 @@ +""" +Suicide scenario requested test +https://github.com/ethereum/tests/issues/1325 +""" + +import pytest + +from ethereum_test_forks import Cancun, Fork +from ethereum_test_tools import ( + Account, + Environment, + StateTestFiller, + TestAddress, + TestAddress2, + Transaction, + to_address, +) +from ethereum_test_tools.vm.opcode import Opcodes as Op + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md" +REFERENCE_SPEC_VERSION = "2f8299df31bb8173618901a03a8366a3183479b0" + + +@pytest.fixture +def env(): # noqa: D103 + return Environment( + coinbase="0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + difficulty=0x020000, + gas_limit=71794957647893862, + number=1, + timestamp=1000, + ) + + +@pytest.mark.valid_from("Merge") +@pytest.mark.parametrize("first_suicide", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL]) +@pytest.mark.parametrize("second_suicide", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL]) +def test_reentrancy_selfdestruct_revert( + env: Environment, + fork: Fork, + first_suicide: Op, + second_suicide: Op, + state_test: StateTestFiller, +): + """ + Suicide reentrancy scenario: + + Call|Callcode|Delegatecall the contract S. + S self destructs. + Call the revert proxy contract R. + R Calls|Callcode|Delegatecall S. + S self destructs (for the second time). + R reverts (including the effects of the second selfdestruct). + It is expected the S is self destructed after the transaction. + """ + address_to = TestAddress2 + address_s = to_address(0x1000000000000000000000000000000000000001) + address_r = to_address(0x1000000000000000000000000000000000000002) + suicide_d = to_address(0x03E8) + + def construct_call_s(call_type: Op, money: int): + if call_type in [Op.CALLCODE, Op.CALL]: + return call_type(Op.GAS, Op.PUSH20(address_s), money, 0, 0, 0, 0) + else: + return call_type(Op.GAS, Op.PUSH20(address_s), money, 0, 0, 0) + + pre = { + address_to: Account( + balance=1000000000000000000, + nonce=0, + code=Op.SSTORE(1, construct_call_s(first_suicide, 0)) + + Op.SSTORE(2, Op.CALL(Op.GAS, Op.PUSH20(address_r), 0, 0, 0, 0, 0)) + + Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE()) + + Op.SSTORE(3, Op.MLOAD(0)), + storage={0x01: 0x0100, 0x02: 0x0100, 0x03: 0x0100}, + ), + address_s: Account( + balance=3000000000000000000, + nonce=0, + code=Op.SELFDESTRUCT(1000), + storage={}, + ), + address_r: Account( + balance=5000000000000000000, + nonce=0, + # Send money when calling it suicide second time to make sure the funds not transferred + code=Op.MSTORE(0, Op.ADD(15, construct_call_s(second_suicide, 100))) + + Op.REVERT(0, 32), + storage={}, + ), + TestAddress: Account( + balance=7000000000000000000, + nonce=0, + code="0x", + storage={}, + ), + } + + post = { + # Second caller unchanged as call gets reverted + address_r: Account(balance=5000000000000000000, storage={}), + } + + if first_suicide in [Op.CALLCODE, Op.DELEGATECALL]: + if fork >= Cancun: + # On Cancun even callcode/delegatecall does not remove the account, so the value remain + post[address_to] = Account( + storage={ + 0x01: 0x01, # First call to contract S->suicide success + 0x02: 0x00, # Second call to contract S->suicide reverted + 0x03: 16, # Reverted value to check that revert really worked + }, + ) + else: + # Callcode executed first suicide from sender. sender is deleted + post[address_to] = Account.NONEXISTENT # type: ignore + + # Original suicide account remains in state + post[address_s] = Account(balance=3000000000000000000, storage={}) + # Suicide destination + post[suicide_d] = Account( + balance=1000000000000000000, + ) + + # On Cancun suicide no longer destroys the account from state, just cleans the balance + if first_suicide in [Op.CALL]: + post[address_to] = Account( + storage={ + 0x01: 0x01, # First call to contract S->suicide success + 0x02: 0x00, # Second call to contract S->suicide reverted + 0x03: 16, # Reverted value to check that revert really worked + }, + ) + if fork >= Cancun: + # On Cancun suicide does not remove the account, just sends the balance + post[address_s] = Account(balance=0, code="0x6103e8ff", storage={}) + else: + post[address_s] = Account.NONEXISTENT # type: ignore + + # Suicide destination + post[suicide_d] = Account( + balance=3000000000000000000, + ) + + tx = Transaction( + ty=0x0, + chain_id=0x0, + nonce=0, + to=address_to, + gas_price=10, + protected=False, + data="", + gas_limit=500000, + value=0, + ) + + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py index bcfa98a505d..074ed393bd6 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py @@ -11,7 +11,7 @@ import pytest from ethereum.crypto.hash import keccak256 -from ethereum_test_forks import Cancun, Fork, is_fork +from ethereum_test_forks import Cancun, Fork from ethereum_test_tools import ( Account, Block, @@ -50,7 +50,7 @@ @pytest.fixture def eip_enabled(fork: Fork) -> bool: """Whether the EIP is enabled or not.""" - return is_fork(fork, SELFDESTRUCT_ENABLE_FORK) + return fork >= SELFDESTRUCT_ENABLE_FORK @pytest.fixture @@ -412,7 +412,7 @@ def test_create_selfdestruct_same_tx( protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) @pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2]) @@ -540,7 +540,7 @@ def test_self_destructing_initcode( protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) @pytest.mark.parametrize("tx_value", [0, 100_000]) @@ -595,7 +595,7 @@ def test_self_destructing_initcode_create_tx( protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) @pytest.mark.parametrize("create_opcode", [Op.CREATE2]) # Can only recreate using CREATE2 @@ -872,7 +872,7 @@ def test_selfdestruct_pre_existing( protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) @pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 1]) @@ -1148,7 +1148,7 @@ def test_delegatecall_from_new_contract_to_pre_existing_contract( protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) @pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2]) @@ -1305,4 +1305,4 @@ def test_delegatecall_from_pre_existing_contract_to_new_contract( protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py index 9ffc2189410..47e07abfa6f 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py @@ -300,7 +300,7 @@ def test_selfdestruct_created_in_same_tx_with_revert( # noqa SC200 protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) @pytest.fixture @@ -415,4 +415,4 @@ def test_selfdestruct_not_created_in_same_tx_with_revert( protected=False, ) - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py b/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py index ab29b0ca649..352f1139de0 100644 --- a/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py +++ b/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py @@ -4,6 +4,7 @@ Test BLOBGASFEE opcode [EIP-7516: BLOBBASEFEE opcode](https://eips.ethereum.org/EIPS/eip-7516) """ # noqa: E501 +from dataclasses import replace from itertools import count from typing import Dict @@ -114,7 +115,7 @@ def test_blobbasefee_stack_overflow( state_test( env=Environment(), pre=pre, - txs=[tx], + tx=tx, post=post, ) @@ -147,21 +148,54 @@ def test_blobbasefee_out_of_gas( state_test( env=Environment(), pre=pre, - txs=[tx], + tx=tx, post=post, ) @pytest.mark.valid_at_transition_to("Cancun") def test_blobbasefee_before_fork( - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, pre: Dict, tx: Transaction, ): """ Tests that the BLOBBASEFEE opcode results on exception when called before the fork. """ - code_caller_storage = Storage() + # Fork happens at timestamp 15_000 + timestamp = 7_500 + code_caller_pre_storage = Storage({1: 1}) + pre[code_caller_address] = replace(pre[code_caller_address], storage=code_caller_pre_storage) + post = { + code_caller_address: Account( + storage={1: 0}, + ), + code_callee_address: Account( + balance=0, + ), + } + state_test( + env=Environment( + timestamp=timestamp, + ), + pre=pre, + tx=tx, + post=post, + ) + + +@pytest.mark.valid_at_transition_to("Cancun") +def test_blobbasefee_during_fork( + blockchain_test: BlockchainTestFiller, + pre: Dict, + tx: Transaction, +): + """ + Tests that the BLOBBASEFEE opcode results on exception when called before the fork and + succeeds when called after the fork. + """ + code_caller_pre_storage = Storage() + code_caller_post_storage = Storage() nonce = count(0) @@ -169,18 +203,21 @@ def test_blobbasefee_before_fork( blocks = [] - for number, timestamp in enumerate(timestamps): + for block_number, timestamp in enumerate(timestamps, start=1): blocks.append( Block( txs=[tx.with_nonce(next(nonce))], timestamp=timestamp, ), ) - code_caller_storage[number + 1] = 0 if timestamp < 15_000 else 1 + # pre-set storage just to make sure we detect the change + code_caller_pre_storage[block_number] = 1 if timestamp < 15_000 else 0 + code_caller_post_storage[block_number] = 0 if timestamp < 15_000 else 1 + pre[code_caller_address] = replace(pre[code_caller_address], storage=code_caller_pre_storage) post = { code_caller_address: Account( - storage=code_caller_storage, + storage=code_caller_post_storage, ), code_callee_address: Account( balance=0, diff --git a/tests/frontier/opcodes/test_dup.py b/tests/frontier/opcodes/test_dup.py index e6bd27ef50a..4d9c25d9f61 100644 --- a/tests/frontier/opcodes/test_dup.py +++ b/tests/frontier/opcodes/test_dup.py @@ -4,18 +4,41 @@ Test the DUP opcodes. """ +import pytest + from ethereum_test_forks import Frontier, Homestead -from ethereum_test_tools import ( - Account, - Environment, - StateTestFiller, - Storage, - Transaction, - to_address, -) +from ethereum_test_tools import Account, Environment +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import StateTestFiller, Storage, Transaction, to_address -def test_dup(state_test: StateTestFiller, fork: str): +@pytest.mark.parametrize( + "dup_opcode", + [ + Op.DUP1, + Op.DUP2, + Op.DUP3, + Op.DUP4, + Op.DUP5, + Op.DUP6, + Op.DUP7, + Op.DUP8, + Op.DUP9, + Op.DUP10, + Op.DUP11, + Op.DUP12, + Op.DUP13, + Op.DUP14, + Op.DUP15, + Op.DUP16, + ], + ids=lambda op: str(op), +) +def test_dup( + state_test: StateTestFiller, + fork: str, + dup_opcode: Op, +): """ Test the DUP1-DUP16 opcodes. @@ -26,83 +49,58 @@ def test_dup(state_test: StateTestFiller, fork: str): """ # noqa: E501 env = Environment() pre = {"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": Account(balance=1000000000000000000000)} - txs = [] post = {} + account = to_address(0x100) + + # Push 0x00 - 0x10 onto the stack + account_code = b"".join([Op.PUSH1(i) for i in range(0x11)]) + + # Use the DUP opcode + account_code += dup_opcode + + # Save each stack value into different keys in storage + account_code += b"".join([Op.PUSH1(i) + Op.SSTORE for i in range(0x11)]) + + pre[account] = Account(code=account_code) + + tx = Transaction( + ty=0x0, + nonce=0, + to=account, + gas_limit=500000, + gas_price=10, + protected=False if fork in [Frontier, Homestead] else True, + data="", + ) + """ - We are setting up 16 accounts, ranging from 0x100 to 0x10f. - They push values into the stack from 0-16, but each contract uses a - different DUP opcode, and depending on the opcode used, the item copied - into the storage changes. + Storage will be structured as follows: + + 0x00: 0x10-0x01 (Depending on DUP opcode) + 0x01: 0x10 + 0x02: 0x0F + 0x03: 0x0E + 0x04: 0x0D + 0x05: 0x0C + 0x06: 0x0B + 0x07: 0x0A + 0x08: 0x09 + 0x09: 0x08 + 0x0A: 0x07 + 0x0B: 0x06 + 0x0C: 0x05 + 0x0D: 0x04 + 0x0E: 0x03 + 0x0F: 0x02 + 0x10: 0x01 + + DUP1 copies the first element of the stack (0x10). + DUP16 copies the 16th element of the stack (0x01). """ - for i in range(0, 16): - """ - Account 0x100 uses DUP1, - Account 0x10f uses DUP16. - """ - account = to_address(0x100 + i) - dup_opcode = 0x80 + i - - pre[account] = Account( - code=( - # Push 0 - 16 onto the stack - """0x6000 6001 6002 6003 6004 6005 6006 6007 6008 6009 - 600A 600B 600C 600D 600E 600F 6010""" - + - # Use the DUP opcode for this account - hex(dup_opcode)[2:] - + - # Save each stack value into different keys in storage - """6000 55 6001 55 6002 55 6003 55 6004 55 6005 55 - 6006 55 6007 55 6008 55 6009 55 600A 55 600B 55 - 600C 55 600D 55 600E 55 600F 55 6010 55""" - ) - ) - - """ - Also we are sending one transaction to each account. - The storage of each will only change by one item: storage[0] - The value depends on the DUP opcode used. - """ - - tx = Transaction( - ty=0x0, - nonce=i, - to=account, - gas_limit=500000, - gas_price=10, - protected=False if fork in [Frontier, Homestead] else True, - data="", - ) - txs.append(tx) - - """ - Storage will be structured as follows: - - 0x00: 0x10-0x01 (Depending on DUP opcode) - 0x01: 0x10 - 0x02: 0x0F - 0x03: 0x0E - 0x04: 0x0D - 0x05: 0x0C - 0x06: 0x0B - 0x07: 0x0A - 0x08: 0x09 - 0x09: 0x08 - 0x0A: 0x07 - 0x0B: 0x06 - 0x0C: 0x05 - 0x0D: 0x04 - 0x0E: 0x03 - 0x0F: 0x02 - 0x10: 0x01 - - DUP1 copies the first element of the stack (0x10). - DUP16 copies the 16th element of the stack (0x01). - """ - s: Storage.StorageDictType = dict(zip(range(1, 17), range(16, 0, -1))) - s[0] = 16 - i - - post[account] = Account(storage=s) - - state_test(env=env, pre=pre, post=post, txs=txs) + s: Storage.StorageDictType = dict(zip(range(1, 17), range(16, 0, -1))) + s[0] = 16 - (dup_opcode.int() - 0x80) + + post[account] = Account(storage=s) + + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/homestead/yul/test_yul_example.py b/tests/homestead/yul/test_yul_example.py index b4996139447..1e6fad07a27 100644 --- a/tests/homestead/yul/test_yul_example.py +++ b/tests/homestead/yul/test_yul_example.py @@ -59,4 +59,4 @@ def test_yul(state_test: StateTestFiller, yul: YulCompiler, fork: Fork): ), } - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/istanbul/eip1344_chainid/test_chainid.py b/tests/istanbul/eip1344_chainid/test_chainid.py index 32eaffc3278..0218fe0975c 100644 --- a/tests/istanbul/eip1344_chainid/test_chainid.py +++ b/tests/istanbul/eip1344_chainid/test_chainid.py @@ -51,4 +51,4 @@ def test_chainid(state_test: StateTestFiller): to_address(0x100): Account(code="0x4660015500", storage={"0x01": "0x01"}), } - state_test(env=env, pre=pre, post=post, txs=[tx]) + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py b/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py index 32324eff199..516b1fec56e 100644 --- a/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py +++ b/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py @@ -10,7 +10,7 @@ import pytest -from ethereum_test_forks import Shanghai, is_fork +from ethereum_test_forks import Shanghai from ethereum_test_tools import ( Account, CodeGasMeasure, @@ -117,7 +117,7 @@ def test_warm_coinbase_call_out_of_gas( post = {} - if use_sufficient_gas and is_fork(fork=fork, which=Shanghai): + if use_sufficient_gas and fork >= Shanghai: post[caller_address] = Account( storage={ # On shanghai and beyond, calls with only 100 gas to @@ -138,7 +138,7 @@ def test_warm_coinbase_call_out_of_gas( env=env, pre=pre, post=post, - txs=[tx], + tx=tx, tag="opcode_" + opcode, ) @@ -244,7 +244,7 @@ def test_warm_coinbase_gas_usage(state_test, fork, opcode, code_gas_measure): measure_address: Account(code=code_gas_measure, balance=1000000000000000000000), } - if is_fork(fork, Shanghai): + if fork >= Shanghai: expected_gas = GAS_REQUIRED_CALL_WARM_ACCOUNT # Warm account access cost after EIP-3651 else: expected_gas = 2600 # Cold account access cost before EIP-3651 @@ -269,6 +269,6 @@ def test_warm_coinbase_gas_usage(state_test, fork, opcode, code_gas_measure): env=env, pre=pre, post=post, - txs=[tx], + tx=tx, tag="opcode_" + opcode.lower(), ) diff --git a/tests/shanghai/eip3855_push0/test_push0.py b/tests/shanghai/eip3855_push0/test_push0.py index 582bc9e83a3..313aa261b9b 100644 --- a/tests/shanghai/eip3855_push0/test_push0.py +++ b/tests/shanghai/eip3855_push0/test_push0.py @@ -71,7 +71,7 @@ def test_push0_key_sstore( pre[addr_1] = Account(code=code) post[addr_1] = Account(storage={0x00: 0x01}) - state_test(env=env, pre=pre, post=post, txs=[tx], tag="key_sstore") + state_test(env=env, pre=pre, post=post, tx=tx, tag="key_sstore") def test_push0_fill_stack( @@ -92,7 +92,7 @@ def test_push0_fill_stack( pre[addr_1] = Account(code=code) post[addr_1] = Account(storage={0x00: 0x01}) - state_test(env=env, pre=pre, post=post, txs=[tx], tag="fill_stack") + state_test(env=env, pre=pre, post=post, tx=tx, tag="fill_stack") def test_push0_stack_overflow( @@ -112,7 +112,7 @@ def test_push0_stack_overflow( pre[addr_1] = Account(code=code) post[addr_1] = Account(storage={0x00: 0x00}) - state_test(env=env, pre=pre, post=post, txs=[tx], tag="stack_overflow") + state_test(env=env, pre=pre, post=post, tx=tx, tag="stack_overflow") def test_push0_storage_overwrite( @@ -131,7 +131,7 @@ def test_push0_storage_overwrite( pre[addr_1] = Account(code=code, storage={0x00: 0x0A, 0x01: 0x0A}) post[addr_1] = Account(storage={0x00: 0x02, 0x01: 0x00}) - state_test(env=env, pre=pre, post=post, txs=[tx], tag="storage_overwrite") + state_test(env=env, pre=pre, post=post, tx=tx, tag="storage_overwrite") def test_push0_during_staticcall( @@ -159,7 +159,7 @@ def test_push0_during_staticcall( pre[addr_2] = Account(code=code_2) post[addr_1] = Account(storage={0x00: 0x01, 0x01: 0xFF}) - state_test(env=env, pre=pre, post=post, txs=[tx], tag="during_staticcall") + state_test(env=env, pre=pre, post=post, tx=tx, tag="during_staticcall") def test_push0_before_jumpdest( @@ -178,7 +178,7 @@ def test_push0_before_jumpdest( pre[addr_1] = Account(code=code) post[addr_1] = Account(storage={0x00: 0x01}) - state_test(env=env, pre=pre, post=post, txs=[tx], tag="before_jumpdest") + state_test(env=env, pre=pre, post=post, tx=tx, tag="before_jumpdest") def test_push0_gas_cost( @@ -200,4 +200,4 @@ def test_push0_gas_cost( pre[addr_1] = Account(code=code) post[addr_1] = Account(storage={0x00: 0x02}) - state_test(env=env, pre=pre, post=post, txs=[tx], tag="gas_cost") + state_test(env=env, pre=pre, post=post, tx=tx, tag="gas_cost") diff --git a/tests/shanghai/eip3860_initcode/test_initcode.py b/tests/shanghai/eip3860_initcode/test_initcode.py index 9ac605b032a..ace8fb6f403 100644 --- a/tests/shanghai/eip3860_initcode/test_initcode.py +++ b/tests/shanghai/eip3860_initcode/test_initcode.py @@ -15,8 +15,6 @@ from ethereum_test_tools import ( Account, - Block, - BlockchainTestFiller, Environment, Initcode, StateTestFiller, @@ -200,7 +198,7 @@ def get_initcode_name(val): ], ids=get_initcode_name, ) -def test_contract_creating_tx(blockchain_test: BlockchainTestFiller, initcode: Initcode): +def test_contract_creating_tx(state_test: StateTestFiller, initcode: Initcode): """ Test cases using a contract creating transaction @@ -232,24 +230,21 @@ def test_contract_creating_tx(blockchain_test: BlockchainTestFiller, initcode: I gas_price=10, ) - block = Block(txs=[tx]) - if len(initcode) > MAX_INITCODE_SIZE and eip_3860_active: # Initcode is above the max size, tx inclusion in the block makes # it invalid. post[created_contract_address] = Account.NONEXISTENT tx.error = "max initcode size exceeded" - block.exception = "max initcode size exceeded" else: # Initcode is at or below the max size, tx inclusion in the block # is ok and the contract is successfully created. post[created_contract_address] = Account(code=Op.STOP) - blockchain_test( + state_test( pre=pre, post=post, - blocks=[block], - genesis_environment=env, + tx=tx, + env=env, tag=f"{initcode.name}", ) @@ -382,14 +377,6 @@ def tx( error=tx_error, ) - @pytest.fixture - def block(self, tx, tx_error) -> Block: - """ - Test that the tx_error is also propagated on the Block for the case of - too little intrinsic gas. - """ - return Block(txs=[tx], exception=tx_error) - @pytest.fixture def post( self, @@ -413,14 +400,14 @@ def post( def test_gas_usage( self, - blockchain_test: BlockchainTestFiller, + state_test: StateTestFiller, gas_test_case: str, initcode: Initcode, exact_intrinsic_gas, exact_execution_gas, env, pre, - block, + tx, post, ): """ @@ -436,11 +423,11 @@ def test_gas_usage( "equivalent to that of 'test_exact_intrinsic_gas'." ) - blockchain_test( + state_test( pre=pre, post=post, - blocks=[block], - genesis_environment=env, + tx=tx, + env=env, tag=f"{initcode.name}_{gas_test_case}", ) @@ -632,6 +619,6 @@ def test_create_opcode_initcode( env=env, pre=pre, post=post, - txs=[tx], + tx=tx, tag=f"{initcode.name}_{opcode}", ) diff --git a/whitelist.txt b/whitelist.txt index 3064961f765..c6c87ea6b2f 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -15,8 +15,10 @@ bb besu big0 big1 +blobgasfee blockchain BlockchainTest +BlockchainTests BlockchainTestFiller blockhash blocknum @@ -60,6 +62,7 @@ customizations Customizations danceratopz dao +dataclasses datastructures delitem dev @@ -111,6 +114,7 @@ git's github Github glightbox +globals go-ethereum's gwei hash32 @@ -229,12 +233,14 @@ squidfunk src stackoverflow StateTest +StateTests StateTestFiller staticcalled stExample str streetsidesoftware subcall +subclasscheck subdirectories subdirectory subgraph @@ -242,7 +248,9 @@ substring sudo t8n tamasfe +testability TestAddress +TestContractCreationGasUsage TestMultipleWithdrawalsSameAddress textwrap time15k @@ -504,6 +512,9 @@ precompile precompiles deployer 0x +modexp +0x00 +0x10 fi url