From 15acaaaaa0287ecba54ea73bf2068d68a715de5b Mon Sep 17 00:00:00 2001 From: Alexey Sharov Date: Fri, 16 Oct 2026 15:30:40 +0700 Subject: [PATCH] evmone v0.25.0 Re-vendor evmone/ from upstream tag v0.25.0 (was v0.23.0); intx stays v0.15.0. capi.cpp: the evmmax namespace is gone since 0.24, the bn254 helpers live in evmone::crypto. --- capi.cpp | 8 +- evmone/.bumpversion.cfg | 2 +- evmone/.github/workflows/codspeed.yml | 111 ++++ evmone/AGENTS.md | 58 +- evmone/CHANGELOG.md | 157 +++++ evmone/CMakeLists.txt | 16 +- evmone/README.md | 3 + evmone/circle.yml | 154 +++-- evmone/cmake/Hunter/config.cmake | 20 +- evmone/cmake/blst.cmake | 4 +- evmone/codecov.yml | 2 - evmone/evmc/include/evmc/evmc.h | 26 +- evmone/evmc/include/evmc/evmc.hpp | 26 +- evmone/evmc/include/evmc/helpers.h | 3 +- evmone/lib/CMakeLists.txt | 1 - evmone/lib/evmmax/CMakeLists.txt | 14 - evmone/lib/evmone/CMakeLists.txt | 4 + evmone/lib/evmone/constants.hpp | 9 + evmone/lib/evmone/create_address.cpp | 5 +- evmone/lib/evmone/execution_state.hpp | 25 +- evmone/lib/evmone/instructions.hpp | 59 +- evmone/lib/evmone/instructions_calls.cpp | 138 +++-- evmone/lib/evmone/instructions_storage.cpp | 49 +- evmone/lib/evmone/instructions_traits.hpp | 57 +- evmone/lib/evmone/lru_cache.hpp | 4 +- evmone/lib/evmone/state_gas.hpp | 48 ++ evmone/lib/evmone_precompiles/CMakeLists.txt | 3 +- evmone/lib/evmone_precompiles/bn254.cpp | 4 +- evmone/lib/evmone_precompiles/bn254.hpp | 4 +- evmone/lib/evmone_precompiles/ecc.hpp | 6 +- .../evmone_precompiles/modarith.hpp} | 189 +++--- evmone/lib/evmone_precompiles/modexp.cpp | 109 ++-- evmone/lib/evmone_precompiles/mulmod.cpp | 19 +- evmone/lib/evmone_precompiles/mulmod.hpp | 8 + .../pairing/bn254/fields.hpp | 4 +- .../pairing/bn254/pairing.cpp | 112 ++-- .../pairing/bn254/utils.hpp | 8 +- .../pairing/field_template.hpp | 4 +- evmone/lib/evmone_precompiles/secp256k1.cpp | 4 +- evmone/lib/evmone_precompiles/secp256k1.hpp | 4 +- evmone/lib/evmone_precompiles/secp256r1.cpp | 4 +- evmone/lib/evmone_precompiles/secp256r1.hpp | 4 +- evmone/test/CMakeLists.txt | 44 +- evmone/test/bench/bench.cpp | 15 +- evmone/test/bench/helpers.hpp | 15 + evmone/test/bench/synthetic_benchmarks.cpp | 7 +- evmone/test/blockchaintest/.clang-tidy | 3 - evmone/test/blockchaintest/CMakeLists.txt | 12 - evmone/test/blockchaintest/blockchaintest.cpp | 147 ----- .../blockchaintest/blockchaintest_runner.cpp | 481 ---------------- .../blockchaintest/blockchaintest_runner.hpp | 11 - evmone/test/integration/CMakeLists.txt | 57 +- .../integration/blockchaintest/CMakeLists.txt | 37 -- .../integration/evmone-cli/CMakeLists.txt | 49 ++ .../integration/evmone-cli/run/CMakeLists.txt | 43 ++ .../test/integration/evmone-cli/run/code.hex | 1 + .../{ => evmone-cli}/t8n/CMakeLists.txt | 101 ++-- .../{ => evmone-cli}/t8n/base_fee/alloc.json | 0 .../{ => evmone-cli}/t8n/base_fee/env.json | 0 .../{ => evmone-cli}/t8n/base_fee/txs.json | 0 .../t8n/cancun_create_tx/alloc.json | 0 .../t8n/cancun_create_tx/env.json | 0 .../t8n/cancun_create_tx/txs.json | 0 .../t8n/osaka_blob_schedule/alloc.json | 17 + .../t8n/osaka_blob_schedule/blobParams.json | 0 .../t8n/osaka_blob_schedule/env.json | 0 .../t8n/osaka_blob_schedule/txs.json | 0 .../t8n/prague_empty_requests/alloc.json | 17 + .../t8n/prague_empty_requests/env.json | 0 .../t8n/prague_empty_requests/txs.json | 0 .../t8n/prague_pre_state_only}/alloc.json | 0 .../t8n/prague_pre_state_only/env.json | 6 + .../t8n/prague_pre_state_only/txs.json | 15 + .../evmone-cli/test/CMakeLists.txt | 106 ++++ .../test/blockchaintest/CMakeLists.txt | 98 ++++ .../blockchaintest/eip7778_block_gas.json | 38 +- .../blockchaintest/stops_at_one_test.json | 128 +++++ .../test}/blockchaintest/test.json | 0 .../unrecovered_sender_blob_gas.json | 0 .../unrecovered_sender_deposit_log.json | 0 .../test/blockchaintest/unsupported_rlp.json | 47 ++ .../evmone-cli/test/statetest/CMakeLists.txt | 150 +++++ .../statetest/filter/one_failing_of_two.json | 0 .../test}/statetest/tests1/B/T.json | 0 .../test/statetest/tests1/SuiteA/index.json | 4 + .../test/statetest/tests1/SuiteA/notes.txt | 1 + .../test}/statetest/tests1/SuiteA/test1.json | 0 .../statetest/tests1/SuiteA/test2_multi.json | 0 .../test}/statetest/tests2/SuiteA/test1.json | 0 .../evmone-cli/test/testcmd/declined.json | 99 ++++ .../evmone-cli/test/testcmd_fault/faults.json | 135 +++++ .../test/testcmd_skipped/all_unsupported.json | 15 + .../test/testcmd_skipped/empty.json | 1 + .../test/testcmd_skipped/not_an_object.json | 7 + .../test/testcmd_skipped/partial_shapes.json | 24 + .../test/testcmd_skipped/pre_alloc.json | 27 + evmone/test/integration/export/CMakeLists.txt | 2 +- .../test/integration/statetest/CMakeLists.txt | 147 ----- .../statetest/tx/invalid_encoding.json | 43 -- .../statetest/tx/invalid_nonce.json | 42 -- .../statetest/tx/invalid_signature.json | 43 -- .../t8n/prague_empty_requests/alloc.json | 7 - .../test/internal_benchmarks/CMakeLists.txt | 4 +- .../{evmmax_bench.cpp => modarith_bench.cpp} | 34 +- .../precompiles_bench/precompiles_bench.cpp | 59 +- evmone/test/state/CMakeLists.txt | 49 +- evmone/test/state/account.hpp | 22 +- evmone/test/state/authorization.cpp | 4 +- evmone/test/state/bal.cpp | 148 +++++ evmone/test/state/bal.hpp | 81 +++ evmone/test/state/errors.hpp | 6 + evmone/test/state/host.cpp | 182 +++--- evmone/test/state/host.hpp | 2 +- evmone/test/state/precompiles.cpp | 34 +- evmone/test/state/requests.hpp | 8 + evmone/test/state/state.cpp | 503 +++++++++++----- evmone/test/state/state.hpp | 48 +- evmone/test/state/state_diff.hpp | 7 +- evmone/test/state/system_contracts.cpp | 21 +- evmone/test/state/system_contracts.hpp | 7 + evmone/test/state/transaction.cpp | 4 +- evmone/test/state/transaction.hpp | 15 +- evmone/test/statetest/.clang-tidy | 3 - evmone/test/statetest/CMakeLists.txt | 11 - evmone/test/statetest/statetest.cpp | 169 ------ evmone/test/unittests/CMakeLists.txt | 21 +- .../unittests/blockchaintest_loader_test.cpp | 39 ++ .../unittests/blockchaintest_runner_test.cpp | 162 ++++++ ..._bn254_add_test.cpp => bn254_add_test.cpp} | 4 +- ...54_from_bytes.cpp => bn254_from_bytes.cpp} | 8 +- ..._bn254_mul_test.cpp => bn254_mul_test.cpp} | 8 +- ...airing_test.cpp => bn254_pairing_test.cpp} | 10 +- evmone/test/unittests/evm_eip2929_test.cpp | 8 +- evmone/test/unittests/evm_storage_test.cpp | 7 +- evmone/test/unittests/evmone_test.cpp | 29 + .../{evmmax_test.cpp => modarith_test.cpp} | 20 +- .../unittests/precompiles_expmod_test.cpp | 29 + .../unittests/precompiles_secp256r1_test.cpp | 2 +- ..._secp256k1_test.cpp => secp256k1_test.cpp} | 22 +- .../test/unittests/state_rlp_decode_test.cpp | 4 +- evmone/test/unittests/state_transition.cpp | 26 +- evmone/test/unittests/state_transition.hpp | 12 +- .../unittests/state_transition_call_test.cpp | 19 + .../state_transition_create_test.cpp | 4 +- ...tate_transition_eip7778_block_gas_test.cpp | 14 +- ...tate_transition_eip8037_state_gas_test.cpp | 391 +++++++++++++ .../unittests/state_transition_touch_test.cpp | 70 +++ .../unittests/state_transition_tx_test.cpp | 49 +- evmone/test/unittests/state_tx_test.cpp | 33 +- .../test/unittests/statediff_export_test.cpp | 57 ++ .../test/unittests/statetest_runner_test.cpp | 278 +++++++++ evmone/test/unittests/test_collector_test.cpp | 78 +++ evmone/test/unittests/test_driver_test.cpp | 211 +++++++ evmone/test/unittests/test_report_test.cpp | 253 ++++++++ evmone/test/unittests/tooling_t8n_test.cpp | 316 +++++++++- evmone/test/utils/CMakeLists.txt | 8 + evmone/test/utils/block_transition.cpp | 60 +- evmone/test/utils/block_transition.hpp | 6 +- evmone/test/utils/blockchaintest.hpp | 30 +- evmone/test/utils/blockchaintest_loader.cpp | 8 +- evmone/test/utils/blockchaintest_runner.cpp | 538 ++++++++++++++++++ evmone/test/utils/error_matching.cpp | 1 + evmone/test/utils/mpt_hash.cpp | 2 +- evmone/test/utils/statetest.hpp | 46 +- evmone/test/utils/statetest_export.cpp | 35 ++ evmone/test/utils/statetest_loader.cpp | 14 +- .../{statetest => utils}/statetest_runner.cpp | 66 ++- evmone/test/utils/t8n.cpp | 29 +- evmone/test/utils/test_collector.cpp | 80 +++ evmone/test/utils/test_collector.hpp | 33 ++ evmone/test/utils/test_driver.cpp | 336 +++++++++++ evmone/test/utils/test_driver.hpp | 105 ++++ evmone/test/utils/test_report.cpp | 37 ++ evmone/test/utils/test_report.hpp | 177 ++++++ evmone/test/utils/test_state.cpp | 19 +- evmone/test/utils/test_state.hpp | 13 +- evmone/test/utils/utils.hpp | 15 + evmone/tools/evmone/main.cpp | 51 ++ 178 files changed, 6737 insertions(+), 2308 deletions(-) create mode 100644 evmone/.github/workflows/codspeed.yml delete mode 100644 evmone/lib/evmmax/CMakeLists.txt create mode 100644 evmone/lib/evmone/state_gas.hpp rename evmone/{include/evmmax/evmmax.hpp => lib/evmone_precompiles/modarith.hpp} (56%) delete mode 100644 evmone/test/blockchaintest/.clang-tidy delete mode 100644 evmone/test/blockchaintest/CMakeLists.txt delete mode 100644 evmone/test/blockchaintest/blockchaintest.cpp delete mode 100644 evmone/test/blockchaintest/blockchaintest_runner.cpp delete mode 100644 evmone/test/blockchaintest/blockchaintest_runner.hpp delete mode 100644 evmone/test/integration/blockchaintest/CMakeLists.txt create mode 100644 evmone/test/integration/evmone-cli/CMakeLists.txt create mode 100644 evmone/test/integration/evmone-cli/run/CMakeLists.txt create mode 100644 evmone/test/integration/evmone-cli/run/code.hex rename evmone/test/integration/{ => evmone-cli}/t8n/CMakeLists.txt (69%) rename evmone/test/integration/{ => evmone-cli}/t8n/base_fee/alloc.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/base_fee/env.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/base_fee/txs.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/cancun_create_tx/alloc.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/cancun_create_tx/env.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/cancun_create_tx/txs.json (100%) create mode 100644 evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/alloc.json rename evmone/test/integration/{ => evmone-cli}/t8n/osaka_blob_schedule/blobParams.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/osaka_blob_schedule/env.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/osaka_blob_schedule/txs.json (100%) create mode 100644 evmone/test/integration/evmone-cli/t8n/prague_empty_requests/alloc.json rename evmone/test/integration/{ => evmone-cli}/t8n/prague_empty_requests/env.json (100%) rename evmone/test/integration/{ => evmone-cli}/t8n/prague_empty_requests/txs.json (100%) rename evmone/test/integration/{t8n/osaka_blob_schedule => evmone-cli/t8n/prague_pre_state_only}/alloc.json (100%) create mode 100644 evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/env.json create mode 100644 evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/txs.json create mode 100644 evmone/test/integration/evmone-cli/test/CMakeLists.txt create mode 100644 evmone/test/integration/evmone-cli/test/blockchaintest/CMakeLists.txt rename evmone/test/integration/{ => evmone-cli/test}/blockchaintest/eip7778_block_gas.json (82%) create mode 100644 evmone/test/integration/evmone-cli/test/blockchaintest/stops_at_one_test.json rename evmone/test/integration/{ => evmone-cli/test}/blockchaintest/test.json (100%) rename evmone/test/integration/{ => evmone-cli/test}/blockchaintest/unrecovered_sender_blob_gas.json (100%) rename evmone/test/integration/{ => evmone-cli/test}/blockchaintest/unrecovered_sender_deposit_log.json (100%) create mode 100644 evmone/test/integration/evmone-cli/test/blockchaintest/unsupported_rlp.json create mode 100644 evmone/test/integration/evmone-cli/test/statetest/CMakeLists.txt rename evmone/test/integration/{ => evmone-cli/test}/statetest/filter/one_failing_of_two.json (100%) rename evmone/test/integration/{ => evmone-cli/test}/statetest/tests1/B/T.json (100%) create mode 100644 evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/index.json create mode 100644 evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/notes.txt rename evmone/test/integration/{ => evmone-cli/test}/statetest/tests1/SuiteA/test1.json (100%) rename evmone/test/integration/{ => evmone-cli/test}/statetest/tests1/SuiteA/test2_multi.json (100%) rename evmone/test/integration/{ => evmone-cli/test}/statetest/tests2/SuiteA/test1.json (100%) create mode 100644 evmone/test/integration/evmone-cli/test/testcmd/declined.json create mode 100644 evmone/test/integration/evmone-cli/test/testcmd_fault/faults.json create mode 100644 evmone/test/integration/evmone-cli/test/testcmd_skipped/all_unsupported.json create mode 100644 evmone/test/integration/evmone-cli/test/testcmd_skipped/empty.json create mode 100644 evmone/test/integration/evmone-cli/test/testcmd_skipped/not_an_object.json create mode 100644 evmone/test/integration/evmone-cli/test/testcmd_skipped/partial_shapes.json create mode 100644 evmone/test/integration/evmone-cli/test/testcmd_skipped/pre_alloc.json delete mode 100644 evmone/test/integration/statetest/CMakeLists.txt delete mode 100644 evmone/test/integration/statetest/tx/invalid_encoding.json delete mode 100644 evmone/test/integration/statetest/tx/invalid_nonce.json delete mode 100644 evmone/test/integration/statetest/tx/invalid_signature.json delete mode 100644 evmone/test/integration/t8n/prague_empty_requests/alloc.json rename evmone/test/internal_benchmarks/{evmmax_bench.cpp => modarith_bench.cpp} (52%) create mode 100644 evmone/test/state/bal.cpp create mode 100644 evmone/test/state/bal.hpp delete mode 100644 evmone/test/statetest/.clang-tidy delete mode 100644 evmone/test/statetest/CMakeLists.txt delete mode 100644 evmone/test/statetest/statetest.cpp create mode 100644 evmone/test/unittests/blockchaintest_runner_test.cpp rename evmone/test/unittests/{evmmax_bn254_add_test.cpp => bn254_add_test.cpp} (99%) rename evmone/test/unittests/{evmmax_bn254_from_bytes.cpp => bn254_from_bytes.cpp} (93%) rename evmone/test/unittests/{evmmax_bn254_mul_test.cpp => bn254_mul_test.cpp} (99%) rename evmone/test/unittests/{evmmax_bn254_pairing_test.cpp => bn254_pairing_test.cpp} (97%) rename evmone/test/unittests/{evmmax_test.cpp => modarith_test.cpp} (91%) rename evmone/test/unittests/{evmmax_secp256k1_test.cpp => secp256k1_test.cpp} (97%) create mode 100644 evmone/test/unittests/state_transition_eip8037_state_gas_test.cpp create mode 100644 evmone/test/unittests/statediff_export_test.cpp create mode 100644 evmone/test/unittests/statetest_runner_test.cpp create mode 100644 evmone/test/unittests/test_collector_test.cpp create mode 100644 evmone/test/unittests/test_driver_test.cpp create mode 100644 evmone/test/unittests/test_report_test.cpp create mode 100644 evmone/test/utils/blockchaintest_runner.cpp rename evmone/test/{statetest => utils}/statetest_runner.cpp (58%) create mode 100644 evmone/test/utils/test_collector.cpp create mode 100644 evmone/test/utils/test_collector.hpp create mode 100644 evmone/test/utils/test_driver.cpp create mode 100644 evmone/test/utils/test_driver.hpp create mode 100644 evmone/test/utils/test_report.cpp create mode 100644 evmone/test/utils/test_report.hpp diff --git a/capi.cpp b/capi.cpp index beb1b6c..58a0477 100644 --- a/capi.cpp +++ b/capi.cpp @@ -14,18 +14,18 @@ int evmone_capi_ec_add( unsigned char* out, const unsigned char* x_in, const unsigned char* y_in) { - const auto x = evmmax::bn254::AffinePoint::from_bytes( + const auto x = evmone::crypto::bn254::AffinePoint::from_bytes( std::span{x_in, 64}); - const auto y = evmmax::bn254::AffinePoint::from_bytes( + const auto y = evmone::crypto::bn254::AffinePoint::from_bytes( std::span{y_in, 64}); if (!x.has_value() || !y.has_value()) return 0; - if (!evmmax::bn254::validate(*x) || !evmmax::bn254::validate(*y)) + if (!evmone::crypto::bn254::validate(*x) || !evmone::crypto::bn254::validate(*y)) return 0; - const auto res = evmmax::ecc::add_affine(*x, *y); + const auto res = evmone::crypto::ecc::add_affine(*x, *y); res.to_bytes(std::span{out, 64}); return 1; } diff --git a/evmone/.bumpversion.cfg b/evmone/.bumpversion.cfg index 5196dc3..6cfeee8 100644 --- a/evmone/.bumpversion.cfg +++ b/evmone/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.23.0 +current_version = 0.25.0 tag = True sign_tags = True tag_message = evmone {new_version} diff --git a/evmone/.github/workflows/codspeed.yml b/evmone/.github/workflows/codspeed.yml new file mode 100644 index 0000000..26d429c --- /dev/null +++ b/evmone/.github/workflows/codspeed.yml @@ -0,0 +1,111 @@ +# evmone: Fast Ethereum Virtual Machine implementation +# Copyright 2026 The evmone Authors. +# SPDX-License-Identifier: Apache-2.0 + +name: CodSpeed + +on: + push: + branches: + - master + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +env: + BUILD_DIR: build + CMAKE_BUILD_PARALLEL_LEVEL: 4 + # The newest GCC on the ubuntu-24.04 image, which otherwise defaults to GCC 13. + CC: gcc-14 + CXX: g++-14 + # A benchmark is identified by the name of the fixture it ran, so a release which renames + # the fixtures starts the series over. Bump this deliberately, with the archive's digest. + BENCHMARK_FIXTURES_RELEASE: benchmark@v0.0.7 + BENCHMARK_FIXTURES_SHA256: efa7cbec58bedab28870e260eca1f5271b1c3d81c952f221ba6432bff88fee5d + FIXTURES: fixtures/blockchain_tests/benchmark + +jobs: + benchmark: + name: Benchmark ${{ matrix.name }} + # CodSpeed ships its instrumented Valgrind for Ubuntu 22.04, Ubuntu 24.04 and Debian 12 only, + # and bails out with "Unsupported system" anywhere else, so the image cannot be `latest`. + runs-on: ubuntu-24.04 + permissions: + contents: read # required for actions/checkout + id-token: write # required for OIDC authentication with CodSpeed + strategy: + fail-fast: false + # CodSpeed merges the jobs of a workflow run into one report, but rejects too many + # benchmarks in a single upload (750 were), so the fixtures are split into jobs. Two sweeps + # measured elsewhere are left out: modexp.json (the precompiles benchmark sweeps it) and + # log_benchmark.json (log.json sweeps the same opcodes). + matrix: + include: + - name: evmone + # The precompiles benchmark runs both the mainnet inputs and the modexp parameter sweep: + # the mainnet moduli are all odd and 256-bit, so they cover one code path and hide the + # work on the others. + run: >- + $BUILD_DIR/bin/evmone-bench test/evm-benchmarks/benchmarks --benchmark_filter='baseline/(analyse|execute)' && + $BUILD_DIR/bin/evmone-precompiles-bench + # The worst-case blocks of the execution spec tests, run as the tests they are: each + # checks its post state and is measured on the way. Every one is filled for seven block + # sizes and the smallest is taken, covering all in a fraction of the simulator time. + - name: instruction + fixtures: compute/instruction + run: >- + $BUILD_DIR/bin/evmone test $FIXTURES/compute/instruction -k benchmark-gas-value_1M + --ignore call_context --ignore account_query --ignore log/log_benchmark.json + - name: instruction/call_context + fixtures: compute/instruction/call_context + run: $BUILD_DIR/bin/evmone test $FIXTURES/compute/instruction/call_context -k benchmark-gas-value_1M + - name: instruction/account_query + fixtures: compute/instruction/account_query + run: $BUILD_DIR/bin/evmone test $FIXTURES/compute/instruction/account_query -k benchmark-gas-value_1M + - name: precompile + fixtures: compute/precompile + run: $BUILD_DIR/bin/evmone test $FIXTURES/compute/precompile -k benchmark-gas-value_1M --ignore modexp/modexp.json + - name: scenario + fixtures: compute/scenario + run: $BUILD_DIR/bin/evmone test $FIXTURES/compute/scenario -k benchmark-gas-value_1M + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Install GMP + run: sudo apt-get -q update && sudo apt-get -qy install libgmp-dev + + - name: Configure + # The Release build type keeps the -O3 optimizations on, -g adds the debug information + # needed by CodSpeed to attribute the measurements to the source code. CodSpeed recommends + # (and warns about) RelWithDebInfo, but that would downgrade the optimizations to -O2. + run: > + cmake -S . -B $BUILD_DIR + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_FLAGS=-g + -DCMAKE_CXX_FLAGS=-g + -DBUILD_SHARED_LIBS=OFF + -DEVMONE_TESTING=ON + -DEVMONE_PRECOMPILES_GMP=ON + -DEVMONE_PRECOMPILES_LIBSECP256K1=ON + -DCODSPEED_MODE=simulation + + - name: Build + run: cmake --build $BUILD_DIR --target evmone-bench evmone-precompiles-bench evmone-cli + + # Only the job's own fixtures are unpacked. + - name: Download the benchmark fixtures + if: matrix.fixtures + run: | + curl -L --fail --retry 3 --output-dir /tmp -O \ + https://github.com/ethereum/execution-spec-tests/releases/download/$BENCHMARK_FIXTURES_RELEASE/fixtures_benchmark.tar.gz + echo "$BENCHMARK_FIXTURES_SHA256 /tmp/fixtures_benchmark.tar.gz" | sha256sum -c + tar -xzf /tmp/fixtures_benchmark.tar.gz $FIXTURES/${{ matrix.fixtures }} + + - name: Benchmark + uses: CodSpeedHQ/action@v5 + with: + mode: simulation + run: ${{ matrix.run }} diff --git a/evmone/AGENTS.md b/evmone/AGENTS.md index b652ff7..58bd38d 100644 --- a/evmone/AGENTS.md +++ b/evmone/AGENTS.md @@ -2,54 +2,22 @@ ## Review guidelines -When reviewing changes (code, tests, build scripts, documentation, workflows), expand the review beyond correctness and style. Actively look for: +evmone implements the Ethereum execution specification, so any divergence from it is a consensus bug and outranks every other finding. Compare against the latest merged EIPs and ethereum/execution-specs, not only the released test fixtures, which lag them. -### Vulnerabilities -- Injection risks (SQL/command/template), unsafe string handling, path traversal, deserialization hazards. -- Memory-safety issues (OOB reads/writes, use-after-free patterns, integer overflow/underflow, uninitialized data). -- Crypto misuse (non-constant-time operations on secrets, weak randomness, incorrect nonce handling, missing domain separation, insecure parameters). -- Authentication/authorization gaps, privilege escalation paths, and broken access controls. -- Supply-chain risks (unpinned dependencies, unsafe download/exec patterns, overly broad CI permissions). +Also look for: +- Attacker-controlled inputs (bytecode, transactions, precompile inputs) that cause a crash, undefined behavior, or work not bounded by the gas charged. +- Off-by-one, sign/width, and overflow errors, especially in shifts, carries/borrows, and limb arithmetic. +- Word-size assumptions (CI also builds 32-bit x86 and riscv32). +- Unpinned or unverified downloads and overly broad permissions in CI workflows. +- Non-obvious invariants that no `assert` pins. -### Security regressions -- Changes that weaken previous security properties (e.g., removal of validation, relaxed checks, reduced entropy, widened accepted inputs). -- New feature flags or configuration paths that silently disable protections. -- Debug/diagnostic code that could leak secrets or sensitive data in logs, metrics, crashes, or error messages. +Behavioral coverage comes from the execution-specs tests, which CI runs on every PR. Suggest a unit test only for what they cannot pin; behavior they miss is a gap in those tests, not a missing unit test. -### Safety issues -- Unexpected behavior under malformed, adversarial, or extreme inputs (fuzz-like thinking). -- Denial-of-service vectors (algorithmic complexity, unbounded loops/recursion, large allocations, input-triggered worst cases). -- Concurrency hazards (data races, deadlocks, TOCTOU, improper locking/atomic usage). -- Resource handling (file descriptors, handles, memory, temporary files, cleanup on error paths). +evmone deliberately implements the simplest code for the current specification, so hardcoded current constants, missing extension points for future EIPs, and missing fast paths for degenerate inputs are not findings. Suggest maintainability changes only when they reduce bug risk. -### Correctness bugs -- Off-by-one errors, boundary conditions, and sign/width issues (especially around shifts, carries/borrows, and limb arithmetic). -- Undefined/implementation-defined behavior (C/C++), especially shifts, aliasing, and overflow. -- Error handling and propagation: ensure failures are detected, returned, and tested. -- Cross-platform/endianness/word-size assumptions that may break on other targets. +## Building and testing -### What to include in review feedback -- Call out risks explicitly and suggest mitigations or tests. -- Recommend additional unit tests, property tests, fuzzing targets, or negative tests where appropriate. -- Suggest clarity/maintainability improvements when they reduce future bug risk. -- Flag any unclear invariants, missing comments, or undocumented assumptions. +Out-of-source CMake builds live under `build/`. If one exists, build it directly, e.g. `cmake --build build/debug`. -Other suggestions and improvements are welcome as long as they are constructive, actionable, and help improve quality, security, and maintainability. - -## Building - -This repository uses out-of-source CMake builds. Common build directories live under `build/`, e.g.: - -- `build/debug` -- `build/release` - -If a build directory already exists, build it directly: - -- `cmake --build build/debug` - -## Testing - -All tests can be run via CTest. Make sure the build is up to date. - -- `ctest --test-dir build/debug --output-on-failure` -- to filter tests use `-R ` +- Unit and integration tests: `ctest --test-dir build/debug --output-on-failure` (filter with `-R `). +- execution-specs tests: `build/debug/bin/evmone test /state_tests /blockchain_tests`, with the fixtures from the ethereum/execution-specs releases. diff --git a/evmone/CHANGELOG.md b/evmone/CHANGELOG.md index d34d16b..6cb3554 100644 --- a/evmone/CHANGELOG.md +++ b/evmone/CHANGELOG.md @@ -5,6 +5,154 @@ Documentation of all notable changes to the **evmone** project. The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning]. +## [0.25.0] — 2026-10-14 + +This release completes the Amsterdam EVM revision: with the Block-Level Access Lists +and the last EIPs below, evmone implements all the Glamsterdam ([EIP-7773]) changes +affecting EVM state transition. + +### Added + +- **Amsterdam EVM revision**: the final set of EIPs. + - [EIP-7928]: Block-Level Access List is built along the block execution + and validated against the header hash. + [#1752](https://github.com/ipsilon/evmone/pull/1752) + [#1756](https://github.com/ipsilon/evmone/pull/1756) + [#1757](https://github.com/ipsilon/evmone/pull/1757) + - [EIP-2780]: Resource-based intrinsic transaction gas cost. + [#1733](https://github.com/ipsilon/evmone/pull/1733) + [#1740](https://github.com/ipsilon/evmone/pull/1740) + - [EIP-8282]: Builder Execution Requests: support for new `0x03` and `0x04` requests. + [#1747](https://github.com/ipsilon/evmone/pull/1747) + - [EIP-8037]: the transaction gas limit is capped at `2³²-1` (both gas dimensions together). + [#1744](https://github.com/ipsilon/evmone/pull/1744) + - `evmone t8n` outputs the block access list and its hash. + [#1758](https://github.com/ipsilon/evmone/pull/1758) + +### Changed + +- `EXTCODECOPY` calls the Host's `copy_code()` also when copying zero bytes (needed for BAL). + [#1753](https://github.com/ipsilon/evmone/pull/1753) +- **Faster state transition** in `evmone test` and `evmone t8n`: + the test state caches the code hashes (up to 40% faster), + and the account lookups and the journal rollback are cheaper. + [#1751](https://github.com/ipsilon/evmone/pull/1751) + [#1760](https://github.com/ipsilon/evmone/pull/1760) + [#1761](https://github.com/ipsilon/evmone/pull/1761) + [#1763](https://github.com/ipsilon/evmone/pull/1763) + +### Fixed + +- The rollback of a CREATE over an existing account left it marked as just + created, so the account touched and left empty was not deleted ([EIP-161]). + This gave a wrong state root from Spurious Dragon to London. + [#1762](https://github.com/ipsilon/evmone/pull/1762) +- A failed block-start system call ([EIP-4788], [EIP-2935]) kept its state + changes. Unreachable with the canonical system contracts. + [#1745](https://github.com/ipsilon/evmone/pull/1745) +- The withdrawals ([EIP-4895]) were applied after the block-end system calls + ([EIP-7002], [EIP-7251]) instead of before them. + Observable only with modified system contracts. + [#1755](https://github.com/ipsilon/evmone/pull/1755) +- Cold access to a non-existent account showed up as a deleted account in the state diff. + [#1712](https://github.com/ipsilon/evmone/pull/1712) +- `evmone t8n` applied zero block reward from Paris, which touched the coinbase. + [#1758](https://github.com/ipsilon/evmone/pull/1758) + +## [0.24.0] — 2026-09-23 + +This release brings the state gas repricing to the Amsterdam EVM revision +and consolidates the test tools into a single `evmone test` command. + +### Added + +- **Amsterdam EVM revision**: next set of EIPs. + - [EIP-8037]: State Creation Gas Cost Increase — a second gas dimension + metering the state growth, funded by a per-frame state gas reservoir. + [#1672](https://github.com/ipsilon/evmone/pull/1672) + - [EIP-8038]: State-access gas cost update. + [#1731](https://github.com/ipsilon/evmone/pull/1731) + [#1732](https://github.com/ipsilon/evmone/pull/1732) +- **The `evmone test` command** runs both state and blockchain test fixtures, + recognizing the format of each fixture. It prints a pytest-like report + and supports the `--ignore` and `--collect-only` options. + [#1683](https://github.com/ipsilon/evmone/pull/1683) + [#1684](https://github.com/ipsilon/evmone/pull/1684) + [#1685](https://github.com/ipsilon/evmone/pull/1685) + [#1687](https://github.com/ipsilon/evmone/pull/1687) + [#1699](https://github.com/ipsilon/evmone/pull/1699) + [#1716](https://github.com/ipsilon/evmone/pull/1716) +- The `--trace-summary` of `evmone test` reports the logs hash, + and the new `--state-diff` option adds the transaction state diff to it. + [#1722](https://github.com/ipsilon/evmone/pull/1722) + [#1723](https://github.com/ipsilon/evmone/pull/1723) + +### Changed + +- **EVMC ABI version bumped to 19**: `evmc_message::state_gas` and + `evmc_result::state_gas` carry the [EIP-8037] state gas reservoir. + [#1672](https://github.com/ipsilon/evmone/pull/1672) +- The call depth limit is not checked from Osaka on: the [EIP-7825] transaction + gas limit together with the 63/64 call gas rule keeps the depth below 500. + [#1704](https://github.com/ipsilon/evmone/pull/1704) +- **`modexp` optimizations**: the Montgomery multiplication carries are folded + into a single addition (halving the 256-bit `modexp` time with GCC on x86-64) + and the exponent is reduced for the power-of-two part of the modulus + (up to ~30% faster for even moduli with wide exponents). + [#1662](https://github.com/ipsilon/evmone/pull/1662) + [#1708](https://github.com/ipsilon/evmone/pull/1708) +- **BN254 pairing (`ecpairing`) optimizations**: a single Miller loop + accumulator shares the Fq12 squarings across all pairs. + [#1544](https://github.com/ipsilon/evmone/pull/1544) + [#1637](https://github.com/ipsilon/evmone/pull/1637) + [#1646](https://github.com/ipsilon/evmone/pull/1646) +- The EVMMAX modular arithmetic moved into the `evmone::crypto` namespace + of the precompiles library; the `evmone::evmmax` target and + the `include/evmmax/evmmax.hpp` header are gone. + [#1652](https://github.com/ipsilon/evmone/pull/1652) + [#1657](https://github.com/ipsilon/evmone/pull/1657) + [#1659](https://github.com/ipsilon/evmone/pull/1659) +- The state-independent CALL costs are charged before the [EIP-7702] + delegation lookup, as in [execution-specs]. + [#1668](https://github.com/ipsilon/evmone/pull/1668) +- **CMake 3.25** is the minimum required version. + [#1660](https://github.com/ipsilon/evmone/pull/1660) +- Dependencies upgraded: libsecp256k1 v0.8.0, blst v0.3.17, googletest v1.18.0. + google/benchmark is fetched with FetchContent instead of Hunter. + [#1650](https://github.com/ipsilon/evmone/pull/1650) + [#1651](https://github.com/ipsilon/evmone/pull/1651) + [#1656](https://github.com/ipsilon/evmone/pull/1656) + [#1661](https://github.com/ipsilon/evmone/pull/1661) +- The [Execution Spec Tests] fixtures upgraded to `tests@v21.0.0` + and `tests-glamsterdam-devnet@v8.1.4`. + [#1682](https://github.com/ipsilon/evmone/pull/1682) + [#1734](https://github.com/ipsilon/evmone/pull/1734) + +### Removed + +- The `evmone-statetest` and `evmone-blockchaintest` tools, + replaced by `evmone test`. The `--gtest_*` options are gone with them. + [#1685](https://github.com/ipsilon/evmone/pull/1685) + +### Fixed + +- The blob fee in the transaction affordability check could overflow, + letting a transaction with a huge `max_fee_per_blob_gas` pass the balance check. + [#1714](https://github.com/ipsilon/evmone/pull/1714) +- A reverted cold access to a non-existent account restored it as an existing + empty account, which showed up as a spurious deleted account in the state diff. + [#1709](https://github.com/ipsilon/evmone/pull/1709) +- `evmone t8n` emitted receipts with a hard-coded success status and no logs, + and reported the post-refund block `gasUsed` in Amsterdam ([EIP-7778]). + [#1667](https://github.com/ipsilon/evmone/pull/1667) + [#1698](https://github.com/ipsilon/evmone/pull/1698) +- A subcommand name among another subcommand's arguments (e.g. a trailing `run`) + was silently executed instead of the requested command. + [#1691](https://github.com/ipsilon/evmone/pull/1691) +- A blockchain test which failed early skipped all the following tests + in the same fixture file. + [#1690](https://github.com/ipsilon/evmone/pull/1690) + ## [0.23.0] — 2026-08-11 This release continues the implementation of the Amsterdam EVM revision @@ -1465,6 +1613,8 @@ It delivers fully-compatible and high-speed EVM implementation. - Exposes [EVMC] 6 ABI. - The [intx 0.2.0](https://github.com/chfast/intx/releases/tag/v0.2.0) library is used for 256-bit precision arithmetic. +[0.25.0]: https://github.com/ipsilon/evmone/releases/tag/v0.25.0 +[0.24.0]: https://github.com/ipsilon/evmone/releases/tag/v0.24.0 [0.23.0]: https://github.com/ipsilon/evmone/releases/tag/v0.23.0 [0.22.0]: https://github.com/ipsilon/evmone/releases/tag/v0.22.0 [0.21.0]: https://github.com/ipsilon/evmone/releases/tag/v0.21.0 @@ -1497,6 +1647,7 @@ It delivers fully-compatible and high-speed EVM implementation. [EIP-2]: https://eips.ethereum.org/EIPS/eip-2 [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 +[EIP-161]: https://eips.ethereum.org/EIPS/eip-161 [EIP-170]: https://eips.ethereum.org/EIPS/eip-170 [EIP-663]: https://eips.ethereum.org/EIPS/eip-663 [EIP-1153]: https://eips.ethereum.org/EIPS/eip-1153 @@ -1506,6 +1657,7 @@ It delivers fully-compatible and high-speed EVM implementation. [EIP-2537]: https://eips.ethereum.org/EIPS/eip-2537 [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +[EIP-2780]: https://eips.ethereum.org/EIPS/eip-2780 [EIP-2929]: https://eips.ethereum.org/EIPS/eip-2929 [EIP-2935]: https://eips.ethereum.org/EIPS/eip-2935 [EIP-3155]: https://eips.ethereum.org/EIPS/eip-3155 @@ -1534,6 +1686,7 @@ It delivers fully-compatible and high-speed EVM implementation. [EIP-7692]: https://eips.ethereum.org/EIPS/eip-7692 [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 [EIP-7708]: https://eips.ethereum.org/EIPS/eip-7708 +[EIP-7773]: https://eips.ethereum.org/EIPS/eip-7773 [EIP-7778]: https://eips.ethereum.org/EIPS/eip-7778 [EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843 [EIP-7594]: https://eips.ethereum.org/EIPS/eip-7594 @@ -1543,6 +1696,7 @@ It delivers fully-compatible and high-speed EVM implementation. [EIP-7883]: https://eips.ethereum.org/EIPS/eip-7883 [EIP-7892]: https://eips.ethereum.org/EIPS/eip-7892 [EIP-7918]: https://eips.ethereum.org/EIPS/eip-7918 +[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 [EIP-7934]: https://eips.ethereum.org/EIPS/eip-7934 [EIP-7939]: https://eips.ethereum.org/EIPS/eip-7939 [EIP-7951]: https://eips.ethereum.org/EIPS/eip-7951 @@ -1550,7 +1704,10 @@ It delivers fully-compatible and high-speed EVM implementation. [EIP-7976]: https://eips.ethereum.org/EIPS/eip-7976 [EIP-7981]: https://eips.ethereum.org/EIPS/eip-7981 [EIP-8024]: https://eips.ethereum.org/EIPS/eip-8024 +[EIP-8037]: https://eips.ethereum.org/EIPS/eip-8037 +[EIP-8038]: https://eips.ethereum.org/EIPS/eip-8038 [EIP-8246]: https://eips.ethereum.org/EIPS/eip-8246 +[EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282 [Spurious Dragon]: https://eips.ethereum.org/EIPS/eip-607 [Petersburg]: https://eips.ethereum.org/EIPS/eip-1716 diff --git a/evmone/CMakeLists.txt b/evmone/CMakeLists.txt index 17b1470..456225f 100644 --- a/evmone/CMakeLists.txt +++ b/evmone/CMakeLists.txt @@ -2,7 +2,7 @@ # Copyright 2019 The evmone Authors. # SPDX-License-Identifier: Apache-2.0 -cmake_minimum_required(VERSION 3.22...4.2) +cmake_minimum_required(VERSION 3.25...4.2) option(BUILD_SHARED_LIBS "Build evmone as a shared library" ON) option(COVERAGE "Build with coverage instrumentation" OFF) @@ -23,7 +23,7 @@ cable_set_build_type(DEFAULT Release CONFIGURATION_TYPES Release Debug) include(Hunter/init) project(evmone LANGUAGES CXX C) -set(PROJECT_VERSION 0.23.0) +set(PROJECT_VERSION 0.25.0) option(EVMONE_TOOLS "Build evmone tools" ${PROJECT_IS_TOP_LEVEL}) @@ -36,15 +36,11 @@ if(PROJECT_VERSION_MAJOR EQUAL 0) set(PROJECT_SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}) endif() -if(COMMAND block) # TODO(cmake-3.25) - block() - set(CMAKE_C_CLANG_TIDY "") - set(CMAKE_CXX_CLANG_TIDY "") - add_subdirectory(evmc) - endblock() -else() +block() + set(CMAKE_C_CLANG_TIDY "") + set(CMAKE_CXX_CLANG_TIDY "") add_subdirectory(evmc) -endif() +endblock() cable_configure_compiler(NO_STACK_PROTECTION) if(CABLE_COMPILER_GNULIKE) diff --git a/evmone/README.md b/evmone/README.md index 1ae2f1e..59d072e 100644 --- a/evmone/README.md +++ b/evmone/README.md @@ -3,6 +3,7 @@ [![ethereum badge]][ethereum] [![readme style standard badge]][standard readme] [![codecov badge]][codecov] +[![codspeed badge]][codspeed] [![circleci badge]][circleci] [![appveyor badge]][appveyor] [![license badge]][Apache License, Version 2.0] @@ -128,6 +129,7 @@ Licensed under the [Apache License, Version 2.0]. [appveyor]: https://ci.appveyor.com/project/chfast/evmone/branch/master [circleci]: https://circleci.com/gh/ethereum/evmone/tree/master [codecov]: https://codecov.io/gh/ethereum/evmone/ +[codspeed]: https://app.codspeed.io/ipsilon/evmone [Apache License, Version 2.0]: LICENSE [ethereum]: https://ethereum.org [EVMC]: https://github.com/ethereum/evmc @@ -143,6 +145,7 @@ Licensed under the [Apache License, Version 2.0]. [appveyor badge]: https://img.shields.io/appveyor/ci/chfast/evmone/master.svg?logo=appveyor [circleci badge]: https://img.shields.io/circleci/project/github/ethereum/evmone/master.svg?logo=circleci [codecov badge]: https://img.shields.io/codecov/c/github/ethereum/evmone.svg?logo=codecov +[codspeed badge]: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json [ethereum badge]: https://img.shields.io/badge/ethereum-EVM-informational.svg?logo=ethereum [license badge]: https://img.shields.io/github/license/ethereum/evmone.svg?logo=apache [readme style standard badge]: https://img.shields.io/badge/readme%20style-standard-brightgreen.svg diff --git a/evmone/circle.yml b/evmone/circle.yml index dd98db5..da54271 100644 --- a/evmone/circle.yml +++ b/evmone/circle.yml @@ -35,7 +35,7 @@ executors: CMAKE_BUILD_PARALLEL_LEVEL: 16 linux-gcc-min: docker: - - image: ethereum/cpp-build-env:17-gcc-11 + - image: ethereum/cpp-build-env:20-gcc-11 resource_class: small environment: CMAKE_BUILD_PARALLEL_LEVEL: 2 @@ -162,9 +162,10 @@ commands: fixtures_suffix: type: string default: "" - filter: + ignore: + description: "--ignore flags for the paths not to collect" type: string - default: "*" + default: "" steps: - download_execution_spec_tests: repo: <> @@ -176,17 +177,58 @@ commands: working_directory: ~/build command: > LLVM_PROFILE_FILE=state_tests.profraw - bin/evmone-statetest --gtest_filter='<>' ~/spec-tests/fixtures/state_tests + bin/evmone test <> ~/spec-tests/fixtures/state_tests - run: name: "Execution spec tests (<>, blockchain_tests)" # Tests for in-development EVM revision currently passing. working_directory: ~/build command: > LLVM_PROFILE_FILE=blockchain_tests.profraw - bin/evmone-blockchaintest --gtest_filter='<>' ~/spec-tests/fixtures/blockchain_tests + bin/evmone test <> ~/spec-tests/fixtures/blockchain_tests + + configure: + description: "Configure" + parameters: + testing: + description: "Build the tests and the test tools" + type: boolean + default: true + steps: + - run: + name: "Configure (testing << parameters.testing >>)" + working_directory: ~/build + command: | + if [ "$TOOLCHAIN" ]; then + export toolchain_option="-DTOOLCHAIN=$TOOLCHAIN" + fi + cmake ../project $toolchain_option -DCMAKE_INSTALL_PREFIX=~/install -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_COMPILE_WARNING_AS_ERROR=TRUE -DEVMONE_TESTING=<< parameters.testing >> $CMAKE_OPTIONS + + compile: + description: "Compile" + steps: + - run: + name: "Build" + command: cmake --build ~/build + + smoke_test: + description: "Run the tool once to check the build works" + steps: + - run: + name: "Smoke test the tool" + working_directory: ~/build + command: | + bin/evmone --version + bin/evmone run 600160005260206000f3 | tee out.txt + grep -q "Result: *success" out.txt + grep -q "Output: *0\{63\}1" out.txt build: description: "Build" + parameters: + tools_only_first: + description: "Build with testing off first and smoke-test the tool, then reconfigure" + type: boolean + default: false steps: - checkout - checkout_submodules @@ -212,22 +254,22 @@ commands: - restore_cache: name: "Restore Hunter cache" key: &hunter-cache-key hunter-{{arch}}-{{checksum "build.info"}}-{{checksum "cmake/Hunter/init.cmake"}}-{{checksum "cmake/Hunter/config.cmake"}} - - run: - name: "Configure" - working_directory: ~/build - command: | - if [ "$TOOLCHAIN" ]; then - export toolchain_option="-DTOOLCHAIN=$TOOLCHAIN" - fi - cmake ../project $toolchain_option -DCMAKE_INSTALL_PREFIX=~/install -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_COMPILE_WARNING_AS_ERROR=TRUE -DEVMONE_TESTING=ON $CMAKE_OPTIONS + # Testing off is the only configuration that never fetches GTest. It goes first because the + # Hunter cache is saved after the configure below, which is the one that has GTest in it. + - when: + condition: << parameters.tools_only_first >> + steps: + - configure: + testing: false + - compile + - smoke_test + - configure - save_cache: name: "Save Hunter cache" key: *hunter-cache-key paths: - ~/.hunter/_Base/Cache - - run: - name: "Build" - command: cmake --build ~/build + - compile - run: name: "Install" command: cmake --build ~/build --target install @@ -267,7 +309,7 @@ commands: default: "" binaries: type: string - default: evmone evmone-unittests evmone-statetest evmone-blockchaintest + default: evmone evmone-unittests steps: - run: name: "Collect coverage data (clang, <>)" @@ -423,20 +465,20 @@ jobs: CMAKE_OPTIONS: -DCOVERAGE=1 steps: - build + # The development (devnet) tests. Disabled while tests@v21.0.1 covers them. + # - run_execution_spec_tests: + # release: tests-glamsterdam-devnet@v8.1.4 + # fixtures_suffix: _glamsterdam-devnet + # - collect_coverage_clang: + # flags: eest-develop + # ignore_filename_regex: lib/evmone/(advanced|cpu_check|eof|lru_cache|tracing|vm)|test/(experimental|unittests) + # binaries: evmone - run_execution_spec_tests: - release: tests-glamsterdam-devnet@v6.1.1 - fixtures_suffix: _glamsterdam-devnet - filter: "-for_amsterdam/*:for_bpo2toamsterdamattime15k/*" - - collect_coverage_clang: - flags: eest-develop - ignore_filename_regex: lib/evmone/(advanced|cpu_check|eof|lru_cache|tracing|vm)|test/(experimental|unittests) - binaries: evmone-statetest evmone-blockchaintest - - run_execution_spec_tests: - release: tests@v20.0.1 + release: tests@v21.0.1 - collect_coverage_clang: flags: eest-stable ignore_filename_regex: lib/evmone/(advanced|cpu_check|eof|lru_cache|tracing|vm)|test/(experimental|unittests) - binaries: evmone-statetest evmone-blockchaintest + binaries: evmone ethereum-tests: executor: linux-clang-latest @@ -467,8 +509,7 @@ jobs: working_directory: ~/build command: > LLVM_PROFILE_FILE=blockchain_tests_valid.profraw - bin/evmone-blockchaintest - --gtest_filter='-bcValidBlockTest.SimpleTx3LowS' + bin/evmone test ~/tests/BlockchainTests/ValidBlocks ~/tests/LegacyTests/Cancun/BlockchainTests/ValidBlocks - run: @@ -476,8 +517,10 @@ jobs: working_directory: ~/build command: > LLVM_PROFILE_FILE=blockchain_tests_invalid.profraw - bin/evmone-blockchaintest - --gtest_filter='-bc4895-withdrawals.shanghaiWithoutWithdrawalsRLP:bcInvalidHeaderTest.*:bcUncleHeaderValidity.gasLimitTooLowExactBound' + bin/evmone test + --ignore bc4895-withdrawals/shanghaiWithoutWithdrawalsRLP.json + --ignore bcInvalidHeaderTest + --ignore bcUncleHeaderValidity/gasLimitTooLowExactBound.json ~/tests/BlockchainTests/InvalidBlocks ~/tests/LegacyTests/Cancun/BlockchainTests/InvalidBlocks - collect_coverage_clang: @@ -491,12 +534,12 @@ jobs: steps: - build - download_execution_spec_tests: - release: tests@v20.0.1 + release: tests@v21.0.1 - run: name: "Execution spec tests (state_tests)" working_directory: ~/build command: > - bin/evmone-statetest ~/spec-tests/fixtures/state_tests + bin/evmone test ~/spec-tests/fixtures/state_tests - collect_coverage_clang: flags: eest-libsecp256k1 @@ -511,19 +554,20 @@ jobs: command: sudo apt-get -q update && sudo apt-get -qy install libgmp-dev - build - download_execution_spec_tests: - release: tests@v20.0.1 + release: tests@v21.0.1 - run: name: "Execution spec tests (state_tests)" working_directory: ~/build command: > - bin/evmone-statetest ~/spec-tests/fixtures/state_tests + bin/evmone test ~/spec-tests/fixtures/state_tests - collect_coverage_clang: flags: eest-develop-gmp gcc-min: executor: linux-gcc-min steps: - - build + - build: + tools_only_first: true - test clang-min: @@ -531,7 +575,8 @@ jobs: environment: CMAKE_OPTIONS=-DEVMONE_X86_64_ARCH_LEVEL=3 steps: - - build + - build: + tools_only_first: true - test gcc-latest-memcheck: @@ -566,17 +611,46 @@ jobs: - build - test - run_execution_spec_tests: - release: tests@v20.0.1 + release: tests@v21.0.1 clang-tidy: executor: linux-clang-selfhosted environment: CMAKE_BUILD_PARALLEL_LEVEL: 16 CMAKE_GENERATOR: Ninja - CMAKE_OPTIONS: -DCMAKE_CXX_CLANG_TIDY=clang-tidy + CMAKE_OPTIONS: -DCMAKE_CXX_CLANG_TIDY=clang-tidy-cache;clang-tidy BUILD_TYPE: None + CTCACHE_DIR: /tmp/ctcache + # Hash comments too, otherwise editing a NOLINT reuses the old result. + CTCACHE_KEEP_COMMENTS: 1 steps: + - run: + name: "Check clang-tidy version" + # The version is part of the cache key: ctcache does not hash the analyzer itself. + command: clang-tidy --version | tee /tmp/clang-tidy-version + - restore_cache: + name: "Restore clang-tidy cache" + key: clang-tidy-cache-{{checksum "/tmp/clang-tidy-version"}} + - run: + name: "Setup clang-tidy-cache" + command: | + mkdir -p /tmp/ctcache + find /tmp/ctcache -type f -mtime +7 -delete + # ctcache 1.2.0, pinned by commit because tags are mutable. + curl -L --fail --retry 3 -o /tmp/clang-tidy-cache https://raw.githubusercontent.com/matus-chochlik/ctcache/e33c063ca6e52b48bcefbc45d46d8c150ffa81ca/src/ctcache/clang_tidy_cache.py + sudo install -m 0755 /tmp/clang-tidy-cache /usr/local/bin/clang-tidy-cache + clang-tidy-cache --zero-stats - build + - run: + name: "clang-tidy cache stats" + when: always + command: clang-tidy-cache --print-stats + - save_cache: + name: "Save clang-tidy cache" + when: always + key: clang-tidy-cache-{{checksum "/tmp/clang-tidy-version"}}-{{epoch}} + paths: + - /tmp/ctcache clang-latest-coverage: executor: linux-clang-latest @@ -638,7 +712,7 @@ jobs: executor: linux-base steps: - install_cmake: - version: 3.22.6 + version: 3.25.3 - build - test diff --git a/evmone/cmake/Hunter/config.cmake b/evmone/cmake/Hunter/config.cmake index 5a9452f..dcea47c 100644 --- a/evmone/cmake/Hunter/config.cmake +++ b/evmone/cmake/Hunter/config.cmake @@ -2,8 +2,6 @@ # Copyright 2018 The evmone Authors. # SPDX-License-Identifier: Apache-2.0 -include(hunter_cmake_args) - hunter_config( intx VERSION 0.15.0 @@ -13,9 +11,9 @@ hunter_config( hunter_config( GTest - VERSION 1.17.0 - URL https://github.com/google/googletest/archive/v1.17.0.tar.gz - SHA1 8a8ee424e8275ec4b480ba4a0d1ba94b5dee3ee4 + VERSION 1.18.0 + URL https://github.com/google/googletest/archive/v1.18.0.tar.gz + SHA1 91002f753cec495a3214e04c5602b7d3cc79a452 ) hunter_config( @@ -31,15 +29,3 @@ hunter_config( URL https://github.com/nlohmann/json/archive/v3.12.0.tar.gz SHA1 815212d8acbddc87009667c52ba98a8404efec18 ) - -# Propagate BENCHMARK_ENABLE_LIBPFM to google/benchmark. -# https://github.com/google/benchmark/blob/v1.9.4/CMakeLists.txt#L42 -option(BENCHMARK_ENABLE_LIBPFM "Enable performance counters provided by libpfm" OFF) - -hunter_config( - benchmark - VERSION 1.9.5 - CMAKE_ARGS BENCHMARK_ENABLE_LIBPFM=${BENCHMARK_ENABLE_LIBPFM} - URL https://github.com/google/benchmark/archive/v1.9.5.tar.gz - SHA1 1923d665fc134fca137fea0951403d043b5b7732 -) diff --git a/evmone/cmake/blst.cmake b/evmone/cmake/blst.cmake index 3401763..0c17a70 100644 --- a/evmone/cmake/blst.cmake +++ b/evmone/cmake/blst.cmake @@ -24,8 +24,8 @@ ExternalProject_Add( blst EXCLUDE_FROM_ALL TRUE PREFIX ${PROJECT_BINARY_DIR}/deps - URL https://github.com/supranational/blst/archive/refs/tags/v0.3.16.tar.gz - URL_HASH SHA256=e04805b7d6ef9e1d89b7f511a5b86136c57b455d97924d7324da2305a864673f + URL https://github.com/supranational/blst/archive/refs/tags/v0.3.17.tar.gz + URL_HASH SHA256=c3fef37b566b67419703b2bbb648e15176276af9a880c22aa8f5f2e8cecc2e4d DOWNLOAD_NO_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND ${BLST_BUILD_SCRIPT} diff --git a/evmone/codecov.yml b/evmone/codecov.yml index 10161ea..c223503 100644 --- a/evmone/codecov.yml +++ b/evmone/codecov.yml @@ -19,10 +19,8 @@ component_management: paths: - tools/** - test/bench/** - - test/blockchaintest/** - test/eofparse/** - test/eoftest/** - - test/statetest/** - test/utils/** - component_id: tests name: tests diff --git a/evmone/evmc/include/evmc/evmc.h b/evmone/evmc/include/evmc/evmc.h index 2807838..29626ab 100644 --- a/evmone/evmc/include/evmc/evmc.h +++ b/evmone/evmc/include/evmc/evmc.h @@ -45,7 +45,7 @@ enum * * @see @ref versioning */ - EVMC_ABI_VERSION = 18 + EVMC_ABI_VERSION = 19 }; @@ -120,6 +120,11 @@ struct evmc_message */ int64_t gas; + /** + * The amount of state gas available (EIP-8037). + */ + int64_t state_gas; + /** * The recipient of the message. * @@ -392,6 +397,16 @@ struct evmc_result; */ typedef void (*evmc_release_result_fn)(const struct evmc_result* result); +/** The state-gas counters of an execution (EIP-8037). */ +struct evmc_state_gas +{ + /** The amount of state gas left. */ + int64_t left = 0; + + /** The portion of the consumed state gas taken from evmc_result::gas_left. */ + int64_t spilled = 0; +}; + /** The EVM code execution result. */ struct evmc_result { @@ -414,6 +429,15 @@ struct evmc_result */ int64_t gas_refund; + /** + * The state-gas counters after execution (EIP-8037). + * + * If evmc_result::status_code is other than ::EVMC_SUCCESS, evmc_state_gas::left MUST equal + * ::evmc_message::state_gas and evmc_state_gas::spilled MUST be 0. The VM returns the spill + * to evmc_result::gas_left for ::EVMC_REVERT; any other failure consumes it with gas_left. + */ + struct evmc_state_gas state_gas; + /** * The reference to output data. * diff --git a/evmone/evmc/include/evmc/evmc.hpp b/evmone/evmc/include/evmc/evmc.hpp index 6eeb912..08da50a 100644 --- a/evmone/evmc/include/evmc/evmc.hpp +++ b/evmone/evmc/include/evmc/evmc.hpp @@ -321,6 +321,9 @@ inline const char* to_string(evmc_revision rev) noexcept /// Alias for evmc_make_result(). constexpr auto make_result = evmc_make_result; +/// @copydoc evmc_state_gas +using StateGas = evmc_state_gas; + /// @copydoc evmc_result /// /// This is a RAII wrapper for evmc_result and objects of this type @@ -332,6 +335,7 @@ class Result : private evmc_result using evmc_result::gas_refund; using evmc_result::output_data; using evmc_result::output_size; + using evmc_result::state_gas; using evmc_result::status_code; /// Creates the result from the provided arguments. @@ -344,23 +348,39 @@ class Result : private evmc_result /// @param _gas_refund The amount of refunded gas. /// @param _output_data The pointer to the output. /// @param _output_size The output size. + /// @param _state_gas The state-gas fields. explicit Result(evmc_status_code _status_code, int64_t _gas_left, int64_t _gas_refund, const uint8_t* _output_data, - size_t _output_size) noexcept + size_t _output_size, + StateGas _state_gas = {}) noexcept : evmc_result{make_result(_status_code, _gas_left, _gas_refund, _output_data, _output_size)} - {} + { + state_gas = _state_gas; + } /// Creates the result without output. /// /// @param _status_code The status code. /// @param _gas_left The amount of gas left. /// @param _gas_refund The amount of refunded gas. + /// @param _state_gas The state-gas fields. explicit Result(evmc_status_code _status_code = EVMC_INTERNAL_ERROR, int64_t _gas_left = 0, - int64_t _gas_refund = 0) noexcept + int64_t _gas_refund = 0, + StateGas _state_gas = {}) noexcept : evmc_result{make_result(_status_code, _gas_left, _gas_refund, nullptr, 0)} + { + state_gas = _state_gas; + } + + /// Creates the result without output and without gas left. + /// + /// @param _status_code The status code. + /// @param _state_gas The state-gas fields. + explicit Result(evmc_status_code _status_code, StateGas _state_gas) noexcept + : Result{_status_code, 0, 0, _state_gas} {} /// Converting constructor from raw evmc_result. diff --git a/evmone/evmc/include/evmc/helpers.h b/evmone/evmc/include/evmc/helpers.h index ce9d2e0..fe46846 100644 --- a/evmone/evmc/include/evmc/helpers.h +++ b/evmone/evmc/include/evmc/helpers.h @@ -120,8 +120,7 @@ static inline struct evmc_result evmc_make_result(enum evmc_status_code status_c const uint8_t* output_data, size_t output_size) { - struct evmc_result result; - memset(&result, 0, sizeof(result)); + struct evmc_result result = {}; if (output_size != 0) { diff --git a/evmone/lib/CMakeLists.txt b/evmone/lib/CMakeLists.txt index c6c7dd4..cb56dbb 100644 --- a/evmone/lib/CMakeLists.txt +++ b/evmone/lib/CMakeLists.txt @@ -3,5 +3,4 @@ # SPDX-License-Identifier: Apache-2.0 add_subdirectory(evmone_precompiles) -add_subdirectory(evmmax) add_subdirectory(evmone) diff --git a/evmone/lib/evmmax/CMakeLists.txt b/evmone/lib/evmmax/CMakeLists.txt deleted file mode 100644 index 81b673e..0000000 --- a/evmone/lib/evmmax/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -# evmone: Fast Ethereum Virtual Machine implementation -# Copyright 2023 The evmone Authors. -# SPDX-License-Identifier: Apache-2.0 - -add_library(evmmax INTERFACE) -add_library(evmone::evmmax ALIAS evmmax) -target_compile_features(evmmax INTERFACE cxx_std_20) -target_include_directories(evmmax INTERFACE ${PROJECT_SOURCE_DIR}/include) -target_link_libraries(evmmax INTERFACE intx::intx) -target_sources( - evmmax - PRIVATE - ${PROJECT_SOURCE_DIR}/include/evmmax/evmmax.hpp -) diff --git a/evmone/lib/evmone/CMakeLists.txt b/evmone/lib/evmone/CMakeLists.txt index 9ca5032..ae6165d 100644 --- a/evmone/lib/evmone/CMakeLists.txt +++ b/evmone/lib/evmone/CMakeLists.txt @@ -35,6 +35,10 @@ add_library(evmone ) target_compile_features(evmone PUBLIC cxx_std_20) target_link_libraries(evmone PUBLIC evmc::evmc intx::intx PRIVATE evmone::precompiles) + +# Nested EVM calls recurse ~2.6 KB of stack per frame, too much for Windows' 1 MB default. +target_link_options(evmone INTERFACE $<$:/STACK:8388608>) + target_include_directories(evmone PUBLIC $$ ) diff --git a/evmone/lib/evmone/constants.hpp b/evmone/lib/evmone/constants.hpp index 6bbb9f9..5b1feb5 100644 --- a/evmone/lib/evmone/constants.hpp +++ b/evmone/lib/evmone/constants.hpp @@ -27,4 +27,13 @@ constexpr auto MAX_NONCE = 0xffff'ffff'ffff'ffff; /// The gas given back to a value-transferring CALL, the Yellow Paper's G_callstipend. constexpr auto CALL_STIPEND = 2300; + +/// The fixed cost per state byte (EIP-8037). +constexpr auto COST_PER_STATE_BYTE = 1530; + +/// State-gas cost of creating a new account (EIP-8037). +constexpr auto NEW_ACCOUNT_STATE_GAS = 120 * COST_PER_STATE_BYTE; + +/// State-gas cost of allocating a storage slot (EIP-8037). +constexpr auto STORAGE_SET_STATE_GAS = 64 * COST_PER_STATE_BYTE; } // namespace evmone diff --git a/evmone/lib/evmone/create_address.cpp b/evmone/lib/evmone/create_address.cpp index 411ddf0..d356443 100644 --- a/evmone/lib/evmone/create_address.cpp +++ b/evmone/lib/evmone/create_address.cpp @@ -28,7 +28,10 @@ address compute_create_address(const address& sender, uint64_t sender_nonce) noe } else // Prefixed integer encoding. { - const auto num_nonzero_bytes = (std::bit_width(sender_nonce) + 7) / 8; + // TODO(gcc-13): bit_width returns int after + // [LWG 3656](https://cplusplus.github.io/LWG/issue3656). + // NOLINTNEXTLINE(readability-redundant-casting) + const auto num_nonzero_bytes = static_cast((std::bit_width(sender_nonce) + 7) / 8); *p++ = static_cast(RLP_STR_BASE + num_nonzero_bytes); intx::be::unsafe::store(p, sender_nonce); p = std::shift_left(p, p + MAX_NONCE_SIZE, MAX_NONCE_SIZE - num_nonzero_bytes); diff --git a/evmone/lib/evmone/execution_state.hpp b/evmone/lib/evmone/execution_state.hpp index 9511612..d9c35eb 100644 --- a/evmone/lib/evmone/execution_state.hpp +++ b/evmone/lib/evmone/execution_state.hpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include "state_gas.hpp" #include #include #include @@ -154,6 +155,9 @@ class ExecutionState const advanced::AdvancedCodeAnalysis* advanced; } analysis{}; + /// The frame's state-gas counters (EIP-8037). + StateGas state_gas; + /// Stack space allocation. /// /// This is the last field to make other fields' offsets of reasonable values. @@ -164,7 +168,11 @@ class ExecutionState ExecutionState(const evmc_message& message, evmc_revision revision, const evmc_host_interface& host_interface, evmc_host_context* host_ctx, bytes_view _code) noexcept - : msg{&message}, host{host_interface, host_ctx}, rev{revision}, original_code{_code} + : msg{&message}, + host{host_interface, host_ctx}, + rev{revision}, + original_code{_code}, + state_gas{{.left = message.state_gas}} {} /// Resets the contents of the ExecutionState so that it could be reused. @@ -173,6 +181,7 @@ class ExecutionState bytes_view _code) noexcept { gas_refund = 0; + state_gas = {{.left = message.state_gas}}; memory.clear(); msg = &message; host = {host_interface, host_ctx}; @@ -202,13 +211,23 @@ class ExecutionState /// success, and the output is the memory range recorded in the state. inline evmc_result make_execution_result(ExecutionState& state, int64_t gas_left) noexcept { + if (state.rev >= EVMC_AMSTERDAM && state.status != EVMC_SUCCESS) + { + // Unsuccessful frame doesn't commit any state changes, roll-back all state-gas costs. + gas_left += state.state_gas.spilled; + state.state_gas.left = state.msg->state_gas; + state.state_gas.spilled = 0; + } + // An exceptional halt consumes all gas; only a success or revert keeps gas_left. if (state.status != EVMC_SUCCESS && state.status != EVMC_REVERT) gas_left = 0; const auto gas_refund = (state.status == EVMC_SUCCESS) ? state.gas_refund : 0; assert(state.output_size != 0 || state.output_offset == 0); - return evmc::make_result(state.status, gas_left, gas_refund, - state.output_size != 0 ? &state.memory[state.output_offset] : nullptr, state.output_size); + return evmc::Result{state.status, gas_left, gas_refund, + state.output_size != 0 ? &state.memory[state.output_offset] : nullptr, state.output_size, + state.state_gas} + .release_raw(); } } // namespace evmone diff --git a/evmone/lib/evmone/instructions.hpp b/evmone/lib/evmone/instructions.hpp index ae2afe8..3dc6fa8 100644 --- a/evmone/lib/evmone/instructions.hpp +++ b/evmone/lib/evmone/instructions.hpp @@ -4,6 +4,7 @@ #pragma once #include "baseline.hpp" +#include "constants.hpp" #include "execution_state.hpp" #include "instructions_traits.hpp" #include "instructions_xmacro.hpp" @@ -433,7 +434,7 @@ inline Result balance(StackTop stack, int64_t gas_left, ExecutionState& state) n if (state.rev >= EVMC_BERLIN && state.host.access_account(addr) == EVMC_ACCESS_COLD) { - if ((gas_left -= instr::additional_cold_account_access_cost) < 0) + if ((gas_left -= additional_cold_account_access(state.rev)) < 0) return {EVMC_OUT_OF_GAS, gas_left}; } @@ -580,7 +581,7 @@ inline Result extcodesize(StackTop stack, int64_t gas_left, ExecutionState& stat if (state.rev >= EVMC_BERLIN && state.host.access_account(addr) == EVMC_ACCESS_COLD) { - if ((gas_left -= instr::additional_cold_account_access_cost) < 0) + if ((gas_left -= additional_cold_account_access(state.rev)) < 0) return {EVMC_OUT_OF_GAS, gas_left}; } @@ -604,19 +605,19 @@ inline Result extcodecopy(StackTop stack, int64_t gas_left, ExecutionState& stat if (state.rev >= EVMC_BERLIN && state.host.access_account(addr) == EVMC_ACCESS_COLD) { - if ((gas_left -= instr::additional_cold_account_access_cost) < 0) + if ((gas_left -= additional_cold_account_access(state.rev)) < 0) return {EVMC_OUT_OF_GAS, gas_left}; } - if (s > 0) - { - const auto src = - (max_buffer_size < input_index) ? max_buffer_size : static_cast(input_index); - const auto dst = static_cast(mem_index); - const auto num_bytes_copied = state.host.copy_code(addr, src, &state.memory[dst], s); - if (const auto num_bytes_to_clear = s - num_bytes_copied; num_bytes_to_clear > 0) - std::memset(&state.memory[dst + num_bytes_copied], 0, num_bytes_to_clear); - } + // The account is read even for a 0-byte copy, whose memory offset may be out of bounds. + const auto src = + (max_buffer_size < input_index) ? max_buffer_size : static_cast(input_index); + const auto dst = static_cast(mem_index); + const auto num_bytes_copied = + state.host.copy_code(addr, src, s != 0 ? &state.memory[dst] : nullptr, s); + assert(num_bytes_copied <= s); + if (const auto num_bytes_to_clear = s - num_bytes_copied; num_bytes_to_clear > 0) + std::memset(&state.memory[dst + num_bytes_copied], 0, num_bytes_to_clear); return {EVMC_SUCCESS, gas_left}; } @@ -661,7 +662,7 @@ inline Result extcodehash(StackTop stack, int64_t gas_left, ExecutionState& stat if (state.rev >= EVMC_BERLIN && state.host.access_account(addr) == EVMC_ACCESS_COLD) { - if ((gas_left -= instr::additional_cold_account_access_cost) < 0) + if ((gas_left -= additional_cold_account_access(state.rev)) < 0) return {EVMC_OUT_OF_GAS, gas_left}; } @@ -814,7 +815,7 @@ inline void tload(StackTop stack, ExecutionState& state) noexcept inline Result tstore(StackTop stack, int64_t gas_left, ExecutionState& state) noexcept { if (state.in_static_mode()) - return {EVMC_STATIC_MODE_VIOLATION, 0}; + return {EVMC_STATIC_MODE_VIOLATION, gas_left}; const auto key = intx::be::store(stack.pop()); const auto value = intx::be::store(stack.pop()); @@ -1003,7 +1004,7 @@ inline Result log(StackTop stack, int64_t gas_left, ExecutionState& state) noexc static_assert(NumTopics <= 4); if (state.in_static_mode()) - return {EVMC_STATIC_MODE_VIOLATION, 0}; + return {EVMC_STATIC_MODE_VIOLATION, gas_left}; const auto& offset = stack.pop(); const auto& size = stack.pop(); @@ -1069,7 +1070,7 @@ inline TermResult selfdestruct(StackTop stack, int64_t gas_left, ExecutionState& if (state.rev >= EVMC_BERLIN && state.host.access_account(beneficiary) == EVMC_ACCESS_COLD) { - if ((gas_left -= instr::cold_account_access_cost) < 0) + if ((gas_left -= cold_account_access(state.rev)) < 0) return {EVMC_OUT_OF_GAS, gas_left}; } @@ -1081,17 +1082,29 @@ inline TermResult selfdestruct(StackTop stack, int64_t gas_left, ExecutionState& // sending value to a non-existing account. if (!state.host.account_exists(beneficiary)) { - if ((gas_left -= 25000) < 0) - return {EVMC_OUT_OF_GAS, gas_left}; + if (state.rev >= EVMC_AMSTERDAM) + { + // Balance update costs ACCOUNT_WRITE, charged first so in case of OOG + // the state-gas is not consumed. + if ((gas_left -= ACCOUNT_WRITE) < 0) + return {EVMC_OUT_OF_GAS, gas_left}; + if (!state.state_gas.charge(gas_left, NEW_ACCOUNT_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + } + else + { + if ((gas_left -= 25000) < 0) + return {EVMC_OUT_OF_GAS, gas_left}; + } } } } - if (state.host.selfdestruct(state.msg->recipient, beneficiary)) - { - if (state.rev < EVMC_LONDON) - state.gas_refund += 24000; - } + const auto first_time = state.host.selfdestruct(state.msg->recipient, beneficiary); + + if (first_time && state.rev < EVMC_LONDON) + state.gas_refund += 24000; + return {EVMC_SUCCESS, gas_left}; } diff --git a/evmone/lib/evmone/instructions_calls.cpp b/evmone/lib/evmone/instructions_calls.cpp index 13d6fbd..dbd630d 100644 --- a/evmone/lib/evmone/instructions_calls.cpp +++ b/evmone/lib/evmone/instructions_calls.cpp @@ -8,13 +8,14 @@ #include "instructions.hpp" #include -constexpr int64_t CALL_VALUE_COST = 9000; -constexpr int64_t ACCOUNT_CREATION_COST = 25000; - namespace evmone::instr::core { namespace { +constexpr auto CALL_VALUE_COST = 9000; +constexpr auto CALL_VALUE_COST_AMSTERDAM = ACCOUNT_WRITE + CALL_STIPEND; +constexpr auto ACCOUNT_CREATION_COST = 25000; + /// Get target address of a code executing instruction. /// /// Returns EIP-7702 delegate address if addr is delegated, or addr itself otherwise. @@ -31,14 +32,44 @@ inline std::variant get_target_address( const auto delegate_account_access_cost = (state.host.access_account(*delegate_addr) == EVMC_ACCESS_COLD ? - instr::cold_account_access_cost : - instr::warm_storage_read_cost); + cold_account_access(state.rev) : + WARM_ACCESS); if ((gas_left -= delegate_account_access_cost) < 0) return Result{EVMC_OUT_OF_GAS, gas_left}; + // From Amsterdam the delegate is read even if the call then fails on the balance (EIP-7928). + // TODO: Avoid looking the delegate up a second time, e.g. read it only when the call fails + // before loading its code, or extend EVMC to read the account warmed by access_account(). + if (state.rev >= EVMC_AMSTERDAM) + (void)state.host.account_exists(*delegate_addr); + return *delegate_addr; } + +/// Absorbs a child's state-gas back to the parent (EIP-8037). +inline void absorb_child_state_gas( + int64_t& gas_left, ExecutionState& state, const evmc::Result& result) noexcept +{ + assert(result.state_gas.left >= 0); + assert(result.state_gas.spilled >= 0); + + // At most one of the two pools is ever non-empty. + assert(state.state_gas.left == 0 || state.state_gas.spilled == 0); + assert(result.state_gas.left == 0 || result.state_gas.spilled == 0); + + // In a non-successful result, all is returned back. + assert(result.status_code == EVMC_SUCCESS || + (result.state_gas.left == state.state_gas.left && result.state_gas.spilled == 0)); + + // Accumulate the spilled state-gas. + state.state_gas.spilled += result.state_gas.spilled; + + // Rebalance the state-gas refills: the caller must move callee's refills to gas_left up to the + // caller's spilled counter. Do this by refilling all returned state-gas to zeroed `left`. + state.state_gas.left = 0; + state.state_gas.refill(gas_left, result.state_gas.left); +} } // namespace /// Converts an opcode to matching EVMC call kind. @@ -73,7 +104,7 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce const auto gas = stack.pop(); const auto dst = intx::be::trunc(stack.pop()); - const auto value = (!HAS_VALUE_ARG) ? 0 : stack.pop(); + const auto value = HAS_VALUE_ARG ? stack.pop() : 0; const auto has_value = value != 0; const auto input_offset_u256 = stack.pop(); const auto input_size_u256 = stack.pop(); @@ -83,18 +114,13 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce stack.push(0); // Assume failure. state.return_data.clear(); - if (state.rev >= EVMC_BERLIN && state.host.access_account(dst) == EVMC_ACCESS_COLD) + if constexpr (Op == OP_CALL) { - if ((gas_left -= instr::additional_cold_account_access_cost) < 0) - return {EVMC_OUT_OF_GAS, gas_left}; + // TODO: gas_left is used as no-op and ignored by caller. Refactor this. + if (has_value && state.in_static_mode()) + return {EVMC_STATIC_MODE_VIOLATION, gas_left}; } - const auto target_addr_or_result = get_target_address(dst, gas_left, state); - if (const auto* result = std::get_if(&target_addr_or_result)) - return *result; - - const auto& code_addr = std::get(target_addr_or_result); - if (!check_memory(gas_left, state.memory, input_offset_u256, input_size_u256)) return {EVMC_OUT_OF_GAS, gas_left}; @@ -106,6 +132,42 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce const auto output_offset = static_cast(output_offset_u256); const auto output_size = static_cast(output_size_u256); + if constexpr (HAS_VALUE_ARG) + { + const auto call_value_cost = + state.rev >= EVMC_AMSTERDAM ? CALL_VALUE_COST_AMSTERDAM : CALL_VALUE_COST; + if (has_value && (gas_left -= call_value_cost) < 0) + return {EVMC_OUT_OF_GAS, gas_left}; + } + + if (state.rev >= EVMC_BERLIN && state.host.access_account(dst) == EVMC_ACCESS_COLD) + { + if ((gas_left -= additional_cold_account_access(state.rev)) < 0) + return {EVMC_OUT_OF_GAS, gas_left}; + } + + const auto target_addr_or_result = get_target_address(dst, gas_left, state); + if (const auto* result = std::get_if(&target_addr_or_result)) + return *result; + + const auto& code_addr = std::get(target_addr_or_result); + + bool new_account_charged = false; // NOLINT(*-const-correctness) + if constexpr (Op == OP_CALL) + { + if ((has_value || state.rev < EVMC_SPURIOUS_DRAGON) && !state.host.account_exists(dst)) + { + if (state.rev >= EVMC_AMSTERDAM) + { + if (!state.state_gas.charge(gas_left, NEW_ACCOUNT_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + new_account_charged = true; + } + else if ((gas_left -= ACCOUNT_CREATION_COST) < 0) + return {EVMC_OUT_OF_GAS, gas_left}; + } + } + evmc_message msg{.kind = to_call_kind(Op)}; msg.flags = (Op == OP_STATICCALL) ? uint32_t{EVMC_STATIC} : state.msg->flags; if (dst != code_addr) @@ -113,6 +175,7 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce else msg.flags &= ~std::underlying_type_t{EVMC_DELEGATED}; msg.depth = state.msg->depth + 1; + msg.state_gas = state.state_gas.left; msg.recipient = (Op == OP_CALL || Op == OP_STATICCALL) ? dst : state.msg->recipient; msg.code_address = code_addr; msg.sender = (Op == OP_DELEGATECALL) ? state.msg->sender : state.msg->recipient; @@ -126,23 +189,6 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce msg.input_size = input_size; } - if constexpr (HAS_VALUE_ARG) - { - auto cost = has_value ? CALL_VALUE_COST : 0; - - if constexpr (Op == OP_CALL) - { - if (has_value && state.in_static_mode()) - return {EVMC_STATIC_MODE_VIOLATION, gas_left}; - - if ((has_value || state.rev < EVMC_SPURIOUS_DRAGON) && !state.host.account_exists(dst)) - cost += ACCOUNT_CREATION_COST; - } - - if ((gas_left -= cost) < 0) - return {EVMC_OUT_OF_GAS, gas_left}; - } - msg.gas = std::numeric_limits::max(); if (gas < msg.gas) msg.gas = static_cast(gas); @@ -166,11 +212,15 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce msg.gas += CALL_STIPEND; gas_left += CALL_STIPEND; if (intx::be::load(state.host.get_balance(state.msg->recipient)) < value) + { + if (new_account_charged) + state.state_gas.refill(gas_left, NEW_ACCOUNT_STATE_GAS); return {EVMC_SUCCESS, gas_left}; // "Light" failure. + } } } - if (state.msg->depth >= 1024) + if (state.rev < EVMC_OSAKA && state.msg->depth >= 1024) return {EVMC_SUCCESS, gas_left}; // "Light" failure. const auto result = state.host.call(msg); @@ -183,6 +233,14 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce const auto gas_used = msg.gas - result.gas_left; gas_left -= gas_used; state.gas_refund += result.gas_refund; + absorb_child_state_gas(gas_left, state, result); + + if constexpr (Op == OP_CALL) + { + if (new_account_charged && result.status_code != EVMC_SUCCESS) + state.state_gas.refill(gas_left, NEW_ACCOUNT_STATE_GAS); + } + return {EVMC_SUCCESS, gas_left}; } @@ -227,7 +285,7 @@ Result create_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noex if ((gas_left -= init_code_cost) < 0) return {EVMC_OUT_OF_GAS, gas_left}; - if (state.msg->depth >= 1024) + if (state.rev < EVMC_OSAKA && state.msg->depth >= 1024) return {EVMC_SUCCESS, gas_left}; // "Light" failure. if (endowment != 0 && @@ -252,10 +310,19 @@ Result create_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noex if (state.rev >= EVMC_BERLIN) state.host.access_account(msg.recipient); + bool new_account_charged = false; + if (state.rev >= EVMC_AMSTERDAM && !state.host.account_exists(msg.recipient)) + { + if (!state.state_gas.charge(gas_left, NEW_ACCOUNT_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + new_account_charged = true; + } + msg.gas = gas_left; if (state.rev >= EVMC_TANGERINE_WHISTLE) msg.gas -= msg.gas / 64; + msg.state_gas = state.state_gas.left; msg.input_data = init_code.data(); msg.input_size = init_code.size(); msg.sender = sender; @@ -265,6 +332,9 @@ Result create_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noex const auto result = state.host.call(msg); gas_left -= msg.gas - result.gas_left; state.gas_refund += result.gas_refund; + absorb_child_state_gas(gas_left, state, result); + if (new_account_charged && result.status_code != EVMC_SUCCESS) + state.state_gas.refill(gas_left, NEW_ACCOUNT_STATE_GAS); state.return_data.assign(result.output_data, result.output_size); if (result.status_code == EVMC_SUCCESS) diff --git a/evmone/lib/evmone/instructions_storage.cpp b/evmone/lib/evmone/instructions_storage.cpp index 58d91de..c831547 100644 --- a/evmone/lib/evmone/instructions_storage.cpp +++ b/evmone/lib/evmone/instructions_storage.cpp @@ -17,32 +17,39 @@ struct StorageCostSpec int16_t set; ///< Storage addition cost, YP: G_{sset} int16_t reset; ///< Storage modification cost, YP: G_{sreset} int16_t clear; ///< Storage deletion refund, YP: R_{sclear} + int16_t cold; ///< Additional cold access cost (EIP-2929). }; /// Table of gas cost specification for storage instructions per EVM revision. /// TODO: This can be moved to instruction traits and be used in other places: e.g. -/// SLOAD cost, replacement for warm_storage_read_cost. -constexpr auto storage_cost_spec = []() noexcept { +/// SLOAD cost, replacement for WARM_ACCESS. +constexpr auto STORAGE_COST_SPEC = []() noexcept { std::array tbl{}; // Legacy cost schedule. - for (auto rev : {EVMC_FRONTIER, EVMC_HOMESTEAD, EVMC_TANGERINE_WHISTLE, EVMC_SPURIOUS_DRAGON, - EVMC_BYZANTIUM, EVMC_PETERSBURG}) - tbl[rev] = {false, 200, 20000, 5000, 15000}; + for (const auto rev : {EVMC_FRONTIER, EVMC_HOMESTEAD, EVMC_TANGERINE_WHISTLE, + EVMC_SPURIOUS_DRAGON, EVMC_BYZANTIUM, EVMC_PETERSBURG}) + tbl[rev] = {false, 200, 20000, 5000, 15000, 0}; // Net cost schedule. - tbl[EVMC_ISTANBUL] = {true, 800, 20000, 5000, 15000}; + tbl[EVMC_ISTANBUL] = {true, 800, 20000, 5000, 15000, 0}; tbl[EVMC_BERLIN] = { - true, instr::warm_storage_read_cost, 20000, 5000 - instr::cold_sload_cost, 15000}; + true, WARM_ACCESS, 20000, 5000 - COLD_STORAGE_ACCESS, 15000, COLD_STORAGE_ACCESS}; tbl[EVMC_LONDON] = { - true, instr::warm_storage_read_cost, 20000, 5000 - instr::cold_sload_cost, 4800}; + true, WARM_ACCESS, 20000, 5000 - COLD_STORAGE_ACCESS, 4800, COLD_STORAGE_ACCESS}; tbl[EVMC_PARIS] = tbl[EVMC_LONDON]; tbl[EVMC_SHANGHAI] = tbl[EVMC_LONDON]; tbl[EVMC_CANCUN] = tbl[EVMC_LONDON]; tbl[EVMC_PRAGUE] = tbl[EVMC_LONDON]; tbl[EVMC_OSAKA] = tbl[EVMC_LONDON]; + tbl[EVMC_AMSTERDAM] = tbl[EVMC_LONDON]; - tbl[EVMC_EXPERIMENTAL] = tbl[EVMC_LONDON]; + tbl[EVMC_AMSTERDAM].set = WARM_ACCESS + STORAGE_WRITE; + tbl[EVMC_AMSTERDAM].reset = tbl[EVMC_AMSTERDAM].set; + tbl[EVMC_AMSTERDAM].clear = (STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800 / 5000; + tbl[EVMC_AMSTERDAM].cold = ADDITIONAL_COLD_STORAGE_ACCESS; + + tbl[EVMC_EXPERIMENTAL] = tbl[EVMC_AMSTERDAM]; return tbl; }(); @@ -54,7 +61,7 @@ struct StorageStoreCost }; // The lookup table of SSTORE costs by the storage update status. -constexpr auto sstore_costs = []() noexcept { +constexpr auto SSTORE_COSTS = []() noexcept { std::array, EVMC_MAX_REVISION + 1> tbl{}; @@ -62,7 +69,7 @@ constexpr auto sstore_costs = []() noexcept { for (size_t rev = EVMC_FRONTIER; rev <= EVMC_MAX_REVISION; ++rev) { auto& e = tbl[rev]; - if (const auto c = storage_cost_spec[rev]; !c.net_cost) // legacy + if (const auto c = STORAGE_COST_SPEC[rev]; !c.net_cost) // legacy { e[EVMC_STORAGE_ADDED] = {c.set, 0}; e[EVMC_STORAGE_DELETED] = {c.reset, c.clear}; @@ -105,9 +112,7 @@ Result sload(StackTop stack, int64_t gas_left, ExecutionState& state) noexcept { // The warm storage access cost is already applied (from the cost table). // Here we need to apply additional cold storage access cost. - constexpr auto additional_cold_sload_cost = - instr::cold_sload_cost - instr::warm_storage_read_cost; - if ((gas_left -= additional_cold_sload_cost) < 0) + if ((gas_left -= ADDITIONAL_COLD_STORAGE_ACCESS) < 0) return {EVMC_OUT_OF_GAS, gas_left}; } @@ -130,14 +135,26 @@ Result sstore(StackTop stack, int64_t gas_left, ExecutionState& state) noexcept const auto gas_cost_cold = (state.rev >= EVMC_BERLIN && state.host.access_storage(state.msg->recipient, key) == EVMC_ACCESS_COLD) ? - instr::cold_sload_cost : + STORAGE_COST_SPEC[state.rev].cold : 0; const auto status = state.host.set_storage(state.msg->recipient, key, value); - const auto [gas_cost_warm, gas_refund] = sstore_costs[state.rev][status]; + const auto [gas_cost_warm, gas_refund] = SSTORE_COSTS[state.rev][status]; const auto gas_cost = gas_cost_warm + gas_cost_cold; if ((gas_left -= gas_cost) < 0) return {EVMC_OUT_OF_GAS, gas_left}; + + if (state.rev >= EVMC_AMSTERDAM) + { + // The refill part can be done here because gas_left check always succeeds in this case. + static_assert(COLD_STORAGE_ACCESS + WARM_ACCESS <= CALL_STIPEND); + if (status == EVMC_STORAGE_ADDED_DELETED) + state.state_gas.refill(gas_left, STORAGE_SET_STATE_GAS); + else if (status == EVMC_STORAGE_ADDED && + !state.state_gas.charge(gas_left, STORAGE_SET_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + } + state.gas_refund += gas_refund; return {EVMC_SUCCESS, gas_left}; } diff --git a/evmone/lib/evmone/instructions_traits.hpp b/evmone/lib/evmone/instructions_traits.hpp index d42429f..86733aa 100644 --- a/evmone/lib/evmone/instructions_traits.hpp +++ b/evmone/lib/evmone/instructions_traits.hpp @@ -13,18 +13,33 @@ namespace evmone::instr /// The special gas cost value marking an EVM instruction as "undefined". constexpr int16_t undefined = -1; -/// EIP-2929 constants (https://eips.ethereum.org/EIPS/eip-2929). +/// State-access cost constants (EIP-2929, EIP-8038). /// @{ -inline constexpr auto cold_sload_cost = 2100; -inline constexpr auto cold_account_access_cost = 2600; -inline constexpr auto warm_storage_read_cost = 100; +inline constexpr auto WARM_ACCESS = 100; +inline constexpr auto COLD_STORAGE_ACCESS = 2100; +inline constexpr auto COLD_ACCOUNT_ACCESS = 2600; +inline constexpr auto COLD_ACCOUNT_ACCESS_AMSTERDAM = 3000; +inline constexpr auto ACCOUNT_WRITE = 9000; +inline constexpr auto STORAGE_WRITE = 10000; +inline constexpr auto CREATE_ACCESS = ACCOUNT_WRITE + COLD_ACCOUNT_ACCESS_AMSTERDAM; + +/// The full cold-account-access cost for the given revision. +constexpr auto cold_account_access(evmc_revision rev) noexcept +{ + return rev >= EVMC_AMSTERDAM ? COLD_ACCOUNT_ACCESS_AMSTERDAM : COLD_ACCOUNT_ACCESS; +} -/// Additional cold account access cost. +/// Additional cold account access cost over the unconditionally-charged warm cost. /// -/// The warm access cost is unconditionally applied for every account access instruction. +/// The warm access cost is part of the base cost of every account access instruction. /// If the access turns out to be cold, this cost must be applied additionally. -inline constexpr auto additional_cold_account_access_cost = - cold_account_access_cost - warm_storage_read_cost; +constexpr auto additional_cold_account_access(evmc_revision rev) noexcept +{ + return cold_account_access(rev) - WARM_ACCESS; +} + +/// Additional cold storage access cost over the unconditionally-charged warm cost. +inline constexpr auto ADDITIONAL_COLD_STORAGE_ACCESS = COLD_STORAGE_ACCESS - WARM_ACCESS; /// @} @@ -144,15 +159,15 @@ constexpr inline GasCostTable gas_costs = []() noexcept { table[EVMC_ISTANBUL][OP_SLOAD] = 800; table[EVMC_BERLIN] = table[EVMC_ISTANBUL]; - table[EVMC_BERLIN][OP_EXTCODESIZE] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_EXTCODECOPY] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_EXTCODEHASH] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_BALANCE] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_CALL] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_CALLCODE] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_DELEGATECALL] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_STATICCALL] = warm_storage_read_cost; - table[EVMC_BERLIN][OP_SLOAD] = warm_storage_read_cost; + table[EVMC_BERLIN][OP_EXTCODESIZE] = WARM_ACCESS; + table[EVMC_BERLIN][OP_EXTCODECOPY] = WARM_ACCESS; + table[EVMC_BERLIN][OP_EXTCODEHASH] = WARM_ACCESS; + table[EVMC_BERLIN][OP_BALANCE] = WARM_ACCESS; + table[EVMC_BERLIN][OP_CALL] = WARM_ACCESS; + table[EVMC_BERLIN][OP_CALLCODE] = WARM_ACCESS; + table[EVMC_BERLIN][OP_DELEGATECALL] = WARM_ACCESS; + table[EVMC_BERLIN][OP_STATICCALL] = WARM_ACCESS; + table[EVMC_BERLIN][OP_SLOAD] = WARM_ACCESS; table[EVMC_LONDON] = table[EVMC_BERLIN]; table[EVMC_LONDON][OP_BASEFEE] = 2; @@ -165,8 +180,8 @@ constexpr inline GasCostTable gas_costs = []() noexcept { table[EVMC_CANCUN] = table[EVMC_SHANGHAI]; table[EVMC_CANCUN][OP_BLOBHASH] = 3; table[EVMC_CANCUN][OP_BLOBBASEFEE] = 2; - table[EVMC_CANCUN][OP_TLOAD] = warm_storage_read_cost; - table[EVMC_CANCUN][OP_TSTORE] = warm_storage_read_cost; + table[EVMC_CANCUN][OP_TLOAD] = WARM_ACCESS; + table[EVMC_CANCUN][OP_TSTORE] = WARM_ACCESS; table[EVMC_CANCUN][OP_MCOPY] = 3; table[EVMC_PRAGUE] = table[EVMC_CANCUN]; @@ -179,6 +194,10 @@ constexpr inline GasCostTable gas_costs = []() noexcept { table[EVMC_AMSTERDAM][OP_DUPN] = 3; table[EVMC_AMSTERDAM][OP_SWAPN] = 3; table[EVMC_AMSTERDAM][OP_EXCHANGE] = 3; + table[EVMC_AMSTERDAM][OP_CREATE] = CREATE_ACCESS; + table[EVMC_AMSTERDAM][OP_CREATE2] = CREATE_ACCESS; + table[EVMC_AMSTERDAM][OP_EXTCODESIZE] = 2 * WARM_ACCESS; + table[EVMC_AMSTERDAM][OP_EXTCODECOPY] = 2 * WARM_ACCESS; table[EVMC_EXPERIMENTAL] = table[EVMC_AMSTERDAM]; diff --git a/evmone/lib/evmone/lru_cache.hpp b/evmone/lib/evmone/lru_cache.hpp index dc3e83d..92fc27d 100644 --- a/evmone/lib/evmone/lru_cache.hpp +++ b/evmone/lib/evmone/lru_cache.hpp @@ -151,9 +151,7 @@ class LRUCache // After successful insertion also create the LRU list entry and connect it with // the map entry. This reference is valid and unchanged through // the whole cache lifetime. - // TODO(clang): no matching constructor for initialization of 'LRUEntry' - it->second = - lru_list_.emplace(lru_list_.end(), LRUEntry{it->first, std::move(value)}); + it->second = lru_list_.emplace(lru_list_.end(), it->first, std::move(value)); } } } diff --git a/evmone/lib/evmone/state_gas.hpp b/evmone/lib/evmone/state_gas.hpp new file mode 100644 index 0000000..ac7488f --- /dev/null +++ b/evmone/lib/evmone/state_gas.hpp @@ -0,0 +1,48 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include +#include +#include + +namespace evmone +{ +/// A frame's state-gas counters with the charging rules on top (EIP-8037). +struct StateGas : evmc::StateGas +{ + /// Charges `cost`, first from `left`, then from `gas_left` (recorded in `spilled`). + [[nodiscard]] bool charge(int64_t& gas_left, int64_t cost) noexcept + { + assert(cost >= 0); // 0 charge happens in code deployment. + assert(gas_left >= 0); + if (left >= cost) + { + left -= cost; + return true; + } + const auto spill = cost - left; + if (gas_left < spill) + return false; + gas_left -= spill; + spilled += spill; + left = 0; + return true; + } + + /// Refund state-gas. + /// + /// Give the `cost` to `gas_left` (up to `spilled`) and `left` (whatever remains). + void refill(int64_t& gas_left, int64_t cost) noexcept + { + assert(cost >= 0); // 0 refill happens in absorb. + const auto to_gas_left = std::min(cost, spilled); + gas_left += to_gas_left; + spilled -= to_gas_left; + left += cost - to_gas_left; + } +}; +} // namespace evmone diff --git a/evmone/lib/evmone_precompiles/CMakeLists.txt b/evmone/lib/evmone_precompiles/CMakeLists.txt index 2d40fea..ab114da 100644 --- a/evmone/lib/evmone_precompiles/CMakeLists.txt +++ b/evmone/lib/evmone_precompiles/CMakeLists.txt @@ -6,7 +6,7 @@ include(blst) add_library(evmone_precompiles STATIC) add_library(evmone::precompiles ALIAS evmone_precompiles) -target_link_libraries(evmone_precompiles PUBLIC evmc::evmc_cpp PRIVATE evmone::evmmax blst::blst) +target_link_libraries(evmone_precompiles PUBLIC evmc::evmc_cpp intx::intx PRIVATE blst::blst) target_include_directories(evmone_precompiles INTERFACE ..) target_sources( evmone_precompiles PRIVATE @@ -25,6 +25,7 @@ target_sources( pairing/bn254/pairing.cpp pairing/bn254/utils.hpp pairing/field_template.hpp + modarith.hpp modexp.hpp modexp.cpp mulmod.hpp diff --git a/evmone/lib/evmone_precompiles/bn254.cpp b/evmone/lib/evmone_precompiles/bn254.cpp index 7c65617..3affffb 100644 --- a/evmone/lib/evmone_precompiles/bn254.cpp +++ b/evmone/lib/evmone_precompiles/bn254.cpp @@ -4,7 +4,7 @@ #include "bn254.hpp" -namespace evmmax::bn254 +namespace evmone::crypto::bn254 { static_assert(AffinePoint{} == 0, "default constructed is the point at infinity"); @@ -33,4 +33,4 @@ AffinePoint mul(const AffinePoint& pt, const uint256& c) noexcept const auto pr = msm(k1.value, p, k2.value, q); return ecc::to_affine(pr); } -} // namespace evmmax::bn254 +} // namespace evmone::crypto::bn254 diff --git a/evmone/lib/evmone_precompiles/bn254.hpp b/evmone/lib/evmone_precompiles/bn254.hpp index 4d9fa95..11f3302 100644 --- a/evmone/lib/evmone_precompiles/bn254.hpp +++ b/evmone/lib/evmone_precompiles/bn254.hpp @@ -9,7 +9,7 @@ #include #include -namespace evmmax::bn254 +namespace evmone::crypto::bn254 { using namespace intx; @@ -98,4 +98,4 @@ AffinePoint mul(const AffinePoint& pt, const uint256& c) noexcept; /// std::nullopt on error. std::optional pairing_check(std::span> pairs) noexcept; -} // namespace evmmax::bn254 +} // namespace evmone::crypto::bn254 diff --git a/evmone/lib/evmone_precompiles/ecc.hpp b/evmone/lib/evmone_precompiles/ecc.hpp index bf95cc9..648e080 100644 --- a/evmone/lib/evmone_precompiles/ecc.hpp +++ b/evmone/lib/evmone_precompiles/ecc.hpp @@ -3,11 +3,11 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include +#include "modarith.hpp" #include #include -namespace evmmax::ecc +namespace evmone::crypto::ecc { template struct Constant : std::integral_constant @@ -614,4 +614,4 @@ std::array, 2> decompose( return {k1, k2}; } -} // namespace evmmax::ecc +} // namespace evmone::crypto::ecc diff --git a/evmone/include/evmmax/evmmax.hpp b/evmone/lib/evmone_precompiles/modarith.hpp similarity index 56% rename from evmone/include/evmmax/evmmax.hpp rename to evmone/lib/evmone_precompiles/modarith.hpp index 9658100..3a32afc 100644 --- a/evmone/include/evmmax/evmmax.hpp +++ b/evmone/lib/evmone_precompiles/modarith.hpp @@ -6,10 +6,11 @@ #include #include -namespace evmmax +namespace evmone::crypto { -/// Compute the modular inverse of the number modulo 2³²: inv⋅a = 1 mod 2³². -constexpr uint32_t modinv(uint32_t a) noexcept +/// Computes the modular inverse of a modulo 2³², satisfying a⋅inv = 1 mod 2³². +/// Only odd arguments are invertible modulo a power of two. +constexpr uint32_t modinv_pow2(uint32_t a) noexcept { assert(a % 2 == 1); // The argument must be odd, otherwise the inverse does not exist. @@ -33,11 +34,12 @@ constexpr uint32_t modinv(uint32_t a) noexcept return inv; } -/// Compute the modular inverse of the number modulo 2⁶⁴: inv⋅a = 1 mod 2⁶⁴. -constexpr uint64_t modinv(uint64_t a) noexcept +/// Computes the modular inverse of a modulo 2⁶⁴, satisfying a⋅inv = 1 mod 2⁶⁴. +/// Only odd arguments are invertible modulo a power of two. +constexpr uint64_t modinv_pow2(uint64_t a) noexcept { assert(a % 2 == 1); // The argument must be odd, otherwise the inverse does not exist. - uint64_t inv = modinv(static_cast(a)); // Start with inversion mod 2³². + uint64_t inv = modinv_pow2(static_cast(a)); // Start with inversion mod 2³². inv *= 2 - a * inv; // One Newton-Raphson iteration: 64 bits correct. assert(inv * a == 1); // Verify the result. return inv; @@ -49,7 +51,7 @@ constexpr uint64_t compute_mont_mod_inv(const UintT& mod) noexcept { // Compute the inversion mod[0]⁻¹ mod 2⁶⁴, then the final result is N' = -mod[0]⁻¹ // because this gives mod⋅N' = -1 mod 2⁶⁴ = 2⁶⁴-1. - return -modinv(mod[0]); + return -modinv_pow2(mod[0]); } constexpr std::pair addmul( @@ -59,7 +61,97 @@ constexpr std::pair addmul( return {p[1], p[0]}; } -/// The modular arithmetic operations for EVMMAX (EVM Modular Arithmetic Extensions). +/// Performs a modular addition. Requires x < mod and y < mod. +template +constexpr UintT modadd(const UintT& x, const UintT& y, const UintT& mod) noexcept +{ + assert(x < mod); + assert(y < mod); + const auto s = addc(x, y); // TODO: cannot overflow if modulus is sparse (e.g. 255 bits). + const auto d = subc(s.value, mod); + return (!s.carry && d.carry) ? s.value : d.value; +} + +/// Performs a modular subtraction. Requires x < mod and y < mod. +template +constexpr UintT modsub(const UintT& x, const UintT& y, const UintT& mod) noexcept +{ + assert(x < mod); + assert(y < mod); + const auto d = subc(x, y); + const auto s = d.value + mod; + return (d.carry) ? s : d.value; +} + +/// Computes scale⋅x⁻¹ % mod, i.e. the modular inverse of x scaled by the given factor. +/// Returns 0 for non-invertible x (including x == 0). +/// Requires an odd mod not less than 3, and both x and scale less than mod. +/// +/// The scale is the initial value of the Bézout coefficient u and the algorithm is linear in it, +/// so any factor can be folded into the result for free. +template +constexpr UintT modinv_scaled(const UintT& x, const UintT& scale, const UintT& mod) noexcept +{ + assert((mod & 1) == 1); + assert(mod >= 3); + assert(x < mod); + assert(scale < mod); // Otherwise the first modsub() below gets a non-reduced operand. + + // Precompute inverse of 2 modulo mod: inv2 * 2 % mod == 1. + // The 1/2 is inexact division that can be fixed by adding "0" to the numerator + // and making it even: (mod + 1) / 2. To avoid potential overflow of (1 + mod) + // we rewrite it further to (mod - 1 + 2) / 2 = (mod - 1) / 2 + 1 = ⌊mod / 2⌋ + 1. + const auto inv2 = (mod >> 1) + 1; + + // Use extended binary Euclidean algorithm. This evolves variables a and b until a is 0. + // Then GCD(x, mod) is in b. If GCD(x, mod) == 1 then the inversion exists and is in v. + // This follows the classic algorithm (Algorithm 1) presented in + // "Optimized Binary GCD for Modular Inversion". + // https://eprint.iacr.org/2020/972.pdf#algorithm.1 + // TODO: The same paper has additional optimizations that could be applied. + UintT a = x; + UintT b = mod; + UintT u = scale; + UintT v = 0; + + while (a != 0) + { + if ((a & 1) != 0) + { + // if a is odd, update it to a - b. + if (const auto [d, less] = subc(a, b); less) + { + // swap a and b in case a < b. + b = a; + a = -d; + + using namespace std; + swap(u, v); + } + else + { + a = d; + } + u = modsub(u, v, mod); + } + + // Compute a / 2 % mod, a is even so division is exact and can be computed as ⌊a / 2⌋. + a >>= 1; + + // Compute u / 2 % mod. If u is even, this can be computed as ⌊u / 2⌋. + // Otherwise, (u - 1 + 1) / 2 = ⌊u / 2⌋ + (1 / 2 % mod). + const auto u_odd = (u & 1) != 0; + u >>= 1; + if (u_odd) + u += inv2; // if u is odd, add back ½ % mod. + } + + if (b != 1) [[unlikely]] + v = 0; // not invertible + return v; +} + +/// The modular arithmetic operations using the Montgomery form of the values. template class ModArith { @@ -140,88 +232,27 @@ class ModArith return static_cast(t); } - /// Performs a modular addition. It is required that x < mod and y < mod, but x and y may be - /// but are not required to be in Montgomery form. + /// Performs a modular addition. constexpr UintT add(const UintT& x, const UintT& y) const noexcept { - const auto s = addc(x, y); // TODO: cannot overflow if modulus is sparse (e.g. 255 bits). - const auto d = subc(s.value, mod_); - return (!s.carry && d.carry) ? s.value : d.value; + // Using generic procedure is fine for Montgomery forms. + return modadd(x, y, mod_); } - /// Performs a modular subtraction. It is required that x < mod and y < mod, but x and y may be - /// but are not required to be in Montgomery form. + /// Performs a modular subtraction. constexpr UintT sub(const UintT& x, const UintT& y) const noexcept { - const auto d = subc(x, y); - const auto s = d.value + mod_; - return (d.carry) ? s : d.value; + // Using generic procedure is fine for Montgomery forms. + return modsub(x, y, mod_); } /// Computes modular inverse of x in Montgomery form. Result is in Montgomery form. /// Returns 0 for non-invertible x (including x == 0). constexpr UintT inv(const UintT& x) const noexcept { - assert((mod_ & 1) == 1); - assert(mod_ >= 3); - - // Precompute inverse of 2 modulo mod: inv2 * 2 % mod == 1. - // The 1/2 is inexact division that can be fixed by adding "0" to the numerator - // and making it even: (mod + 1) / 2. To avoid potential overflow of (1 + mod) - // we rewrite it further to (mod - 1 + 2) / 2 = (mod - 1) / 2 + 1 = ⌊mod / 2⌋ + 1. - const auto inv2 = (mod_ >> 1) + 1; - - // Use extended binary Euclidean algorithm. This evolves variables a and b until a is 0. - // Then GCD(x, mod) is in b. If GCD(x, mod) == 1 then the inversion exists and is in v. - // This follows the classic algorithm (Algorithm 1) presented in - // "Optimized Binary GCD for Modular Inversion". - // https://eprint.iacr.org/2020/972.pdf#algorithm.1 - // TODO: The same paper has additional optimizations that could be applied. - UintT a = x; - UintT b = mod_; - - // Bézout's coefficients are originally initialized to 1 and 0. But because the input x - // is in Montgomery form XR the algorithm would compute X⁻¹R⁻¹. To get the expected X⁻¹R, - // we need to multiply the result by R². We can achieve the same effect "for free" - // by initializing u to R² instead of 1. - UintT u = r_squared_; - UintT v = 0; - - while (a != 0) - { - if ((a & 1) != 0) - { - // if a is odd, update it to a - b. - if (const auto [d, less] = subc(a, b); less) - { - // swap a and b in case a < b. - b = a; - a = -d; - - using namespace std; - swap(u, v); - } - else - { - a = d; - } - u = sub(u, v); - } - - // Compute a / 2 % mod, a is even so division is exact and can be computed as ⌊a / 2⌋. - a >>= 1; - - // Compute u / 2 % mod. If u is even, this can be computed as ⌊u / 2⌋. - // Otherwise, (u - 1 + 1) / 2 = ⌊u / 2⌋ + (1 / 2 % mod). - const auto u_odd = (u & 1) != 0; - u >>= 1; - if (u_odd) - u += inv2; // if u is odd, add back ½ % mod. - } - - if (b != 1) [[unlikely]] - v = 0; // not invertible - return v; + // The input XR would invert to X⁻¹R⁻¹, so scaling by R² gives the expected Montgomery + // form X⁻¹R at no extra cost. + return modinv_scaled(x, r_squared_, mod_); } }; -} // namespace evmmax +} // namespace evmone::crypto diff --git a/evmone/lib/evmone_precompiles/modexp.cpp b/evmone/lib/evmone_precompiles/modexp.cpp index d54b0a2..2d871fd 100644 --- a/evmone/lib/evmone_precompiles/modexp.cpp +++ b/evmone/lib/evmone_precompiles/modexp.cpp @@ -3,8 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 #include "modexp.hpp" +#include "modarith.hpp" #include "mulmod.hpp" -#include #include #include #include @@ -115,13 +115,6 @@ void store(std::span r, std::span words) noexcept std::ranges::fill(r.subspan(0, pos), uint8_t{0}); } -/// Compares two same-size little-endian word arrays as unsigned integers: returns true if x < y. -constexpr bool less(std::span x, std::span y) noexcept -{ - assert(x.size() == y.size()); - return std::ranges::lexicographical_compare(std::views::reverse(x), std::views::reverse(y)); -} - /// Right-shifts a little-endian word array by k bits. /// Returns a subspan trimmed to significant (non-zero) words. std::span shr( @@ -263,32 +256,31 @@ void rem(std::span r, std::span u, std::span bytes_; ///< Big-endian bytes with the leading zeros trimmed. public: explicit Exponent(std::span bytes) noexcept - { - const auto it = std::ranges::find_if(bytes, [](auto x) { return x != 0; }); - const auto trimmed_bytes = std::span{it, bytes.end()}; - bit_width_ = trimmed_bytes.empty() ? 0 : - static_cast(std::bit_width(trimmed_bytes[0])) + - (trimmed_bytes.size() - 1) * 8; - data_ = trimmed_bytes.data(); - } + : bytes_{std::ranges::find_if(bytes, [](auto x) { return x != 0; }), bytes.end()} + {} + /// Returns true for the exponent of zero. + [[nodiscard]] bool empty() const noexcept { return bytes_.empty(); } - [[nodiscard]] size_t bit_width() const noexcept { return bit_width_; } + /// The number of significant bits of the exponent. Must not be empty. + [[nodiscard]] size_t bit_width() const noexcept + { + assert(!empty()); + return static_cast(std::bit_width(bytes_.front())) + (bytes_.size() - 1) * 8; + } /// Returns the bit value of the exponent at the given index, counting from the least /// significant bit (e[0] is the bottom bit, e[bit_width() - 1] is the top bit, always set). - bool operator[](size_t index) const noexcept + [[nodiscard]] bool operator[](size_t index) const noexcept { // TODO: Replace this with a custom iterator type. - const auto exp_size = (bit_width_ + 7) / 8; const auto byte_index = index / 8; - const auto byte = data_[exp_size - 1 - byte_index]; const auto bit_index = index % 8; + const auto byte = bytes_[bytes_.size() - 1 - byte_index]; const auto bit = (byte >> bit_index) & 1; return bit != 0; } @@ -298,11 +290,10 @@ class Exponent [[nodiscard]] size_t window(size_t lo, size_t hi) const noexcept { assert(lo <= hi && hi - lo < 8); - const auto exp_size = (bit_width_ + 7) / 8; - const auto byte_index = exp_size - 1 - lo / 8; - auto bytes = size_t{data_[byte_index]}; + const auto byte_index = bytes_.size() - 1 - lo / 8; + auto bytes = size_t{bytes_[byte_index]}; if (byte_index != 0) // Prepend the next more significant byte if there is one. - bytes |= size_t{data_[byte_index - 1]} << 8; + bytes |= size_t{bytes_[byte_index - 1]} << 8; return (bytes >> (lo % 8)) & ((size_t{1} << (hi + 1 - lo)) - 1); } }; @@ -333,6 +324,7 @@ void mul_amm(std::span r, std::span x, assert(y.size() == n); assert(mod.size() == n); assert(mod.back() != 0); + assert(mod[0] * mod_inv + 1 == 0); // The negative modulus inverse identity. assert(r.data() != x.data() && r.data() != y.data()); // r must not alias inputs. const auto r_lo = r.subspan(0, n - 1); @@ -345,9 +337,10 @@ void mul_amm(std::span r, std::span x, const auto c1 = crypto::mul(r, x, y[0]); const auto m = r[0] * mod_inv; - const auto c2 = (umul(mod[0], m) + r[0])[1]; + const auto p = umul(mod[0], m) + r[0]; + assert(p[0] == 0); // The lowest word is canceled by m. - const auto c3 = addmul(r_lo, r_hi, mod_hi, m, c2); + const auto c3 = addmul(r_lo, r_hi, mod_hi, m, p[1]); std::tie(r[n - 1], r_carry) = intx::addc(c1, c3); } @@ -355,18 +348,16 @@ void mul_amm(std::span r, std::span x, for (size_t i = 1; i != n; ++i) { const auto c1 = addmul(r, r, x, y[i]); - const auto [sum1, d1] = intx::addc(c1, uint64_t{r_carry}); const auto m = r[0] * mod_inv; - const auto c2 = (umul(mod[0], m) + r[0])[1]; + const auto p = umul(mod[0], m) + r[0]; + assert(p[0] == 0); // The lowest word is canceled by m. - const auto c3 = addmul(r_lo, r_hi, mod_hi, m, c2); - const auto [sum2, d2] = intx::addc(sum1, c3); - r[n - 1] = sum2; - assert(!(d1 && d2)); - r_carry = d1 || d2; + const auto c3 = addmul(r_lo, r_hi, mod_hi, m, p[1]); + std::tie(r[n - 1], r_carry) = intx::addc(c1, c3, r_carry); } + assert(!r_carry || less(r, mod)); // r_carry => r < mod. if (r_carry) sub(r, mod); } @@ -413,10 +404,10 @@ void modexp_odd(std::span result, std::span base, Expo assert(!mod.empty() && mod.back() != 0); // mod must be trimmed. assert(!base.empty() && base.back() != 0); // base must be trimmed. assert(result.size() == mod.size()); - assert(exp.bit_width() != 0); + assert(!exp.empty()); const auto n = mod.size(); - const auto mod_inv = -evmmax::modinv(mod[0]); + const auto mod_inv = -modinv_pow2(mod[0]); const auto exp_bits = exp.bit_width(); const auto w = window_width(exp_bits); @@ -530,7 +521,7 @@ void modexp_pow2(std::span r, std::span base, Exponent std::span scratch) noexcept { assert(k != 0); // Modulus of 1 should be covered as "odd". - assert(exp.bit_width() != 0); // Exponent of zero must be handled outside. + assert(!exp.empty()); // Exponent of zero must be handled outside. assert(!base.empty() && base.back() != 0); // base must be trimmed. assert(r.data() != base.data()); // No in-place operation. @@ -543,10 +534,44 @@ void modexp_pow2(std::span r, std::span base, Exponent const auto base_k = base.subspan(0, std::min(base.size(), num_pow2_words)); + // The full exponent is not needed: base^exp % 2^k collapses to 0 for an even base + // and depends only on a bounded number of the low exponent bits for an odd one. + auto num_bits = exp.bit_width(); + if ((base[0] & 1) == 0) // base[0] is the least significant word, i.e. the base parity. + { + // For an even base the result is 0 once exp >= k. The bit width comparison is a + // sufficient condition: exp >= 2^(num_bits - 1) >= 2^bit_width(k) > k. + if (num_bits > static_cast(std::bit_width(k))) + { + std::ranges::fill(r_k, uint64_t{0}); + return; + } + } + else + { + // An odd base satisfies base^λ ≡ 1 (mod 2^k), so base^exp ≡ base^(exp mod λ), which + // is the low period_bits of the exponent. + // λ is the Carmichael function: λ(2ᵏ) = 2ᵏ⁻² if k > 2 else 2ᵏ⁻¹. + if (const auto period_bits = k > 2 ? k - 2 : k - 1; num_bits > period_bits) + { + // Trim the leading zero bits of the reduced exponent. + num_bits = period_bits; + while (num_bits != 0 && !exp[num_bits - 1]) + --num_bits; + + if (num_bits == 0) // The exponent is reduced to 0 and base^0 % 2^k == 1. + { + std::ranges::fill(r_k, uint64_t{0}); + r_k[0] = 1; + return; + } + } + } + const auto [_, pad] = std::ranges::copy(base_k, r_k.begin()); std::ranges::fill(std::span{pad, r_k.end()}, uint64_t{0}); - for (auto i = exp.bit_width() - 1; i != 0; --i) + for (auto i = num_bits - 1; i != 0; --i) { mul(tmp, r_k, r_k); std::swap(r_k, tmp); @@ -574,13 +599,13 @@ void modinv_pow2( assert(!r.empty()); assert(scratch.size() >= 2 * r.size()); - r[0] = evmmax::modinv(x[0]); // Good start: 64 correct bits. + r[0] = crypto::modinv_pow2(x[0]); // Good start: 64 correct bits. std::ranges::fill(r.subspan(1), uint64_t{0}); // Zero the rest for correct final subtraction. // Newton-Raphson iteration for modular inverse: inv' = inv * (2 - x * inv). // Rearranged as: inv' = 2 * inv - x * inv^2, which avoids the (2 - x) negation helper // and computes the result directly into r (no copy needed). - // Each iteration doubles the number of correct bits. See evmmax::modinv(). + // Each iteration doubles the number of correct bits. See modinv_pow2(). for (size_t i = 1; i < r.size(); i *= 2) { // We have i-word correct inverse in r[0..i). Double the precision to n = min(2i, r.size()). @@ -628,7 +653,7 @@ void modexp(std::span base_bytes, std::span exp_by const auto result = std::span{alloc.allocate(mod_size), mod_size}; std::ranges::fill(result, uint64_t{0}); - if (exp.bit_width() == 0) // Exponent is 0: + if (exp.empty()) // Exponent is 0: { // Result is 1 except when mod is 1. if (mod_tz != 0 || mod_odd.size() != 1 || mod_odd[0] != 1) // mod != 1 diff --git a/evmone/lib/evmone_precompiles/mulmod.cpp b/evmone/lib/evmone_precompiles/mulmod.cpp index 5f729b8..f0d9389 100644 --- a/evmone/lib/evmone_precompiles/mulmod.cpp +++ b/evmone/lib/evmone_precompiles/mulmod.cpp @@ -11,6 +11,8 @@ void mul_amm_256(std::span r, std::span x, { static constexpr size_t N = 4; + assert(mod[0] * mod_inv + 1 == 0); // The negative modulus inverse identity. + // Local accumulator t[] avoids aliasing penalties when r overlaps x or y. std::array t; // NOLINT(*-pro-type-member-init) const auto t_lo = std::span{t}.subspan<0, N - 1>(); @@ -23,9 +25,10 @@ void mul_amm_256(std::span r, std::span x, const auto c1 = mul(t, x, y[0]); const auto m = t[0] * mod_inv; - const auto c2 = (umul(mod[0], m) + t[0])[1]; + const auto p = umul(mod[0], m) + t[0]; + assert(p[0] == 0); // The lowest word is canceled by m. - const auto c3 = addmul(t_lo, t_hi, mod_hi, m, c2); + const auto c3 = addmul(t_lo, t_hi, mod_hi, m, p[1]); std::tie(t[N - 1], t_carry) = addc(c1, c3); } @@ -34,18 +37,16 @@ void mul_amm_256(std::span r, std::span x, for (size_t i = 1; i != N; ++i) { const auto c1 = addmul(t, t, x, y[i]); - const auto [sum1, d1] = addc(c1, uint64_t{t_carry}); const auto m = t[0] * mod_inv; - const auto c2 = (umul(mod[0], m) + t[0])[1]; + const auto p = umul(mod[0], m) + t[0]; + assert(p[0] == 0); // The lowest word is canceled by m. - const auto c3 = addmul(t_lo, t_hi, mod_hi, m, c2); - const auto [sum2, d2] = addc(sum1, c3); - t[N - 1] = sum2; - assert(!(d1 && d2)); - t_carry = d1 || d2; + const auto c3 = addmul(t_lo, t_hi, mod_hi, m, p[1]); + std::tie(t[N - 1], t_carry) = addc(c1, c3, t_carry); } + assert(!t_carry || less(t, mod)); // t_carry => t < mod. if (t_carry) sub(t, mod); diff --git a/evmone/lib/evmone_precompiles/mulmod.hpp b/evmone/lib/evmone_precompiles/mulmod.hpp index cf9fa65..3f7d87b 100644 --- a/evmone/lib/evmone_precompiles/mulmod.hpp +++ b/evmone/lib/evmone_precompiles/mulmod.hpp @@ -5,12 +5,20 @@ #pragma once #include +#include #include namespace evmone::crypto { using namespace intx; +/// Compares two same-size little-endian word arrays as unsigned integers: returns true if x < y. +constexpr bool less(std::span x, std::span y) noexcept +{ + assert(x.size() == y.size()); + return std::ranges::lexicographical_compare(std::views::reverse(x), std::views::reverse(y)); +} + /// Subtracts y from x: x[] -= y[]. The result is truncated to the size of x. constexpr void sub(std::span x, std::span y) noexcept { diff --git a/evmone/lib/evmone_precompiles/pairing/bn254/fields.hpp b/evmone/lib/evmone_precompiles/pairing/bn254/fields.hpp index 9449a62..f3953b6 100644 --- a/evmone/lib/evmone_precompiles/pairing/bn254/fields.hpp +++ b/evmone/lib/evmone_precompiles/pairing/bn254/fields.hpp @@ -7,7 +7,7 @@ #include "../../ecc.hpp" #include "../field_template.hpp" -namespace evmmax::bn254 +namespace evmone::crypto::bn254 { using namespace intx; @@ -151,4 +151,4 @@ inline Fq12 inverse(const Fq12& f) noexcept return Fq12({c0, c1}); } -} // namespace evmmax::bn254 +} // namespace evmone::crypto::bn254 diff --git a/evmone/lib/evmone_precompiles/pairing/bn254/pairing.cpp b/evmone/lib/evmone_precompiles/pairing/bn254/pairing.cpp index 9a3b3e5..da61d9b 100644 --- a/evmone/lib/evmone_precompiles/pairing/bn254/pairing.cpp +++ b/evmone/lib/evmone_precompiles/pairing/bn254/pairing.cpp @@ -6,7 +6,7 @@ #include "fields.hpp" #include "utils.hpp" -namespace evmmax::bn254 +namespace evmone::crypto::bn254 { namespace { @@ -36,55 +36,79 @@ constexpr void multiply_by_lin_func_value( f.coeffs[1].coeffs[2] * t0y + f.coeffs[0].coeffs[2] * t1x + f.coeffs[0].coeffs[1] * t[2]; } -// 0000000100010010000010000000010000100010000000010010000000001000000100100000010000000000100000100001001000000010001000000001000101 -// NAF rep 00 -> 0, 01 -> 1, 10 -> -1 -// miller loop goes from L-2 to 0 inclusively. NAF rep of 29793968203157093288 (6x+2) is two bits -// longer, but we omit lowest 2 bits. -inline constexpr auto ATE_LOOP_COUNT_NAF = 0x1120804220120081204008212022011_u128; -inline constexpr int LOG_ATE_LOOP_COUNT = 63; - -/// Miller loop according to https://eprint.iacr.org/2010/354.pdf Algorithm 1. -Fq12 miller_loop(const ecc::AffinePoint& Q, const ecc::AffinePoint& P) noexcept +/// The signed digits of the ate loop count 6x+2 = 29793968203157093288, most significant first, +/// with the leading 1 omitted. This is a semi-NAF: adjacent non-zeros occur only at the leading +/// 1, 1, which the non-adjacent form spells as 1, 0, -1. Both have 22 non-zero digits, but this +/// one is a digit shorter, i.e. one loop iteration less. +// clang-format off +inline constexpr int8_t ATE_LOOP_COUNT_DIGITS[] = { + 1, 0, 1, 0, 0, 0, -1, 0, -1, 0, 0, 0, -1, 0, 1, 0, + -1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 1, 0, + 0, -1, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, -1, 0, -1, 0, + 0, 1, 0, 0, 0, -1, 0, 0, -1, 0, 1, 0, 1, 0, 0, 0, +}; +// clang-format on + +/// Miller loop for all the pairs at once, +/// according to https://eprint.iacr.org/2010/354.pdf Algorithm 1. +Fq12 multi_miller_loop(std::span> pairs) noexcept { - auto T = ecc::ProjPoint{Q}; - const auto nQ = -Q; + // The running point of every pair; starting at Q applies the omitted leading digit 1. + // TODO: Avoid the allocation: at most 492 pairs fit the transaction gas limit (EIP-7825). + // TODO: Caching -Q and -P.y next to the running points may be beneficial. + std::vector> Ts; + Ts.reserve(pairs.size()); + for (const auto& [_, Q] : pairs) + Ts.emplace_back(Q); + auto f = Fq12::one(); std::array t; - auto naf = ATE_LOOP_COUNT_NAF; - const auto ny = -P.y; - for (int i = 0; i <= LOG_ATE_LOOP_COUNT; ++i) + for (const auto digit : ATE_LOOP_COUNT_DIGITS) { - T = lin_func_and_dbl(T, t); + // The f <- f^2 * line recurrence is multiplicative over the pairs, so a single + // accumulator serves them all: one squaring per iteration instead of one per pair. f = square(f); - multiply_by_lin_func_value(f, t, P.x, ny); - if (naf & 1) - { - T = lin_func_and_add(T, Q, t); - multiply_by_lin_func_value(f, t, P.x, P.y); - } - else if (naf & 2) + for (size_t j = 0; j != pairs.size(); ++j) { - T = lin_func_and_add(T, nQ, t); - multiply_by_lin_func_value(f, t, P.x, P.y); + const auto& [P, Q] = pairs[j]; + if (P == 0 || Q == 0) // A pair with a point at infinity contributes 1. + continue; + + auto& T = Ts[j]; + T = lin_func_and_dbl(T, t); + multiply_by_lin_func_value(f, t, P.x, -P.y); + + if (digit != 0) + { + T = lin_func_and_add(T, digit > 0 ? Q : -Q, t); + multiply_by_lin_func_value(f, t, P.x, P.y); + } } - naf >>= 2; } - // Frobenius endomorphism for point Q from twisted curve over Fq2 field. - // It's essentially untwist -> frobenius -> twist chain of transformation. - const auto Q1 = endomorphism<1>(Q); + for (size_t j = 0; j != pairs.size(); ++j) + { + const auto& [P, Q] = pairs[j]; + if (P == 0 || Q == 0) + continue; - // Similar to above one. It makes untwist -> frobenius^2 -> twist transformation plus - // negation according to miller loop spec. - const auto nQ2 = -endomorphism<2>(Q); + // Frobenius endomorphism for point Q from twisted curve over Fq2 field. + // It's essentially untwist -> frobenius -> twist chain of transformation. + const auto Q1 = endomorphism<1>(Q); - T = lin_func_and_add(T, Q1, t); - multiply_by_lin_func_value(f, t, P.x, P.y); + // Similar to above one. It makes untwist -> frobenius^2 -> twist transformation plus + // negation according to miller loop spec. + const auto nQ2 = -endomorphism<2>(Q); - lin_func(T, nQ2, t); - multiply_by_lin_func_value(f, t, P.x, P.y); + auto& T = Ts[j]; + T = lin_func_and_add(T, Q1, t); + multiply_by_lin_func_value(f, t, P.x, P.y); + + lin_func(T, nQ2, t); + multiply_by_lin_func_value(f, t, P.x, P.y); + } return f; } @@ -132,26 +156,18 @@ std::optional pairing_check(std::span& p_aff) noexcept const auto _2px = dbl(px); const auto e_px = endomorphism<1>(px); - const auto ee_px = endomorphism<1>(e_px); - const auto eee_2px = endomorphism<1>(endomorphism<2>(_2px)); + const auto ee_px = endomorphism<2>(px); + const auto eee_2px = endomorphism<3>(_2px); const auto l = add(add(px1, e_px), ee_px); @@ -472,4 +472,4 @@ constexpr Fq12 cyclotomic_pow_to_X(const Fq12& a) noexcept return c; } -} // namespace evmmax::bn254 +} // namespace evmone::crypto::bn254 diff --git a/evmone/lib/evmone_precompiles/pairing/field_template.hpp b/evmone/lib/evmone_precompiles/pairing/field_template.hpp index 88c9fd3..d79dc6f 100644 --- a/evmone/lib/evmone_precompiles/pairing/field_template.hpp +++ b/evmone/lib/evmone_precompiles/pairing/field_template.hpp @@ -7,7 +7,7 @@ #include #include -namespace evmmax::ecc +namespace evmone::crypto::ecc { /// Implements extension field over the base field or other extension fields. /// It is a template struct which can be reused for different pairing implementations. @@ -99,4 +99,4 @@ struct ExtFieldElem } }; -} // namespace evmmax::ecc +} // namespace evmone::crypto::ecc diff --git a/evmone/lib/evmone_precompiles/secp256k1.cpp b/evmone/lib/evmone_precompiles/secp256k1.cpp index 11b7758..49e2e98 100644 --- a/evmone/lib/evmone_precompiles/secp256k1.cpp +++ b/evmone/lib/evmone_precompiles/secp256k1.cpp @@ -4,7 +4,7 @@ #include "secp256k1.hpp" #include "keccak.hpp" -namespace evmmax::secp256k1 +namespace evmone::crypto::secp256k1 { namespace { @@ -250,4 +250,4 @@ std::optional field_sqrt(const Curve::Fp& x) noexcept return z; } -} // namespace evmmax::secp256k1 +} // namespace evmone::crypto::secp256k1 diff --git a/evmone/lib/evmone_precompiles/secp256k1.hpp b/evmone/lib/evmone_precompiles/secp256k1.hpp index 08b5108..91a4ef4 100644 --- a/evmone/lib/evmone_precompiles/secp256k1.hpp +++ b/evmone/lib/evmone_precompiles/secp256k1.hpp @@ -8,7 +8,7 @@ #include #include -namespace evmmax::secp256k1 +namespace evmone::crypto::secp256k1 { using namespace intx; @@ -75,4 +75,4 @@ std::optional ecrecover(std::span hash, std::span r_bytes, std::span s_bytes, bool parity, RecoveryMode mode = RecoveryMode::malleable) noexcept; -} // namespace evmmax::secp256k1 +} // namespace evmone::crypto::secp256k1 diff --git a/evmone/lib/evmone_precompiles/secp256r1.cpp b/evmone/lib/evmone_precompiles/secp256r1.cpp index ad63c9a..ffba000 100644 --- a/evmone/lib/evmone_precompiles/secp256r1.cpp +++ b/evmone/lib/evmone_precompiles/secp256r1.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "secp256r1.hpp" -namespace evmmax::secp256r1 +namespace evmone::crypto::secp256r1 { namespace { @@ -61,4 +61,4 @@ bool verify(const ethash::hash256& h, const uint256& r, const uint256& s, const return x1 == r; } -} // namespace evmmax::secp256r1 +} // namespace evmone::crypto::secp256r1 diff --git a/evmone/lib/evmone_precompiles/secp256r1.hpp b/evmone/lib/evmone_precompiles/secp256r1.hpp index b69d29c..bd1a4b8 100644 --- a/evmone/lib/evmone_precompiles/secp256r1.hpp +++ b/evmone/lib/evmone_precompiles/secp256r1.hpp @@ -6,7 +6,7 @@ #include "ecc.hpp" #include "hash_types.h" -namespace evmmax::secp256r1 +namespace evmone::crypto::secp256r1 { using namespace intx; @@ -42,4 +42,4 @@ constexpr AffinePoint G{0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a139 bool verify(const ethash::hash256& h, const uint256& r, const uint256& s, const uint256& qx, const uint256& qy) noexcept; -} // namespace evmmax::secp256r1 +} // namespace evmone::crypto::secp256r1 diff --git a/evmone/test/CMakeLists.txt b/evmone/test/CMakeLists.txt index 4261870..a20fa65 100644 --- a/evmone/test/CMakeLists.txt +++ b/evmone/test/CMakeLists.txt @@ -2,6 +2,7 @@ # Copyright 2019-2020 The evmone Authors. # SPDX-License-Identifier: Apache-2.0 +include(FetchContent) include(GoogleTest) set(evmone_private_include_dir ${PROJECT_SOURCE_DIR}/lib) @@ -12,22 +13,55 @@ find_package(CLI11 CONFIG REQUIRED) hunter_add_package(GTest) find_package(GTest CONFIG REQUIRED) -hunter_add_package(benchmark) -find_package(benchmark CONFIG REQUIRED) +block(PROPAGATE benchmark_libs) + set(BUILD_SHARED_LIBS OFF) + set(BENCHMARK_ENABLE_TESTING OFF) + set(BENCHMARK_ENABLE_INSTALL OFF) + set(benchmark_libs benchmark benchmark_main) + if(NOT CODSPEED_MODE) + FetchContent_Declare( + benchmark + URL https://github.com/google/benchmark/archive/refs/tags/v1.9.5.tar.gz + URL_HASH SHA256=9631341c82bac4a288bef951f8b26b41f69021794184ece969f8473977eaa340 + ) + else() + # Replace google/benchmark with CodSpeed's fork of it to report the benchmark results + # to https://codspeed.io. It provides the same benchmark::benchmark target, + # so the benchmark targets need no changes. + FetchContent_Declare( + benchmark + # Fork of v2.4.0 with fixes not yet available upstream. + GIT_REPOSITORY https://github.com/chfast/codspeed-cpp + GIT_TAG 0ecdd6e7d8f9e858e8e0b2fbe54ff7f427bec528 + GIT_SHALLOW FALSE + SOURCE_SUBDIR google_benchmark + ) + list(APPEND benchmark_libs codspeed instrument_hooks) + # Also reports the tests `evmone test` runs as benchmarks (utils/test_driver.cpp). + target_link_libraries(evmone.testutils PRIVATE codspeed) + endif() + FetchContent_MakeAvailable(benchmark) +endblock() + +set_target_properties( + ${benchmark_libs} PROPERTIES + COMPILE_OPTIONS "$<$:-w>;$<$:-w>" + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + SYSTEM TRUE # TODO(cmake-3.26): Use the SYSTEM option of FetchContent_Declare(). +) include(CableBuildInfo) cable_add_buildinfo_library(PROJECT_NAME evmone) add_subdirectory(bench) -add_subdirectory(blockchaintest) add_subdirectory(experimental) add_subdirectory(integration) add_subdirectory(internal_benchmarks) add_subdirectory(precompiles_bench) -add_subdirectory(statetest) add_subdirectory(unittests) -set(targets evmone-bench evmone-bench-internal evmone-blockchaintest evmone-precompiles-bench evmone-state evmone-statetest evmone-unittests) +set(targets evmone-bench evmone-bench-internal evmone-precompiles-bench evmone-state evmone-unittests) if(EVMONE_FUZZING) add_subdirectory(fuzzer) diff --git a/evmone/test/bench/bench.cpp b/evmone/test/bench/bench.cpp index 8c45f83..f6ef1e4 100644 --- a/evmone/test/bench/bench.cpp +++ b/evmone/test/bench/bench.cpp @@ -116,7 +116,7 @@ void register_benchmarks(std::span benchmark_cases) { if (advanced_vm != nullptr) { - RegisterBenchmark("advanced/analyse/" + b.name, [&b](State& state) { + RegisterBenchmark(bench_name("advanced/analyse/" + b.name), [&b](State& state) { bench_analyse( state, default_revision, b.code); })->Unit(kMicrosecond); @@ -124,7 +124,7 @@ void register_benchmarks(std::span benchmark_cases) if (baseline_vm != nullptr) { - RegisterBenchmark("baseline/analyse/" + b.name, [&b](State& state) { + RegisterBenchmark(bench_name("baseline/analyse/" + b.name), [&b](State& state) { bench_analyse( state, default_revision, b.code); })->Unit(kMicrosecond); @@ -136,7 +136,7 @@ void register_benchmarks(std::span benchmark_cases) if (advanced_vm != nullptr) { - const auto name = "advanced/execute/" + case_name; + const auto name = bench_name("advanced/execute/" + case_name); RegisterBenchmark(name, [&vm = *advanced_vm, &b, &input](State& state) { bench_advanced_execute(state, vm, b.code, input.input, input.expected_output); })->Unit(kMicrosecond); @@ -144,7 +144,7 @@ void register_benchmarks(std::span benchmark_cases) if (baseline_vm != nullptr) { - const auto name = "baseline/execute/" + case_name; + const auto name = bench_name("baseline/execute/" + case_name); RegisterBenchmark(name, [&vm = *baseline_vm, &b, &input](State& state) { bench_baseline_execute(state, vm, b.code, input.input, input.expected_output); })->Unit(kMicrosecond); @@ -152,7 +152,7 @@ void register_benchmarks(std::span benchmark_cases) if (basel_cg_vm != nullptr) { - const auto name = "bnocgoto/execute/" + case_name; + const auto name = bench_name("bnocgoto/execute/" + case_name); RegisterBenchmark(name, [&vm = *basel_cg_vm, &b, &input](State& state) { bench_baseline_execute(state, vm, b.code, input.input, input.expected_output); })->Unit(kMicrosecond); @@ -160,7 +160,7 @@ void register_benchmarks(std::span benchmark_cases) for (auto& [vm_name, vm] : registered_vms) { - const auto name = std::string{vm_name} + "/total/" + case_name; + const auto name = bench_name(std::string{vm_name} + "/total/" + case_name); RegisterBenchmark(name, [&vm, &b, &input](State& state) { bench_evmc_execute(state, vm, b.code, input.input, input.expected_output); })->Unit(kMicrosecond); @@ -234,7 +234,10 @@ std::tuple> parseargs(int argc, char** argv) int main(int argc, char** argv) { +#ifndef CODSPEED_ENABLED + // Not available in the google/benchmark version CodSpeed's fork is based on, nor needed there. MaybeReenterWithoutASLR(argc, argv); +#endif using namespace evmone::test; try diff --git a/evmone/test/bench/helpers.hpp b/evmone/test/bench/helpers.hpp index 2f0f71a..650fa54 100644 --- a/evmone/test/bench/helpers.hpp +++ b/evmone/test/bench/helpers.hpp @@ -11,11 +11,26 @@ #include #include #include +#include namespace evmone::test { extern std::map registered_vms; +/// Decorates a dynamically registered benchmark name with the location of the registration. +/// CodSpeed identifies benchmarks by the "source_file::name" URI, which the BENCHMARK() macro +/// adds automatically, but RegisterBenchmark() does not. Without CodSpeed this is a no-op. +/// The default argument is evaluated at the call site, naming the file registering the benchmark. +inline std::string bench_name(std::string name, + [[maybe_unused]] const std::source_location loc = std::source_location::current()) +{ +#ifdef CODSPEED_ENABLED + return codspeed::get_path_relative_to_workspace(loc.file_name()) + "::" + std::move(name); +#else + return name; +#endif +} + constexpr auto default_revision = EVMC_ISTANBUL; constexpr auto default_gas_limit = std::numeric_limits::max(); diff --git a/evmone/test/bench/synthetic_benchmarks.cpp b/evmone/test/bench/synthetic_benchmarks.cpp index 0aaa7d9..f564bd3 100644 --- a/evmone/test/bench/synthetic_benchmarks.cpp +++ b/evmone/test/bench/synthetic_benchmarks.cpp @@ -252,9 +252,9 @@ void register_synthetic_benchmarks() for (auto& [vm_name, vm] : registered_vms) { - RegisterBenchmark(std::string{vm_name} + "/total/synth/loop_v1", + RegisterBenchmark(bench_name(std::string{vm_name} + "/total/synth/loop_v1"), [&vm](State& state) { bench_evmc_execute(state, vm, generate_loop_v1({})); }); - RegisterBenchmark(std::string{vm_name} + "/total/synth/loop_v2", + RegisterBenchmark(bench_name(std::string{vm_name} + "/total/synth/loop_v2"), [&vm](State& state) { bench_evmc_execute(state, vm, generate_loop_v2({})); }); } @@ -262,7 +262,8 @@ void register_synthetic_benchmarks() { for (auto& [vm_name, vm] : registered_vms) { - RegisterBenchmark(std::string{vm_name} + "/total/synth/" + to_string(params), + RegisterBenchmark( + bench_name(std::string{vm_name} + "/total/synth/" + to_string(params)), [&vm, params]( State& state) { bench_evmc_execute(state, vm, generate_code(params)); }) ->Unit(kMicrosecond); diff --git a/evmone/test/blockchaintest/.clang-tidy b/evmone/test/blockchaintest/.clang-tidy deleted file mode 100644 index efc628c..0000000 --- a/evmone/test/blockchaintest/.clang-tidy +++ /dev/null @@ -1,3 +0,0 @@ -InheritParentConfig: true -Checks: > - -clang-analyzer-cplusplus.NewDeleteLeaks diff --git a/evmone/test/blockchaintest/CMakeLists.txt b/evmone/test/blockchaintest/CMakeLists.txt deleted file mode 100644 index 66b996e..0000000 --- a/evmone/test/blockchaintest/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# evmone: Fast Ethereum Virtual Machine implementation -# Copyright 2023 The evmone Authors. -# SPDX-License-Identifier: Apache-2.0 - -add_executable(evmone-blockchaintest) -target_link_libraries(evmone-blockchaintest PRIVATE evmone::testutils evmone evmone-buildinfo GTest::gtest CLI11::CLI11) -target_sources( - evmone-blockchaintest PRIVATE - blockchaintest.cpp - blockchaintest_runner.hpp - blockchaintest_runner.cpp -) diff --git a/evmone/test/blockchaintest/blockchaintest.cpp b/evmone/test/blockchaintest/blockchaintest.cpp deleted file mode 100644 index 1fe92be..0000000 --- a/evmone/test/blockchaintest/blockchaintest.cpp +++ /dev/null @@ -1,147 +0,0 @@ -// evmone: Fast Ethereum Virtual Machine implementation -// Copyright 2023 The evmone Authors. -// SPDX-License-Identifier: Apache-2.0 - -#include "blockchaintest_runner.hpp" -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -namespace -{ -/// Implementation of a gtest Test which runs all blockchain tests from a given file. -class BlockchainGTestFile : public testing::Test -{ - fs::path m_json_test_file; - evmc::VM& m_vm; - -public: - explicit BlockchainGTestFile(fs::path json_test_file, evmc::VM& vm) noexcept - : m_json_test_file{std::move(json_test_file)}, m_vm{vm} - {} - - void TestBody() final - { - std::ifstream f{m_json_test_file}; - - try - { - evmone::test::run_blockchain_tests(evmone::test::load_blockchain_tests(f), m_vm); - } - catch (const evmone::test::UnsupportedTestFeature& ex) - { - GTEST_SKIP() << ex.what(); - } - } - - static void register_one(const std::string& suite_name, const fs::path& file, evmc::VM& vm) - { - testing::RegisterTest(suite_name.c_str(), file.stem().string().c_str(), nullptr, nullptr, - file.string().c_str(), 0, - [file, &vm]() -> testing::Test* { return new BlockchainGTestFile(file, vm); }); - } -}; - -/// Implementation of a gtest Test which runs a single blockchain test. -class BlockchainGTest : public testing::Test -{ - const evmone::test::BlockchainTest m_blockchain_test; - evmc::VM& m_vm; - -public: - explicit BlockchainGTest(evmone::test::BlockchainTest blockchain_test, evmc::VM& vm) noexcept - : m_blockchain_test{std::move(blockchain_test)}, m_vm{vm} - {} - - void TestBody() final - { - evmone::test::run_blockchain_tests(std::array{m_blockchain_test}, m_vm); - } - - static void register_one(const evmone::test::BlockchainTest& test, - const std::string& suite_name, const std::string& test_name, const fs::path& file, - evmc::VM& vm) - { - testing::RegisterTest(suite_name.c_str(), test_name.c_str(), nullptr, nullptr, - file.string().c_str(), 0, - [test, &vm]() -> testing::Test* { return new BlockchainGTest(test, vm); }); - } -}; - -void register_test_files(const fs::path& root, evmc::VM& vm) -{ - if (is_directory(root)) - { - std::vector test_files; - std::copy_if(fs::recursive_directory_iterator{root}, fs::recursive_directory_iterator{}, - std::back_inserter(test_files), [](const fs::directory_entry& entry) { - // "index.json" files are just lists of tests generated by other tools. - return entry.is_regular_file() && entry.path().extension() == ".json" && - entry.path().filename() != "index.json"; - }); - std::ranges::sort(test_files); - - for (const auto& p : test_files) - BlockchainGTestFile::register_one(fs::relative(p, root).parent_path().string(), p, vm); - } - else // Treat as a file. - { - std::ifstream f{root}; - try - { - const auto tests = evmone::test::load_blockchain_tests(f); - for (const auto& test : tests) - BlockchainGTest::register_one(test, root.string(), test.name, root, vm); - } - catch (const evmone::test::UnsupportedTestFeature& ex) - { - std::cerr << ex.what() << ": " << root.string() << '\n'; - } - } -} -} // namespace - - -int main(int argc, char* argv[]) -{ - try - { - testing::InitGoogleTest(&argc, argv); // Process GoogleTest flags. - - CLI::App app{"evmone blockchain test runner"}; - - app.set_version_flag("--version", "evmone-blockchaintest " EVMONE_VERSION); - - std::vector paths; - app.add_option("path", paths, - "Path to test file or directory. For a directory, all .json " - "files (except index.json) are considered test files, and each file is treated as a " - "separate test. For a file, all tests in the file are treated as separate tests.") - ->required() - ->check(CLI::ExistingPath); - - bool trace_flag = false; - app.add_flag("--trace", trace_flag, "Enable EVM tracing"); - - CLI11_PARSE(app, argc, argv); - - evmc::VM vm{evmc_create_evmone()}; - - if (trace_flag) - vm.set_option("trace", "1"); - - for (const auto& p : paths) - register_test_files(p, vm); - - return RUN_ALL_TESTS(); - } - catch (const std::exception& ex) - { - std::cerr << ex.what() << "\n"; - return -1; - } -} diff --git a/evmone/test/blockchaintest/blockchaintest_runner.cpp b/evmone/test/blockchaintest/blockchaintest_runner.cpp deleted file mode 100644 index aa821e8..0000000 --- a/evmone/test/blockchaintest/blockchaintest_runner.cpp +++ /dev/null @@ -1,481 +0,0 @@ -// evmone: Fast Ethereum Virtual Machine implementation -// Copyright 2023 The evmone Authors. -// SPDX-License-Identifier: Apache-2.0 - -#include "blockchaintest_runner.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace evmone::test -{ - -/// The CL gossip protocol constraint of the maximum block size (EIP-7934). -constexpr size_t MAX_BLOCK_SIZE = 10 * 1024 * 1024; -/// The safety margin for beacon block content (EIP-7934). -constexpr size_t SAFETY_MARGIN = 2 * 1024 * 1024; -/// The maximum EL block size when RLP encoded (EIP-7934). -constexpr size_t MAX_RLP_BLOCK_SIZE = MAX_BLOCK_SIZE - SAFETY_MARGIN; - -namespace -{ -/// Validates block-level validity unrelated to individual transactions. -/// -/// Returns an empty error_code if the block is valid, otherwise the specific validation error. -std::error_code validate_block(evmc_revision rev, state::BlobParams blob_params, - const TestBlock& test_block, const BlockHeader* parent_header, bool parent_has_ommers) noexcept -{ - using namespace state; - - // Fail if parent header was not found: the block references a parent that is neither the - // genesis nor any previously-accepted block (an unknown or rejected parent). - if (parent_header == nullptr) - return make_error_code(UNKNOWN_PARENT); - - if (test_block.block_info.number != parent_header->block_number + 1) - return make_error_code(INVALID_BLOCK_NUMBER); - - if (test_block.block_info.gas_used > test_block.block_info.gas_limit) - return make_error_code(INCORRECT_BLOCK_FORMAT); - - // Some tests have gas limit at INT64_MAX, so we cast to uint64_t to avoid overflow. - const auto parent_header_gas_limit_u64 = static_cast(parent_header->gas_limit); - const auto test_block_gas_limit_u64 = static_cast(test_block.block_info.gas_limit); - if (test_block_gas_limit_u64 >= - parent_header_gas_limit_u64 + parent_header_gas_limit_u64 / 1024) - return make_error_code(INVALID_GASLIMIT); - if (test_block_gas_limit_u64 <= - parent_header_gas_limit_u64 - parent_header_gas_limit_u64 / 1024) - return make_error_code(INVALID_GASLIMIT); - - // Block gas limit minimum from Yellow Paper. - if (test_block.block_info.gas_limit < 5000) - return make_error_code(INVALID_GASLIMIT); - - // FIXME: Some tests have timestamp not fitting into int64_t, type has to be uint64_t. - if (static_cast(test_block.block_info.timestamp) <= - static_cast(parent_header->timestamp)) - return make_error_code(INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT); - - if (test_block.block_info.difficulty != - calculate_difficulty(parent_header->difficulty, parent_has_ommers, parent_header->timestamp, - test_block.block_info.timestamp, test_block.block_info.number, rev)) - return make_error_code(INCORRECT_BLOCK_FORMAT); - - if (rev >= EVMC_PARIS && !test_block.block_info.ommers.empty()) - return make_error_code(INCORRECT_BLOCK_FORMAT); - - for (const auto& ommer : test_block.block_info.ommers) - { - // Check that ommer block number difference with current block is within allowed range. - // https://github.com/ethereum/execution-specs/blob/ee73be5c4d83a2e3c358bd14990878002e52ba9e/src/ethereum/gray_glacier/fork.py#L623 - if (ommer.delta < 1 || ommer.delta > 6) - return make_error_code(INCORRECT_BLOCK_FORMAT); - } - - if (test_block.block_info.extra_data.size() > 32) - return make_error_code(INCORRECT_BLOCK_FORMAT); - - if (rev >= EVMC_LONDON) - { - const auto calculated_base_fee = calc_base_fee( - parent_header->gas_limit, parent_header->gas_used, parent_header->base_fee_per_gas); - if (test_block.block_info.base_fee != calculated_base_fee) - return make_error_code(INVALID_BASEFEE_PER_GAS); - } - - if (rev >= EVMC_CANCUN) - { - // `excess_blob_gas` and `blob_gas_used` mandatory after Cancun and invalid before. - if (!test_block.block_info.excess_blob_gas.has_value() || - !test_block.block_info.blob_gas_used.has_value()) - return make_error_code(INCORRECT_BLOCK_FORMAT); - - // Check that the excess blob gas was updated correctly. - // According to EIP-7918 current blocks params (`rev`) should be used for parent base fee - // calculation. - const auto parent_blob_base_fee = - compute_blob_gas_price(blob_params, parent_header->excess_blob_gas.value_or(0)); - if (*test_block.block_info.excess_blob_gas != - calc_excess_blob_gas(rev, blob_params, parent_header->blob_gas_used.value_or(0), - parent_header->excess_blob_gas.value_or(0), parent_header->base_fee_per_gas, - parent_blob_base_fee)) - return make_error_code(INCORRECT_EXCESS_BLOB_GAS); - } - else - { - if (test_block.block_info.excess_blob_gas.has_value() || - test_block.block_info.blob_gas_used.has_value()) - return make_error_code(INCORRECT_BLOCK_FORMAT); - } - - // `slot_number` is mandatory from Amsterdam and invalid before (EIP-7843). - if (test_block.block_info.slot_number.has_value() != (rev >= EVMC_AMSTERDAM)) - return make_error_code(INCORRECT_BLOCK_FORMAT); - - // Block is invalid if some of the withdrawal fields failed to be parsed. - if (!test_block.withdrawals_parse_success) - return make_error_code(INCORRECT_BLOCK_FORMAT); - - if (rev >= EVMC_OSAKA && test_block.rlp.size() > MAX_RLP_BLOCK_SIZE) - return make_error_code(RLP_BLOCK_LIMIT_EXCEEDED); - - return {}; -} - -/// Checks the transaction codec against a block's own serialization: every transaction in it must -/// decode, and encode back to the very same bytes. -void expect_transactions_round_trip(bytes_view block_rlp) -{ - bytes_view body; // A block is [header, transactions, ...]. - ASSERT_TRUE(rlp::take_list_payload(block_rlp, body)); - ASSERT_TRUE(block_rlp.empty()) << "trailing bytes after the block"; - bytes_view block_header; - ASSERT_TRUE(rlp::take_list_payload(body, block_header)); // Skipped over. - bytes_view txs; - ASSERT_TRUE(rlp::take_list_payload(body, txs)); - - while (!txs.empty()) - { - const auto item = txs; - rlp::Header h; - ASSERT_TRUE(rlp::decode_header(txs, h)); // Advances txs to the item's payload. - const auto header_size = item.size() - txs.size(); - txs.remove_prefix(h.payload_length); - - // A legacy transaction is an RLP list here, a typed one an RLP string wrapping the - // EIP-2718 envelope; the envelope alone is the transaction. - const auto tx_bytes = h.is_list ? item.substr(0, header_size + h.payload_length) : - item.substr(header_size, h.payload_length); - - const auto tx = state::decode_transaction(tx_bytes); - ASSERT_TRUE(tx.has_value()) << hex(tx_bytes); - EXPECT_EQ(rlp::encode(*tx), tx_bytes); - } -} - -std::optional mining_reward(evmc_revision rev) noexcept -{ - if (rev < EVMC_BYZANTIUM) - return 5'000000000'000000000; - if (rev < EVMC_PETERSBURG) - return 3'000000000'000000000; - if (rev < EVMC_PARIS) - return 2'000000000'000000000; - return std::nullopt; -} - -std::string print_state(const TestState& s) -{ - std::stringstream out; - - for (const auto& [key, acc] : s) - { - out << key << " : \n"; - out << "\tnonce : " << acc.nonce << "\n"; - out << "\tbalance : " << hex0x(acc.balance) << "\n"; - out << "\tcode : " << hex0x(acc.code) << "\n"; - - if (!acc.storage.empty()) - { - out << "\tstorage : \n"; - for (const auto& [s_key, val] : acc.storage) - { - if (!is_zero(val)) // Skip 0 values. - out << "\t\t" << s_key << " : " << hex0x(val) << "\n"; - } - } - } - - return out.str(); -} -} // namespace - -void run_blockchain_tests(std::span tests, evmc::VM& vm) -{ - for (size_t case_index = 0; case_index != tests.size(); ++case_index) - { - const auto& c = tests[case_index]; - const auto rev_schedule = to_rev_schedule(c.network); - SCOPED_TRACE(std::string{evmc::to_string(rev_schedule.get_revision(0))} + '/' + - std::to_string(case_index) + '/' + c.name); - - // Validate the genesis block header. - EXPECT_EQ(c.genesis_block_header.block_number, 0); - EXPECT_EQ(c.genesis_block_header.gas_used, 0); - EXPECT_EQ(c.genesis_block_header.transactions_root, state::EMPTY_MPT_HASH); - EXPECT_EQ(c.genesis_block_header.receipts_root, state::EMPTY_MPT_HASH); - EXPECT_EQ(c.genesis_block_header.withdrawal_root, - rev_schedule.get_revision(c.genesis_block_header.timestamp) >= EVMC_SHANGHAI ? - state::EMPTY_MPT_HASH : - bytes32{}); - EXPECT_EQ(c.genesis_block_header.logs_bloom, bytes_view{state::BloomFilter{}}); - - TestBlockHashes block_hashes{ - {c.genesis_block_header.block_number, c.genesis_block_header.hash}}; - - struct BlockData - { - const BlockHeader* header; - bool has_ommers = false; - TestState post_state; - intx::uint256 total_difficulty; - }; - std::unordered_map block_data{{{c.genesis_block_header.hash, - {&c.genesis_block_header, false, c.pre_state, c.genesis_block_header.difficulty}}}}; - const auto* canonical_state = &c.pre_state; - hash256 canonical_state_root; // Skip pre-state root hash computation (maybe not needed). - auto canonical_tip_hash = c.genesis_block_header.hash; - intx::uint256 max_total_difficulty = c.genesis_block_header.difficulty; - - for (size_t i = 0; i < c.test_blocks.size(); ++i) - { - const auto& test_block = c.test_blocks[i]; - const auto& bi = test_block.block_info; - - const auto parent_data_it = block_data.find(test_block.block_info.parent_hash); - const auto* parent_header = - parent_data_it != block_data.end() ? parent_data_it->second.header : nullptr; - const auto parent_has_ommers = - parent_data_it != block_data.end() && parent_data_it->second.has_ommers; - - const auto rev = rev_schedule.get_revision(bi.timestamp); - const auto blob_params = get_blob_params(c.network, c.blob_schedule, bi.timestamp); - const auto blob_gas_limit = - static_cast(state::max_blob_gas_per_block(blob_params)); - - SCOPED_TRACE(std::string{evmc::to_string(rev)} + '/' + std::to_string(case_index) + - '/' + c.name + '/' + std::to_string(test_block.block_info.number)); - - // Invalid blocks are skipped: they may carry transactions that do not even decode. - if (test_block.expected_exception.empty()) - expect_transactions_round_trip(test_block.rlp); - - const auto block_error = - validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers); - - if (test_block.expected_exception.empty()) - { - ASSERT_FALSE(block_error) - << "Expected block to be valid (validate_block): " << block_error.message(); - - // Block being valid guarantees its parent was found. - assert(parent_data_it != block_data.end()); - const auto& pre_state = parent_data_it->second.post_state; - - auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, - rev, blob_gas_limit, {.block_reward = mining_reward(rev)}); - - ASSERT_FALSE(res.requests_error); - - block_hashes[test_block.expected_block_header.block_number] = - test_block.expected_block_header.hash; - const auto [inserted_it, _] = block_data.insert({test_block.block_info.hash, - { - .header = &test_block.expected_block_header, - .has_ommers = !test_block.block_info.ommers.empty(), - .post_state = std::move(res.block_state), - .total_difficulty = parent_data_it->second.total_difficulty + - test_block.block_info.difficulty, - }}); - - const auto state_root = state::mpt_hash(inserted_it->second.post_state); - - if (inserted_it->second.total_difficulty >= max_total_difficulty) - { - canonical_state = &inserted_it->second.post_state; - canonical_state_root = state_root; - canonical_tip_hash = test_block.expected_block_header.hash; - max_total_difficulty = inserted_it->second.total_difficulty; - } - - EXPECT_TRUE(res.rejected.empty()) - << "Invalid transaction in block expected to be valid"; - EXPECT_EQ(blob_gas_limit - res.blob_gas_left, - static_cast(bi.blob_gas_used.value_or(0))) - << "Transactions used more or less blob gas than expected in block header"; - - EXPECT_EQ(state_root, test_block.expected_block_header.state_root); - - if (rev >= EVMC_SHANGHAI) - { - EXPECT_EQ(state::mpt_hash(test_block.block_info.withdrawals), - test_block.expected_block_header.withdrawal_root); - } - - EXPECT_EQ(state::mpt_hash(test_block.transactions), - test_block.expected_block_header.transactions_root); - EXPECT_EQ( - state::mpt_hash(res.receipts), test_block.expected_block_header.receipts_root); - if (rev >= EVMC_PRAGUE) - { - EXPECT_EQ(calculate_requests_hash(res.requests), - test_block.expected_block_header.requests_hash); - } - EXPECT_EQ(res.gas_used, test_block.expected_block_header.gas_used); - EXPECT_EQ( - bytes_view{res.bloom}, bytes_view{test_block.expected_block_header.logs_bloom}); - } - else - { - if (block_error) - { - // Block correctly rejected at validation; verify the reason matches the - // fixture's expected exception. - EXPECT_TRUE( - is_expected_block_exception(block_error, test_block.expected_exception)) - << "Block invalidity reason mismatch: got " << block_error.message() - << ", expected " << test_block.expected_exception; - continue; - } - - // Block being valid guarantees its parent was found. - assert(parent_data_it != block_data.end()); - const auto& pre_state = parent_data_it->second.post_state; - - // Legacy fixtures name the broken rule in vocabulary evmone does not speak - // (InvalidStateRoot, TooManyUncles); only the spec names can be compared. - const auto names_spec_exception = - test_block.expected_exception.find("Exception.") != std::string::npos; - - // TODO: The transaction senders come from the fixture instead of being recovered - // from the signatures, so evmone never sees the signature the test broke. Such a - // transaction executes as the sender the fixture names and the block is rejected - // by whatever rule that sender happens to break, or by its state root alone. - const auto sender_not_recovered = contains_any( - test_block.expected_exception, "TransactionException.INVALID_SIGNATURE_VRS"); - - const auto res = - apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, rev, - blob_gas_limit, {.block_reward = mining_reward(rev)}); - if (!res.rejected.empty()) - { - // A transaction was rejected: the fixture must name that reason, not merely - // some rejection. - const auto& rejected = res.rejected.front(); - if (names_spec_exception && !sender_not_recovered) - { - EXPECT_TRUE( - is_expected_tx_exception(rejected.error, test_block.expected_exception)) - << "Transaction-level invalidity mismatch: got \"" - << rejected.error.message() << "\", expected " - << test_block.expected_exception; - } - continue; - } - if (res.requests_error) - { - if (!sender_not_recovered) - { - EXPECT_TRUE(is_expected_block_exception( - res.requests_error, test_block.expected_exception)) - << "Block invalidity reason mismatch: got " - << res.requests_error.message() << ", expected " - << test_block.expected_exception; - } - continue; - } - // The block executed, so it is invalid only if it computes something other than - // its header claims. Each difference below is the symptom of one BlockException: - // a block failing a check other than the one the fixture names breaks a different - // rule than the test is about. - // TODO: Of the ommers only the count and the distance to their nephew are - // validated, not the ommer headers themselves, so a fixture that breaks an - // ommer's gas limit, number or timestamp reaches execution and lands here. - const auto ommers_not_validated = !test_block.block_info.ommers.empty(); - - // Asserts the fixture names one of @p names, the exceptions the check that just - // fired is the symptom of. Silent where the reason cannot be compared. - const auto expect_fixture_names = [&](std::string_view names) { - if (!names_spec_exception || ommers_not_validated || sender_not_recovered) - return; - EXPECT_TRUE(contains_any(test_block.expected_exception, names)) - << "Block invalidity reason mismatch: the block failed the check for " - << names << ", expected " << test_block.expected_exception; - }; - - if (blob_gas_limit - res.blob_gas_left != - static_cast(bi.blob_gas_used.value_or(0))) - { - expect_fixture_names( - "BlockException.INCORRECT_BLOB_GAS_USED|" - "BlockException.BLOB_GAS_USED_ABOVE_LIMIT"); - continue; - } - - if (state::mpt_hash(res.block_state) != test_block.expected_block_header.state_root) - { - expect_fixture_names("BlockException.INVALID_STATE_ROOT"); - continue; - } - - if (rev >= EVMC_SHANGHAI && state::mpt_hash(test_block.block_info.withdrawals) != - test_block.expected_block_header.withdrawal_root) - { - expect_fixture_names("BlockException.INVALID_WITHDRAWALS_ROOT"); - continue; - } - if (state::mpt_hash(test_block.transactions) != - test_block.expected_block_header.transactions_root) - { - expect_fixture_names("BlockException.INVALID_TRANSACTIONS_ROOT"); - continue; - } - if (state::mpt_hash(res.receipts) != test_block.expected_block_header.receipts_root) - { - expect_fixture_names("BlockException.INVALID_RECEIPTS_ROOT"); - continue; - } - if (rev >= EVMC_PRAGUE && calculate_requests_hash(res.requests) != - test_block.expected_block_header.requests_hash) - { - expect_fixture_names("BlockException.INVALID_REQUESTS"); - continue; - } - if (res.gas_used != test_block.expected_block_header.gas_used) - { - expect_fixture_names( - "BlockException.INVALID_GAS_USED|" - "BlockException.GAS_USED_OVERFLOW"); - continue; - } - if (bytes_view{res.bloom} != - bytes_view{test_block.expected_block_header.logs_bloom}) - { - expect_fixture_names("BlockException.INVALID_LOG_BLOOM"); - continue; - } - - EXPECT_TRUE(false) << "Expected block to be invalid but resulted valid"; - } - } - EXPECT_EQ(canonical_tip_hash, c.expectation.last_block_hash) - << "Canonical chain tip differs from expected `lastblockhash`"; - - const auto expected_post_hash = - std::holds_alternative(c.expectation.post_state) ? - state::mpt_hash(std::get(c.expectation.post_state)) : - std::get(c.expectation.post_state); - - // Get the final state hash. In case none blocks have been applied, compute genesis one. - const auto canonical_post_hash = - canonical_state_root ? canonical_state_root : state::mpt_hash(c.pre_state); - EXPECT_EQ(canonical_post_hash, expected_post_hash) - << "Result state:\n" - << print_state(*canonical_state) - << (std::holds_alternative(c.expectation.post_state) ? - "\n\nExpected state:\n" + - print_state(std::get(c.expectation.post_state)) : - ""); - } -} - -} // namespace evmone::test diff --git a/evmone/test/blockchaintest/blockchaintest_runner.hpp b/evmone/test/blockchaintest/blockchaintest_runner.hpp deleted file mode 100644 index 896e356..0000000 --- a/evmone/test/blockchaintest/blockchaintest_runner.hpp +++ /dev/null @@ -1,11 +0,0 @@ -// evmone: Fast Ethereum Virtual Machine implementation -// Copyright 2023 The evmone Authors. -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -namespace evmone::test -{ -void run_blockchain_tests(std::span tests, evmc::VM& vm); -} // namespace evmone::test diff --git a/evmone/test/integration/CMakeLists.txt b/evmone/test/integration/CMakeLists.txt index 93b5765..03f0136 100644 --- a/evmone/test/integration/CMakeLists.txt +++ b/evmone/test/integration/CMakeLists.txt @@ -5,62 +5,7 @@ set(PREFIX ${PROJECT_NAME}/integration) if(TARGET evmone-cli) - - add_test(NAME ${PREFIX}/run COMMAND evmone-cli run 6000) - set_tests_properties( - ${PREFIX}/run PROPERTIES PASS_REGULAR_EXPRESSION - "Result: success\nGas used: 3\n") - - add_test(NAME ${PREFIX}/run_rev COMMAND evmone-cli run --rev TangerineWhistle 00) - set_tests_properties( - ${PREFIX}/run_rev PROPERTIES PASS_REGULAR_EXPRESSION - "Executing on TangerineWhistle") - - add_test(NAME ${PREFIX}/run_bad_rev COMMAND evmone-cli run --rev NoSuchRev 00) - set_tests_properties( - ${PREFIX}/run_bad_rev PROPERTIES PASS_REGULAR_EXPRESSION - "unknown revision: NoSuchRev") - - add_test(NAME ${PREFIX}/version COMMAND evmone-cli --version) - set_tests_properties( - ${PREFIX}/version PROPERTIES PASS_REGULAR_EXPRESSION "evmone") - - # Test loading code from a file via @file syntax. - file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/code.hex "60 00 80 80 80 80 01 01 01 02 00") - add_test(NAME ${PREFIX}/file_input COMMAND evmone-cli run @${CMAKE_CURRENT_BINARY_DIR}/code.hex) - set_tests_properties( - ${PREFIX}/file_input PROPERTIES PASS_REGULAR_EXPRESSION - "Result: success\n") - - add_test(NAME ${PREFIX}/histogram COMMAND evmone-cli --histogram run 6000808080800101010200) - set_tests_properties( - ${PREFIX}/histogram PROPERTIES PASS_REGULAR_EXPRESSION - "--- # HISTOGRAM depth=0 -opcode,count -STOP,1 -ADD,3 -MUL,1 -PUSH1,1 -DUP1,4 -") - - add_test(NAME ${PREFIX}/trace COMMAND evmone-cli --trace run 60006002800103) - set_tests_properties( - ${PREFIX}/trace PROPERTIES PASS_REGULAR_EXPRESSION - "\"pc\":0,\"op\":96,\"gas\":\"0xf4240\",\"gasCost\":\"0x3\",\"memSize\":0,\"stack\":\\[\\],\"depth\":1,\"refund\":0,\"opName\":\"PUSH1\"} -{\"pc\":2,\"op\":96,\"gas\":\"0xf423d\",\"gasCost\":\"0x3\",\"memSize\":0,\"stack\":\\[\"0x0\"\\],\"depth\":1,\"refund\":0,\"opName\":\"PUSH1\"} -{\"pc\":4,\"op\":128,\"gas\":\"0xf423a\",\"gasCost\":\"0x3\",\"memSize\":0,\"stack\":\\[\"0x0\",\"0x2\"\\],\"depth\":1,\"refund\":0,\"opName\":\"DUP1\"} -{\"pc\":5,\"op\":1,\"gas\":\"0xf4237\",\"gasCost\":\"0x3\",\"memSize\":0,\"stack\":\\[\"0x0\",\"0x2\",\"0x2\"\\],\"depth\":1,\"refund\":0,\"opName\":\"ADD\"} -{\"pc\":6,\"op\":3,\"gas\":\"0xf4234\",\"gasCost\":\"0x3\",\"memSize\":0,\"stack\":\\[\"0x0\",\"0x4\"\\],\"depth\":1,\"refund\":0,\"opName\":\"SUB\"} -") - + add_subdirectory(evmone-cli) endif() -add_subdirectory(blockchaintest) add_subdirectory(export) -add_subdirectory(statetest) -add_subdirectory(t8n) - -get_directory_property(ALL_TESTS TESTS) -set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) - diff --git a/evmone/test/integration/blockchaintest/CMakeLists.txt b/evmone/test/integration/blockchaintest/CMakeLists.txt deleted file mode 100644 index f15ee53..0000000 --- a/evmone/test/integration/blockchaintest/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -# evmone: Fast Ethereum Virtual Machine implementation -# Copyright 2022 The evmone Authors. -# SPDX-License-Identifier: Apache-2.0 - -# Integration tests for evmone-blockchaintest. - -set(PREFIX ${PREFIX}/blockchaintest) -set(TESTS1 ${CMAKE_CURRENT_SOURCE_DIR}) - -add_test( - NAME ${PREFIX}/json_test - COMMAND evmone-blockchaintest ${TESTS1}/test.json -) -set_tests_properties( - ${PREFIX}/json_test PROPERTIES - # Make sure both tests in the file are executed (both should fail). - PASS_REGULAR_EXPRESSION ".*2 tests from" -) - -# Exercise block-level gas accounting (EIP-7778). -add_test( - NAME ${PREFIX}/eip7778_block_gas - COMMAND evmone-blockchaintest ${TESTS1}/eip7778_block_gas.json -) - -# A block invalid by its signature must not be judged by another check. -add_test( - NAME ${PREFIX}/unrecovered_sender_blob_gas - COMMAND evmone-blockchaintest ${TESTS1}/unrecovered_sender_blob_gas.json -) -add_test( - NAME ${PREFIX}/unrecovered_sender_deposit_log - COMMAND evmone-blockchaintest ${TESTS1}/unrecovered_sender_deposit_log.json -) - -get_directory_property(ALL_TESTS TESTS) -set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) diff --git a/evmone/test/integration/evmone-cli/CMakeLists.txt b/evmone/test/integration/evmone-cli/CMakeLists.txt new file mode 100644 index 0000000..0b2d11e --- /dev/null +++ b/evmone/test/integration/evmone-cli/CMakeLists.txt @@ -0,0 +1,49 @@ +# evmone: Fast Ethereum Virtual Machine implementation +# Copyright 2021 The evmone Authors. +# SPDX-License-Identifier: Apache-2.0 + +# Integration tests for the `evmone` command itself: what belongs to no subcommand. + +set(PREFIX ${PREFIX}/evmone-cli) + +# A subcommand's own name among another's arguments is an argument. CLI11 would otherwise +# take it as a second command and run that one instead, exiting 0 with the wrong work done: +# the dispatch tries run first, so a trailing "run" hijacked every other subcommand. +add_test(NAME ${PREFIX}/one_command_per_invocation COMMAND evmone-cli + t8n --state.fork Cancun run 6000) +set_tests_properties( + ${PREFIX}/one_command_per_invocation PROPERTIES + PASS_REGULAR_EXPRESSION "arguments were not expected" + FAIL_REGULAR_EXPRESSION "Result: *success") + +add_test(NAME ${PREFIX}/one_command_per_invocation_reversed COMMAND evmone-cli + run 6000 t8n) +set_tests_properties( + ${PREFIX}/one_command_per_invocation_reversed PROPERTIES + PASS_REGULAR_EXPRESSION "argument was not expected: t8n" + FAIL_REGULAR_EXPRESSION "Result: *success") + +# A PASS_REGULAR_EXPRESSION makes CTest ignore the exit code, so run them again for that alone. +add_test(NAME ${PREFIX}/one_command_per_invocation_exit_code COMMAND evmone-cli + t8n --state.fork Cancun run 6000) +set_tests_properties( + ${PREFIX}/one_command_per_invocation_exit_code PROPERTIES WILL_FAIL TRUE) + +# t8n is given its required option here: without one the second command exits non-zero by itself, +# so the exit code would say nothing about whether it was taken as a command. +add_test(NAME ${PREFIX}/one_command_per_invocation_reversed_exit_code COMMAND evmone-cli + run 6000 t8n --state.fork Cancun) +set_tests_properties( + ${PREFIX}/one_command_per_invocation_reversed_exit_code PROPERTIES WILL_FAIL TRUE) + +add_test(NAME ${PREFIX}/version COMMAND evmone-cli --version) +set_tests_properties( + ${PREFIX}/version PROPERTIES PASS_REGULAR_EXPRESSION "evmone") + + +get_directory_property(ALL_TESTS TESTS) +set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) + +add_subdirectory(run) +add_subdirectory(t8n) +add_subdirectory(test) diff --git a/evmone/test/integration/evmone-cli/run/CMakeLists.txt b/evmone/test/integration/evmone-cli/run/CMakeLists.txt new file mode 100644 index 0000000..f3fb53a --- /dev/null +++ b/evmone/test/integration/evmone-cli/run/CMakeLists.txt @@ -0,0 +1,43 @@ +# evmone: Fast Ethereum Virtual Machine implementation +# Copyright 2021 The evmone Authors. +# SPDX-License-Identifier: Apache-2.0 + +# Integration tests for the `evmone run` subcommand. + +set(PREFIX ${PREFIX}/run) + +add_test(NAME ${PREFIX}/run COMMAND evmone-cli run 6000) +set_tests_properties( + ${PREFIX}/run PROPERTIES PASS_REGULAR_EXPRESSION + "Result: success\nGas used: 3\n") + +add_test(NAME ${PREFIX}/run_rev COMMAND evmone-cli run --rev TangerineWhistle 00) +set_tests_properties( + ${PREFIX}/run_rev PROPERTIES PASS_REGULAR_EXPRESSION + "Executing on TangerineWhistle") + +add_test(NAME ${PREFIX}/run_bad_rev COMMAND evmone-cli run --rev NoSuchRev 00) +set_tests_properties( + ${PREFIX}/run_bad_rev PROPERTIES PASS_REGULAR_EXPRESSION + "unknown revision: NoSuchRev") + +# Code read from a file rather than the command line, which the @ prefix asks for. The file is +# checked in beside the test, so it is read and edited like every other fixture here. +add_test(NAME ${PREFIX}/file_input COMMAND evmone-cli run @${CMAKE_CURRENT_SOURCE_DIR}/code.hex) +set_tests_properties( + ${PREFIX}/file_input PROPERTIES PASS_REGULAR_EXPRESSION + # An unread file runs as empty code, which succeeds too; the gas is the file's code running. + "Result: success\nGas used: 29\n") + +# What each tracer writes is pinned by the tracing unit tests, which drive them directly. These +# two are the app-level flags reaching the VM, which only a run of the command shows. +add_test(NAME ${PREFIX}/histogram COMMAND evmone-cli --histogram run 6000808080800101010200) +set_tests_properties( + ${PREFIX}/histogram PROPERTIES PASS_REGULAR_EXPRESSION "--- # HISTOGRAM depth=0") + +add_test(NAME ${PREFIX}/trace COMMAND evmone-cli --trace run 60006002800103) +set_tests_properties( + ${PREFIX}/trace PROPERTIES PASS_REGULAR_EXPRESSION "\"opName\":\"PUSH1\"") + +get_directory_property(ALL_TESTS TESTS) +set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) diff --git a/evmone/test/integration/evmone-cli/run/code.hex b/evmone/test/integration/evmone-cli/run/code.hex new file mode 100644 index 0000000..f57b637 --- /dev/null +++ b/evmone/test/integration/evmone-cli/run/code.hex @@ -0,0 +1 @@ +60 00 80 80 80 80 01 01 01 02 00 diff --git a/evmone/test/integration/t8n/CMakeLists.txt b/evmone/test/integration/evmone-cli/t8n/CMakeLists.txt similarity index 69% rename from evmone/test/integration/t8n/CMakeLists.txt rename to evmone/test/integration/evmone-cli/t8n/CMakeLists.txt index ae83fe2..4ab1e75 100644 --- a/evmone/test/integration/t8n/CMakeLists.txt +++ b/evmone/test/integration/evmone-cli/t8n/CMakeLists.txt @@ -6,6 +6,19 @@ set(PREFIX ${PREFIX}/t8n) +# Nothing removes what a case writes, so a run which wrote nothing would be checked against the +# output of the last one. Every case waits for this. +add_test(NAME ${PREFIX}/clean_output_dirs COMMAND ${CMAKE_COMMAND} -E rm -rf + ${CMAKE_CURRENT_BINARY_DIR}/london_base_fee + ${CMAKE_CURRENT_BINARY_DIR}/berlin_base_fee + ${CMAKE_CURRENT_BINARY_DIR}/cancun_create_tx + ${CMAKE_CURRENT_BINARY_DIR}/cancun_create_tx_trace + ${CMAKE_CURRENT_BINARY_DIR}/prague_empty_requests + ${CMAKE_CURRENT_BINARY_DIR}/prague_pre_state_only + ${CMAKE_CURRENT_BINARY_DIR}/osaka_blob_schedule + ${CMAKE_CURRENT_BINARY_DIR}/chainid_hex_max) +set_tests_properties(${PREFIX}/clean_output_dirs PROPERTIES FIXTURES_SETUP outputs) + set(TEST_CASE london_base_fee) add_test( @@ -22,21 +35,18 @@ add_test( --output.basedir ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE} --output.result out.json ) -set_tests_properties(${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED ${TEST_CASE}) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") add_test( NAME ${PREFIX}/${TEST_CASE}/out.json COMMAND ${CMAKE_COMMAND} -E cat ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/out.json ) -string( - JOIN ".*" EXPECTED_OUT - # check computed base fee: - [=["currentBaseFee": "0x8"]=] -) set_tests_properties( ${PREFIX}/${TEST_CASE}/out.json PROPERTIES FIXTURES_CLEANUP ${TEST_CASE} - PASS_REGULAR_EXPRESSION ${EXPECTED_OUT} + # What the fee comes to is a unit test; this is --output.result reaching the file named. + PASS_REGULAR_EXPRESSION [=["currentBaseFee"]=] ) @@ -56,7 +66,8 @@ add_test( --output.basedir ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE} --output.result out.json ) -set_tests_properties(${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED ${TEST_CASE}) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") add_test( NAME ${PREFIX}/${TEST_CASE}/out.json @@ -65,7 +76,8 @@ add_test( set_tests_properties( ${PREFIX}/${TEST_CASE}/out.json PROPERTIES FIXTURES_CLEANUP ${TEST_CASE} - # currentBaseFee should not be in the output before London: + PASS_REGULAR_EXPRESSION [=["gasUsed"]=] + # Only a run shows that --state.fork Berlin reaches t8n as a revision before London. FAIL_REGULAR_EXPRESSION currentBaseFee ) @@ -88,38 +100,28 @@ add_test( --output.alloc outAlloc.json --opcode.count opcodes.json ) -set_tests_properties(${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED ${TEST_CASE}) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") add_test( NAME ${PREFIX}/${TEST_CASE}/out.json COMMAND ${CMAKE_COMMAND} -E cat ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/out.json ) -string( - JOIN ".*" EXPECTED_OUT - # Create blob transaction should be rejected: - [=["error": "TransactionException.TYPE_3_TX_CONTRACT_CREATION"]=] -) set_tests_properties( ${PREFIX}/${TEST_CASE}/out.json PROPERTIES FIXTURES_CLEANUP ${TEST_CASE} - PASS_REGULAR_EXPRESSION ${EXPECTED_OUT} + PASS_REGULAR_EXPRESSION [=["gasUsed"]=] ) add_test( NAME ${PREFIX}/${TEST_CASE}/outAlloc.json COMMAND ${CMAKE_COMMAND} -E cat ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/outAlloc.json ) -string( - JOIN ".*" EXPECTED_OUT_ALLOC - # Beacon Roots system contract has new entry: - [=["0x000f3df6d732807ef1319fb7b8bb8522d0beac02": .*"storage": \{[^}]*"0x00000000000000000000000000000000000000000000000000000000000016ca": "0x0000000000000000000000000000000000000000000000000000000054c99069"]=] - # new account created with code 0x00: - [=["0x6295ee1b4f6dd65047762f924ecd367c17eabf8f": \{[^}]*"code": "0x00",]=] -) set_tests_properties( ${PREFIX}/${TEST_CASE}/outAlloc.json PROPERTIES FIXTURES_CLEANUP ${TEST_CASE} - PASS_REGULAR_EXPRESSION ${EXPECTED_OUT_ALLOC} + # What t8n computed is a unit test; this is --output.alloc reaching the file named. + PASS_REGULAR_EXPRESSION [=["0x000f3df6d732807ef1319fb7b8bb8522d0beac02"]=] ) add_test( @@ -166,23 +168,20 @@ add_test( --output.basedir ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE} --output.result out.json ) -set_tests_properties(${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED ${TEST_CASE}) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") add_test( NAME ${PREFIX}/${TEST_CASE}/out.json COMMAND ${CMAKE_COMMAND} -E cat ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/out.json ) -string( - JOIN ".*" EXPECTED_OUT - # empty requests list - [=["requests": \[\]]=] - # requests hash should be present - [=["requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]=] -) set_tests_properties( ${PREFIX}/${TEST_CASE}/out.json PROPERTIES FIXTURES_CLEANUP ${TEST_CASE} - PASS_REGULAR_EXPRESSION ${EXPECTED_OUT} + PASS_REGULAR_EXPRESSION [=["requestsHash"]=] + # The alloc holds both request contracts, so the hash is of no requests rather than of a + # collection which failed. + FAIL_REGULAR_EXPRESSION blockException ) set(TEST_CASE osaka_blob_schedule) @@ -202,7 +201,8 @@ add_test( --output.basedir ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE} --output.result out.json ) -set_tests_properties(${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED ${TEST_CASE}) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") add_test( NAME ${PREFIX}/${TEST_CASE}/out.json @@ -217,6 +217,9 @@ set_tests_properties( ${PREFIX}/${TEST_CASE}/out.json PROPERTIES FIXTURES_CLEANUP ${TEST_CASE} PASS_REGULAR_EXPRESSION ${EXPECTED_OUT} + # The alloc holds both request contracts, so the gas is a valid block's rather than a + # rejected one's. + FAIL_REGULAR_EXPRESSION blockException ) set(TEST_CASE cancun_create_tx_trace) @@ -236,7 +239,8 @@ add_test( --output.result out.json --trace ) -set_tests_properties(${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED ${TEST_CASE}) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") # The first tx in this fixture is a successful CREATE with bytecode 0x60015ff3 # (PUSH1 0x01 PUSH0 RETURN). Its trace must be written to a per-tx file whose @@ -253,10 +257,6 @@ set_tests_properties( ) -add_test(NAME ${PREFIX}/version COMMAND evmone-cli --version) -set_tests_properties( - ${PREFIX}/version PROPERTIES PASS_REGULAR_EXPRESSION "evmone") - add_test(NAME ${PREFIX}/bad_fork COMMAND evmone-cli t8n --state.fork NoSuchRev) set_tests_properties( ${PREFIX}/bad_fork PROPERTIES PASS_REGULAR_EXPRESSION "unknown revision") @@ -280,7 +280,7 @@ set(TEST_CASE prague_pre_state_only) add_test( NAME ${PREFIX}/${TEST_CASE} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/prague_empty_requests + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_CASE} COMMAND evmone-cli t8n --state.fork Prague @@ -293,7 +293,21 @@ add_test( --output.result out.json --output.body body.rlp ) -set_tests_properties(${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED ${TEST_CASE}) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") + +add_test( + NAME ${PREFIX}/${TEST_CASE}/out.json + COMMAND ${CMAKE_COMMAND} -E cat ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/out.json +) +set_tests_properties( + ${PREFIX}/${TEST_CASE}/out.json PROPERTIES + FIXTURES_CLEANUP ${TEST_CASE} + # The pre-state holds no request contract, so only the skipped system calls keep the block + # valid: this is what tells the mode apart from a reward of zero. + PASS_REGULAR_EXPRESSION [=["stateRoot"]=] + FAIL_REGULAR_EXPRESSION blockException +) add_test( NAME ${PREFIX}/${TEST_CASE}/body.rlp @@ -302,7 +316,10 @@ add_test( set_tests_properties( ${PREFIX}/${TEST_CASE}/body.rlp PROPERTIES FIXTURES_CLEANUP ${TEST_CASE} - PASS_REGULAR_EXPRESSION "^0x[0-9a-f]+" + # The whole encoding: an empty transaction list is hex too, and is what a run which never read + # --input.txs writes. + PASS_REGULAR_EXPRESSION + "^0xf84ef84c8032830186a08080801ba0468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665aea05cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc" ) diff --git a/evmone/test/integration/t8n/base_fee/alloc.json b/evmone/test/integration/evmone-cli/t8n/base_fee/alloc.json similarity index 100% rename from evmone/test/integration/t8n/base_fee/alloc.json rename to evmone/test/integration/evmone-cli/t8n/base_fee/alloc.json diff --git a/evmone/test/integration/t8n/base_fee/env.json b/evmone/test/integration/evmone-cli/t8n/base_fee/env.json similarity index 100% rename from evmone/test/integration/t8n/base_fee/env.json rename to evmone/test/integration/evmone-cli/t8n/base_fee/env.json diff --git a/evmone/test/integration/t8n/base_fee/txs.json b/evmone/test/integration/evmone-cli/t8n/base_fee/txs.json similarity index 100% rename from evmone/test/integration/t8n/base_fee/txs.json rename to evmone/test/integration/evmone-cli/t8n/base_fee/txs.json diff --git a/evmone/test/integration/t8n/cancun_create_tx/alloc.json b/evmone/test/integration/evmone-cli/t8n/cancun_create_tx/alloc.json similarity index 100% rename from evmone/test/integration/t8n/cancun_create_tx/alloc.json rename to evmone/test/integration/evmone-cli/t8n/cancun_create_tx/alloc.json diff --git a/evmone/test/integration/t8n/cancun_create_tx/env.json b/evmone/test/integration/evmone-cli/t8n/cancun_create_tx/env.json similarity index 100% rename from evmone/test/integration/t8n/cancun_create_tx/env.json rename to evmone/test/integration/evmone-cli/t8n/cancun_create_tx/env.json diff --git a/evmone/test/integration/t8n/cancun_create_tx/txs.json b/evmone/test/integration/evmone-cli/t8n/cancun_create_tx/txs.json similarity index 100% rename from evmone/test/integration/t8n/cancun_create_tx/txs.json rename to evmone/test/integration/evmone-cli/t8n/cancun_create_tx/txs.json diff --git a/evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/alloc.json b/evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/alloc.json new file mode 100644 index 0000000..11bd4b6 --- /dev/null +++ b/evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/alloc.json @@ -0,0 +1,17 @@ +{ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "", + "nonce": "0x00", + "balance": "0x02540be400" + }, + "0x00000961ef480eb55e80d19ad83579a64c007002": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + }, + "0x0000bbddc7ce488642fb579f8b00f3a590007251": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + } +} diff --git a/evmone/test/integration/t8n/osaka_blob_schedule/blobParams.json b/evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/blobParams.json similarity index 100% rename from evmone/test/integration/t8n/osaka_blob_schedule/blobParams.json rename to evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/blobParams.json diff --git a/evmone/test/integration/t8n/osaka_blob_schedule/env.json b/evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/env.json similarity index 100% rename from evmone/test/integration/t8n/osaka_blob_schedule/env.json rename to evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/env.json diff --git a/evmone/test/integration/t8n/osaka_blob_schedule/txs.json b/evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/txs.json similarity index 100% rename from evmone/test/integration/t8n/osaka_blob_schedule/txs.json rename to evmone/test/integration/evmone-cli/t8n/osaka_blob_schedule/txs.json diff --git a/evmone/test/integration/evmone-cli/t8n/prague_empty_requests/alloc.json b/evmone/test/integration/evmone-cli/t8n/prague_empty_requests/alloc.json new file mode 100644 index 0000000..11bd4b6 --- /dev/null +++ b/evmone/test/integration/evmone-cli/t8n/prague_empty_requests/alloc.json @@ -0,0 +1,17 @@ +{ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "", + "nonce": "0x00", + "balance": "0x02540be400" + }, + "0x00000961ef480eb55e80d19ad83579a64c007002": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + }, + "0x0000bbddc7ce488642fb579f8b00f3a590007251": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + } +} diff --git a/evmone/test/integration/t8n/prague_empty_requests/env.json b/evmone/test/integration/evmone-cli/t8n/prague_empty_requests/env.json similarity index 100% rename from evmone/test/integration/t8n/prague_empty_requests/env.json rename to evmone/test/integration/evmone-cli/t8n/prague_empty_requests/env.json diff --git a/evmone/test/integration/t8n/prague_empty_requests/txs.json b/evmone/test/integration/evmone-cli/t8n/prague_empty_requests/txs.json similarity index 100% rename from evmone/test/integration/t8n/prague_empty_requests/txs.json rename to evmone/test/integration/evmone-cli/t8n/prague_empty_requests/txs.json diff --git a/evmone/test/integration/t8n/osaka_blob_schedule/alloc.json b/evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/alloc.json similarity index 100% rename from evmone/test/integration/t8n/osaka_blob_schedule/alloc.json rename to evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/alloc.json diff --git a/evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/env.json b/evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/env.json new file mode 100644 index 0000000..8d582b8 --- /dev/null +++ b/evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/env.json @@ -0,0 +1,6 @@ +{ + "currentCoinbase": "0x8888f1f195afa192cfee860698584c030f4c9db1", + "currentNumber": "0x01", + "currentTimestamp": "0x54c99069", + "currentGasLimit": "0x2fefd8" +} diff --git a/evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/txs.json b/evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/txs.json new file mode 100644 index 0000000..1d64142 --- /dev/null +++ b/evmone/test/integration/evmone-cli/t8n/prague_pre_state_only/txs.json @@ -0,0 +1,15 @@ +[ + { + "to": null, + "input": "0x", + "gas": "0x186a0", + "nonce": "0x0", + "value": "0x0", + "gasPrice": "0x32", + "chainId": "0x1", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "v": "0x1b", + "r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae", + "s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc" + } +] diff --git a/evmone/test/integration/evmone-cli/test/CMakeLists.txt b/evmone/test/integration/evmone-cli/test/CMakeLists.txt new file mode 100644 index 0000000..5ec2f2d --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/CMakeLists.txt @@ -0,0 +1,106 @@ +# evmone: Fast Ethereum Virtual Machine implementation +# Copyright 2021 The evmone Authors. +# SPDX-License-Identifier: Apache-2.0 + +# Integration tests for running test fixture files. + +set(PREFIX ${PREFIX}/test) + +set(DATA ${CMAKE_CURRENT_SOURCE_DIR}) + +# One run takes both formats: eip7778_block_gas.json declares its format, the other two are +# recognised by shape. +add_test(NAME ${PREFIX}/test_mixed_formats COMMAND evmone-cli test + ${DATA}/statetest/tests1/SuiteA/test1.json ${DATA}/blockchaintest/eip7778_block_gas.json + ${DATA}/blockchaintest/unrecovered_sender_blob_gas.json) +set_tests_properties( + ${PREFIX}/test_mixed_formats PROPERTIES PASS_REGULAR_EXPRESSION "= 3 passed in") + +# A file recognised as "not a test" is skipped. +add_test(NAME ${PREFIX}/test_not_a_test COMMAND evmone-cli test + ${DATA}/statetest/tests1/SuiteA/index.json) +set_tests_properties( + ${PREFIX}/test_not_a_test PROPERTIES PASS_REGULAR_EXPRESSION + "SKIPPED[^\n]*index\\.json - not a test.*0 passed, 1 skipped in") + +# Check exit code for: passed, failed, skipped (PASS_REGULAR_EXPRESSION ignores exit code). +add_test(NAME ${PREFIX}/test_exit_ok COMMAND evmone-cli test ${DATA}/testcmd) +add_test(NAME ${PREFIX}/test_exit_fail COMMAND evmone-cli test ${DATA}/testcmd_fault) +set_tests_properties(${PREFIX}/test_exit_fail PROPERTIES WILL_FAIL TRUE) +add_test(NAME ${PREFIX}/test_exit_skipped COMMAND evmone-cli test ${DATA}/testcmd_skipped) +set_tests_properties(${PREFIX}/test_exit_skipped PROPERTIES WILL_FAIL TRUE) + +# A file passes on the fixtures it ran; the two it declined are named with their reasons. +add_test(NAME ${PREFIX}/test_dir_declined COMMAND evmone-cli test ${DATA}/testcmd) +set_tests_properties( + ${PREFIX}/test_dir_declined PROPERTIES PASS_REGULAR_EXPRESSION + "SKIPPED[^\n]*::a_bad_rlp - tests with invalidly rlp-encoded blocks[^\n]*\nSKIPPED[^\n]*::c_engine - unsupported fixture format.*= 1 passed in") + +# A selected "not a test" fails even when the runnable fixtures beside it are filtered out. +add_test(NAME ${PREFIX}/test_dir_fault COMMAND evmone-cli test + ${DATA}/testcmd_fault -k b_not_a_test) +set_tests_properties( + ${PREFIX}/test_dir_fault PROPERTIES PASS_REGULAR_EXPRESSION "1 failed, 0 passed") + +# A fixture whose load throws fails only itself; the one after it still runs. +add_test(NAME ${PREFIX}/test_after_exception COMMAND evmone-cli test ${DATA}/testcmd_fault) +set_tests_properties( + ${PREFIX}/test_after_exception PROPERTIES PASS_REGULAR_EXPRESSION + "collected 1 file\n.*c_load_error:\n exception.*d_bad_root:.*state root.*1 failed, 0 passed in") + +# A file is one test, so a file with nothing to run counts once as skipped however many +# fixtures it declined: all_unsupported declines twice and both are named. +add_test(NAME ${PREFIX}/test_dir_skipped COMMAND evmone-cli test ${DATA}/testcmd_skipped) +set_tests_properties( + ${PREFIX}/test_dir_skipped PROPERTIES PASS_REGULAR_EXPRESSION + "all_unsupported\\.json::a_engine - unsupported fixture format: \"blockchain_test_engine\"\nSKIPPED[^\n]*::b_engine_x - .*empty\\.json - not a test.*pre_alloc\\.json - not a test.*0 passed, 5 skipped in") + +# A filter which selects nothing deselects every file: counted apart from skipped, not named, +# and still nothing verified. +add_test(NAME ${PREFIX}/test_dir_deselected COMMAND evmone-cli test + ${DATA}/testcmd -k no_such_name) +set_tests_properties( + ${PREFIX}/test_dir_deselected PROPERTIES + PASS_REGULAR_EXPRESSION "collected 1 file\n\nd[^\n]*\n\n=+ 0 passed, 1 deselected in" + FAIL_REGULAR_EXPRESSION "skipped|SKIPPED") +add_test(NAME ${PREFIX}/test_dir_deselected_exit_code COMMAND evmone-cli test + ${DATA}/testcmd -k no_such_name) +set_tests_properties(${PREFIX}/test_dir_deselected_exit_code PROPERTIES WILL_FAIL TRUE) + +# A file the filter empties is deselected; one which declined its fixtures is still skipped. +add_test(NAME ${PREFIX}/test_dir_deselected_beside_skipped COMMAND evmone-cli test + ${DATA}/testcmd_skipped -k no_such_name) +set_tests_properties( + ${PREFIX}/test_dir_deselected_beside_skipped PROPERTIES + PASS_REGULAR_EXPRESSION "0 passed, 4 skipped, 1 deselected in") + +# The --histogram output reaches the report, and the progress row keeps out of its way: +# a run without it ends every line with the percentage done. +add_test(NAME ${PREFIX}/test_histogram COMMAND evmone-cli test --histogram + ${DATA}/statetest/tests1/SuiteA/test1.json) +set_tests_properties( + ${PREFIX}/test_histogram PROPERTIES + PASS_REGULAR_EXPRESSION "--- # HISTOGRAM.*= 1 passed in" + FAIL_REGULAR_EXPRESSION "\\[100%\\]") + +# The --trace output likewise, and it reports the state test summary as --trace-summary does. +# What the summary holds is pinned by the runner unit tests; here it only has to appear. +add_test(NAME ${PREFIX}/test_trace COMMAND evmone-cli test --trace + ${DATA}/statetest/tests1/SuiteA/test1.json) +set_tests_properties( + ${PREFIX}/test_trace PROPERTIES + PASS_REGULAR_EXPRESSION "\\{\"pc\":0,[^\n]*\"opName\":\"PUSH1\"\\}.*\"pass\":true.*= 1 passed in" + FAIL_REGULAR_EXPRESSION "\\[100%\\]") + +# A named file is loaded whatever its extension. +add_test(NAME ${PREFIX}/test_file_unparsed COMMAND evmone-cli test + ${DATA}/statetest/tests1/SuiteA/notes.txt) +set_tests_properties( + ${PREFIX}/test_file_unparsed PROPERTIES PASS_REGULAR_EXPRESSION + "FAILED[^\n]*notes\\.txt - exception[^\n]*parse error.*1 failed, 0 passed in") + +get_directory_property(ALL_TESTS TESTS) +set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) + +add_subdirectory(blockchaintest) +add_subdirectory(statetest) diff --git a/evmone/test/integration/evmone-cli/test/blockchaintest/CMakeLists.txt b/evmone/test/integration/evmone-cli/test/blockchaintest/CMakeLists.txt new file mode 100644 index 0000000..1035452 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/blockchaintest/CMakeLists.txt @@ -0,0 +1,98 @@ +# evmone: Fast Ethereum Virtual Machine implementation +# Copyright 2022 The evmone Authors. +# SPDX-License-Identifier: Apache-2.0 + +# Integration tests for `evmone test` over blockchain test fixtures. + +set(PREFIX ${PREFIX}/blockchaintest) +set(TESTS1 ${CMAKE_CURRENT_SOURCE_DIR}) + +add_test( + NAME ${PREFIX}/json_test + COMMAND evmone-cli test ${TESTS1}/test.json +) +set_tests_properties( + ${PREFIX}/json_test PROPERTIES + # Both fixtures of the file run and both fail; the file itself counts once. + PASS_REGULAR_EXPRESSION "-call\\] - canonical.*-callcode\\] - canonical.*1 failed, 0 passed" +) + +# Exercise block-level gas accounting (EIP-7778), with tracing on so that flag is covered too. +add_test( + NAME ${PREFIX}/eip7778_block_gas + COMMAND evmone-cli test ${TESTS1}/eip7778_block_gas.json --trace +) +set_tests_properties( + ${PREFIX}/eip7778_block_gas PROPERTIES + # The trace is --trace reaching the VM; the count stands in for the exit code the regex hides. + PASS_REGULAR_EXPRESSION "\"opName\":\"SSTORE\".*= 1 passed in" +) + +# A block invalid by its signature must not be judged by another check. +add_test( + NAME ${PREFIX}/unrecovered_sender_blob_gas + COMMAND evmone-cli test ${TESTS1}/unrecovered_sender_blob_gas.json +) +add_test( + NAME ${PREFIX}/unrecovered_sender_deposit_log + COMMAND evmone-cli test ${TESTS1}/unrecovered_sender_deposit_log.json +) + +# A block with expectException but no rlp_decoded is a shape the loader does not support, so the +# only test in this file is skipped and the run verifies nothing. +add_test( + NAME ${PREFIX}/unsupported_rlp + COMMAND evmone-cli test ${TESTS1}/unsupported_rlp.json +) +set_tests_properties( + ${PREFIX}/unsupported_rlp PROPERTIES + # Why it is skipped is a loader unit test; this is the tool's whole-file skip verdict. + PASS_REGULAR_EXPRESSION "0 passed, 1 skipped" +) + +# A PASS_REGULAR_EXPRESSION makes CTest ignore the exit code, so run it again for that alone. +add_test( + NAME ${PREFIX}/unsupported_rlp_exit_code + COMMAND evmone-cli test ${TESTS1}/unsupported_rlp.json +) +set_tests_properties(${PREFIX}/unsupported_rlp_exit_code PROPERTIES WILL_FAIL TRUE) + +# The directory form registers one test per file, which none of the tests above reach, and skips +# the unsupported one rather than failing on it. +add_test( + NAME ${PREFIX}/directory + COMMAND evmone-cli test ${TESTS1} +) +set_tests_properties( + ${PREFIX}/directory PROPERTIES + PASS_REGULAR_EXPRESSION "SKIPPED [^\n]*unsupported_rlp\\.json::[^\n]* - tests with invalidly" +) + +# A test which gives up gives up only itself. Collected from a directory the whole file is one +# test, so the test after the one which stopped has to report too. +add_test( + NAME ${PREFIX}/stops_at_one_test + COMMAND evmone-cli test ${TESTS1} +) +set_tests_properties( + ${PREFIX}/stops_at_one_test PROPERTIES + PASS_REGULAR_EXPRESSION + "a_stops_early:.*block validity.*b_still_runs:.*post state root" + # Every failure repeats the test name, so a second one means it did not stop. + FAIL_REGULAR_EXPRESSION "a_stops_early:.*a_stops_early:" +) + +# --collect-only lists what would run instead of running it, and --ignore drops a path from that +# list. Over a directory a test is listed as its file. +add_test( + NAME ${PREFIX}/collect_only_directory + COMMAND evmone-cli test ${TESTS1} --collect-only --ignore unsupported_rlp.json +) +set_tests_properties( + ${PREFIX}/collect_only_directory PROPERTIES + PASS_REGULAR_EXPRESSION "eip7778_block_gas\\.json" + FAIL_REGULAR_EXPRESSION "unsupported_rlp" +) + +get_directory_property(ALL_TESTS TESTS) +set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) diff --git a/evmone/test/integration/blockchaintest/eip7778_block_gas.json b/evmone/test/integration/evmone-cli/test/blockchaintest/eip7778_block_gas.json similarity index 82% rename from evmone/test/integration/blockchaintest/eip7778_block_gas.json rename to evmone/test/integration/evmone-cli/test/blockchaintest/eip7778_block_gas.json index 326c57e..986a02f 100644 --- a/evmone/test/integration/blockchaintest/eip7778_block_gas.json +++ b/evmone/test/integration/evmone-cli/test/blockchaintest/eip7778_block_gas.json @@ -49,23 +49,33 @@ "code": "0x00", "nonce": "0x01", "balance": "0x00" + }, + "0x0000bff46984e3725691fa540a8c7589300d8282": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + }, + "0x000064d678505ad48f8ccb093bc65613800e8282": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" } }, "blocks": [ { - "rlp": "0xf90285f90259a00000000000000000000000000000000000000000000000000000000000000001a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794000000000000000000000000000000000000c014a09906207bb91a552f2c9036ff75ffebd15d6262fe1f1dc40afa6e047158ee25e5a06e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efea0c220348cfca03fd761c800a2af15752cee5ba04f836e61f798c4c6222c0cab02b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800184010000008265960c80a000000000000000000000000000000000000000000000000000000000000000008800000000000000000ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218080a00000000000000000000000000000000000000000000000000000000000000000a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e6a502e30180800e830186a094000000000000000000000000000000000000c0de8080c0800101c0c0", + "rlp": "0xf90285f90259a00000000000000000000000000000000000000000000000000000000000000001a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794000000000000000000000000000000000000c014a0bb45ed1059039065164fcd9b9fdea4962c9b11ae5064585db4795cb62027f76aa06e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efea0b159fb124af3ff5f1f77474dab131670ad912ec17fb0026a352fd6ce1ca0f9dab9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800184010000008269e20c80a000000000000000000000000000000000000000000000000000000000000000008800000000000000000ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218080a00000000000000000000000000000000000000000000000000000000000000000a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e6a502e30180800e830186a094000000000000000000000000000000000000c0de8080c0800101c0c0", "blockHeader": { "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001", "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "coinbase": "0x000000000000000000000000000000000000c014", - "stateRoot": "0x9906207bb91a552f2c9036ff75ffebd15d6262fe1f1dc40afa6e047158ee25e5", + "stateRoot": "0xbb45ed1059039065164fcd9b9fdea4962c9b11ae5064585db4795cb62027f76a", "transactionsTrie": "0x6e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efe", - "receiptTrie": "0xc220348cfca03fd761c800a2af15752cee5ba04f836e61f798c4c6222c0cab02", + "receiptTrie": "0xb159fb124af3ff5f1f77474dab131670ad912ec17fb0026a352fd6ce1ca0f9da", "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "difficulty": "0x00", "number": "0x01", "gasLimit": "0x01000000", - "gasUsed": "0x6596", + "gasUsed": "0x69e2", "timestamp": "0x0c", "extraData": "0x", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", @@ -77,7 +87,8 @@ "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "slotNumber": "0x00", - "hash": "0x0000000000000000000000000000000000000000000000000000000000000002" + "hash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "blockAccessListHash": "0x568c97a18f5ddbf46057bd904bcb580e1a244b2db80e50f56f1a8fb08d724292" }, "transactions": [ { @@ -106,7 +117,7 @@ "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { "code": "0x", "nonce": "0x01", - "balance": "0x0254075c4c" + "balance": "0x025407421a" }, "0x000000000000000000000000000000000000c0de": { "code": "0x6000600155", @@ -122,6 +133,16 @@ "code": "0x00", "nonce": "0x01", "balance": "0x00" + }, + "0x0000bff46984e3725691fa540a8c7589300d8282": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + }, + "0x000064d678505ad48f8ccb093bc65613800e8282": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" } }, "config": { @@ -130,6 +151,9 @@ }, "genesisRLP": "0x", "sealEngine": "NoProof", - "_info": {} + "_info": { + "fixture-format": "blockchain_test", + "comment": "Declared as EEST does, so a run takes the declared-format path rather than the shape's." + } } } diff --git a/evmone/test/integration/evmone-cli/test/blockchaintest/stops_at_one_test.json b/evmone/test/integration/evmone-cli/test/blockchaintest/stops_at_one_test.json new file mode 100644 index 0000000..3e08a3c --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/blockchaintest/stops_at_one_test.json @@ -0,0 +1,128 @@ +{ + "a_stops_early": { + "network": "Amsterdam", + "genesisBlockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "coinbase": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": "0x00", + "number": "0x00", + "gasLimit": "0x01000000", + "gasUsed": "0x00", + "timestamp": "0x00", + "extraData": "0x00", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x10", + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "blobGasUsed": "0x00", + "excessBlobGas": "0x00", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "slotNumber": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", + "nonce": "0x00", + "balance": "0x02540be400" + } + }, + "blocks": [ + { + "rlp": "0xf90285f90259a00000000000000000000000000000000000000000000000000000000000000001a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794000000000000000000000000000000000000c014a09906207bb91a552f2c9036ff75ffebd15d6262fe1f1dc40afa6e047158ee25e5a06e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efea0c220348cfca03fd761c800a2af15752cee5ba04f836e61f798c4c6222c0cab02b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800184010000008265968080a000000000000000000000000000000000000000000000000000000000000000008800000000000000000ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218080a00000000000000000000000000000000000000000000000000000000000000000a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e6a502e30180800e830186a094000000000000000000000000000000000000c0de8080c0800101c0c0", + "blockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "coinbase": "0x000000000000000000000000000000000000c014", + "stateRoot": "0x9906207bb91a552f2c9036ff75ffebd15d6262fe1f1dc40afa6e047158ee25e5", + "transactionsTrie": "0x6e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efe", + "receiptTrie": "0xc220348cfca03fd761c800a2af15752cee5ba04f836e61f798c4c6222c0cab02", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": "0x00", + "number": "0x01", + "gasLimit": "0x01000000", + "gasUsed": "0x6596", + "timestamp": "0x00", + "extraData": "0x", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x0e", + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "blobGasUsed": "0x00", + "excessBlobGas": "0x00", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "slotNumber": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + "transactions": [], + "withdrawals": [], + "uncleHeaders": [] + } + ], + "lastblockhash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "postState": {}, + "config": { + "network": "Amsterdam", + "chainid": "0x01" + }, + "genesisRLP": "0x", + "sealEngine": "NoProof", + "_info": { + "comment": "Its block does not advance the timestamp, so it is invalid where the test says it is valid, and the test stops there." + } + }, + "b_still_runs": { + "_info": { + "comment": "Runs only if the test before it stopped by itself rather than taking the rest of the file with it. Its post state is wrong, so reaching it is visible." + }, + "network": "Amsterdam", + "genesisBlockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "coinbase": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": "0x00", + "number": "0x00", + "gasLimit": "0x01000000", + "gasUsed": "0x00", + "timestamp": "0x00", + "extraData": "0x00", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x10", + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "blobGasUsed": "0x00", + "excessBlobGas": "0x00", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "slotNumber": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", + "nonce": "0x00", + "balance": "0x02540be400" + } + }, + "blocks": [], + "lastblockhash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "postState": {}, + "config": { + "network": "Amsterdam", + "chainid": "0x01" + }, + "genesisRLP": "0x", + "sealEngine": "NoProof" + } +} diff --git a/evmone/test/integration/blockchaintest/test.json b/evmone/test/integration/evmone-cli/test/blockchaintest/test.json similarity index 100% rename from evmone/test/integration/blockchaintest/test.json rename to evmone/test/integration/evmone-cli/test/blockchaintest/test.json diff --git a/evmone/test/integration/blockchaintest/unrecovered_sender_blob_gas.json b/evmone/test/integration/evmone-cli/test/blockchaintest/unrecovered_sender_blob_gas.json similarity index 100% rename from evmone/test/integration/blockchaintest/unrecovered_sender_blob_gas.json rename to evmone/test/integration/evmone-cli/test/blockchaintest/unrecovered_sender_blob_gas.json diff --git a/evmone/test/integration/blockchaintest/unrecovered_sender_deposit_log.json b/evmone/test/integration/evmone-cli/test/blockchaintest/unrecovered_sender_deposit_log.json similarity index 100% rename from evmone/test/integration/blockchaintest/unrecovered_sender_deposit_log.json rename to evmone/test/integration/evmone-cli/test/blockchaintest/unrecovered_sender_deposit_log.json diff --git a/evmone/test/integration/evmone-cli/test/blockchaintest/unsupported_rlp.json b/evmone/test/integration/evmone-cli/test/blockchaintest/unsupported_rlp.json new file mode 100644 index 0000000..78540fd --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/blockchaintest/unsupported_rlp.json @@ -0,0 +1,47 @@ +{ + "unsupported_rlp": { + "_info": { + "comment": "The runner takes the sender from the fixture, so it cannot see the broken signature; the transaction executes and the wrong blobGasUsed is the first header mismatch." + }, + "network": "Cancun", + "genesisBlockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "number": "0x00", + "gasLimit": "0x01000000", + "gasUsed": "0x00", + "timestamp": "0x00", + "extraData": "0x00", + "baseFeePerGas": "0x10", + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "blobGasUsed": "0x00", + "excessBlobGas": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", + "nonce": "0x00", + "balance": "0x02540be400" + } + }, + "blocks": [ + { + "expectException": "TransactionException.INVALID_SIGNATURE_VRS", + "rlp": "0x" + } + ], + "lastblockhash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "postState": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", + "nonce": "0x00", + "balance": "0x02540be400" + } + } + } +} diff --git a/evmone/test/integration/evmone-cli/test/statetest/CMakeLists.txt b/evmone/test/integration/evmone-cli/test/statetest/CMakeLists.txt new file mode 100644 index 0000000..2333fd7 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/statetest/CMakeLists.txt @@ -0,0 +1,150 @@ +# evmone: Fast Ethereum Virtual Machine implementation +# Copyright 2022 The evmone Authors. +# SPDX-License-Identifier: Apache-2.0 + +# Integration tests for `evmone test` over state test fixtures. + +set(PREFIX ${PREFIX}/statetest) +set(TESTS1 ${CMAKE_CURRENT_SOURCE_DIR}/tests1) +set(TESTS2 ${CMAKE_CURRENT_SOURCE_DIR}/tests2) +set(TESTS_FILTER ${CMAKE_CURRENT_SOURCE_DIR}/filter) + +add_test( + NAME ${PREFIX}/no_arguments + COMMAND evmone-cli test +) +set_tests_properties( + ${PREFIX}/no_arguments PROPERTIES + PASS_REGULAR_EXPRESSION "path is required" +) + +# A test collected from a directory is listed as its file. SuiteA also holds an index.json and a +# notes.txt, neither of which is a test; both sort before test1.json, so collecting either would +# show up in the listing. +add_test( + NAME ${PREFIX}/tests1_list + COMMAND evmone-cli test ${TESTS1} --collect-only +) +set_tests_properties( + ${PREFIX}/tests1_list PROPERTIES + PASS_REGULAR_EXPRESSION "tests1[^\n]*T\\.json\n[^\n]*tests1[^\n]*test1\\.json\n[^\n]*tests1[^\n]*test2_multi\\.json" +) + +# A file named directly is listed as itself, whatever it holds. +add_test( + NAME ${PREFIX}/single_file_list + COMMAND evmone-cli test ${TESTS1}/SuiteA/test2_multi.json --collect-only +) +set_tests_properties( + ${PREFIX}/single_file_list PROPERTIES + PASS_REGULAR_EXPRESSION "^[^\n]*test2_multi\\.json\n$" +) + +# Several roots are collected in the order given, not regrouped by suite as gtest listed them. +# T.json holds no fixture, but naming it is naming a test, so it is listed like any other. +add_test( + NAME ${PREFIX}/multiple_args_list + COMMAND evmone-cli test ${TESTS1} ${TESTS2} ${TESTS1}/B/T.json ${TESTS1}/SuiteA --collect-only +) +set_tests_properties( + ${PREFIX}/multiple_args_list PROPERTIES + PASS_REGULAR_EXPRESSION "tests1[^\n]*T\\.json\n[^\n]*tests1[^\n]*test1\\.json\n[^\n]*tests1[^\n]*test2_multi\\.json\n[^\n]*tests2[^\n]*test1\\.json\n[^\n]*tests1[^\n]*T\\.json\n[^\n]*tests1[^\n]*test1\\.json\n[^\n]*tests1[^\n]*test2_multi\\.json" +) + +add_test( + NAME ${PREFIX}/invalid_path + COMMAND evmone-cli test invalid.json +) +set_tests_properties( + ${PREFIX}/invalid_path PROPERTIES + PASS_REGULAR_EXPRESSION "path: Path does not exist: invalid\\.json" +) + +add_test( + NAME ${PREFIX}/multi_test + COMMAND evmone-cli test ${TESTS1}/SuiteA/test2_multi.json +) +set_tests_properties( + ${PREFIX}/multi_test PROPERTIES + # Both cases of the file run and both fail; the file itself counts once. + PASS_REGULAR_EXPRESSION "test_case_1.*test_case_2.*1 failed, 0 passed" +) + +add_test( + NAME ${PREFIX}/filter + COMMAND evmone-cli test ${TESTS_FILTER}/one_failing_of_two.json -k passing_test_case +) +set_tests_properties( + ${PREFIX}/filter PROPERTIES + # The whole summary is what proves a case ran and passed: a count alone would hold just as + # well if the filter selected nothing, and ctest stops checking the exit code once a + # PASS_REGULAR_EXPRESSION is set. The summary names failures first, so this matches only a + # run with none. + PASS_REGULAR_EXPRESSION "= 1 passed in" + FAIL_REGULAR_EXPRESSION "failing_test_case" +) + +# The filter is applied per case inside the file's test. The summary line is what proves a case +# ran: forbidding the other name alone would hold just as well if the filter dropped every case. +add_test( + NAME ${PREFIX}/filter_directory + COMMAND evmone-cli test ${TESTS_FILTER} -k passing_test_case --trace-summary +) +set_tests_properties( + ${PREFIX}/filter_directory PROPERTIES + PASS_REGULAR_EXPRESSION "\"pass\":true" + FAIL_REGULAR_EXPRESSION "failing_test_case" +) + +# --ignore drops a path from the collection. The flags come first, as they do in CI: an --ignore +# that swallowed a positional path would leave the roots after it uncollected. +add_test( + NAME ${PREFIX}/ignore + COMMAND evmone-cli test --ignore B ${TESTS1} ${TESTS2} --collect-only +) +set_tests_properties( + ${PREFIX}/ignore PROPERTIES + PASS_REGULAR_EXPRESSION "tests1[^\n]*test1\\.json\n[^\n]*tests1[^\n]*test2_multi\\.json\n[^\n]*tests2[^\n]*test1\\.json" + FAIL_REGULAR_EXPRESSION "T\\.json" +) + +# A file named directly is ignored the same way, by a path relative to that file. +add_test( + NAME ${PREFIX}/ignore_file + COMMAND evmone-cli test --ignore test1.json + ${TESTS1}/SuiteA/test1.json ${TESTS1}/SuiteA/test2_multi.json --collect-only +) +set_tests_properties( + ${PREFIX}/ignore_file PROPERTIES + PASS_REGULAR_EXPRESSION "^[^\n]*test2_multi\\.json\n$" +) + +# Selecting nothing fails rather than passing vacuously. WILL_FAIL only asserts a nonzero exit; +# the driver unit tests pin the code itself. +add_test( + NAME ${PREFIX}/nothing_collected + COMMAND evmone-cli test ${TESTS1} --ignore B --ignore SuiteA +) +set_tests_properties(${PREFIX}/nothing_collected PROPERTIES WILL_FAIL TRUE) + +# A filter which selects nothing verifies nothing either. The file passes when it runs, so +# the nonzero exit says the filter emptied it and not that something failed. +add_test( + NAME ${PREFIX}/filter_matches_nothing + COMMAND evmone-cli test ${TESTS1}/SuiteA/test1.json -k no_such_case +) +set_tests_properties(${PREFIX}/filter_matches_nothing PROPERTIES WILL_FAIL TRUE) + +# Collection reads no file, so one which cannot be parsed is listed like any other. It fails +# when it runs, which is the only time anything reads it. +add_test( + NAME ${PREFIX}/collect_only_unloadable + COMMAND evmone-cli test ${TESTS1}/SuiteA/notes.txt --collect-only +) +set_tests_properties( + ${PREFIX}/collect_only_unloadable PROPERTIES + PASS_REGULAR_EXPRESSION "^[^\n]*notes\\.txt\n$" +) + +get_directory_property(ALL_TESTS TESTS) +set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) diff --git a/evmone/test/integration/statetest/filter/one_failing_of_two.json b/evmone/test/integration/evmone-cli/test/statetest/filter/one_failing_of_two.json similarity index 100% rename from evmone/test/integration/statetest/filter/one_failing_of_two.json rename to evmone/test/integration/evmone-cli/test/statetest/filter/one_failing_of_two.json diff --git a/evmone/test/integration/statetest/tests1/B/T.json b/evmone/test/integration/evmone-cli/test/statetest/tests1/B/T.json similarity index 100% rename from evmone/test/integration/statetest/tests1/B/T.json rename to evmone/test/integration/evmone-cli/test/statetest/tests1/B/T.json diff --git a/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/index.json b/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/index.json new file mode 100644 index 0000000..da71b73 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/index.json @@ -0,0 +1,4 @@ +{ + "comment": "A listing generated by other tools, not a test. It must not be collected.", + "tests": ["test1.json", "test2_multi.json"] +} diff --git a/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/notes.txt b/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/notes.txt new file mode 100644 index 0000000..2f579bf --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/notes.txt @@ -0,0 +1 @@ +Not a test either: only .json files are collected. diff --git a/evmone/test/integration/statetest/tests1/SuiteA/test1.json b/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/test1.json similarity index 100% rename from evmone/test/integration/statetest/tests1/SuiteA/test1.json rename to evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/test1.json diff --git a/evmone/test/integration/statetest/tests1/SuiteA/test2_multi.json b/evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/test2_multi.json similarity index 100% rename from evmone/test/integration/statetest/tests1/SuiteA/test2_multi.json rename to evmone/test/integration/evmone-cli/test/statetest/tests1/SuiteA/test2_multi.json diff --git a/evmone/test/integration/statetest/tests2/SuiteA/test1.json b/evmone/test/integration/evmone-cli/test/statetest/tests2/SuiteA/test1.json similarity index 100% rename from evmone/test/integration/statetest/tests2/SuiteA/test1.json rename to evmone/test/integration/evmone-cli/test/statetest/tests2/SuiteA/test1.json diff --git a/evmone/test/integration/evmone-cli/test/testcmd/declined.json b/evmone/test/integration/evmone-cli/test/testcmd/declined.json new file mode 100644 index 0000000..e65f339 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/testcmd/declined.json @@ -0,0 +1,99 @@ +{ + "a_bad_rlp": { + "_info": { + "comment": "Declines to load: expectException without rlp_decoded, which the loader refuses before reading anything after the blocks." + }, + "network": "Cancun", + "genesisBlockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "number": "0x00", + "gasLimit": "0x01000000", + "gasUsed": "0x00", + "timestamp": "0x00", + "extraData": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "pre": {}, + "blocks": [ + { + "expectException": "TransactionException.INVALID_SIGNATURE_VRS", + "rlp": "0x" + } + ] + }, + "b_state": { + "_info": { + "comment": "A state test beside blockchain ones, which is the whole point of deciding the format per fixture." + }, + "env": { + "currentBaseFee": "0x0a", + "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty": "0x020000", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000020000", + "currentTimestamp": "0x03e8" + }, + "post": { + "London": [ + { + "hash": "0xe8010ce590f401c9d61fef8ab05bea9bcec24281b795e5868809bc4e515aa530", + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "pre": { + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87": { + "balance": "0x0de0b6b3a7640000", + "code": "0x600160010160005500", + "nonce": "0x00", + "storage": {} + }, + "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba": { + "balance": "0x00", + "code": "0x", + "nonce": "0x01", + "storage": {} + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x0de0b6b3a7640000", + "code": "0x", + "nonce": "0x00", + "storage": {} + } + }, + "transaction": { + "data": [ + "0x" + ], + "gasLimit": [ + "0x061a80" + ], + "gasPrice": "0x0a", + "nonce": "0x00", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "value": [ + "0x0186a0" + ] + } + }, + "c_engine": { + "_info": { + "fixture-format": "blockchain_test_engine", + "comment": "Carries pre and blocks, so the shape alone would run it; the declared format is what decides, and this tool does not run that one." + }, + "pre": {}, + "blocks": [] + } +} diff --git a/evmone/test/integration/evmone-cli/test/testcmd_fault/faults.json b/evmone/test/integration/evmone-cli/test/testcmd_fault/faults.json new file mode 100644 index 0000000..968e154 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/testcmd_fault/faults.json @@ -0,0 +1,135 @@ +{ + "a_runs": { + "_info": {}, + "env": { + "currentBaseFee": "0x0a", + "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty": "0x020000", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000020000", + "currentTimestamp": "0x03e8" + }, + "post": { + "London": [ + { + "hash": "0xe8010ce590f401c9d61fef8ab05bea9bcec24281b795e5868809bc4e515aa530", + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "pre": { + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87": { + "balance": "0x0de0b6b3a7640000", + "code": "0x600160010160005500", + "nonce": "0x00", + "storage": {} + }, + "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba": { + "balance": "0x00", + "code": "0x", + "nonce": "0x01", + "storage": {} + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x0de0b6b3a7640000", + "code": "0x", + "nonce": "0x00", + "storage": {} + } + }, + "transaction": { + "data": [ + "0x" + ], + "gasLimit": [ + "0x061a80" + ], + "gasPrice": "0x0a", + "nonce": "0x00", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "value": [ + "0x0186a0" + ] + } + }, + "b_not_a_test": { + "_info": { + "comment": "Not a test: no fixture fields at all. Beside a fixture which runs, so this is unmistakably a fixture file with one broken entry in it." + } + }, + "c_load_error": { + "_info": { + "fixture-format": "state_test", + "comment": "A state test with no pre state, so loading it throws." + } + }, + "d_bad_root": { + "_info": { + "comment": "Runs and fails on the state root, reached only because the throw above did not abandon the file." + }, + "env": { + "currentBaseFee": "0x0a", + "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty": "0x020000", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000020000", + "currentTimestamp": "0x03e8" + }, + "post": { + "London": [ + { + "hash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "pre": { + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87": { + "balance": "0x0de0b6b3a7640000", + "code": "0x600160010160005500", + "nonce": "0x00", + "storage": {} + }, + "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba": { + "balance": "0x00", + "code": "0x", + "nonce": "0x01", + "storage": {} + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x0de0b6b3a7640000", + "code": "0x", + "nonce": "0x00", + "storage": {} + } + }, + "transaction": { + "data": [ + "0x" + ], + "gasLimit": [ + "0x061a80" + ], + "gasPrice": "0x0a", + "nonce": "0x00", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "value": [ + "0x0186a0" + ] + } + } +} diff --git a/evmone/test/integration/evmone-cli/test/testcmd_skipped/all_unsupported.json b/evmone/test/integration/evmone-cli/test/testcmd_skipped/all_unsupported.json new file mode 100644 index 0000000..dba3d29 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/testcmd_skipped/all_unsupported.json @@ -0,0 +1,15 @@ +{ + "a_engine": { + "_info": { + "fixture-format": "blockchain_test_engine", + "comment": "Both fixtures are named on their own, so neither reason is lost." + }, + "blocks": [] + }, + "b_engine_x": { + "_info": { + "fixture-format": "blockchain_test_engine_x" + }, + "blocks": [] + } +} diff --git a/evmone/test/integration/evmone-cli/test/testcmd_skipped/empty.json b/evmone/test/integration/evmone-cli/test/testcmd_skipped/empty.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/testcmd_skipped/empty.json @@ -0,0 +1 @@ +{} diff --git a/evmone/test/integration/evmone-cli/test/testcmd_skipped/not_an_object.json b/evmone/test/integration/evmone-cli/test/testcmd_skipped/not_an_object.json new file mode 100644 index 0000000..723839b --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/testcmd_skipped/not_an_object.json @@ -0,0 +1,7 @@ +[ + { + "comment": "A top-level array, so it holds no named fixture. Its one element would be taken for a blockchain test if the array were walked by index.", + "pre": {}, + "blocks": [] + } +] diff --git a/evmone/test/integration/evmone-cli/test/testcmd_skipped/partial_shapes.json b/evmone/test/integration/evmone-cli/test/testcmd_skipped/partial_shapes.json new file mode 100644 index 0000000..5d85ea8 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/testcmd_skipped/partial_shapes.json @@ -0,0 +1,24 @@ +{ + "a_blocks_no_pre": { + "comment": "Carries one of the two keys a blockchain test is named by, so the shape does not name it and nothing else does either.", + "blocks": [] + }, + "b_tx_no_pre": { + "comment": "The same for a state test: a transaction to apply, but no state to apply it to.", + "transaction": { + "data": [ + "0x" + ], + "gasLimit": [ + "0x061a80" + ], + "gasPrice": "0x0a", + "nonce": "0x00", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "value": [ + "0x0186a0" + ] + } + } +} diff --git a/evmone/test/integration/evmone-cli/test/testcmd_skipped/pre_alloc.json b/evmone/test/integration/evmone-cli/test/testcmd_skipped/pre_alloc.json new file mode 100644 index 0000000..015cd24 --- /dev/null +++ b/evmone/test/integration/evmone-cli/test/testcmd_skipped/pre_alloc.json @@ -0,0 +1,27 @@ +{ + "testIds": [ + "tests/ported_static/vmIOandFlowOperations/test_mload.py::test_mload[fork_Osaka-blockchain_test_engine_x]" + ], + "environment": { + "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01" + }, + "network": "Osaka", + "chainId": 1, + "pre": { + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87": { + "balance": "0x0de0b6b3a7640000", + "code": "0x600160010160005500", + "nonce": "0x00", + "storage": {} + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x0de0b6b3a7640000", + "code": "0x", + "nonce": "0x00", + "storage": {} + } + }, + "comment": "EEST keeps the pre-allocation its fixtures share beside them, and none of it is a test. Several entries, none of which names a format or carries a shape." +} diff --git a/evmone/test/integration/export/CMakeLists.txt b/evmone/test/integration/export/CMakeLists.txt index 6df940b..3f3d142 100644 --- a/evmone/test/integration/export/CMakeLists.txt +++ b/evmone/test/integration/export/CMakeLists.txt @@ -31,7 +31,7 @@ set_tests_properties( add_test( NAME ${PREFIX}/execute_exported_state_tests # TODO: Broken exported tests are filtered out. - COMMAND evmone-statetest ${EXPORT_DIR}/state_tests --gtest_filter=-*block.* + COMMAND evmone-cli test ${EXPORT_DIR}/state_tests --ignore state_transition/block ) set_tests_properties( ${PREFIX}/execute_exported_state_tests PROPERTIES diff --git a/evmone/test/integration/statetest/CMakeLists.txt b/evmone/test/integration/statetest/CMakeLists.txt deleted file mode 100644 index 1548a6c..0000000 --- a/evmone/test/integration/statetest/CMakeLists.txt +++ /dev/null @@ -1,147 +0,0 @@ -# evmone: Fast Ethereum Virtual Machine implementation -# Copyright 2022 The evmone Authors. -# SPDX-License-Identifier: Apache-2.0 - -# Integration tests for evmone-statetest. - -set(PREFIX ${PREFIX}/statetest) -set(TESTS1 ${CMAKE_CURRENT_SOURCE_DIR}/tests1) -set(TESTS2 ${CMAKE_CURRENT_SOURCE_DIR}/tests2) -set(TESTS_TX ${CMAKE_CURRENT_SOURCE_DIR}/tx) -set(TESTS_FILTER ${CMAKE_CURRENT_SOURCE_DIR}/filter) - -add_test( - NAME ${PREFIX}/no_arguments - COMMAND evmone-statetest -) -set_tests_properties( - ${PREFIX}/no_arguments PROPERTIES - PASS_REGULAR_EXPRESSION "path is required" -) - -add_test( - NAME ${PREFIX}/tests1_list - COMMAND evmone-statetest ${TESTS1} --gtest_list_tests -) -set_tests_properties( - ${PREFIX}/tests1_list PROPERTIES - PASS_REGULAR_EXPRESSION [[ -B\. - T -SuiteA\. - test1 - test2_multi -]] -) - -add_test( - NAME ${PREFIX}/single_file_list - COMMAND evmone-statetest ${TESTS1}/SuiteA/test1.json --gtest_list_tests -) -set_tests_properties( - ${PREFIX}/single_file_list PROPERTIES - PASS_REGULAR_EXPRESSION [[ -.*test/integration/statetest/tests1/SuiteA/test1\.json\. - test1 -]] -) - -add_test( - NAME ${PREFIX}/multiple_args_list - COMMAND evmone-statetest ${TESTS1} ${TESTS2} ${TESTS1}/B/T.json ${TESTS1}/SuiteA --gtest_list_tests -) -set_tests_properties( - ${PREFIX}/multiple_args_list PROPERTIES - PASS_REGULAR_EXPRESSION [[ -B\. - T -SuiteA\. - test1 - test2_multi - test1 -\. - test1 - test2_multi -]] -) - -add_test( - NAME ${PREFIX}/invalid_path - COMMAND evmone-statetest invalid.json -) -set_tests_properties( - ${PREFIX}/invalid_path PROPERTIES - PASS_REGULAR_EXPRESSION "path: Path does not exist: invalid\\.json" -) - -add_test( - NAME ${PREFIX}/multi_test - COMMAND evmone-statetest ${TESTS1}/SuiteA/test2_multi.json -) -set_tests_properties( - ${PREFIX}/multi_test PROPERTIES - # Make sure both tests in the file are executed (both should fail). - PASS_REGULAR_EXPRESSION "test_case_1.*test_case_2" -) - -add_test( - NAME ${PREFIX}/trace - COMMAND evmone-statetest ${TESTS1}/SuiteA/test1.json --trace -) -set(EXPECTED_TRACE [[ -{"pc":0,"op":96,"gas":"0x5c878","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"} -{"pc":2,"op":96,"gas":"0x5c875","gasCost":"0x3","memSize":0,"stack":["0x1"],"depth":1,"refund":0,"opName":"PUSH1"} -{"pc":4,"op":1,"gas":"0x5c872","gasCost":"0x3","memSize":0,"stack":["0x1","0x1"],"depth":1,"refund":0,"opName":"ADD"} -{"pc":5,"op":96,"gas":"0x5c86f","gasCost":"0x3","memSize":0,"stack":["0x2"],"depth":1,"refund":0,"opName":"PUSH1"} -{"pc":7,"op":85,"gas":"0x5c86c","gasCost":"0x0","memSize":0,"stack":["0x2","0x0"],"depth":1,"refund":0,"opName":"SSTORE"} -{"pc":8,"op":0,"gas":"0x57218","gasCost":"0x0","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"STOP"} -{"pass":true,"gasUsed":"0xa868","stateRoot":"0xe8010ce590f401c9d61fef8ab05bea9bcec24281b795e5868809bc4e515aa530"} -]]) -# Escape regex special characters. -string(REPLACE "{" "\\{" EXPECTED_TRACE ${EXPECTED_TRACE}) -string(REPLACE "}" "\\}" EXPECTED_TRACE ${EXPECTED_TRACE}) -string(REPLACE "[" "\\[" EXPECTED_TRACE ${EXPECTED_TRACE}) -string(REPLACE "]" "\\]" EXPECTED_TRACE ${EXPECTED_TRACE}) -set_tests_properties( - ${PREFIX}/trace PROPERTIES - PASS_REGULAR_EXPRESSION ${EXPECTED_TRACE} -) - -add_test( - NAME ${PREFIX}/tx_invalid_nonce - COMMAND evmone-statetest ${TESTS_TX}/invalid_nonce.json -) -set_tests_properties( - ${PREFIX}/tx_invalid_nonce PROPERTIES - PASS_REGULAR_EXPRESSION "unexpected invalid transaction: TransactionException.NONCE_MISMATCH_TOO_HIGH" -) - -add_test( - NAME ${PREFIX}/tx_invalid_signature - COMMAND evmone-statetest ${TESTS_TX}/invalid_signature.json -) -set_tests_properties( - ${PREFIX}/tx_invalid_signature PROPERTIES - PASS_REGULAR_EXPRESSION "unexpected invalid transaction: TransactionException.INVALID_SIGNATURE_VRS" -) - -add_test( - NAME ${PREFIX}/tx_invalid_encoding - COMMAND evmone-statetest ${TESTS_TX}/invalid_encoding.json -) -set_tests_properties( - ${PREFIX}/tx_invalid_encoding PROPERTIES - PASS_REGULAR_EXPRESSION "unexpected invalid transaction: invalid transaction encoding" -) - -add_test( - NAME ${PREFIX}/filter - COMMAND evmone-statetest ${TESTS_FILTER}/one_failing_of_two.json -k passing_test_case -) -set_tests_properties( - ${PREFIX}/filter PROPERTIES - FAIL_REGULAR_EXPRESSION "failing_test_case" -) - -get_directory_property(ALL_TESTS TESTS) -set_tests_properties(${ALL_TESTS} PROPERTIES ENVIRONMENT LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/integration-%p.profraw) diff --git a/evmone/test/integration/statetest/tx/invalid_encoding.json b/evmone/test/integration/statetest/tx/invalid_encoding.json deleted file mode 100644 index 7b5286b..0000000 --- a/evmone/test/integration/statetest/tx/invalid_encoding.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "invalid_encoding": { - "env": { - "currentBaseFee": "0x0a", - "currentCoinbase": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - "currentDifficulty": "0x020000", - "currentGasLimit": "0xff112233445566", - "currentNumber": "0x01", - "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000020000", - "currentTimestamp": "0x03e8" - }, - "post": { - "Shanghai": [ - { - "hash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "indexes": { - "data": 0, - "gas": 0, - "value": 0 - }, - "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "txbytes": "0xc0" - } - ] - }, - "pre": {}, - "transaction": { - "data": [ - "0x" - ], - "gasLimit": [ - "0x5208" - ], - "gasPrice": "0x0a", - "nonce": "0x00", - "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - "to": "", - "value": [ - "0x00" - ] - } - } -} diff --git a/evmone/test/integration/statetest/tx/invalid_nonce.json b/evmone/test/integration/statetest/tx/invalid_nonce.json deleted file mode 100644 index 77fb0de..0000000 --- a/evmone/test/integration/statetest/tx/invalid_nonce.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "invalid_nonce": { - "env": { - "currentBaseFee": "0x0a", - "currentCoinbase": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - "currentDifficulty": "0x020000", - "currentGasLimit": "0xff112233445566", - "currentNumber": "0x01", - "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000020000", - "currentTimestamp": "0x03e8" - }, - "post": { - "Shanghai": [ - { - "hash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "indexes": { - "data": 0, - "gas": 0, - "value": 0 - }, - "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - } - ] - }, - "pre": {}, - "transaction": { - "data": [ - "0x" - ], - "gasLimit": [ - "0x00" - ], - "gasPrice": "0x0a", - "nonce": "0x01", - "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - "to": "0x00", - "value": [ - "0x00" - ] - } - } -} diff --git a/evmone/test/integration/statetest/tx/invalid_signature.json b/evmone/test/integration/statetest/tx/invalid_signature.json deleted file mode 100644 index 43de114..0000000 --- a/evmone/test/integration/statetest/tx/invalid_signature.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "invalid_signature": { - "env": { - "currentBaseFee": "0x0a", - "currentCoinbase": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - "currentDifficulty": "0x020000", - "currentGasLimit": "0xff112233445566", - "currentNumber": "0x01", - "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000020000", - "currentTimestamp": "0x03e8" - }, - "post": { - "Shanghai": [ - { - "hash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "indexes": { - "data": 0, - "gas": 0, - "value": 0 - }, - "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "txbytes": "0xeb800a8252088080801b01a0fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" - } - ] - }, - "pre": {}, - "transaction": { - "data": [ - "0x" - ], - "gasLimit": [ - "0x5208" - ], - "gasPrice": "0x0a", - "nonce": "0x00", - "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - "to": "", - "value": [ - "0x00" - ] - } - } -} diff --git a/evmone/test/integration/t8n/prague_empty_requests/alloc.json b/evmone/test/integration/t8n/prague_empty_requests/alloc.json deleted file mode 100644 index 5e93050..0000000 --- a/evmone/test/integration/t8n/prague_empty_requests/alloc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { - "code": "", - "nonce": "0x00", - "balance": "0x02540be400" - } -} diff --git a/evmone/test/internal_benchmarks/CMakeLists.txt b/evmone/test/internal_benchmarks/CMakeLists.txt index c156144..33b6e69 100644 --- a/evmone/test/internal_benchmarks/CMakeLists.txt +++ b/evmone/test/internal_benchmarks/CMakeLists.txt @@ -4,12 +4,12 @@ add_executable( evmone-bench-internal - evmmax_bench.cpp + modarith_bench.cpp find_jumpdest_bench.cpp memory_allocation.cpp lru_cache_bench.cpp ) # TODO: Not sure why evmone::precompiles must be here, but otherwise this don't link. -target_link_libraries(evmone-bench-internal PRIVATE evmone evmone::precompiles evmone::evmmax benchmark::benchmark) +target_link_libraries(evmone-bench-internal PRIVATE evmone evmone::precompiles benchmark::benchmark) target_include_directories(evmone-bench-internal PRIVATE ${evmone_private_include_dir}) diff --git a/evmone/test/internal_benchmarks/evmmax_bench.cpp b/evmone/test/internal_benchmarks/modarith_bench.cpp similarity index 52% rename from evmone/test/internal_benchmarks/evmmax_bench.cpp rename to evmone/test/internal_benchmarks/modarith_bench.cpp index 7f8f7b3..8b2ca8e 100644 --- a/evmone/test/internal_benchmarks/evmmax_bench.cpp +++ b/evmone/test/internal_benchmarks/modarith_bench.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #include -#include +#include using namespace intx; @@ -12,10 +12,12 @@ namespace constexpr auto bn254 = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47_u256; constexpr auto secp256k1 = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f_u256; +// Each pair of operations forms a single dependency chain, so the reported time is the latency. + template -void evmmax_add(benchmark::State& state) +void modarith_add(benchmark::State& state) { - const evmmax::ModArith m{Mod}; + const evmone::crypto::ModArith m{Mod}; auto a = Mod / 2; auto b = Mod / 3; @@ -24,12 +26,14 @@ void evmmax_add(benchmark::State& state) a = m.add(a, b); b = m.add(b, a); } + benchmark::DoNotOptimize(a); + benchmark::DoNotOptimize(b); } template -void evmmax_sub(benchmark::State& state) +void modarith_sub(benchmark::State& state) { - const evmmax::ModArith m{Mod}; + const evmone::crypto::ModArith m{Mod}; auto a = Mod / 2; auto b = Mod / 3; @@ -38,12 +42,14 @@ void evmmax_sub(benchmark::State& state) a = m.sub(a, b); b = m.sub(b, a); } + benchmark::DoNotOptimize(a); + benchmark::DoNotOptimize(b); } template -void evmmax_mul(benchmark::State& state) +void modarith_mul(benchmark::State& state) { - const evmmax::ModArith m{Mod}; + const evmone::crypto::ModArith m{Mod}; auto a = m.to_mont(Mod / 2); auto b = m.to_mont(Mod / 3); @@ -52,12 +58,14 @@ void evmmax_mul(benchmark::State& state) a = m.mul(a, b); b = m.mul(b, a); } + benchmark::DoNotOptimize(a); + benchmark::DoNotOptimize(b); } } // namespace -BENCHMARK_TEMPLATE(evmmax_add, uint256, bn254); -BENCHMARK_TEMPLATE(evmmax_add, uint256, secp256k1); -BENCHMARK_TEMPLATE(evmmax_sub, uint256, bn254); -BENCHMARK_TEMPLATE(evmmax_sub, uint256, secp256k1); -BENCHMARK_TEMPLATE(evmmax_mul, uint256, bn254); -BENCHMARK_TEMPLATE(evmmax_mul, uint256, secp256k1); +BENCHMARK_TEMPLATE(modarith_add, uint256, bn254); +BENCHMARK_TEMPLATE(modarith_add, uint256, secp256k1); +BENCHMARK_TEMPLATE(modarith_sub, uint256, bn254); +BENCHMARK_TEMPLATE(modarith_sub, uint256, secp256k1); +BENCHMARK_TEMPLATE(modarith_mul, uint256, bn254); +BENCHMARK_TEMPLATE(modarith_mul, uint256, secp256k1); diff --git a/evmone/test/precompiles_bench/precompiles_bench.cpp b/evmone/test/precompiles_bench/precompiles_bench.cpp index 2df05ce..b0714e3 100644 --- a/evmone/test/precompiles_bench/precompiles_bench.cpp +++ b/evmone/test/precompiles_bench/precompiles_bench.cpp @@ -23,6 +23,7 @@ namespace { using namespace evmone::state; using namespace evmone::test; +using enum PrecompileId; /// The revision used for the precompile benchmarks. constexpr auto REV = EVMC_OSAKA; @@ -251,6 +252,7 @@ void modexp(benchmark::State& state) intx::be::unsafe::store(&input[32], intx::uint256{exp_len}); intx::be::unsafe::store(&input[64], intx::uint256{base_mod_len}); const std::span payload{&input[3 * 32], payload_len}; + // TODO: The base is always odd, so the even base path of the power-of-two part is not covered. std::fill_n(&payload[0], base_mod_len, 0xff); std::fill_n(&payload[base_mod_len + 1], exp_len - 1, 0xff); payload[base_mod_len] = 0xff >> exp_clz; @@ -324,58 +326,21 @@ BENCHMARK(modexp) MODEXP_ARGS; #endif #undef MODEXP_ARGS -BENCHMARK_TEMPLATE(precompile, PrecompileId::identity, identity_execute); - -namespace bench_ecrecovery -{ -constexpr auto evmone = ecrecover_execute_evmone; -BENCHMARK_TEMPLATE(precompile, PrecompileId::ecrecover, evmone); +BENCHMARK(precompile); +BENCHMARK(precompile); #ifdef EVMONE_PRECOMPILES_LIBSECP256K1 -constexpr auto libsecp256k1 = ecrecover_execute_libsecp256k1; -BENCHMARK_TEMPLATE(precompile, PrecompileId::ecrecover, libsecp256k1); +BENCHMARK(precompile); #endif -} // namespace bench_ecrecovery - -namespace bench_expmod -{ -constexpr auto evmone = expmod_execute_evmone; -BENCHMARK(precompile); +BENCHMARK(precompile); #ifdef EVMONE_PRECOMPILES_GMP -constexpr auto gmp = expmod_execute_gmp; -BENCHMARK(precompile); +BENCHMARK(precompile); #endif -} // namespace bench_expmod - -namespace bench_ecadd -{ -constexpr auto evmmax_cpp = ecadd_execute; -BENCHMARK_TEMPLATE(precompile, PrecompileId::ecadd, evmmax_cpp); -} // namespace bench_ecadd - -namespace bench_ecmul -{ -constexpr auto evmmax_cpp = ecmul_execute; -BENCHMARK_TEMPLATE(precompile, PrecompileId::ecmul, evmmax_cpp); -} // namespace bench_ecmul - -namespace bench_ecpairing -{ -constexpr auto evmmax_cpp = ecpairing_execute; -BENCHMARK_TEMPLATE(precompile, PrecompileId::ecpairing, evmmax_cpp); -} // namespace bench_ecpairing - -namespace bench_kzg -{ -constexpr auto evmone_blst = point_evaluation_execute; -BENCHMARK_TEMPLATE(precompile, PrecompileId::point_evaluation, evmone_blst); -} // namespace bench_kzg +BENCHMARK(precompile); +BENCHMARK(precompile); +BENCHMARK(precompile); +BENCHMARK(precompile); +BENCHMARK(precompile); } // namespace -namespace bench_p256verify -{ -constexpr auto evmone_cpp = p256verify_execute; -BENCHMARK_TEMPLATE(precompile, PrecompileId::p256verify, evmone_cpp); -} // namespace bench_p256verify - BENCHMARK_MAIN(); diff --git a/evmone/test/state/CMakeLists.txt b/evmone/test/state/CMakeLists.txt index 29b9fc6..eea7591 100644 --- a/evmone/test/state/CMakeLists.txt +++ b/evmone/test/state/CMakeLists.txt @@ -14,6 +14,8 @@ target_sources( account.hpp authorization.hpp authorization.cpp + bal.hpp + bal.cpp blob_params.hpp block.hpp block.cpp @@ -47,29 +49,31 @@ option(EVMONE_PRECOMPILES_LIBSECP256K1 "Enable precompiles implementations using if(EVMONE_PRECOMPILES_LIBSECP256K1) FetchContent_Declare( libsecp256k1 - URL https://github.com/bitcoin-core/secp256k1/archive/refs/tags/v0.7.1.tar.gz - URL_HASH SHA256=958f204dbafc117e73a2604285dc2eb2a5128344d3499c114dcba5de54cb7a9e + URL https://github.com/bitcoin-core/secp256k1/archive/refs/tags/v0.8.0.tar.gz + URL_HASH SHA256=eb52b0e9239dff7dc26be5f9623567141b8720ec47da29eb3c1e0a660d17c8bb ) - # TODO(cmake-3.25): Use block(). - set(BUILD_SHARED_LIBS_ORIG ${BUILD_SHARED_LIBS}) - set(BUILD_SHARED_LIBS OFF) - set(SECP256K1_ENABLE_MODULE_ECDH OFF) - set(SECP256K1_ENABLE_MODULE_RECOVERY ON) - set(SECP256K1_ENABLE_MODULE_EXTRAKEYS OFF) - set(SECP256K1_ENABLE_MODULE_SCHNORRSIG OFF) - set(SECP256K1_ENABLE_MODULE_MUSIG OFF) - set(SECP256K1_ENABLE_MODULE_ELLSWIFT OFF) - # On x86_64 using asm is ~1% worse. Untested on ARM. - set(SECP256K1_ASM OFF) - set(SECP256K1_BUILD_BENCHMARK OFF) - set(SECP256K1_BUILD_TESTS OFF) - set(SECP256K1_BUILD_EXHAUSTIVE_TESTS OFF) - set(SECP256K1_BUILD_CTIME_TESTS OFF) - set(SECP256K1_BUILD_EXAMPLES OFF) - # Not needed for static library. - set(SECP256K1_ENABLE_API_VISIBILITY_ATTRIBUTES OFF) - set(SECP256K1_VALGRIND OFF) - FetchContent_MakeAvailable(libsecp256k1) + block() + set(BUILD_SHARED_LIBS OFF) + set(SECP256K1_ENABLE_MODULE_ELLSWIFT OFF) + set(SECP256K1_ENABLE_MODULE_MUSIG OFF) + set(SECP256K1_ENABLE_MODULE_SILENTPAYMENTS OFF) + set(SECP256K1_ENABLE_MODULE_SCHNORRSIG OFF) + set(SECP256K1_ENABLE_MODULE_EXTRAKEYS OFF) + set(SECP256K1_ENABLE_MODULE_RECOVERY ON) + set(SECP256K1_ENABLE_MODULE_ECDH OFF) + # On x86_64 using asm is ~1% worse. Untested on ARM. + set(SECP256K1_ASM OFF) + set(SECP256K1_BUILD_BENCHMARK OFF) + set(SECP256K1_BUILD_TESTS OFF) + set(SECP256K1_BUILD_EXHAUSTIVE_TESTS OFF) + set(SECP256K1_BUILD_CTIME_TESTS OFF) + set(SECP256K1_INSTALL OFF) + set(SECP256K1_BUILD_EXAMPLES OFF) + # Not needed for static library. + set(SECP256K1_ENABLE_API_VISIBILITY_ATTRIBUTES OFF) + set(SECP256K1_VALGRIND OFF) + FetchContent_MakeAvailable(libsecp256k1) + endblock() foreach(TARGET secp256k1 secp256k1_precomputed) set_target_properties( ${TARGET} @@ -87,7 +91,6 @@ if(EVMONE_PRECOMPILES_LIBSECP256K1) precompiles_libsecp256k1.hpp precompiles_libsecp256k1.cpp ) - set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_ORIG}) endif() option(EVMONE_PRECOMPILES_GMP "Enable precompiles implementations via the GMP/MPIR library" OFF) diff --git a/evmone/test/state/account.hpp b/evmone/test/state/account.hpp index b135ec7..6643073 100644 --- a/evmone/test/state/account.hpp +++ b/evmone/test/state/account.hpp @@ -24,6 +24,10 @@ struct StorageValue bytes32 original; evmc_access_status access_status = EVMC_ACCESS_COLD; + + /// The value has been loaded from the initial state. + /// A slot may be warmed (EIP-2929) but not read (EIP-7928). + bool loaded = false; }; /// The state account. @@ -59,8 +63,14 @@ struct Account /// Revertible. evmc_access_status access_status = EVMC_ACCESS_COLD; - /// The account does not exist in the state. - /// Revertible, usually false→true. + /// The account has been loaded from the initial state by its first read. + /// Warming (EIP-2929) is not a read (EIP-7928). + /// Not revertible: a read stays a read. + bool loaded = false; + + /// The account does not exist in the state: it is missing from the initial state or its + /// creation has been reverted. Meaningful only for a loaded account. + /// Revertible, cleared when the account is created. bool nonexistent = false; /// The account has been destructed and should be erased at the end of a transaction. @@ -78,18 +88,12 @@ struct Account bool erase_if_empty = false; /// The account has been created in the current transaction. - /// - /// FIXME: Not reverted on CREATE rollback; a leaked value suppresses the - /// EIP-161 touch-delete of the now-empty account (state-root divergence). bool just_created = false; /// This account's code has been modified. - /// - /// FIXME: Not reverted on CREATE rollback; a leaked value adds a - /// false-positive code entry to the state diff. bool code_changed = false; - /// If the account has non-empty initial storage (when accessing the cold account). + /// If the account has non-empty initial storage (set when the account is loaded). bool has_initial_storage = false; [[nodiscard]] bool is_empty() const noexcept diff --git a/evmone/test/state/authorization.cpp b/evmone/test/state/authorization.cpp index 399e1d7..db9098b 100644 --- a/evmone/test/state/authorization.cpp +++ b/evmone/test/state/authorization.cpp @@ -34,7 +34,7 @@ std::optional
recover_authority(const Authorization& auth) noexcept const auto h = compute_authorization_signing_hash(auth); const auto r = intx::be::store(auth.r); const auto s = intx::be::store(auth.s); - return evmmax::secp256k1::ecrecover( - h.bytes, r.bytes, s.bytes, auth.y_parity == 1, evmmax::secp256k1::RecoveryMode::strict); + return crypto::secp256k1::ecrecover( + h.bytes, r.bytes, s.bytes, auth.y_parity == 1, crypto::secp256k1::RecoveryMode::strict); } } // namespace evmone::state diff --git a/evmone/test/state/bal.cpp b/evmone/test/state/bal.cpp new file mode 100644 index 0000000..b68fea6 --- /dev/null +++ b/evmone/test/state/bal.cpp @@ -0,0 +1,148 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "bal.hpp" +#include "../utils/rlp.hpp" +#include "account.hpp" + +namespace evmone::state +{ +// The RLP encoders of the BlockAccessList parts. They are static, not in an unnamed namespace, +// because rlp::encode() finds them through ADL. + +template +static bytes rlp_encode(const BlockAccessList::Change& c) +{ + return rlp::encode_tuple(uint64_t{c.index}, c.value); +} + +static bytes rlp_encode(const BlockAccessList::SlotChanges& s) +{ + return rlp::encode_tuple(s.slot, s.changes); +} + +static bytes rlp_encode(const BlockAccessList::AccountChanges& a) +{ + return rlp::encode_tuple(a.addr, a.storage_changes, a.storage_reads, a.balance_changes, + a.nonce_changes, a.code_changes); +} + +bytes BlockAccessList::rlp_encode() const +{ + // TODO: rlp::encode() asserts lengths below 16 MiB (3 length bytes). The storage reads exceed + // it from a block gas limit of about 1.1G (one item per GAS_PER_ITEM gas). + return rlp::encode(accounts); +} + +hash256 BlockAccessList::hash() const +{ + return keccak256(rlp_encode()); +} + +bool BlockAccessList::exceeds_gas_limit(uint64_t block_gas_limit) const noexcept +{ + uint64_t items = 0; + for (const auto& acc : accounts) // The reads are disjoint from the writes. + items += 1 + acc.storage_changes.size() + acc.storage_reads.size(); + return items > block_gas_limit / GAS_PER_ITEM; +} + +namespace +{ +/// Records the value at the index unless it is the one the index started with: the last recorded +/// or the initial one. The last index gets two diffs (the withdrawals and the block-end system +/// calls), so a change already recorded at the index is replaced. +template +void record(std::vector>& changes, uint32_t index, const T& value, + const T& initial) +{ + if (!changes.empty() && changes.back().index == index) + changes.pop_back(); + assert(changes.empty() || changes.back().index < index); + if (value != (changes.empty() ? initial : changes.back().value)) + changes.push_back({index, value}); +} +} // namespace + +void BalBuilder::record_diff( + uint32_t index, const StateDiff& diff, const StateView& pre_block_state) +{ + for (const auto& [addr, keys] : diff.reads) + { + // A read account is in the BAL even without changes. + auto& reads = m_accounts[addr].storage_reads; + reads.insert(reads.end(), keys.begin(), keys.end()); + } + + const auto listed = [&](const address& addr) -> AccountData& { + auto& ad = m_accounts[addr]; + if (!ad.initial.has_value()) // A missing account reads as an empty one. + { + ad.initial = pre_block_state.get_account(addr).value_or( + StateView::Account{.code_hash = Account::EMPTY_CODE_HASH}); + } + return ad; + }; + + for (const auto& entry : diff.modified_accounts) + { + auto& ad = listed(entry.addr); + record(ad.nonce_changes, index, entry.nonce, ad.initial->nonce); + record(ad.balance_changes, index, entry.balance, ad.initial->balance); + // The code may be set to the same bytes, e.g. by a pair of self-cancelling + // authorizations in a transaction (EIP-7702). + if (entry.code.has_value()) + { + record( + ad.code_changes, index, *entry.code, pre_block_state.get_account_code(entry.addr)); + } + for (const auto& [slot, value] : entry.modified_storage) + { + auto& writes = ad.storage_writes[slot]; // A diff lists only the changed slots. + assert(writes.empty() || writes.back().index < index); + writes.push_back({index, intx::be::load(value)}); + } + } + + for (const auto& addr : diff.deleted_accounts) + { + auto& ad = listed(addr); + // Only a created or an empty account is deleted (EIP-161, EIP-6780). + assert(ad.initial->nonce == 0 && ad.initial->code_hash == Account::EMPTY_CODE_HASH); + record(ad.balance_changes, index, uint256{0}, ad.initial->balance); + } +} + +BlockAccessList BalBuilder::build() +{ + BlockAccessList bal; + bal.accounts.reserve(m_accounts.size()); + for (auto& [addr, data] : m_accounts) + { + auto& acc = bal.accounts.emplace_back(); + acc.addr = addr; + + acc.storage_changes.reserve(data.storage_writes.size()); + for (auto& [slot, writes] : data.storage_writes) + acc.storage_changes.push_back({intx::be::load(slot), std::move(writes)}); + std::ranges::sort(acc.storage_changes, {}, &BlockAccessList::SlotChanges::slot); + + for (const auto& slot : data.storage_reads) + { + if (!data.storage_writes.contains(slot)) // A written slot is not a read. + acc.storage_reads.push_back(intx::be::load(slot)); + } + std::ranges::sort(acc.storage_reads); + acc.storage_reads.erase( + std::ranges::unique(acc.storage_reads).begin(), acc.storage_reads.end()); + + acc.balance_changes = std::move(data.balance_changes); + acc.nonce_changes = std::move(data.nonce_changes); + acc.code_changes = std::move(data.code_changes); + } + std::ranges::sort(bal.accounts, {}, &BlockAccessList::AccountChanges::addr); + m_accounts.clear(); + return bal; +} +} // namespace evmone::state diff --git a/evmone/test/state/bal.hpp b/evmone/test/state/bal.hpp new file mode 100644 index 0000000..16aec37 --- /dev/null +++ b/evmone/test/state/bal.hpp @@ -0,0 +1,81 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "hash_utils.hpp" +#include "state_diff.hpp" +#include "state_view.hpp" + +namespace evmone::state +{ +/// The block-level access list (EIP-7928): the storage writes and the balance, nonce and code +/// changes per block access index (0 for the block-start system calls, i+1 for the transaction i, +/// n+1 for the withdrawals and the block-end system calls) and the storage reads of the block. +/// The keccak256 hash of its RLP encoding is the header's blockAccessListHash. +struct BlockAccessList +{ + /// The gas the block gas limit must cover per item (EIP-7928). + static constexpr uint64_t GAS_PER_ITEM = 2000; + + /// A value set at a block access index. + template + struct Change + { + uint32_t index = 0; + T value; + }; + + struct SlotChanges + { + uint256 slot; + std::vector> changes; ///< sorted by index, unique + }; + + struct AccountChanges + { + address addr; + std::vector storage_changes; ///< sorted by slot + std::vector storage_reads; ///< sorted, disjoint from the writes + std::vector> balance_changes; + std::vector> nonce_changes; + std::vector> code_changes; + }; + + std::vector accounts; ///< sorted by addr, unique + + [[nodiscard]] bytes rlp_encode() const; + + [[nodiscard]] hash256 hash() const; + + /// Whether the items (the accounts and their unique storage slots) exceed + /// block_gas_limit / GAS_PER_ITEM. + [[nodiscard]] bool exceeds_gas_limit(uint64_t block_gas_limit) const noexcept; +}; + +/// Builds the BlockAccessList from the state diffs of a block. +class BalBuilder +{ + struct AccountData + { + std::unordered_map>> storage_writes; + std::vector storage_reads; ///< With duplicates; build() sorts and deduplicates. + std::vector> balance_changes; + std::vector> nonce_changes; + std::vector> code_changes; + + /// The account before the block, fetched the first time a diff lists it. + std::optional initial; + }; + std::unordered_map m_accounts; + +public: + /// Records the reads and the state changes of a transaction or a system call at @p index. + /// A change is against the one recorded last or, if none, against @p pre_block_state, + /// so every diff applied to the block state must be recorded, in order. + void record_diff(uint32_t index, const StateDiff& diff, const StateView& pre_block_state); + + /// Sorts the records into the BlockAccessList, leaving the builder empty. + [[nodiscard]] BlockAccessList build(); +}; +} // namespace evmone::state diff --git a/evmone/test/state/errors.hpp b/evmone/test/state/errors.hpp index 2a66edd..8a60033 100644 --- a/evmone/test/state/errors.hpp +++ b/evmone/test/state/errors.hpp @@ -51,6 +51,8 @@ enum ErrorCode : int // NOLINT(*-use-enum-class) INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT, UNKNOWN_PARENT, INVALID_BLOCK_NUMBER, + INVALID_BLOCK_HASH, + INVALID_BAL_HASH, // Block requests collection (EIP-7685). INVALID_DEPOSIT_EVENT_LAYOUT, @@ -135,6 +137,10 @@ inline const std::error_category& evmone_category() noexcept return "BlockException.UNKNOWN_PARENT"; case INVALID_BLOCK_NUMBER: return "BlockException.INVALID_BLOCK_NUMBER"; + case INVALID_BLOCK_HASH: + return "BlockException.INVALID_BLOCK_HASH"; + case INVALID_BAL_HASH: + return "BlockException.INVALID_BAL_HASH"; case INVALID_DEPOSIT_EVENT_LAYOUT: return "BlockException.INVALID_DEPOSIT_EVENT_LAYOUT"; case SYSTEM_CONTRACT_EMPTY: diff --git a/evmone/test/state/host.cpp b/evmone/test/state/host.cpp index bbefb81..35704c0 100644 --- a/evmone/test/state/host.cpp +++ b/evmone/test/state/host.cpp @@ -6,13 +6,14 @@ #include "precompiles.hpp" #include "system_contracts.hpp" #include +#include namespace evmone::state { bool Host::account_exists(const address& addr) const noexcept { - const auto* const acc = m_state.find(addr); - return acc != nullptr && (m_rev < EVMC_SPURIOUS_DRAGON || !acc->is_empty()); + const auto& acc = m_state.get(addr); + return !acc.nonexistent && (m_rev < EVMC_SPURIOUS_DRAGON || !acc.is_empty()); } bytes32 Host::get_storage(const address& addr, const bytes32& key) const noexcept @@ -26,8 +27,10 @@ evmc_storage_status Host::set_storage( // Follow EVMC documentation https://evmc.ethereum.org/storagestatus.html#autotoc_md3 // and EIP-2200 specification https://eips.ethereum.org/EIPS/eip-2200. + assert(!m_state.get(addr).nonexistent); auto& storage_slot = m_state.get_storage(addr, key); - const auto& [current, original, _] = storage_slot; + const auto& current = storage_slot.current; + const auto& original = storage_slot.original; const auto dirty = original != current; const auto restored = original == value; @@ -69,14 +72,12 @@ evmc_storage_status Host::set_storage( uint256be Host::get_balance(const address& addr) const noexcept { - const auto* const acc = m_state.find(addr); - return (acc != nullptr) ? intx::be::store(acc->balance) : uint256be{}; + return intx::be::store(m_state.get(addr).balance); } uint64_t Host::get_nonce(const address& addr) const noexcept { - const auto* const acc = m_state.find(addr); - return (acc != nullptr) ? acc->nonce : 0; + return m_state.get(addr).nonce; } namespace @@ -111,11 +112,11 @@ size_t Host::get_code_size(const address& addr) const noexcept bytes32 Host::get_code_hash(const address& addr) const noexcept { - const auto* const acc = m_state.find(addr); - if (acc == nullptr || acc->is_empty()) + const auto& acc = m_state.get(addr); + if (acc.is_empty()) return {}; - return acc->code_hash; + return acc.code_hash; } size_t Host::copy_code(const address& addr, size_t code_offset, uint8_t* buffer_data, @@ -130,14 +131,13 @@ size_t Host::copy_code(const address& addr, size_t code_offset, uint8_t* buffer_ bool Host::selfdestruct(const address& addr, const address& beneficiary) noexcept { - if (m_state.find(beneficiary) == nullptr) - m_state.journal_new_account(beneficiary); auto& acc = m_state.get(addr); + assert(!acc.nonexistent); const auto balance = acc.balance; auto& beneficiary_acc = m_state.touch(beneficiary); - m_state.journal_balance_change(beneficiary, beneficiary_acc.balance); - m_state.journal_balance_change(addr, balance); + m_state.journal_balance_change(beneficiary_acc); + m_state.journal_balance_change(acc); if (m_rev >= EVMC_CANCUN && !acc.just_created) { @@ -169,7 +169,7 @@ bool Host::selfdestruct(const address& addr, const address& beneficiary) noexcep // Mark the destruction if not done already. if (!acc.destructed) { - m_state.journal_account_flags(addr, acc); + m_state.journal_account_flags(acc); acc.destructed = true; return true; } @@ -181,35 +181,31 @@ evmc::Result Host::create(const evmc_message& msg) noexcept assert(msg.kind == EVMC_CREATE || msg.kind == EVMC_CREATE2); assert(msg.recipient != address{}); // Must be computed already. - // TODO: find()+insert() probes m_modified twice for a new recipient. - auto* new_acc = m_state.find(msg.recipient); - if (new_acc == nullptr) - { - new_acc = &m_state.insert(msg.recipient); - m_state.journal_new_account(msg.recipient); - } - else + auto [new_acc, created] = m_state.get_or_create(msg.recipient); + if (!created) { - if (is_create_collision(*new_acc)) - return evmc::Result{EVMC_FAILURE}; // TODO: Add EVMC errors for creation failures. - m_state.journal_create(msg.recipient); + // TODO: Add EVMC errors for creation failures. + if (is_create_collision(new_acc)) + return evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; + m_state.journal_create(new_acc); } - assert(new_acc != nullptr); - assert(new_acc->nonce == 0); + assert(new_acc.nonce == 0); + assert(!new_acc.just_created && !new_acc.code_changed); // The create rollback clears them. if (m_rev >= EVMC_SPURIOUS_DRAGON) - new_acc->nonce = 1; // No need to journal: create revert will 0 the nonce. + new_acc.nonce = 1; // No need to journal: create revert will 0 the nonce. - new_acc->just_created = true; + new_acc.just_created = true; auto& sender_acc = m_state.get(msg.sender); // TODO: Duplicated account lookup. + assert(!sender_acc.nonexistent); const auto value = intx::be::load(msg.value); assert(sender_acc.balance >= value && "EVM must guarantee balance"); - m_state.journal_balance_change(msg.sender, sender_acc.balance); - m_state.journal_balance_change(msg.recipient, new_acc->balance); + m_state.journal_balance_change(sender_acc); + m_state.journal_balance_change(new_acc); sender_acc.balance -= value; - new_acc->balance += value; // The new account may be prefunded. + new_acc.balance += value; // The new account may be prefunded. if (m_rev >= EVMC_AMSTERDAM) emit_transfer_log(m_logs, msg.sender, msg.recipient, value); @@ -229,30 +225,43 @@ evmc::Result Host::create(const evmc_message& msg) noexcept const size_t max_code_size = m_rev >= EVMC_AMSTERDAM ? MAX_CODE_SIZE_AMSTERDAM : MAX_CODE_SIZE; if (m_rev >= EVMC_SPURIOUS_DRAGON && code.size() > max_code_size) - return evmc::Result{EVMC_FAILURE}; + return evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; // Reject new contract code starting with the 0xEF byte (EIP-3541). if (m_rev >= EVMC_LONDON && code.starts_with(0xEF)) - return evmc::Result{EVMC_CONTRACT_VALIDATION_FAILURE}; + return evmc::Result{EVMC_CONTRACT_VALIDATION_FAILURE, {.left = msg.state_gas}}; - // Code deployment cost. - const auto cost = std::ssize(code) * 200; - gas_left -= cost; - if (gas_left < 0) + StateGas state_gas{result.state_gas}; // The initcode's state-gas for code deposit. + if (m_rev >= EVMC_AMSTERDAM) { - return (m_rev == EVMC_FRONTIER) ? - evmc::Result{EVMC_SUCCESS, result.gas_left, result.gas_refund} : - evmc::Result{EVMC_FAILURE}; + // The code deposit splits into an execution-gas and a state-gas component (EIP-8037). + const auto execution_cost = 6 * ((std::ssize(code) + 31) / 32); + const auto state_cost = std::ssize(code) * COST_PER_STATE_BYTE; + gas_left -= execution_cost; + if (gas_left < 0 || !state_gas.charge(gas_left, state_cost)) + return evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; + } + else + { + // Code deployment cost. + const auto cost = std::ssize(code) * 200; + gas_left -= cost; + if (gas_left < 0) + { + return (m_rev == EVMC_FRONTIER) ? + evmc::Result{EVMC_SUCCESS, result.gas_left, result.gas_refund} : + evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; + } } if (!code.empty()) { - new_acc->code_hash = keccak256(code); - new_acc->code = code; - new_acc->code_changed = true; + new_acc.code_hash = keccak256(code); + new_acc.code = code; + new_acc.code_changed = true; } - return evmc::Result{result.status_code, gas_left, result.gas_refund}; + return evmc::Result{result.status_code, gas_left, result.gas_refund, state_gas}; } evmc::Result Host::execute_message(const evmc_message& msg) noexcept @@ -262,11 +271,6 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept if (msg.kind == EVMC_CALL) { - auto* recipient_acc = m_state.find(msg.recipient); - if (recipient_acc == nullptr) - m_state.journal_new_account(msg.recipient); - // TODO: Both branches will insert new account so better to do it in common path. - if (evmc::is_zero(msg.value)) { m_state.touch(msg.recipient); @@ -276,18 +280,18 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept // We skip touching if we send value, because account cannot end up empty. // It will either have value, or code that transfers this value out, or will be // selfdestructed anyway. - if (recipient_acc == nullptr) - recipient_acc = &m_state.insert(msg.recipient); + auto& recipient_acc = m_state.get_or_create(msg.recipient).first; // Transfer value: sender → recipient. // The sender's balance is already checked therefore the sender account must exist. const auto value = intx::be::load(msg.value); auto& sender_acc = m_state.get(msg.sender); + assert(!sender_acc.nonexistent); assert(sender_acc.balance >= value); - m_state.journal_balance_change(msg.sender, sender_acc.balance); - m_state.journal_balance_change(msg.recipient, recipient_acc->balance); + m_state.journal_balance_change(sender_acc); + m_state.journal_balance_change(recipient_acc); sender_acc.balance -= value; - recipient_acc->balance += value; + recipient_acc.balance += value; if (m_rev >= EVMC_AMSTERDAM) emit_transfer_log(m_logs, msg.sender, msg.recipient, value); @@ -300,8 +304,8 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept // TODO: get_code() performs the account lookup. Add a way to get an account with code? const auto code = m_state.get_code(msg.code_address); - if (code.empty()) - return evmc::Result{EVMC_SUCCESS, msg.gas}; // Skip trivial execution. + if (code.empty()) // Skip trivial execution. + return evmc::Result{EVMC_SUCCESS, msg.gas, 0, {.left = msg.state_gas}}; return m_vm.execute(*this, m_rev, msg, code.data(), code.size()); } @@ -312,8 +316,9 @@ evmc::Result Host::call(const evmc_message& msg) noexcept { // Bump the creator's nonce (already done for depth 0). Not reverted if creation fails. auto& sender_acc = m_state.get(msg.sender); + assert(!sender_acc.nonexistent); assert(sender_acc.nonce != MAX_NONCE); - m_state.journal_bump_nonce(msg.sender); + m_state.journal_bump_nonce(sender_acc); ++sender_acc.nonce; } @@ -324,15 +329,14 @@ evmc::Result Host::call(const evmc_message& msg) noexcept if (result.status_code != EVMC_SUCCESS) { + assert(result.state_gas.left == msg.state_gas); + assert(result.state_gas.spilled == 0); + // The 0x03 (RIPEMD-160) touch quirk: a touch on this address is // never reverted. It only matters when the account is empty, so gate it by rev range. static constexpr auto ADDR_03 = 0x03_address; - bool is_03_touched = false; - if (m_rev < EVMC_PARIS && m_rev >= EVMC_SPURIOUS_DRAGON) [[unlikely]] - { - const auto* const acc_03 = m_state.find(ADDR_03); - is_03_touched = acc_03 != nullptr && acc_03->erase_if_empty; - } + const bool is_03_touched = m_rev < EVMC_PARIS && m_rev >= EVMC_SPURIOUS_DRAGON && + m_state.get(ADDR_03).erase_if_empty; // Revert. m_state.rollback(state_checkpoint); @@ -354,19 +358,19 @@ evmc_tx_context Host::get_tx_context() const noexcept const auto effective_gas_price = m_block.base_fee + priority_gas_price; return evmc_tx_context{ - intx::be::store(effective_gas_price), // By EIP-1559. - m_tx.sender, - m_block.coinbase, - m_block.number, - m_block.timestamp, - m_block.gas_limit, - m_block.prev_randao, - uint256be{m_block.chain_id}, - uint256be{m_block.base_fee}, - intx::be::store(m_block.blob_base_fee.value_or(0)), - m_tx.blob_hashes.data(), - m_tx.blob_hashes.size(), - m_block.slot_number.value_or(0), + .tx_gas_price = intx::be::store(effective_gas_price), // By EIP-1559. + .tx_origin = m_tx.sender, + .block_coinbase = m_block.coinbase, + .block_number = m_block.number, + .block_timestamp = m_block.timestamp, + .block_gas_limit = m_block.gas_limit, + .block_prev_randao = m_block.prev_randao, + .chain_id = uint256be{m_block.chain_id}, + .block_base_fee = uint256be{m_block.base_fee}, + .blob_base_fee = intx::be::store(m_block.blob_base_fee.value_or(0)), + .blob_hashes = m_tx.blob_hashes.data(), + .blob_hashes_count = m_tx.blob_hashes.size(), + .block_slot_number = m_block.slot_number.value_or(0), }; } @@ -386,27 +390,21 @@ evmc_access_status Host::access_account(const address& addr) noexcept if (m_rev < EVMC_BERLIN) return EVMC_ACCESS_COLD; // Ignore before Berlin. - auto* acc = m_state.find(addr); - - if (acc != nullptr && acc->access_status == EVMC_ACCESS_WARM) - return EVMC_ACCESS_WARM; - if (is_precompile(m_rev, addr)) // Precompiles are always warm. Don't insert to state. return EVMC_ACCESS_WARM; - // TODO: On a modified-set miss the account is looked up twice. This can be improved with - // a try_emplace-like API, but the miss happens only in ~39% of the calls on Mainnet. - if (acc == nullptr) - acc = &m_state.insert(addr, {.erase_if_empty = true}); + auto& acc = m_state.access(addr); + if (acc.access_status == EVMC_ACCESS_WARM) + return EVMC_ACCESS_WARM; - m_state.journal_account_flags(addr, *acc); - acc->access_status = EVMC_ACCESS_WARM; + m_state.journal_account_flags(acc); + acc.access_status = EVMC_ACCESS_WARM; return EVMC_ACCESS_COLD; } evmc_access_status Host::access_storage(const address& addr, const bytes32& key) noexcept { - auto& storage_slot = m_state.get_storage(addr, key); + auto& storage_slot = m_state.access_storage(addr, key); if (storage_slot.access_status == EVMC_ACCESS_WARM) return EVMC_ACCESS_WARM; // Nothing changes, skip journaling. m_state.journal_storage_change(storage_slot); @@ -425,7 +423,9 @@ evmc::bytes32 Host::get_transient_storage(const address& addr, const bytes32& ke void Host::set_transient_storage( const address& addr, const bytes32& key, const bytes32& value) noexcept { - auto& slot = m_state.get(addr).transient_storage[key]; + auto& acc = m_state.get(addr); + assert(!acc.nonexistent); + auto& slot = acc.transient_storage[key]; m_state.journal_transient_storage_change(slot); slot = value; } diff --git a/evmone/test/state/host.hpp b/evmone/test/state/host.hpp index 4b2ffcf..1ea7dc9 100644 --- a/evmone/test/state/host.hpp +++ b/evmone/test/state/host.hpp @@ -32,9 +32,9 @@ class Host : public evmc::Host evmc::Result call(const evmc_message& msg) noexcept override; -private: [[nodiscard]] bool account_exists(const address& addr) const noexcept override; +private: [[nodiscard]] bytes32 get_storage( const address& addr, const bytes32& key) const noexcept override; diff --git a/evmone/test/state/precompiles.cpp b/evmone/test/state/precompiles.cpp index 557eb1b..ea83c76 100644 --- a/evmone/test/state/precompiles.cpp +++ b/evmone/test/state/precompiles.cpp @@ -333,7 +333,7 @@ ExecutionResult ecrecover_execute_evmone(const uint8_t* input, size_t input_size return {EVMC_SUCCESS, 0}; const auto& [hash, sig_bytes, parity] = *o; - const auto res = evmmax::secp256k1::ecrecover( + const auto res = crypto::secp256k1::ecrecover( hash, sig_bytes.subspan<0, 32>(), sig_bytes.subspan<32, 32>(), parity); if (!res) return {EVMC_SUCCESS, 0}; @@ -357,7 +357,7 @@ ExecutionResult ecrecover_execute_libsecp256k1(const uint8_t* input, size_t inpu if (!ecrecover_libsecp256k1(pubkey, hash, sig_bytes, parity)) return {EVMC_SUCCESS, 0}; - const auto addr = evmmax::secp256k1::to_address(pubkey); + const auto addr = crypto::secp256k1::to_address(pubkey); const auto it = std::fill_n(output, 32 - sizeof(addr), 0); std::copy_n(addr.bytes, sizeof(addr), it); return {EVMC_SUCCESS, 32}; @@ -496,7 +496,7 @@ ExecutionResult ecadd_execute(const uint8_t* input, size_t input_size, uint8_t* const auto input_span = std::span{input_buffer}; - using namespace evmmax::bn254; + using namespace crypto::bn254; const auto p = AffinePoint::from_bytes(input_span.subspan<0, 64>()); const auto q = AffinePoint::from_bytes(input_span.subspan<64, 64>()); @@ -505,7 +505,7 @@ ExecutionResult ecadd_execute(const uint8_t* input, size_t input_size, uint8_t* if (!validate(*p) || !validate(*q)) [[unlikely]] return {EVMC_PRECOMPILE_FAILURE, 0}; - const auto res = evmmax::ecc::add_affine(*p, *q); + const auto res = crypto::ecc::add_affine(*p, *q); const std::span output_span{output, 64}; res.to_bytes(output_span); return {EVMC_SUCCESS, output_span.size()}; @@ -522,7 +522,7 @@ ExecutionResult ecmul_execute(const uint8_t* input, size_t input_size, uint8_t* const auto input_span = std::span{input_buffer}; - using namespace evmmax::bn254; + using namespace crypto::bn254; const auto p = AffinePoint::from_bytes(input_span.subspan<0, 64>()); if (!p.has_value() || !validate(*p)) [[unlikely]] @@ -530,7 +530,7 @@ ExecutionResult ecmul_execute(const uint8_t* input, size_t input_size, uint8_t* const auto c = intx::be::unsafe::load(input_buffer + 64); - const auto res = evmmax::bn254::mul(*p, c); + const auto res = crypto::bn254::mul(*p, c); const std::span output_span{output, 64}; res.to_bytes(output_span); return {EVMC_SUCCESS, output_span.size()}; @@ -546,11 +546,11 @@ ExecutionResult ecpairing_execute(const uint8_t* input, size_t input_size, uint8 if (input_size % PAIR_SIZE != 0) return {EVMC_PRECOMPILE_FAILURE, 0}; - std::vector> pairs; + std::vector> pairs; pairs.reserve(input_size / PAIR_SIZE); // TODO: may throw std::bad_alloc. for (auto input_ptr = input; input_ptr != input + input_size; input_ptr += PAIR_SIZE) { - namespace bn = evmmax::bn254; + namespace bn = crypto::bn254; const auto p = bn::AffinePoint::from_bytes(std::span{input_ptr, 64}); if (!p.has_value()) [[unlikely]] return {EVMC_PRECOMPILE_FAILURE, 0}; @@ -567,7 +567,7 @@ ExecutionResult ecpairing_execute(const uint8_t* input, size_t input_size, uint8 pairs.emplace_back(*p, q); } - const auto res = evmmax::bn254::pairing_check(pairs); + const auto res = crypto::bn254::pairing_check(pairs); if (!res.has_value()) return {EVMC_PRECOMPILE_FAILURE, 0}; @@ -769,7 +769,7 @@ ExecutionResult p256verify_execute(const uint8_t* input, size_t input_size, uint const auto qx = intx::be::unsafe::load(input + 96); const auto qy = intx::be::unsafe::load(input + 128); - if (!evmmax::secp256r1::verify(h, r, s, qx, qy)) + if (!crypto::secp256r1::verify(h, r, s, qx, qy)) return {EVMC_SUCCESS, 0}; // In case of invalid signature, return empty output. // Return 1_u256. @@ -864,16 +864,20 @@ evmc::Result call_precompile(evmc_revision rev, const evmc_message& msg) noexcep const auto [gas_cost, max_output_size] = analyze(input, rev); const auto gas_left = msg.gas - gas_cost; if (gas_left < 0) - return evmc::Result{EVMC_OUT_OF_GAS}; + return evmc::Result{EVMC_OUT_OF_GAS, {.left = msg.state_gas}}; // Allocate buffer for the precompile's output and pass its ownership to evmc::Result. // TODO: This can be done more elegantly by providing constructor evmc::Result(std::unique_ptr). const auto output_data = new (std::nothrow) uint8_t[max_output_size]; // TODO: handle nullptr. const auto [status_code, output_size] = execute(msg.input_data, msg.input_size, output_data, max_output_size); - const evmc_result result{status_code, status_code == EVMC_SUCCESS ? gas_left : 0, 0, - output_data, output_size, - [](const evmc_result* res) noexcept { delete[] res->output_data; }}; - return evmc::Result{result}; + return evmc::Result{{ + .status_code = status_code, + .gas_left = status_code == EVMC_SUCCESS ? gas_left : 0, + .state_gas = {.left = msg.state_gas}, + .output_data = output_data, + .output_size = output_size, + .release = [](const evmc_result* res) noexcept { delete[] res->output_data; }, + }}; } } // namespace evmone::state diff --git a/evmone/test/state/requests.hpp b/evmone/test/state/requests.hpp index 7c398e0..1fd1fe8 100644 --- a/evmone/test/state/requests.hpp +++ b/evmone/test/state/requests.hpp @@ -40,6 +40,14 @@ struct Requests /// Consolidation requests. /// Introduced by EIP-7251 https://eips.ethereum.org/EIPS/eip-7251. consolidation = 2, + + /// Builder deposit requests. + /// Introduced by EIP-8282 https://eips.ethereum.org/EIPS/eip-8282. + builder_deposit = 3, + + /// Builder exit requests. + /// Introduced by EIP-8282 https://eips.ethereum.org/EIPS/eip-8282. + builder_exit = 4, }; /// Raw encoded data of requests object: first byte is type, the rest is request objects. diff --git a/evmone/test/state/state.cpp b/evmone/test/state/state.cpp index 1046e29..1bd2c3d 100644 --- a/evmone/test/state/state.cpp +++ b/evmone/test/state/state.cpp @@ -9,6 +9,8 @@ #include "state_view.hpp" #include #include +#include +#include #include #include @@ -61,9 +63,15 @@ struct TransactionCost TransactionCost compute_tx_intrinsic_cost(evmc_revision rev, const Transaction& tx) noexcept { static constexpr auto TX_BASE_COST = 21000; + static constexpr auto TX_BASE_COST_AMSTERDAM = 12000; + static constexpr auto TX_VALUE_COST = 6000; static constexpr auto TX_CREATE_COST = 32000; static constexpr auto ACCESS_LIST_ADDRESS_COST = 2400; static constexpr auto ACCESS_LIST_STORAGE_KEY_COST = 1900; + static constexpr auto ACCESS_LIST_ADDRESS_COST_AMSTERDAM = + instr::additional_cold_account_access(EVMC_AMSTERDAM); + static constexpr auto ACCESS_LIST_STORAGE_KEY_COST_AMSTERDAM = + instr::ADDITIONAL_COLD_STORAGE_ACCESS; static constexpr auto ACCESS_LIST_ADDRESS_BYTES = 20; static constexpr auto ACCESS_LIST_STORAGE_KEY_BYTES = 32; static constexpr auto DATA_TOKEN_COST = 4; @@ -71,9 +79,28 @@ TransactionCost compute_tx_intrinsic_cost(evmc_revision rev, const Transaction& static constexpr auto TOTAL_COST_FLOOR_PER_TOKEN = 10; static constexpr auto TOTAL_COST_FLOOR_PER_BYTE = 16 * 4; + // EXECUTION_PER_AUTH_BASE_COST, the execution cost of processing a single authorization: + // the calldata cost of the 101-byte authorization tuple, the signature recovery, the cold + // authority access and two warm writes (EIP-8037). + static constexpr auto AUTHORIZATION_EXECUTION_COST = + 101 * 16 + 3000 + instr::COLD_ACCOUNT_ACCESS_AMSTERDAM + 2 * instr::WARM_ACCESS; + const auto is_create = !tx.to.has_value(); - const auto create_cost = (is_create && rev >= EVMC_HOMESTEAD) ? TX_CREATE_COST : 0; + const auto recipient_cost = [&] { + if (rev < EVMC_AMSTERDAM) // EIP-2780. + return 0; + if (is_create) + return instr::CREATE_ACCESS; // Cold account access + account write. + if (*tx.to == tx.sender) [[unlikely]] // Transaction to self. + return 0; + return instr::COLD_ACCOUNT_ACCESS_AMSTERDAM + (tx.value != 0 ? TX_VALUE_COST : 0); + }(); + const auto base_cost = + ((rev >= EVMC_AMSTERDAM) ? TX_BASE_COST_AMSTERDAM : TX_BASE_COST) + recipient_cost; + + const auto create_cost = + (is_create && rev >= EVMC_HOMESTEAD && rev < EVMC_AMSTERDAM) ? TX_CREATE_COST : 0; const auto num_tokens = static_cast(compute_tx_data_tokens(rev, tx.data)); const auto data_cost = num_tokens * DATA_TOKEN_COST; @@ -82,11 +109,16 @@ TransactionCost compute_tx_intrinsic_cost(evmc_revision rev, const Transaction& const auto access_list_num_bytes = static_cast(num_addresses * ACCESS_LIST_ADDRESS_BYTES + num_storage_keys * ACCESS_LIST_STORAGE_KEY_BYTES); - const auto access_list_cost = static_cast( - num_addresses * ACCESS_LIST_ADDRESS_COST + num_storage_keys * ACCESS_LIST_STORAGE_KEY_COST); + const auto address_cost = + (rev >= EVMC_AMSTERDAM) ? ACCESS_LIST_ADDRESS_COST_AMSTERDAM : ACCESS_LIST_ADDRESS_COST; + const auto storage_key_cost = (rev >= EVMC_AMSTERDAM) ? ACCESS_LIST_STORAGE_KEY_COST_AMSTERDAM : + ACCESS_LIST_STORAGE_KEY_COST; + const auto access_list_cost = static_cast(num_addresses) * address_cost + + static_cast(num_storage_keys) * storage_key_cost; - const auto auth_list_cost = - static_cast(tx.authorization_list.size()) * AUTHORIZATION_EMPTY_ACCOUNT_COST; + const auto auth_cost = + (rev >= EVMC_AMSTERDAM) ? AUTHORIZATION_EXECUTION_COST : AUTHORIZATION_EMPTY_ACCOUNT_COST; + const auto auth_list_cost = static_cast(tx.authorization_list.size()) * auth_cost; const auto initcode_cost = (is_create && rev >= EVMC_SHANGHAI) ? INITCODE_WORD_COST * num_words(tx.data.size()) : 0; @@ -95,7 +127,7 @@ TransactionCost compute_tx_intrinsic_cost(evmc_revision rev, const Transaction& const auto access_list_data_cost = (rev >= EVMC_AMSTERDAM) ? access_list_num_bytes * TOTAL_COST_FLOOR_PER_BYTE : 0; - const auto intrinsic_cost = TX_BASE_COST + create_cost + data_cost + access_list_data_cost + + const auto intrinsic_cost = base_cost + create_cost + data_cost + access_list_data_cost + access_list_cost + auth_list_cost + initcode_cost; int64_t data_min_cost = 0; @@ -104,21 +136,32 @@ TransactionCost compute_tx_intrinsic_cost(evmc_revision rev, const Transaction& else if (rev >= EVMC_PRAGUE) // Cost per token capturing num of zero-nonzero bytes (EIP-7623). data_min_cost = TOTAL_COST_FLOOR_PER_TOKEN * num_tokens; - // Compute "floor" cost (EIP-7623). + // Compute the calldata floor cost (EIP-7623), including the access list data (EIP-7981). const auto min_cost = - (rev >= EVMC_PRAGUE) ? TX_BASE_COST + data_min_cost + access_list_data_cost : 0; + (rev >= EVMC_PRAGUE) ? base_cost + data_min_cost + access_list_data_cost : 0; return {intrinsic_cost, min_cost}; } -int64_t process_authorization_list( - State& state, uint64_t chain_id, const AuthorizationList& authorization_list) +/// A revertible applied authorization (EIP-2780). +struct AppliedAuthorization +{ + Account* authority = nullptr; + std::optional initial_code_hash; +}; + +/// Applies the authorization list (EIP-7702) and returns the delegation refund. From Amsterdam, +/// charges the state-dependent costs instead of the refund and records the applied authorizations +/// to revert them on halt; returns nullopt when out of gas (EIP-2780). +[[nodiscard]] std::optional process_authorization_list(State& state, const Transaction& tx, + evmc_revision rev, int64_t& gas_left, StateGas& state_gas, + std::vector& applied) { int64_t delegation_refund = 0; - for (const auto& auth : authorization_list) + for (const auto& auth : tx.authorization_list) { // 1. Verify the chain id is either 0 or the chain’s current ID. - if (auth.chain_id != 0 && auth.chain_id != chain_id) + if (auth.chain_id != 0 && auth.chain_id != tx.chain_id) continue; // 2. Verify the nonce is less than 2**64 - 1. @@ -133,7 +176,7 @@ int64_t process_authorization_list( // Get or create the authority account. // It is still empty at this point until nonce bump following successful authorization. - auto& authority = state.get_or_insert(*authority_addr, {.erase_if_empty = true}); + auto& authority = state.get_or_insert(*authority_addr); // 4. Add authority to accessed_addresses (as defined in EIP-2929.) authority.access_status = EVMC_ACCESS_WARM; @@ -148,19 +191,56 @@ int64_t process_authorization_list( if (auth.nonce != authority.nonce) continue; + if (rev >= EVMC_AMSTERDAM) // The state-dependent costs replace the refund (EIP-2780). + { + // TODO: Extract the 23 to global constant. Also use to check delegation (fuzzing). + static constexpr auto AUTHORIZATION_STATE_GAS = 23 * COST_PER_STATE_BYTE; + // TODO: Replace the linear search with a "written" account flag + // or by processing the authorizations sorted by authority. + const auto already_applied = std::ranges::find(applied, &authority, + &AppliedAuthorization::authority) != applied.end(); + // The first write to the authority: it is not the sender, not the value recipient + // (paid by TX_VALUE_COST) and not an already applied authority. + if (authority_addr != tx.sender && (tx.value == 0 || tx.to != authority_addr) && + !already_applied) + { + if ((gas_left -= instr::ACCOUNT_WRITE) < 0) + return std::nullopt; + } + // A new account and a net-new delegation indicator: no code at the transaction start + // and none set since. An applied authority is not empty because of its nonce bump. + assert(!authority.is_empty() || !authority.code_changed); + if (authority.code_hash == Account::EMPTY_CODE_HASH && !authority.code_changed) + { + int64_t state_gas_cost = 0; + // TODO: State knows the nonexistent → existent transition. Record it as a flag. + if (authority.is_empty()) + state_gas_cost += NEW_ACCOUNT_STATE_GAS; + if (!is_zero(auth.addr)) + state_gas_cost += AUTHORIZATION_STATE_GAS; + if (!state_gas.charge(gas_left, state_gas_cost)) + return std::nullopt; + } + assert(!authority.code_changed || already_applied); + applied.emplace_back(&authority, + authority.code_changed ? std::nullopt : std::optional{authority.code_hash}); + } // 7. Add PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST gas to the global refund counter // if authority exists in the trie. // Successful authorization validation makes an account non-empty. // We apply the refund only if the account has existed before. // We detect "exists in the trie" by inspecting _empty_ property (EIP-161) because _empty_ // implies an account doesn't exist in the state (EIP-7523). - if (!authority.is_empty()) + else if (!authority.is_empty()) { static constexpr auto EXISTING_AUTHORITY_REFUND = AUTHORIZATION_EMPTY_ACCOUNT_COST - AUTHORIZATION_BASE_COST; delegation_refund += EXISTING_AUTHORITY_REFUND; } + // 9. Increase the nonce of authority by one. + ++authority.nonce; + // As a special case, if address is 0 do not write the designation. // Clear the account’s code and reset the account’s code hash to the empty hash. if (is_zero(auth.addr)) @@ -187,14 +267,11 @@ int64_t process_authorization_list( authority.code_hash = keccak256(designation); } } - - // 9. Increase the nonce of authority by one. - ++authority.nonce; } return delegation_refund; } -evmc_message build_message(const Transaction& tx, int64_t execution_gas_limit) noexcept +evmc_message build_message(const Transaction& tx, const TransactionProperties& tx_props) noexcept { const auto recipient = tx.to.has_value() ? *tx.to : compute_create_address(tx.sender, tx.nonce); @@ -202,7 +279,8 @@ evmc_message build_message(const Transaction& tx, int64_t execution_gas_limit) n .kind = tx.to.has_value() ? EVMC_CALL : EVMC_CREATE, .flags = 0, .depth = 0, - .gas = execution_gas_limit, + .gas = tx_props.execution_gas_limit, + .state_gas = tx_props.state_gas_limit, .recipient = recipient, .sender = tx.sender, .input_data = tx.data.data(), @@ -213,6 +291,85 @@ evmc_message build_message(const Transaction& tx, int64_t execution_gas_limit) n .code_size = 0, }; } + +/// Applies the authorizations (EIP-7702), resolves the delegation and calls the top-level message. +[[nodiscard]] evmc::Result process_top_level( + State& state, Host& host, evmc_revision rev, const Transaction& tx, evmc_message msg) +{ + const auto state_gas_limit = msg.state_gas; + StateGas state_gas{{.left = state_gas_limit}}; + + std::vector applied; + const auto delegation_refund = + process_authorization_list(state, tx, rev, msg.gas, state_gas, applied); + + // The authorizations' state-gas (left and spilled) stays consumed even if the call fails + // (EIP-2780). + const auto auth_state_gas = state_gas; + + // Resolves the delegation and, from Amsterdam, charges the recipient creation or the + // delegation target access. These are exclusive because a delegated recipient exists. + const auto prepare_call = [&] { + // Creating the recipient account costs state-gas, refilled if the call fails (EIP-8037). + if (!tx.to.has_value()) + return rev < EVMC_AMSTERDAM || host.account_exists(msg.recipient) || + state_gas.charge(msg.gas, NEW_ACCOUNT_STATE_GAS); + + const auto delegate = get_delegate_address(host, *tx.to); + if (!delegate.has_value()) + return rev < EVMC_AMSTERDAM || tx.value == 0 || host.account_exists(*tx.to) || + state_gas.charge(msg.gas, NEW_ACCOUNT_STATE_GAS); + + assert(host.account_exists(*tx.to)); + msg.code_address = *delegate; + msg.flags |= EVMC_DELEGATED; + const auto warm = host.access_account(msg.code_address) == EVMC_ACCESS_WARM; + if (rev < EVMC_AMSTERDAM) + return true; + // The delegation target access (EIP-2780). + msg.gas -= warm ? instr::WARM_ACCESS : instr::COLD_ACCOUNT_ACCESS_AMSTERDAM; + return msg.gas >= 0; + }; + + if (!delegation_refund.has_value() || !prepare_call()) + { + // Out of gas before the call: the execution-gas is consumed, the state-gas is returned + // and the authorizations are reverted (EIP-2780). + assert(rev >= EVMC_AMSTERDAM); + for (const auto& [authority, initial_code_hash] : applied) + { + --authority->nonce; + if (initial_code_hash.has_value()) + { + authority->code_changed = false; + + // Restore initial code hash. TODO: This is only needed for .is_empty() to work. + authority->code_hash = *initial_code_hash; + } + } + assert( + std::ranges::none_of(applied, [](const auto& a) { return a.authority->code_changed; })); + return evmc::Result{EVMC_OUT_OF_GAS, {.left = state_gas_limit}}; + } + + msg.state_gas = state_gas.left; + auto result = host.call(msg); + if (result.status_code == EVMC_SUCCESS) + { + result.state_gas.spilled += state_gas.spilled; + } + else + { + // Rollback state-gas costs. + assert(result.state_gas.left == msg.state_gas); + assert(result.state_gas.spilled == 0); + if (result.status_code == EVMC_REVERT) + result.gas_left += state_gas.spilled - auth_state_gas.spilled; + result.state_gas = auth_state_gas; + } + result.gas_refund += *delegation_refund; // Kept even if the call fails (EIP-7702). + return result; +} } // namespace StateDiff State::build_diff(evmc_revision rev) const @@ -221,8 +378,25 @@ StateDiff State::build_diff(evmc_revision rev) const diff.modified_accounts.reserve(m_modified.size()); for (const auto& [addr, m] : m_modified) { - if (m.nonexistent) + if (rev >= EVMC_AMSTERDAM && m.loaded) + { + // Loading is reading (EIP-7928): warming doesn't load and rollback doesn't unload. + auto& keys = diff.reads.emplace_back(addr, std::vector{}).second; + for (const auto& [k, v] : m.storage) + { + if (v.loaded) + keys.emplace_back(k); + } + } + if (!m.loaded || m.nonexistent) + { + // A not-loaded or nonexistent account holds no change. + assert(m.is_empty() && !m.destructed && !m.erase_if_empty && !m.just_created && + !m.code_changed); + assert(std::ranges::all_of(m.storage | std::views::values, + [](const auto& v) { return v.current == v.original; })); continue; + } if (m.destructed) { if (rev >= EVMC_AMSTERDAM && m.balance != 0) @@ -247,9 +421,7 @@ StateDiff State::build_diff(evmc_revision rev) const // Unconditionally report nonce and balance as modified. // TODO: We don't have information if the balance/nonce has actually changed. // One option is to just keep the original values. This may be handy for RPC. - // TODO(clang): In AppleClang 15 emplace_back without StateDiff::Entry doesn't compile. - // NOLINTNEXTLINE(modernize-use-emplace) - auto& a = diff.modified_accounts.emplace_back(StateDiff::Entry{addr, m.nonce, m.balance}); + auto& a = diff.modified_accounts.emplace_back(addr, m.nonce, m.balance); // Output only the new code. // TODO: Output also the code hash. It will be needed for DB update and MPT hash. @@ -265,90 +437,109 @@ StateDiff State::build_diff(evmc_revision rev) const return diff; } -Account& State::insert(const address& addr, Account account) +std::pair State::get_or_create(const address& addr) { - assert(!account.nonexistent); // No need to insert nonexistent accounts. - const auto [it, inserted] = m_modified.try_emplace(addr, std::move(account)); - if (!inserted) - { - assert(it->second.nonexistent); // Overwrite only nonexistent accounts. - it->second = std::move(account); - } - return it->second; -} - -Account* State::find(const address& addr) noexcept -{ - // TODO: Avoid the double lookup (find+insert). Nonexistent accounts are still re-queried from - // the initial state on every call; they could be cached as nonexistent nodes. - if (const auto it = m_modified.find(addr); it != m_modified.end()) - return it->second.nonexistent ? nullptr : &it->second; - if (const auto cacc = m_initial.get_account(addr); cacc) - return &insert(addr, {.nonce = cacc->nonce, - .balance = cacc->balance, - .code_hash = cacc->code_hash, - .has_initial_storage = cacc->has_storage}); - return nullptr; + auto& acc = get(addr); + if (!acc.nonexistent) + return {acc, false}; + journal_account_flags(acc); + // The account is created in place: a nonexistent account holds nothing but the warm status + // and the warm storage slots, which must survive (EIP-2929). + acc.nonexistent = false; + return {acc, true}; } Account& State::get(const address& addr) noexcept { - auto acc = find(addr); - assert(acc != nullptr); - return *acc; + auto& acc = access(addr); + if (!acc.loaded) [[unlikely]] + { + // Load the account from the initial state. A miss is cached as a nonexistent account, + // so the initial state is queried once per address rather than once per access. + assert(acc.is_empty() && !acc.nonexistent && !acc.destructed && !acc.erase_if_empty); + acc.loaded = true; + if (const auto cacc = m_initial.get_account(addr); cacc) + { + acc.nonce = cacc->nonce; + acc.balance = cacc->balance; + acc.code_hash = cacc->code_hash; + acc.has_initial_storage = cacc->has_storage; + } + else + { + acc.nonexistent = true; + } + } + // A nonexistent account is readable: its zero fields are what a missing account holds. + assert(!acc.nonexistent || + (acc.is_empty() && !acc.erase_if_empty && !acc.just_created && !acc.code_changed)); + return acc; } -Account& State::get_or_insert(const address& addr, Account account) +Account& State::get_or_insert(const address& addr) { - if (const auto acc = find(addr); acc != nullptr) - return *acc; - return insert(addr, std::move(account)); + auto& acc = get(addr); + if (acc.nonexistent) + { + acc.nonexistent = false; + acc.erase_if_empty = true; + } + return acc; } bytes_view State::get_code(const address& addr) { - auto* a = find(addr); - if (a == nullptr) - return {}; - if (a->code_hash == Account::EMPTY_CODE_HASH) + // A nonexistent account has the empty code hash, so it needs no separate check. + auto& a = get(addr); + if (a.code_hash == Account::EMPTY_CODE_HASH) return {}; - if (a->code.empty()) - a->code = m_initial.get_account_code(addr); - return a->code; + if (a.code.empty()) + a.code = m_initial.get_account_code(addr); + return a.code; } Account& State::touch(const address& addr) { - auto& acc = get_or_insert(addr, {.erase_if_empty = true}); - if (!acc.erase_if_empty && acc.is_empty()) + auto& acc = get(addr); + if (!acc.erase_if_empty && acc.is_empty()) // Also true for a nonexistent account. { - journal_account_flags(addr, acc); + journal_account_flags(acc); + acc.nonexistent = false; acc.erase_if_empty = true; } return acc; } +StorageValue& State::access_storage(const address& addr, const bytes32& key) +{ + return access(addr).storage[key]; +} + StorageValue& State::get_storage(const address& addr, const bytes32& key) { // TODO: Avoid account lookup by giving the reference to the account's storage to Host. - auto& acc = get(addr); - const auto [it, missing] = acc.storage.try_emplace(key); - if (missing) + auto& acc = access(addr); + assert(acc.loaded); // The storage of the executing account, loaded to run its code. + auto& slot = acc.storage[key]; + if (!slot.loaded) { - const auto initial_value = m_initial.get_storage(addr, key); - it->second = {initial_value, initial_value}; + // Load the value from the initial state. A slot only warmed keeps its access status. + slot.loaded = true; + slot.original = m_initial.get_storage(addr, key); + slot.current = slot.original; } - return it->second; + return slot; } -void State::journal_balance_change(const address& addr, const intx::uint256& prev_balance) +void State::journal_balance_change(Account& acc) { - m_journal.emplace_back(JournalBalanceChange{{addr}, prev_balance}); + m_journal.emplace_back(JournalBalanceChange{{&acc}, acc.balance}); } void State::journal_storage_change(StorageValue& slot) { - m_journal.emplace_back(JournalStorageChange{&slot, slot.current, slot.access_status}); + m_journal.emplace_back( + JournalStorageChange{&slot, slot.current, slot.access_status, slot.loaded}); } void State::journal_transient_storage_change(bytes32& slot) @@ -356,61 +547,79 @@ void State::journal_transient_storage_change(bytes32& slot) m_journal.emplace_back(JournalTransientStorageChange{&slot, slot}); } -void State::journal_bump_nonce(const address& addr) +void State::journal_bump_nonce(Account& acc) { - m_journal.emplace_back(JournalNonceBump{addr}); + m_journal.emplace_back(JournalNonceBump{{&acc}}); } -void State::journal_create(const address& addr) +void State::journal_create(Account& acc) { - m_journal.emplace_back(JournalCreate{{addr}}); + m_journal.emplace_back(JournalCreate{{&acc}}); } -void State::journal_new_account(const address& addr) +void State::journal_account_flags(Account& acc) { - // Revert restores the account to "nonexistent". The other flags are irrelevant/default. - m_journal.emplace_back(JournalAccountFlags{{addr}, EVMC_ACCESS_COLD, true, false, false}); + m_journal.emplace_back(JournalAccountFlags{ + {&acc}, acc.access_status, acc.nonexistent, acc.destructed, acc.erase_if_empty}); } -void State::journal_account_flags(const address& addr, const Account& acc) +namespace { - m_journal.emplace_back(JournalAccountFlags{ - {addr}, acc.access_status, acc.nonexistent, acc.destructed, acc.erase_if_empty}); +/// Resets the account value fields and flags set by a create. The balance and the storage are +/// restored by their own journal entries, which are always replayed first. +void clear_value(Account& a) noexcept +{ + a.nonce = 0; + a.code_hash = Account::EMPTY_CODE_HASH; + a.code.clear(); + a.just_created = false; + a.code_changed = false; } +} // namespace void State::rollback(size_t checkpoint) { - while (m_journal.size() != checkpoint) + assert(checkpoint <= m_journal.size()); + for (const auto& entry : m_journal | std::views::drop(checkpoint) | std::views::reverse) { std::visit( - [this](const auto& e) { + [](const auto& e) { using T = std::decay_t; if constexpr (std::is_same_v) { - get(e.addr).nonce -= 1; + auto& a = *e.acc; + assert(!a.nonexistent); // Replayed before the flags entry un-creating it. + a.nonce -= 1; } else if constexpr (std::is_same_v) { - auto& a = get(e.addr); + auto& a = *e.acc; a.access_status = e.access_status; - a.nonexistent = e.nonexistent; a.destructed = e.destructed; a.erase_if_empty = e.erase_if_empty; - // TODO: On restoring nonexistent (un-created create) the node keeps its - // code/storage/transient buffers until tx end; could clear them here. + if (e.nonexistent) + { + // An account which did not exist holds nothing. Only the non-existence + // is restored: the existence is set by journaled creation or by loading. + a.nonexistent = true; + assert(a.balance == 0); // Restored by the balance entries replayed first. + clear_value(a); + } } else if constexpr (std::is_same_v) { // Revert a create over a pre-existing account. // TODO: Why this account is not always "touched"? - auto& a = get(e.addr); - a.nonce = 0; - a.code_hash = Account::EMPTY_CODE_HASH; - a.code.clear(); + auto& a = *e.acc; + assert(!a.nonexistent); + clear_value(a); } else if constexpr (std::is_same_v) { - e.slot->current = e.prev_value; + if (e.prev_loaded) + e.slot->current = e.prev_value; + else // Warmed before loading: any later value change is already reverted. + assert(e.slot->current == e.slot->original); e.slot->access_status = e.prev_access_status; } else if constexpr (std::is_same_v) @@ -419,7 +628,9 @@ void State::rollback(size_t checkpoint) } else if constexpr (std::is_same_v) { - get(e.addr).balance = e.prev_balance; + auto& a = *e.acc; + assert(!a.nonexistent); // Replayed before the flags entry un-creating it. + a.balance = e.prev_balance; } else { @@ -427,16 +638,16 @@ void State::rollback(size_t checkpoint) static_assert(std::is_void_v, "unhandled journal entry type"); } }, - m_journal.back()); - m_journal.pop_back(); + entry); } + m_journal.resize(checkpoint); } -/// Validates transaction and computes its execution gas limit (the amount of gas provided to EVM). -/// @return Execution gas limit or transaction validation error. +/// Validates transaction and computes the gas limits it provides to the EVM. +/// @return The transaction's computed gas properties or a validation error. std::variant validate_transaction( const StateView& state_view, const BlockInfo& block, const Transaction& tx, evmc_revision rev, - int64_t block_gas_left, int64_t blob_gas_left) noexcept + int64_t block_gas_left, int64_t block_state_gas_left, int64_t blob_gas_left) noexcept { if (tx.chain_id_protected() && tx.chain_id != block.chain_id) return make_error_code(INVALID_CHAIN_ID); @@ -497,11 +708,25 @@ std::variant validate_transaction( assert(tx.max_priority_gas_price <= tx.max_gas_price); - if (rev >= EVMC_OSAKA && tx.gas_limit > MAX_TX_GAS_LIMIT) + if (rev == EVMC_OSAKA && tx.gas_limit > MAX_TX_GAS_LIMIT) + return make_error_code(GAS_LIMIT_EXCEEDS_MAXIMUM); + + if (rev >= EVMC_AMSTERDAM && tx.gas_limit > int64_t{MAX_TX_TOTAL_GAS_LIMIT}) return make_error_code(GAS_LIMIT_EXCEEDS_MAXIMUM); - if (tx.gas_limit > block_gas_left) - return make_error_code(GAS_ALLOWANCE_EXCEEDED); + if (rev < EVMC_AMSTERDAM) + { + if (tx.gas_limit > block_gas_left) + return make_error_code(GAS_ALLOWANCE_EXCEEDED); + } + else + { + // Check gas limits in both dimensions. + if (std::min(tx.gas_limit, int64_t{MAX_TX_GAS_LIMIT}) > block_gas_left) + return make_error_code(GAS_ALLOWANCE_EXCEEDED); + if (tx.gas_limit > block_state_gas_left) + return make_error_code(GAS_ALLOWANCE_EXCEEDED); + } if (tx.max_gas_price < block.base_fee) return make_error_code(INSUFFICIENT_MAX_FEE_PER_GAS); @@ -537,20 +762,22 @@ std::variant validate_transaction( max_total_fee += tx.value; if (tx.type == Transaction::Type::blob) - { - const auto total_blob_gas = tx.blob_gas_used(); - // FIXME: Can overflow uint256. - max_total_fee += total_blob_gas * tx.max_blob_gas_price; - } + max_total_fee += umul(uint256{tx.blob_gas_used()}, tx.max_blob_gas_price); if (sender_acc.balance < max_total_fee) return make_error_code(INSUFFICIENT_ACCOUNT_FUNDS); const auto [intrinsic_cost, min_cost] = compute_tx_intrinsic_cost(rev, tx); - if (tx.gas_limit < std::max(intrinsic_cost, min_cost)) + + // The transaction state-gas limit is all above the cap constant (EIP-8037). + const auto state_gas_limit = + rev >= EVMC_AMSTERDAM ? std::max(tx.gas_limit - MAX_TX_GAS_LIMIT, int64_t{0}) : 0; + + // Transaction gas limit with state-gas limit excluded must cover intrinsic and min cost. + if (tx.gas_limit - state_gas_limit < std::max(intrinsic_cost, min_cost)) return make_error_code(INTRINSIC_GAS_TOO_LOW); - const auto execution_gas_limit = tx.gas_limit - intrinsic_cost; - return TransactionProperties{execution_gas_limit, min_cost}; + const auto execution_gas_limit = tx.gas_limit - intrinsic_cost - state_gas_limit; + return TransactionProperties{execution_gas_limit, state_gas_limit, min_cost}; } StateDiff finalize(const StateView& state_view, evmc_revision rev, const address& coinbase, @@ -590,9 +817,6 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc assert(sender_acc.nonce < MAX_NONCE); // Required for valid tx. ++sender_acc.nonce; // Bump sender nonce. - const auto delegation_refund = - process_authorization_list(state, tx.chain_id, tx.authorization_list); - const auto base_fee = (rev >= EVMC_LONDON) ? block.base_fee : 0; assert(tx.max_gas_price >= base_fee); // Required for valid tx. assert(tx.max_gas_price >= tx.max_priority_gas_price); // Required for valid tx. @@ -618,7 +842,7 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc Host host{rev, vm, state, block, block_hashes, tx}; - auto message = build_message(tx, tx_props.execution_gas_limit); + const auto message = build_message(tx, tx_props); sender_acc.access_status = EVMC_ACCESS_WARM; // Sender is always warm. host.access_account(message.recipient); // Recipient (incl. create address) is always warm. @@ -628,47 +852,48 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc if (is_precompile(rev, a)) // Precompile storage is never accessed. continue; for (const auto& key : storage_keys) - state.get_storage(a, key).access_status = EVMC_ACCESS_WARM; + state.access_storage(a, key).access_status = EVMC_ACCESS_WARM; } // EIP-3651: Warm COINBASE. - // This may create an empty coinbase account. The account cannot be created unconditionally - // because this breaks old revisions. if (rev >= EVMC_SHANGHAI) host.access_account(block.coinbase); - if (tx.to.has_value()) - { - if (const auto delegate = get_delegate_address(host, *tx.to)) - { - message.code_address = *delegate; - message.flags |= EVMC_DELEGATED; - host.access_account(message.code_address); - } - } - - const auto result = host.call(message); + const auto result = process_top_level(state, host, rev, tx, message); - const auto gas_used_b4_refund = tx.gas_limit - result.gas_left; + const auto gas_used_b4_refund = tx.gas_limit - result.gas_left - result.state_gas.left; - const auto max_refund_quotient = rev >= EVMC_LONDON ? 5 : 2; - const auto refund_limit = gas_used_b4_refund / max_refund_quotient; - const auto refund = std::min(delegation_refund + result.gas_refund, refund_limit); + const auto refund_limit = rev >= EVMC_LONDON ? gas_used_b4_refund / 5 : gas_used_b4_refund / 2; + const auto refund = std::min(result.gas_refund, refund_limit); auto gas_used = gas_used_b4_refund - refund; - assert(gas_used > 0); + assert(gas_used > 0); // The refund is capped at a fraction of the gas used. // The gas used by the transaction must be at least the min_gas_cost (EIP-7623). gas_used = std::max(gas_used, tx_props.min_gas_cost); - // For block gas accounting, compute the gas refund capped by the min gas cost (EIP-7778). - const auto block_gas_used = std::max(gas_used_b4_refund, tx_props.min_gas_cost); - const auto gas_refund = block_gas_used - gas_used; + const auto state_gas_used = + tx_props.state_gas_limit - result.state_gas.left + result.state_gas.spilled; + assert(state_gas_used >= 0); + + // For block gas accounting, exclude refunds and enforce the min gas cost, raised by the + // state gas so state-gas spending cannot discount it (EIP-7778, EIP-8037). + const auto block_gas_used = + (rev >= EVMC_AMSTERDAM) ? + std::max(gas_used_b4_refund, tx_props.min_gas_cost + state_gas_used) : + gas_used; sender_acc.balance += tx_max_cost - gas_used * effective_gas_price; state.touch(block.coinbase).balance += gas_used * priority_gas_price; // Cumulative gas used is unknown in this scope. - TransactionReceipt receipt{tx.type, result.status_code, gas_used, gas_refund, {}, - host.take_logs(), {}, state.build_diff(rev)}; + TransactionReceipt receipt{ + .type = tx.type, + .status = result.status_code, + .gas_used = gas_used, + .block_gas_used = block_gas_used, + .state_gas_used = state_gas_used, + .logs = host.take_logs(), + .state_diff = state.build_diff(rev), + }; // Cannot put it into constructor call because logs are std::moved from host instance. receipt.logs_bloom_filter = compute_bloom_filter(receipt.logs); diff --git a/evmone/test/state/state.hpp b/evmone/test/state/state.hpp index 7240704..e44b774 100644 --- a/evmone/test/state/state.hpp +++ b/evmone/test/state/state.hpp @@ -21,7 +21,7 @@ class State { struct JournalBase { - address addr; + Account* acc = nullptr; ///< Account in a node-based container (stable refs). }; struct JournalBalanceChange : JournalBase @@ -42,6 +42,7 @@ class State StorageValue* slot = nullptr; ///< Storage slot in a node-based container (stable refs). bytes32 prev_value; evmc_access_status prev_access_status; + bool prev_loaded; ///< A slot warmed before loading has no value to restore. }; struct JournalTransientStorageChange @@ -62,7 +63,7 @@ class State /// The read-only view of the initial (cold) state. const StateView& m_initial; - /// The accounts loaded from the initial state and potentially modified. + /// The accounts warmed, loaded from the initial state or modified. std::unordered_map m_modified; /// The state journal: the list of changes made to the state @@ -75,21 +76,26 @@ class State State(State&&) = delete; State& operator=(State&&) = delete; - /// Inserts the new account at the address. - /// There must not exist any account under this address before. - Account& insert(const address& addr, Account account = {}); + // TODO: [[gnu::flatten]] on get(), get_storage() and access_storage() makes GCC inline the + // hash map lookups: up to ~10% fewer instructions on warm storage access. try_emplace() is + // an alternative to operator[] with different inlining hints: GCC 15/16 inline its lookup. - /// Returns the pointer to the account at the address if the account exists. Null otherwise. - Account* find(const address& addr) noexcept; + /// Returns the account at the address without loading it from the initial state. + Account& access(const address& addr) noexcept { return m_modified[addr]; } - /// Gets the account at the address (the account must exist). + /// Returns the account at the address. An account absent from the state is returned as a + /// node flagged nonexistent, so the reference is always valid and the flag is the answer to + /// "does this account exist?". Account& get(const address& addr) noexcept; - /// Gets an existing account or inserts new account. - Account& get_or_insert(const address& addr, Account account = {}); + /// Gets an existing account or creates new erasable account without journaling. + Account& get_or_insert(const address& addr); bytes_view get_code(const address& addr); + /// Returns the storage slot without loading its value from the initial state. + StorageValue& access_storage(const address& addr, const bytes32& key); + StorageValue& get_storage(const address& addr, const bytes32& key); StateDiff build_diff(evmc_revision rev) const; @@ -104,24 +110,25 @@ class State /// Methods performing changes to the state which can be reverted by rollback(). /// @{ - /// Touches (as in EIP-161) an existing account or inserts new erasable account. + /// Gets the account, creating it if it doesn't exist. The creation is journaled. + /// @return The account and whether it has been created. + std::pair get_or_create(const address& addr); + + /// Touches (as in EIP-161) an existing account or creates new erasable account. Account& touch(const address& addr); - void journal_balance_change(const address& addr, const intx::uint256& prev_balance); + void journal_balance_change(Account& acc); void journal_storage_change(StorageValue& slot); void journal_transient_storage_change(bytes32& slot); - void journal_bump_nonce(const address& addr); + void journal_bump_nonce(Account& acc); /// Journals a create over a pre-existing account; revert resets its nonce and code. - void journal_create(const address& addr); - - /// Journals a new-account creation; revert un-creates it (restores "does not exist"). - void journal_new_account(const address& addr); + void journal_create(Account& acc); - void journal_account_flags(const address& addr, const Account& acc); + void journal_account_flags(Account& acc); /// @} }; @@ -143,8 +150,9 @@ TransactionReceipt transition(const StateView& state, const BlockInfo& block, /// Validate a transaction. /// -/// @return Computed execution gas limit or validation error. +/// @param block_state_gas_left Remaining block state-gas (EIP-8037). +/// @return The transaction's computed gas properties or a validation error. [[nodiscard]] std::variant validate_transaction( const StateView& state_view, const BlockInfo& block, const Transaction& tx, evmc_revision rev, - int64_t block_gas_left, int64_t blob_gas_left) noexcept; + int64_t block_gas_left, int64_t block_state_gas_left, int64_t blob_gas_left) noexcept; } // namespace evmone::state diff --git a/evmone/test/state/state_diff.hpp b/evmone/test/state/state_diff.hpp index 4268804..ac5b240 100644 --- a/evmone/test/state/state_diff.hpp +++ b/evmone/test/state/state_diff.hpp @@ -44,8 +44,11 @@ struct StateDiff /// List of deleted accounts. /// /// This list doesn't have common addresses with modified_accounts. - /// Note that from the Cancun revision (because of the modification to the SELFDESTRUCT) - /// accounts cannot be deleted and this list is always empty. + /// From Cancun only an account self-destructed in the transaction creating it (EIP-6780) + /// and an empty touched account (EIP-161) are deleted. std::vector
deleted_accounts; + + /// The accounts read and their storage slots read, from Amsterdam (EIP-7928). + std::vector>> reads; }; } // namespace evmone::state diff --git a/evmone/test/state/system_contracts.cpp b/evmone/test/state/system_contracts.cpp index 7a5cd43..fc68a51 100644 --- a/evmone/test/state/system_contracts.cpp +++ b/evmone/test/state/system_contracts.cpp @@ -6,6 +6,7 @@ #include "errors.hpp" #include "host.hpp" #include "state_view.hpp" +#include namespace evmone::state { @@ -64,6 +65,16 @@ constexpr std::array REQUESTS_SYSTEM_CONTRACTS{ CONSOLIDATION_REQUEST_ADDRESS, Requests::Type::consolidation, }, + RequestsSystemContract{ + EVMC_AMSTERDAM, + BUILDER_DEPOSIT_CONTRACT_ADDRESS, + Requests::Type::builder_deposit, + }, + RequestsSystemContract{ + EVMC_AMSTERDAM, + BUILDER_EXIT_CONTRACT_ADDRESS, + Requests::Type::builder_exit, + }, }; constexpr auto by_rev = [](const auto& a, const auto& b) noexcept { return a.since < b.since; }; @@ -80,6 +91,7 @@ evmc::Result execute_system_call(State& state, const BlockInfo& block, const evmc_message msg{ .kind = EVMC_CALL, .gas = 30'000'000, + .state_gas = 16 * STORAGE_SET_STATE_GAS, // Additional state-gas (EIP-8037). .recipient = addr, .sender = SYSTEM_ADDRESS, .input_data = input.data(), @@ -103,14 +115,17 @@ StateDiff system_call_block_start(const StateView& state_view, const BlockInfo& // Skip the call if the target account doesn't exist. This is by EIP-4788 spec. // > if no code exists at [address], the call must fail silently. - const auto code = state_view.get_account_code(addr); + const auto code = state.get_code(addr); if (code.empty()) continue; + // The call is unchecked: a failure only reverts its state changes for testing. const auto input32 = get_input(block, block_hashes); + const auto checkpoint = state.checkpoint(); const auto res = execute_system_call(state, block, block_hashes, rev, vm, addr, code, input32); - assert(res.status_code == EVMC_SUCCESS); + if (res.status_code != EVMC_SUCCESS) [[unlikely]] + state.rollback(checkpoint); } // TODO: Should we return empty diff if no system contracts? return state.build_diff(rev); @@ -127,7 +142,7 @@ std::variant system_call_block_end(const StateV break; // Because entries are ordered, there are no other contracts for this revision. // Fail if the target account doesn't exist. This is by EIP-7002 and EIP-7251 spec. - const auto code = state_view.get_account_code(addr); + const auto code = state.get_code(addr); if (code.empty()) return make_error_code(SYSTEM_CONTRACT_EMPTY); diff --git a/evmone/test/state/system_contracts.hpp b/evmone/test/state/system_contracts.hpp index aaccaaf..7b88f9e 100644 --- a/evmone/test/state/system_contracts.hpp +++ b/evmone/test/state/system_contracts.hpp @@ -28,6 +28,13 @@ constexpr auto WITHDRAWAL_REQUEST_ADDRESS = 0x00000961EF480EB55E80D19AD83579A64C /// The address of the system contract processing consolidations (EIP-7251). constexpr auto CONSOLIDATION_REQUEST_ADDRESS = 0x0000BBDDC7CE488642FB579F8B00F3A590007251_address; +/// The address of the system contract processing builder deposits (EIP-8282). +constexpr auto BUILDER_DEPOSIT_CONTRACT_ADDRESS = + 0x0000BFF46984E3725691FA540A8C7589300D8282_address; + +/// The address of the system contract processing builder exits (EIP-8282). +constexpr auto BUILDER_EXIT_CONTRACT_ADDRESS = 0x000064D678505AD48F8CCB093BC65613800E8282_address; + struct BlockInfo; struct StateDiff; class BlockHashes; diff --git a/evmone/test/state/transaction.cpp b/evmone/test/state/transaction.cpp index 31d37eb..b528b94 100644 --- a/evmone/test/state/transaction.cpp +++ b/evmone/test/state/transaction.cpp @@ -170,7 +170,7 @@ std::optional
recover_sender(const Transaction& tx, bytes_view txbytes) rlp::internal::wrap_list(preimage)); const auto r_bytes = intx::be::store(tx.r); const auto s_bytes = intx::be::store(tx.s); - return evmmax::secp256k1::ecrecover( - h.bytes, r_bytes.bytes, s_bytes.bytes, y_parity, evmmax::secp256k1::RecoveryMode::strict); + return crypto::secp256k1::ecrecover( + h.bytes, r_bytes.bytes, s_bytes.bytes, y_parity, crypto::secp256k1::RecoveryMode::strict); } } // namespace evmone::state diff --git a/evmone/test/state/transaction.hpp b/evmone/test/state/transaction.hpp index 70879ae..1aafc1b 100644 --- a/evmone/test/state/transaction.hpp +++ b/evmone/test/state/transaction.hpp @@ -17,6 +17,9 @@ namespace evmone::state /// The maximum allowed gas limit for a transaction (EIP-7825). constexpr auto MAX_TX_GAS_LIMIT = 0x1000000; // 2**24 +/// The maximum allowed transaction gas limit of both dimensions: execution + state (EIP-8037). +constexpr auto MAX_TX_TOTAL_GAS_LIMIT = std::numeric_limits::max(); + using AccessList = std::vector>>; /// Decodes an EIP-7702 authorization. @@ -104,6 +107,9 @@ struct TransactionProperties /// The amount of gas provided to the EVM for the transaction execution. int64_t execution_gas_limit = 0; + /// The amount of state-gas spendable by EVM on state increase (since EIP-8037). + int64_t state_gas_limit = 0; + /// The minimal amount of gas the transaction must use. int64_t min_gas_cost = 0; }; @@ -132,9 +138,12 @@ struct TransactionReceipt /// Amount of gas used by this transaction (after refund, with the min gas applied). int64_t gas_used = 0; - /// Amount of gas refund applied to gas_used (capped by the min gas cost of EIP-7623). - /// Effectively, the difference between "block" and "user" gas. - int64_t gas_refund = 0; + /// Amount of gas counted against the block gas limit by this transaction (EIP-7778). + int64_t block_gas_used = 0; + + /// The amount of state-gas used by this transaction (since EIP-8037). + /// It is the state-gas part of #block_gas_used; the rest is the execution-gas part. + int64_t state_gas_used = 0; /// Amount of gas used by this and previous transactions in the block. int64_t cumulative_gas_used = 0; diff --git a/evmone/test/statetest/.clang-tidy b/evmone/test/statetest/.clang-tidy deleted file mode 100644 index efc628c..0000000 --- a/evmone/test/statetest/.clang-tidy +++ /dev/null @@ -1,3 +0,0 @@ -InheritParentConfig: true -Checks: > - -clang-analyzer-cplusplus.NewDeleteLeaks diff --git a/evmone/test/statetest/CMakeLists.txt b/evmone/test/statetest/CMakeLists.txt deleted file mode 100644 index ba46197..0000000 --- a/evmone/test/statetest/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# evmone: Fast Ethereum Virtual Machine implementation -# Copyright 2022 The evmone Authors. -# SPDX-License-Identifier: Apache-2.0 - -add_executable(evmone-statetest) -target_link_libraries(evmone-statetest PRIVATE evmone::testutils evmone evmone-buildinfo GTest::gtest CLI11::CLI11) -target_sources( - evmone-statetest PRIVATE - statetest.cpp - statetest_runner.cpp -) diff --git a/evmone/test/statetest/statetest.cpp b/evmone/test/statetest/statetest.cpp deleted file mode 100644 index ec6ec7e..0000000 --- a/evmone/test/statetest/statetest.cpp +++ /dev/null @@ -1,169 +0,0 @@ -// evmone: Fast Ethereum Virtual Machine implementation -// Copyright 2022 The evmone Authors. -// SPDX-License-Identifier: Apache-2.0 - -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -namespace -{ -/// Implementation of a gtest Test which runs all state tests from a given file. -class StateTestFile : public testing::Test -{ - fs::path m_json_test_file; - std::optional m_filter; - evmc::VM& m_vm; - bool m_trace = false; - -public: - explicit StateTestFile(fs::path json_test_file, const std::optional& filter, - evmc::VM& vm, bool trace) noexcept - : m_json_test_file{std::move(json_test_file)}, m_filter{filter}, m_vm{vm}, m_trace{trace} - {} - - void TestBody() final - { - std::ifstream f{m_json_test_file}; - const auto tests = evmone::test::load_state_tests(f); - for (const auto& test : tests) - { - if (m_filter.has_value() && test.name.find(*m_filter) == std::string::npos) - continue; - evmone::test::run_state_test(test, m_vm, m_trace); - } - } - - static void register_one(const std::string& suite_name, const fs::path& file, - const std::optional& filter, evmc::VM& vm, bool trace) - { - testing::RegisterTest(suite_name.c_str(), file.stem().string().c_str(), nullptr, nullptr, - file.string().c_str(), 0, [file, filter, &vm, trace]() -> testing::Test* { - return new StateTestFile(file, filter, vm, trace); - }); - } -}; - -/// Implementation of a gtest Test which runs a single state test. -class StateTest : public testing::Test -{ - evmone::test::StateTransitionTest m_state_transition_test; - evmc::VM& m_vm; - bool m_trace = false; - -public: - explicit StateTest( - evmone::test::StateTransitionTest state_transition_test, evmc::VM& vm, bool trace) noexcept - : m_state_transition_test{std::move(state_transition_test)}, m_vm{vm}, m_trace{trace} - {} - - void TestBody() final { evmone::test::run_state_test(m_state_transition_test, m_vm, m_trace); } - - static void register_one(const evmone::test::StateTransitionTest& test, - const std::string& suite_name, const std::string& test_name, const fs::path& file, - evmc::VM& vm, bool trace) - { - testing::RegisterTest(suite_name.c_str(), test_name.c_str(), nullptr, nullptr, - file.string().c_str(), 0, - [test, &vm, trace]() -> testing::Test* { return new StateTest(test, vm, trace); }); - } -}; - -void register_test_files( - const fs::path& root, const std::optional& filter, evmc::VM& vm, bool trace) -{ - if (is_directory(root)) - { - std::vector test_files; - std::copy_if(fs::recursive_directory_iterator{root}, fs::recursive_directory_iterator{}, - std::back_inserter(test_files), [](const fs::directory_entry& entry) { - // "index.json" files are just lists of tests generated by other tools. - return entry.is_regular_file() && entry.path().extension() == ".json" && - entry.path().filename() != "index.json"; - }); - std::ranges::sort(test_files); - - for (const auto& p : test_files) - StateTestFile::register_one( - fs::relative(p, root).parent_path().string(), p, filter, vm, trace); - } - else // Treat as a file. - { - std::ifstream f{root}; - const auto tests = evmone::test::load_state_tests(f); - for (const auto& test : tests) - { - if (filter.has_value() && test.name.find(*filter) == std::string::npos) - continue; - StateTest::register_one(test, root.string(), test.name, root, vm, trace); - } - } -} -} // namespace - - -int main(int argc, char* argv[]) -{ - // The default test filter. To enable all tests use `--gtest_filter=*`. - testing::FLAGS_gtest_filter = - "-" - // Slow tests: - "stCreateTest.CreateOOGafterMaxCodesize:" // pass - "stQuadraticComplexityTest.Call50000_sha256:" // pass - "stTimeConsuming.static_Call50000_sha256:" // pass - "stTimeConsuming.CALLBlake2f_MaxRounds:" // pass - "VMTests/vmPerformance.*:" // pass - ; - - try - { - testing::InitGoogleTest(&argc, argv); // Process GoogleTest flags. - - CLI::App app{"evmone state test runner"}; - - app.set_version_flag("--version", "evmone-statetest " EVMONE_VERSION); - - std::vector paths; - app.add_option("path", paths, - "Path to test file or directory. For a directory, all .json " - "files (except index.json) are considered test files, and each file is treated as a " - "separate test. For a file, all tests in the file are treated as separate tests.") - ->required() - ->check(CLI::ExistingPath); - - std::optional filter; - app.add_option("-k", filter, - "Test name filter. Run only tests with names containing the specified string."); - - bool trace = false; - bool trace_summary = false; - const auto trace_opt = app.add_flag("--trace", trace, "Enable EVM tracing"); - app.add_flag("--trace-summary", trace_summary, "Output trace summary only") - ->excludes(trace_opt); - - CLI11_PARSE(app, argc, argv); - - evmc::VM vm{evmc_create_evmone(), {{"O", "0"}}}; - - if (trace) - { - std::ios::sync_with_stdio(false); - vm.set_option("trace", "1"); - } - - for (const auto& p : paths) - register_test_files(p, filter, vm, trace || trace_summary); - - return RUN_ALL_TESTS(); - } - catch (const std::exception& ex) - { - std::cerr << ex.what() << "\n"; - return -1; - } -} diff --git a/evmone/test/unittests/CMakeLists.txt b/evmone/test/unittests/CMakeLists.txt index 3bb936f..40442e4 100644 --- a/evmone/test/unittests/CMakeLists.txt +++ b/evmone/test/unittests/CMakeLists.txt @@ -9,6 +9,7 @@ target_sources( analysis_test.cpp baseline_analysis_test.cpp blockchaintest_loader_test.cpp + blockchaintest_runner_test.cpp bytecode_test.cpp create_address_test.cpp evm_fixture.cpp @@ -31,12 +32,12 @@ target_sources( evm_other_test.cpp evm_benchmark_test.cpp evm_undefined_instructions_test.cpp - evmmax_bn254_add_test.cpp - evmmax_bn254_from_bytes.cpp - evmmax_bn254_mul_test.cpp - evmmax_bn254_pairing_test.cpp - evmmax_test.cpp - evmmax_secp256k1_test.cpp + bn254_add_test.cpp + bn254_from_bytes.cpp + bn254_mul_test.cpp + bn254_pairing_test.cpp + modarith_test.cpp + secp256k1_test.cpp evmone_test.cpp execution_state_test.cpp exportable_fixture.hpp @@ -70,6 +71,7 @@ target_sources( state_transition_create_test.cpp state_transition_eip7702_test.cpp state_transition_eip7778_block_gas_test.cpp + state_transition_eip8037_state_gas_test.cpp state_transition_extcode_test.cpp state_transition_selfdestruct_test.cpp state_transition_snippets_test.cpp @@ -78,16 +80,21 @@ target_sources( state_transition_transient_storage_test.cpp state_transition_tx_test.cpp state_tx_test.cpp + statediff_export_test.cpp statetest_loader_block_info_test.cpp statetest_loader_test.cpp statetest_loader_tx_test.cpp statetest_logs_hash_test.cpp + statetest_runner_test.cpp statetest_withdrawals_test.cpp + test_collector_test.cpp + test_driver_test.cpp + test_report_test.cpp tooling_run_test.cpp tooling_t8n_test.cpp tracing_test.cpp ) -target_link_libraries(evmone-unittests PRIVATE evmone evmone::evmmax evmone::experimental evmone::precompiles evmone::state evmone::testutils blst::blst GTest::gtest GTest::gtest_main) +target_link_libraries(evmone-unittests PRIVATE evmone evmone::experimental evmone::precompiles evmone::state evmone::testutils blst::blst GTest::gtest GTest::gtest_main) target_include_directories(evmone-unittests PRIVATE ${evmone_private_include_dir}) if(NOT CMAKE_CXX_CLANG_TIDY) target_precompile_headers( diff --git a/evmone/test/unittests/blockchaintest_loader_test.cpp b/evmone/test/unittests/blockchaintest_loader_test.cpp index 4b6dce5..7e1b3f2 100644 --- a/evmone/test/unittests/blockchaintest_loader_test.cpp +++ b/evmone/test/unittests/blockchaintest_loader_test.cpp @@ -469,3 +469,42 @@ TEST(json_loader, blockchain_test_slot_number_absent_differs_from_zero) EXPECT_EQ(btt[0].test_blocks[0].block_info.slot_number, 0); EXPECT_FALSE(btt[0].test_blocks[1].block_info.slot_number.has_value()); } + +/// A block with `expectException` but no `rlp_decoded` gives the loader no `FixtureBlock` to +/// build the invalid block from, so it refuses the whole test rather than the one block. +TEST(json_loader, blockchain_test_unsupported_rlp) +{ + // The stream is built inside the lambda: on a failure EXPECT_THAT reruns the matcher for its + // explanation, and a stream consumed by the first run would then report a false one. + constexpr std::string_view fixture = R"({ + "unsupported_rlp": { + "network": "Cancun", + "genesisBlockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsTrie": "0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptTrie": "0x0000000000000000000000000000000000000000000000000000000000000000", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "number": "0x00", + "gasLimit": "0x01000000", + "gasUsed": "0x00", + "timestamp": "0x00", + "extraData": "0x", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "pre": {}, + "blocks": [ + {"expectException": "TransactionException.INVALID_SIGNATURE_VRS"} + ] + } + })"; + + EXPECT_THAT( + [&] { + std::istringstream input{std::string{fixture}}; + load_blockchain_tests(input); + }, + ThrowsMessage( + "tests with invalidly rlp-encoded blocks are not supported")); +} diff --git a/evmone/test/unittests/blockchaintest_runner_test.cpp b/evmone/test/unittests/blockchaintest_runner_test.cpp new file mode 100644 index 0000000..87797bb --- /dev/null +++ b/evmone/test/unittests/blockchaintest_runner_test.cpp @@ -0,0 +1,162 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Tests of the blockchain test runner's verdicts. EEST reaches validate_block()'s rejections on +/// a green run, but not the reports below them: those need evmone and a fixture to disagree. +/// Neither is reachable from the suites the coverage job runs. + +#include +#include +#include +#include +#include +#include + +using namespace evmone; +using namespace evmone::test; +using namespace evmc::literals; + +namespace +{ +constexpr auto GENESIS_HASH = 0x9e11_bytes32; +constexpr int64_t GAS_LIMIT = 0x100000; + +/// A fixture with one block that validate_block() accepts, for a test to then break in one way. +BlockchainTest one_block_fixture() +{ + BlockchainTest t; + t.name = "unit"; + t.network = "Prague"; + + auto& g = t.genesis_block_header; + g.gas_limit = GAS_LIMIT; + g.transactions_root = state::EMPTY_MPT_HASH; + g.receipts_root = state::EMPTY_MPT_HASH; + g.withdrawal_root = state::EMPTY_MPT_HASH; + g.hash = GENESIS_HASH; + + TestBlock b; + b.block_info.number = g.block_number + 1; + b.block_info.parent_hash = g.hash; + b.block_info.gas_limit = g.gas_limit; + b.block_info.timestamp = g.timestamp + 1; + b.block_info.blob_gas_used = 0; // Both are mandatory from Cancun. + b.block_info.excess_blob_gas = 0; + t.test_blocks.push_back(b); + + // A rejected block leaves the chain at genesis and the state as the pre-state. + t.expectation.last_block_hash = g.hash; + t.expectation.post_state = mpt_hash(t.pre_state); + return t; +} + +std::vector run(const BlockchainTest& t) +{ + std::vector failures; + TestReport report{[&](const Failure& failure) { failures.push_back(failure); }}; + evmc::VM vm{evmc_create_evmone()}; + run_blockchain_test(t, vm, report); + return failures; +} +} // namespace + +TEST(blockchaintest_runner, block_rejection_rules) +{ + struct Case + { + std::string_view what_is_broken; + void (*brk)(TestBlock&); + std::string_view exception; + }; + static constexpr Case CASES[]{ + {"a parent no block has", [](TestBlock& b) { b.block_info.parent_hash = 0xdead_bytes32; }, + "BlockException.UNKNOWN_PARENT"}, + {"a number that does not follow the parent", [](TestBlock& b) { b.block_info.number = 7; }, + "BlockException.INVALID_BLOCK_NUMBER"}, + {"more gas used than the limit allows", + [](TestBlock& b) { b.block_info.gas_used = b.block_info.gas_limit + 1; }, + "BlockException.INCORRECT_BLOCK_FORMAT"}, + {"a gas limit above the parent's adjustment range", + [](TestBlock& b) { b.block_info.gas_limit = GAS_LIMIT * 2; }, + "BlockException.INVALID_GASLIMIT"}, + {"a gas limit below the parent's adjustment range", + [](TestBlock& b) { b.block_info.gas_limit = GAS_LIMIT / 2; }, + "BlockException.INVALID_GASLIMIT"}, + {"a timestamp no newer than the parent's", [](TestBlock& b) { b.block_info.timestamp = 0; }, + "BlockException.INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT"}, + {"a difficulty the parent does not imply", + [](TestBlock& b) { b.block_info.difficulty = 1; }, + "BlockException.INCORRECT_BLOCK_FORMAT"}, + {"ommers, which merged forks do not have", + [](TestBlock& b) { b.block_info.ommers.emplace_back(address{}, 1u); }, + "BlockException.INCORRECT_BLOCK_FORMAT"}, + {"no blob gas fields, mandatory from Cancun", + [](TestBlock& b) { b.block_info.blob_gas_used.reset(); }, + "BlockException.INCORRECT_BLOCK_FORMAT"}, + {"excess blob gas the parent does not imply", + [](TestBlock& b) { b.block_info.excess_blob_gas = 0x20000; }, + "BlockException.INCORRECT_EXCESS_BLOB_GAS"}, + {"a slot number, which arrives only with Amsterdam", + [](TestBlock& b) { b.block_info.slot_number = 1; }, + "BlockException.INCORRECT_BLOCK_FORMAT"}, + {"a withdrawal that did not parse", + [](TestBlock& b) { b.withdrawals_parse_success = false; }, + "BlockException.INCORRECT_BLOCK_FORMAT"}, + }; + + for (const auto& c : CASES) + { + SCOPED_TRACE(c.what_is_broken); + auto t = one_block_fixture(); + c.brk(t.test_blocks[0]); + t.test_blocks[0].expected_exception = c.exception; + + // Matched rather than EXPECT_TRUE(...empty()) so a regression prints what was reported. + EXPECT_THAT(run(t), testing::IsEmpty()); + } +} + +TEST(blockchaintest_runner, post_state_mismatch_dump) +{ + auto t = one_block_fixture(); + t.test_blocks.clear(); // Nothing applied: the post state is the pre-state. + t.pre_state[0xaa_address] = {.nonce = 1, .balance = 2, .code = bytes{0xfe}}; + t.pre_state[0xaa_address].storage[0x01_bytes32] = 0x02_bytes32; + t.expectation.post_state = TestState{}; // A different state, so the roots differ. + + const auto failures = run(t); + ASSERT_EQ(failures.size(), 1u); + EXPECT_EQ(failures[0].what, "post state root"); + EXPECT_THAT(failures[0].detail, testing::HasSubstr("Result state:")); + EXPECT_THAT(failures[0].detail, testing::HasSubstr("Expected state:")); + // Printed last of an account, so reaching it means the whole dump was built. + EXPECT_THAT(failures[0].detail, testing::HasSubstr("storage :")); +} + +TEST(blockchaintest_runner, invalid_block_rlp_reported) +{ + struct Case + { + bytes rlp; + std::string_view detail; + }; + const Case CASES[]{ + {bytes{0x00}, "not a list"}, + {bytes{0xc0, 0x00}, "trailing bytes after the block"}, + {bytes{0xc1, 0x00}, "the header is not a list"}, + {bytes{0xc1, 0xc0}, "the transactions are not a list"}, + }; + + for (const auto& c : CASES) + { + SCOPED_TRACE(c.detail); + auto t = one_block_fixture(); + t.test_blocks[0].rlp = c.rlp; // Only checked for blocks the fixture expects to be valid. + + const auto failures = run(t); // The RLP check runs before the block is validated. + ASSERT_FALSE(failures.empty()); + EXPECT_EQ(failures[0].what, "block RLP"); + EXPECT_EQ(failures[0].detail, c.detail); + } +} diff --git a/evmone/test/unittests/evmmax_bn254_add_test.cpp b/evmone/test/unittests/bn254_add_test.cpp similarity index 99% rename from evmone/test/unittests/evmmax_bn254_add_test.cpp rename to evmone/test/unittests/bn254_add_test.cpp index 3974673..2c3c83d 100644 --- a/evmone/test/unittests/evmmax_bn254_add_test.cpp +++ b/evmone/test/unittests/bn254_add_test.cpp @@ -6,7 +6,7 @@ #include #include -using namespace evmmax::bn254; +using namespace evmone::crypto::bn254; using namespace evmone::test; namespace @@ -91,7 +91,7 @@ constexpr TestCase TEST_CASES[]{ }; } // namespace -TEST(evmmax, bn254_add) +TEST(crypto, bn254_add) { std::set seen_inputs; diff --git a/evmone/test/unittests/evmmax_bn254_from_bytes.cpp b/evmone/test/unittests/bn254_from_bytes.cpp similarity index 93% rename from evmone/test/unittests/evmmax_bn254_from_bytes.cpp rename to evmone/test/unittests/bn254_from_bytes.cpp index 701031e..5b038b0 100644 --- a/evmone/test/unittests/evmmax_bn254_from_bytes.cpp +++ b/evmone/test/unittests/bn254_from_bytes.cpp @@ -6,10 +6,10 @@ #include #include -using namespace evmmax::bn254; +using namespace evmone::crypto::bn254; using namespace evmone::test; -TEST(evmmax, bn254_point_from_bytes_valid) +TEST(crypto, bn254_point_from_bytes_valid) { static constexpr std::string_view TEST_CASES[]{ "0000000000000000000000000000000000000000000000000000000000000001" @@ -31,7 +31,7 @@ TEST(evmmax, bn254_point_from_bytes_valid) } } -TEST(evmmax, bn254_point_from_bytes_not_on_curve) +TEST(crypto, bn254_point_from_bytes_not_on_curve) { static constexpr std::string_view TEST_CASES[]{ "0000000000000000000000000000000000000000000000000000000000000001" @@ -51,7 +51,7 @@ TEST(evmmax, bn254_point_from_bytes_not_on_curve) } } -TEST(evmmax, bn254_point_from_bytes_fp_invalid) +TEST(crypto, bn254_point_from_bytes_fp_invalid) { static constexpr std::string_view TEST_CASES[]{ "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47" diff --git a/evmone/test/unittests/evmmax_bn254_mul_test.cpp b/evmone/test/unittests/bn254_mul_test.cpp similarity index 99% rename from evmone/test/unittests/evmmax_bn254_mul_test.cpp rename to evmone/test/unittests/bn254_mul_test.cpp index 2bd24be..8185905 100644 --- a/evmone/test/unittests/evmmax_bn254_mul_test.cpp +++ b/evmone/test/unittests/bn254_mul_test.cpp @@ -6,10 +6,10 @@ #include #include -using namespace evmmax::bn254; +using namespace evmone::crypto::bn254; using namespace evmone::test; -TEST(evmmax, bn254_decompose) +TEST(crypto, bn254_decompose) { struct TestCase { @@ -176,7 +176,7 @@ TEST(evmmax, bn254_decompose) }, }; - static constexpr auto decompose = evmmax::ecc::decompose; + static constexpr auto decompose = evmone::crypto::ecc::decompose; for (const auto& t : TEST_CASES) { @@ -327,7 +327,7 @@ constexpr TestCase TEST_CASES[]{ }; } // namespace -TEST(evmmax, bn254_mul) +TEST(crypto, bn254_mul) { std::set seen_inputs; diff --git a/evmone/test/unittests/evmmax_bn254_pairing_test.cpp b/evmone/test/unittests/bn254_pairing_test.cpp similarity index 97% rename from evmone/test/unittests/evmmax_bn254_pairing_test.cpp rename to evmone/test/unittests/bn254_pairing_test.cpp index a28f1cf..4f16e61 100644 --- a/evmone/test/unittests/evmmax_bn254_pairing_test.cpp +++ b/evmone/test/unittests/bn254_pairing_test.cpp @@ -5,11 +5,11 @@ #include "evmone_precompiles/bn254.hpp" #include -using namespace evmmax::bn254; +using namespace evmone::crypto::bn254; using namespace intx; -TEST(evmmax, bn254_pairing) +TEST(crypto, bn254_pairing) { const auto P1 = AffinePoint{ 0x1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f59_u256, @@ -100,7 +100,7 @@ TEST(evmmax, bn254_pairing) } } -TEST(evmmax, bn254_pairing_invalid_input) +TEST(crypto, bn254_pairing_invalid_input) { const std::vector> valid_input{{ { @@ -155,7 +155,7 @@ TEST(evmmax, bn254_pairing_invalid_input) } // Case taken from https://www.evm.codes/precompiled -TEST(evmmax_bn254, evm_codes_example) +TEST(bn254, evm_codes_example) { // Pair 1 const auto p1 = AffinePoint{ @@ -196,7 +196,7 @@ TEST(evmmax_bn254, evm_codes_example) EXPECT_EQ(result.value(), true) << "Pairing result should be true, matching Solidity."; } -TEST(evmmax_bn254, evm_codes_example_changed_order) +TEST(bn254, evm_codes_example_changed_order) { // Pair 1 const auto p1 = AffinePoint{ diff --git a/evmone/test/unittests/evm_eip2929_test.cpp b/evmone/test/unittests/evm_eip2929_test.cpp index 00f32b0..db78ce2 100644 --- a/evmone/test/unittests/evm_eip2929_test.cpp +++ b/evmone/test/unittests/evm_eip2929_test.cpp @@ -63,13 +63,17 @@ TEST_P(evm, eip2929_case2) EXPECT_GAS_USED(EVMC_SUCCESS, 2835); EXPECT_EQ(result.output_size, 0); + // Each EXTCODECOPY accesses the account and then copies its code, even 0 bytes. const auto& r = host.recorded_account_accesses; - ASSERT_EQ(r.size(), 5); + ASSERT_EQ(r.size(), 8); EXPECT_EQ(r[0], msg.sender); EXPECT_EQ(r[1], msg.recipient); EXPECT_EQ(r[2], 0x00000000000000000000000000000000000000ff_address); EXPECT_EQ(r[3], 0x00000000000000000000000000000000000000ff_address); - EXPECT_EQ(r[4], msg.recipient); + EXPECT_EQ(r[4], 0x00000000000000000000000000000000000000ff_address); + EXPECT_EQ(r[5], 0x00000000000000000000000000000000000000ff_address); + EXPECT_EQ(r[6], msg.recipient); + EXPECT_EQ(r[7], msg.recipient); } TEST_P(evm, eip2929_case3) diff --git a/evmone/test/unittests/evm_storage_test.cpp b/evmone/test/unittests/evm_storage_test.cpp index 2c23990..46d64e2 100644 --- a/evmone/test/unittests/evm_storage_test.cpp +++ b/evmone/test/unittests/evm_storage_test.cpp @@ -5,6 +5,7 @@ /// This file contains EVM unit tests that access or modify the contract storage. #include "evm_fixture.hpp" +#include #include using namespace evmc::literals; @@ -227,6 +228,7 @@ TEST_P(evm, sstore_cost_net_gas_metering) int64_t set = -1; int64_t reset = -1; int64_t clear = -1; + int64_t state_set = 0; ///< Storage creation, charged in state gas (EIP-8037). }; const auto test = [this](const evmc::bytes32& original, const evmc::bytes32& current, @@ -246,8 +248,9 @@ TEST_P(evm, sstore_cost_net_gas_metering) cost_constants[EVMC_ISTANBUL] = {800, 20000, 5000, 15000}; cost_constants[EVMC_BERLIN] = {100, 20000, 2900, 15000}; cost_constants[EVMC_LONDON] = {100, 20000, 2900, 4800}; + cost_constants[EVMC_AMSTERDAM] = {100, 10100, 10100, 11616, evmone::STORAGE_SET_STATE_GAS}; - for (const auto r : {EVMC_ISTANBUL, EVMC_BERLIN, EVMC_LONDON}) + for (const auto r : {EVMC_ISTANBUL, EVMC_BERLIN, EVMC_LONDON, EVMC_AMSTERDAM}) { rev = r; const auto& c = cost_constants.at(static_cast(r)); @@ -260,7 +263,7 @@ TEST_P(evm, sstore_cost_net_gas_metering) test(O, Y, Z, b + c.warm_access, 0); test(X, Y, Z, b + c.warm_access, 0); - test(O, O, Z, b + c.set, 0); // added + test(O, O, Z, b + c.set + c.state_set, 0); // added test(X, X, O, b + c.reset, c.clear); // deleted test(X, X, Z, b + c.reset, 0); // modified test(X, O, Z, b + c.warm_access, -c.clear); // deleted added diff --git a/evmone/test/unittests/evmone_test.cpp b/evmone/test/unittests/evmone_test.cpp index ad2d35e..fcdc46e 100644 --- a/evmone/test/unittests/evmone_test.cpp +++ b/evmone/test/unittests/evmone_test.cpp @@ -15,6 +15,35 @@ TEST(evmone, info) EXPECT_TRUE(vm.is_abi_compatible()); } +TEST(evmc, result_with_state_gas) +{ + const auto result = evmc::Result{EVMC_SUCCESS, 1, 2, {.left = 3, .spilled = 4}}; + EXPECT_EQ(result.status_code, EVMC_SUCCESS); + EXPECT_EQ(result.gas_left, 1); + EXPECT_EQ(result.gas_refund, 2); + EXPECT_EQ(result.state_gas.left, 3); + EXPECT_EQ(result.state_gas.spilled, 4); + EXPECT_EQ(result.output_data, nullptr); + EXPECT_EQ(result.output_size, 0); + + const auto default_result = evmc::Result{}; + EXPECT_EQ(default_result.state_gas.left, 0); + EXPECT_EQ(default_result.state_gas.spilled, 0); + + const uint8_t output[] = {0x01}; + const auto output_result = + evmc::Result{EVMC_REVERT, 1, 0, output, std::size(output), {.left = 5, .spilled = 6}}; + EXPECT_EQ(output_result.state_gas.left, 5); + EXPECT_EQ(output_result.state_gas.spilled, 6); + + const auto failure_result = evmc::Result{EVMC_OUT_OF_GAS, {.left = 7}}; + EXPECT_EQ(failure_result.status_code, EVMC_OUT_OF_GAS); + EXPECT_EQ(failure_result.gas_left, 0); + EXPECT_EQ(failure_result.gas_refund, 0); + EXPECT_EQ(failure_result.state_gas.left, 7); + EXPECT_EQ(failure_result.state_gas.spilled, 0); +} + TEST(evmone, set_option_invalid) { auto vm = evmc_create_evmone(); diff --git a/evmone/test/unittests/evmmax_test.cpp b/evmone/test/unittests/modarith_test.cpp similarity index 91% rename from evmone/test/unittests/evmmax_test.cpp rename to evmone/test/unittests/modarith_test.cpp index a721e0d..231f62e 100644 --- a/evmone/test/unittests/evmmax_test.cpp +++ b/evmone/test/unittests/modarith_test.cpp @@ -2,12 +2,12 @@ // Copyright 2023 The evmone Authors. // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include using namespace intx; -using namespace evmmax; +using namespace evmone::crypto; constexpr auto P23 = 23_u256; constexpr auto BN254Mod = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47_u256; @@ -26,14 +26,14 @@ struct ModA : ModArith }; template -class evmmax_test : public testing::Test +class modarith_test : public testing::Test {}; using test_types = testing::Types, ModA, ModA, ModA, ModA>; -TYPED_TEST_SUITE(evmmax_test, test_types); +TYPED_TEST_SUITE(modarith_test, test_types); -TYPED_TEST(evmmax_test, to_from_mont) +TYPED_TEST(modarith_test, to_from_mont) { const typename TypeParam::uint v = 1; @@ -42,7 +42,7 @@ TYPED_TEST(evmmax_test, to_from_mont) EXPECT_EQ(s.from_mont(x), v); } -TYPED_TEST(evmmax_test, to_from_mont_0) +TYPED_TEST(modarith_test, to_from_mont_0) { const TypeParam s; EXPECT_EQ(s.to_mont(0), 0); @@ -78,7 +78,7 @@ static auto get_test_values(const Mod& m) noexcept static_assert(m.mul(a, b) == m.to_mont(33)); } -TYPED_TEST(evmmax_test, add) +TYPED_TEST(modarith_test, add) { const TypeParam m; const auto values = get_test_values(m); @@ -103,7 +103,7 @@ TYPED_TEST(evmmax_test, add) } } -TYPED_TEST(evmmax_test, sub) +TYPED_TEST(modarith_test, sub) { const TypeParam m; const auto values = get_test_values(m); @@ -128,7 +128,7 @@ TYPED_TEST(evmmax_test, sub) } } -TYPED_TEST(evmmax_test, mul) +TYPED_TEST(modarith_test, mul) { const TypeParam m; const auto values = get_test_values(m); @@ -148,7 +148,7 @@ TYPED_TEST(evmmax_test, mul) } } -TYPED_TEST(evmmax_test, inv) +TYPED_TEST(modarith_test, inv) { const TypeParam m; for (const auto& x : get_test_values(m)) diff --git a/evmone/test/unittests/precompiles_expmod_test.cpp b/evmone/test/unittests/precompiles_expmod_test.cpp index b03fe58..89d6043 100644 --- a/evmone/test/unittests/precompiles_expmod_test.cpp +++ b/evmone/test/unittests/precompiles_expmod_test.cpp @@ -358,6 +358,35 @@ TEST_P(expmod, inputs) {"03", "08f83d563ebc382e09e4b8245edebc817af708", "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", "8016137e4c542dd66f4ab5f668fc0ac76d43353a675f3d4616a56f23757e463ca1093164385ef006"}, + // Exponent reduction for the power-of-two part of the modulus. + // Odd base, k=1: the exponent reduces to nothing, the power-of-two part is 1. + {"05", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "06", "05"}, + // Odd base, k=8: the exponent 64 == 2^6 reduces to 0, the power-of-two part is 1. + {"03", "40", "ff00", "aa01"}, + // Odd base, k=8: the exponent reduces to its low 6 bits. + {"ab", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "7fffffffffffffffffffffffffffff00", "47548b40b49c4c051dfb86e2c2c5fc03"}, + // Odd base, k=8: the reduced exponent has leading zero bits to skip. + {"ab", "44", "ff00", "f9b1"}, + // Even base with exp >= k: the power-of-two part is 0. + {"02", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "7fffffffffffffffffffffffffffff00", "00000000002000000000000000000000"}, + // Even base with exp < k: the power-of-two part is 2^32. + {"02", "20", "ffffffffffffffff0000000000000000", "00000000000000000000000100000000"}, + // Even base with exp < k of the same bit width: the comparison must be strict. + {"02", "09", "5000", "0200"}, + // Modulus 2^64 (trivial odd part) with odd base. + {"ab", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "010000000000000000", "0080bfa02fe80bfa03"}, + // Modulus 2^64 with even base and exp >= k. + {"06", "0100", "010000000000000000", "000000000000000000"}, + // Even modulus with k=2: the exponent reduces to a single bit. + {"07", "010000000000000001", "0300000000000000000000000000000004", + "00746574c451e5b990a792326fa8db4c8b"}, + // Odd base, k=3: the smallest k reduced with λ(2^k) = 2^(k-2). + {"05", "03", "18", "05"}, + // Even base with the low word of zero: the parity comes from the low word, not the top. + {"010000000000000000", "02", "06", "04"}, }; for (const auto& [base_hex, exp_hex, mod_hex, expected_result_hex] : test_cases) diff --git a/evmone/test/unittests/precompiles_secp256r1_test.cpp b/evmone/test/unittests/precompiles_secp256r1_test.cpp index d00c5b3..707e125 100644 --- a/evmone/test/unittests/precompiles_secp256r1_test.cpp +++ b/evmone/test/unittests/precompiles_secp256r1_test.cpp @@ -5,7 +5,7 @@ #include #include -using namespace evmmax::secp256r1; +using namespace evmone::crypto::secp256r1; using namespace intx; namespace diff --git a/evmone/test/unittests/evmmax_secp256k1_test.cpp b/evmone/test/unittests/secp256k1_test.cpp similarity index 97% rename from evmone/test/unittests/evmmax_secp256k1_test.cpp rename to evmone/test/unittests/secp256k1_test.cpp index 1339291..735dbb8 100644 --- a/evmone/test/unittests/evmmax_secp256k1_test.cpp +++ b/evmone/test/unittests/secp256k1_test.cpp @@ -6,7 +6,7 @@ #include #include -using namespace evmmax::secp256k1; +using namespace evmone::crypto::secp256k1; using namespace evmc::literals; using namespace evmone::test; @@ -18,8 +18,8 @@ namespace /// Convenience wrapper for point multiplication test. AffinePoint mul(const AffinePoint& p, const uint256& c) noexcept { - const auto r = evmmax::ecc::mul(p, c); - return evmmax::ecc::to_affine(r); + const auto r = evmone::crypto::ecc::mul(p, c); + return evmone::crypto::ecc::to_affine(r); } } // namespace @@ -58,7 +58,7 @@ TEST(secp256k1, field_sqrt_invalid) TEST(secp256k1, scalar_inv) { - const evmmax::ModArith n{Curve::ORDER}; + const evmone::crypto::ModArith n{Curve::ORDER}; for (const auto& t : { 1_u256, @@ -140,7 +140,7 @@ TEST(secp256k1, point_to_address) EXPECT_EQ(to_address({}), 0x3f17f1962B36e491b30A40b2405849e597Ba5FB5_address); } -TEST(evmmax, secp256k1_hash_to_number) +TEST(crypto, secp256k1_hash_to_number) { const auto max_h = ~uint256{}; const auto hm = max_h % Curve::FIELD_PRIME; @@ -150,7 +150,7 @@ TEST(evmmax, secp256k1_hash_to_number) EXPECT_EQ(hm2, hm); } -TEST(evmmax, secp256k1_pt_add_inf) +TEST(crypto, secp256k1_pt_add_inf) { const AffinePoint p1{0x18f4057699e2d9679421de8f4e11d7df9fa4b9e7cb841ea48aed75f1567b9731_u256, 0x6db5b7ecd8e226c06f538d15173267bf1e78acc02bb856e83b3d6daec6a68144_u256}; @@ -162,7 +162,7 @@ TEST(evmmax, secp256k1_pt_add_inf) EXPECT_EQ(add_affine(inf, inf), inf); } -TEST(evmmax, secp256k1_pt_add) +TEST(crypto, secp256k1_pt_add) { const AffinePoint p1{0x18f4057699e2d9679421de8f4e11d7df9fa4b9e7cb841ea48aed75f1567b9731_u256, 0x6db5b7ecd8e226c06f538d15173267bf1e78acc02bb856e83b3d6daec6a68144_u256}; @@ -195,7 +195,7 @@ TEST(evmmax, secp256k1_pt_add) } } -TEST(evmmax, secp256k1_pt_mul_inf) +TEST(crypto, secp256k1_pt_mul_inf) { const AffinePoint p1{0x18f4057699e2d9679421de8f4e11d7df9fa4b9e7cb841ea48aed75f1567b9731_u256, 0x6db5b7ecd8e226c06f538d15173267bf1e78acc02bb856e83b3d6daec6a68144_u256}; @@ -209,7 +209,7 @@ TEST(evmmax, secp256k1_pt_mul_inf) EXPECT_EQ(mul(inf, Curve::ORDER), inf); } -TEST(evmmax, secp256k1_pt_mul) +TEST(crypto, secp256k1_pt_mul) { const AffinePoint p1{0x18f4057699e2d9679421de8f4e11d7df9fa4b9e7cb841ea48aed75f1567b9731_u256, 0x6db5b7ecd8e226c06f538d15173267bf1e78acc02bb856e83b3d6daec6a68144_u256}; @@ -319,7 +319,7 @@ const TestCase TEST_CASES[]{ }; } // namespace -TEST(evmmax, ecrecovery_malleable) +TEST(crypto, ecrecovery_malleable) { for (const auto& [input_hex, expected_output_hex] : TEST_CASES) { @@ -350,7 +350,7 @@ TEST(evmmax, ecrecovery_malleable) } } -TEST(evmmax, ecrecovery_strict) +TEST(crypto, ecrecovery_strict) { const auto order_half = "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0"_hex; ASSERT_EQ(order_half.size(), 32); diff --git a/evmone/test/unittests/state_rlp_decode_test.cpp b/evmone/test/unittests/state_rlp_decode_test.cpp index 2756689..e8c00df 100644 --- a/evmone/test/unittests/state_rlp_decode_test.cpp +++ b/evmone/test/unittests/state_rlp_decode_test.cpp @@ -667,7 +667,7 @@ TEST(state_rlp_decode, recover_sender_rejects_out_of_range_s) // s = the curve order is outside [1, secp256k1n) yet a canonical 32-byte integer, so the // transaction decodes and only the recovery rejects it. auto tx = MINIMAL_LEGACY_TX; - tx.s = evmmax::secp256k1::Curve::ORDER; + tx.s = crypto::secp256k1::Curve::ORDER; EXPECT_FALSE(recover(rlp::encode(tx)).has_value()); } @@ -676,7 +676,7 @@ TEST(state_rlp_decode, recover_sender_rejects_high_s) // EIP-2 bounds s to the lower half of the curve order, on top of the [1, secp256k1n) range. // One above the bound differs from the largest accepted s in nothing else. auto tx = MINIMAL_LEGACY_TX; - tx.s = evmmax::secp256k1::Curve::ORDER / 2; + tx.s = crypto::secp256k1::Curve::ORDER / 2; EXPECT_TRUE(recover(rlp::encode(tx)).has_value()); tx.s += 1; diff --git a/evmone/test/unittests/state_transition.cpp b/evmone/test/unittests/state_transition.cpp index e801449..51caa34 100644 --- a/evmone/test/unittests/state_transition.cpp +++ b/evmone/test/unittests/state_transition.cpp @@ -61,7 +61,8 @@ void state_transition::TearDown() // After EVMC_PRAGUE, get_blob_params will not work like that without a blob schedule. // TODO: add a blob schedule to use with state_transition tests, should they be added. const auto res = test::transition(state, block, block_hashes, tx, rev, selected_vm, - block.gas_limit, static_cast(state::max_blob_gas_per_block(get_blob_params(rev)))); + block.gas_limit, block.gas_limit, + static_cast(state::max_blob_gas_per_block(get_blob_params(rev)))); test::finalize(state, rev, block.coinbase, block_reward, block.ommers, block.withdrawals); const auto& post = state; @@ -86,9 +87,9 @@ void state_transition::TearDown() { EXPECT_EQ(receipt.gas_used, *expect.gas_used); } - if (expect.gas_refund.has_value()) + if (expect.block_gas_used.has_value()) { - EXPECT_EQ(receipt.gas_refund, *expect.gas_refund); + EXPECT_EQ(receipt.block_gas_used, *expect.block_gas_used); } if (expect.logs.has_value()) { @@ -101,6 +102,25 @@ void state_transition::TearDown() << "log " << i << " topics"; } } + if (expect.state_gas.has_value()) + { + EXPECT_EQ(receipt.state_gas_used, *expect.state_gas); + } + + const auto& diff = receipt.state_diff; + for (const auto& [addr, expected_acc] : expect.post) + { + if (!expected_acc.in_diff.has_value()) + continue; + const auto present = + std::ranges::find(diff.deleted_accounts, addr) != diff.deleted_accounts.end() || + std::ranges::find(diff.modified_accounts, addr, &StateDiff::Entry::addr) != + diff.modified_accounts.end(); + EXPECT_EQ(present, *expected_acc.in_diff) + << addr + << (present ? ": unexpectedly in the state diff" : ": missing from the state diff"); + } + // Update default expectations - valid transaction means coinbase exists unless explicitly // requested otherwise if (!expect.post.contains(Coinbase)) diff --git a/evmone/test/unittests/state_transition.hpp b/evmone/test/unittests/state_transition.hpp index 90a263f..8193114 100644 --- a/evmone/test/unittests/state_transition.hpp +++ b/evmone/test/unittests/state_transition.hpp @@ -45,6 +45,10 @@ class state_transition : public ExportableFixture struct ExpectedAccount { bool exists = true; + + /// Whether the account is expected to be mentioned in the transaction's state diff. + std::optional in_diff; + std::optional nonce; std::optional balance; std::optional code; @@ -63,14 +67,16 @@ class state_transition : public ExportableFixture /// The expected amount of gas used by the transaction. std::optional gas_used; - /// The expected EIP-7778 block-side gas refund stored on the receipt - /// (`gas_used + gas_refund` equals `max(pre-refund gas, EIP-7623 floor)`). - std::optional gas_refund; + /// The expected amount of gas counted against the block gas limit (EIP-7778). + std::optional block_gas_used; /// The expected logs emitted by the transaction. When set, the receipt's logs must match /// exactly: count, address, data, topics, and order. std::optional> logs; + /// The expected state-gas component of the receipt (EIP-8037). + std::optional state_gas; + /// The expected post-execution state. std::unordered_map post; diff --git a/evmone/test/unittests/state_transition_call_test.cpp b/evmone/test/unittests/state_transition_call_test.cpp index 7f0cc9b..5dc2d1b 100644 --- a/evmone/test/unittests/state_transition_call_test.cpp +++ b/evmone/test/unittests/state_transition_call_test.cpp @@ -18,6 +18,7 @@ TEST_F(state_transition, call_value_to_empty) expect.post[To].balance = 0; expect.post[BENEFICIARY].balance = 1; + expect.post[BENEFICIARY].in_diff = true; // The received value is a real modification. } TEST_F(state_transition, delegatecall_static_legacy) @@ -50,3 +51,21 @@ TEST_F(state_transition, delegatecall_static_legacy) expect.post[CALLEE1].storage[0x01_bytes32] = 0xdd_bytes32; expect.post[CALLEE2].storage[0x01_bytes32] = 0xdd_bytes32; } + +TEST_F(state_transition, osaka_call_depth_limit_unreachable) +{ + // EIP-7825 caps the transaction gas and the 63/64 rule leaves each frame a 64th less, so the + // cheapest possible self-recursion runs out of gas at depth 494 and the 1024 limit is + // unreachable. gas_used pins it: a frame stopped by the limit would keep the gas it forwarded. + rev = EVMC_OSAKA; + tx.gas_limit = state::MAX_TX_GAS_LIMIT; + block.gas_limit = tx.gas_limit; + pre[Sender].balance = intx::uint256{tx.gas_limit} * tx.max_gas_price + 1; + tx.to = To; + pre[To] = { + .code = + delegatecall(OP_ADDRESS).gas(OP_GAS).input(push0(), push0()).output(push0(), push0())}; + + expect.gas_used = 76'314; // 21'000 intrinsic + 493 frames x 112 + 98 in the final frame + expect.post[To].exists = true; +} diff --git a/evmone/test/unittests/state_transition_create_test.cpp b/evmone/test/unittests/state_transition_create_test.cpp index e004e5a..f587f18 100644 --- a/evmone/test/unittests/state_transition_create_test.cpp +++ b/evmone/test/unittests/state_transition_create_test.cpp @@ -431,7 +431,7 @@ TEST_F(state_transition, eip7954_create_tx_at_max_code_size) // A create transaction deploying code of exactly the new limit succeeds. rev = EVMC_AMSTERDAM; static constexpr auto code_size = 0x10000; // MAX_CODE_SIZE_AMSTERDAM. - tx.gas_limit = 16'000'000; // Covers the ~13.1M code-deposit gas (200/byte). + tx.gas_limit = 110'000'000; // Covers the ~100M code-deposit state gas (EIP-8037). block.gas_limit = tx.gas_limit; pre[Sender].balance = tx.gas_limit * tx.max_gas_price; tx.data = ret(0, code_size); // Init code returns `code_size` zero bytes as the deployed code. @@ -445,7 +445,7 @@ TEST_F(state_transition, eip7954_create_tx_above_max_code_size) // Code one byte above the new 0x10000 limit is still rejected on Amsterdam (EIP-7954). rev = EVMC_AMSTERDAM; static constexpr auto code_size = 0x10000 + 1; - tx.gas_limit = 16'000'000; // Enough to deposit the code, so only the limit can reject it. + tx.gas_limit = 110'000'000; // Enough to deposit the code, so only the limit can reject it. block.gas_limit = tx.gas_limit; pre[Sender].balance = tx.gas_limit * tx.max_gas_price; tx.data = ret(0, code_size); // Init code returns code one byte over the limit. diff --git a/evmone/test/unittests/state_transition_eip7778_block_gas_test.cpp b/evmone/test/unittests/state_transition_eip7778_block_gas_test.cpp index 4ff8c54..36afd22 100644 --- a/evmone/test/unittests/state_transition_eip7778_block_gas_test.cpp +++ b/evmone/test/unittests/state_transition_eip7778_block_gas_test.cpp @@ -10,17 +10,17 @@ using namespace evmone::test; TEST_F(state_transition, eip7778_sstore_clear_refund_amsterdam) { - // EIP-7778: a clearing SSTORE produces a 4800 refund. The receipt exposes - // it as `gas_refund` so the block accumulates `gas_used + gas_refund` - // (the pre-refund gas), independent of what the user pays. + // EIP-7778: a clearing SSTORE produces a 4800 refund, but the block counts the pre-refund gas + // independently of what the user pays. rev = EVMC_AMSTERDAM; tx.to = To; pre[To] = {.storage = {{0x01_bytes32, 0x42_bytes32}}, .code = sstore(1, 0)}; - // Pre-refund: 21000 intrinsic + 5000 (cold SSTORE reset) + 6 (two PUSHes) = 26006. - // London-schedule clear refund 4800 ≤ cap 26006/5, so the full refund applies. - expect.gas_used = 26006 - 4800; - expect.gas_refund = 4800; + // Intrinsic, two PUSHes and the cold SSTORE clear (EIP-8038). The clear refund (11616) is + // capped at a fifth of the pre-refund gas (EIP-3529). + constexpr auto PRE_REFUND = 15'000 + 6 + 12'100; + expect.gas_used = PRE_REFUND - PRE_REFUND / 5; + expect.block_gas_used = PRE_REFUND; expect.post[To].exists = true; expect.post[To].storage[0x01_bytes32] = 0x00_bytes32; } diff --git a/evmone/test/unittests/state_transition_eip8037_state_gas_test.cpp b/evmone/test/unittests/state_transition_eip8037_state_gas_test.cpp new file mode 100644 index 0000000..c5c95d5 --- /dev/null +++ b/evmone/test/unittests/state_transition_eip8037_state_gas_test.cpp @@ -0,0 +1,391 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "state_transition.hpp" +#include +#include +#include + +using namespace evmc::literals; +using namespace evmone::test; + +TEST_F(state_transition, eip8037_create_tx_collision_excess_reservoir_refunded) +{ + // A create transaction colliding with an existing account (EIP-7610) returns its state-gas + // reservoir instead of forfeiting it, so the sender is billed at most MAX_TX_GAS_LIMIT. + rev = EVMC_AMSTERDAM; + + constexpr int64_t TX_GAS_LIMIT = 18'000'000; + static_assert(TX_GAS_LIMIT > MAX_TX_GAS_LIMIT); // The excess forms the reservoir. + + block.gas_limit = TX_GAS_LIMIT * 2; + tx.gas_limit = TX_GAS_LIMIT; // tx.to stays nullopt: a create transaction. + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + pre[create_address] = {.nonce = 1, .code = bytecode{OP_STOP}}; + + // The colliding account is alive, so the preparation charge does not apply. + expect.status = EVMC_FAILURE; + expect.gas_used = MAX_TX_GAS_LIMIT; + expect.block_gas_used = expect.gas_used; // No EVM gas refund. + expect.state_gas = 0; + expect.post[create_address] = {.nonce = 1, .code = bytecode{OP_STOP}}; +} + +TEST_F(state_transition, eip8037_create_tx_revert_refunds_spilled_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + tx.data = revert(0, 0); + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + expect.status = EVMC_REVERT; + // The calldata floor of the 5-byte initcode. NEW_ACCOUNT is refunded. + expect.gas_used = 24'000 + 5 * 64; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = 0; + expect.post[create_address].exists = false; +} + +TEST_F(state_transition, eip8037_create_tx_halt_returns_excess_reservoir) +{ + rev = EVMC_AMSTERDAM; + + constexpr int64_t TX_GAS_LIMIT = 18'000'000; + static_assert(TX_GAS_LIMIT > MAX_TX_GAS_LIMIT); + + block.gas_limit = TX_GAS_LIMIT * 2; + tx.gas_limit = TX_GAS_LIMIT; + tx.data = bytecode{OP_INVALID}; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + expect.status = EVMC_INVALID_INSTRUCTION; + // The halt consumes the execution-gas dimension, but the unused reservoir is returned. + expect.gas_used = MAX_TX_GAS_LIMIT; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = 0; + expect.post[create_address].exists = false; +} + +TEST_F(state_transition, eip8037_create_tx_charges_new_account_and_code_deposit) +{ + rev = EVMC_AMSTERDAM; + tx.data = ret(0, 1); // Deploy a single zero byte. + + expect.state_gas = NEW_ACCOUNT_STATE_GAS + COST_PER_STATE_BYTE; + expect.post[compute_create_address(Sender, pre[Sender].nonce)].code = bytes{0x00}; +} + +TEST_F(state_transition, eip8037_create_tx_with_value_pays_new_account_once) +{ + rev = EVMC_AMSTERDAM; + tx.value = 1; + + expect.gas_used = 24'000 + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; + expect.post[compute_create_address(Sender, pre[Sender].nonce)] = {.nonce = 1, .balance = 1}; +} + +TEST_F(state_transition, eip8037_create_tx_uses_reservoir_then_execution_gas) +{ + rev = EVMC_AMSTERDAM; + tx.gas_limit = MAX_TX_GAS_LIMIT + NEW_ACCOUNT_STATE_GAS / 2; + block.gas_limit = tx.gas_limit; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + expect.gas_used = 24'000 + NEW_ACCOUNT_STATE_GAS; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = NEW_ACCOUNT_STATE_GAS; + expect.post[compute_create_address(Sender, pre[Sender].nonce)].nonce = 1; +} + +TEST_F(state_transition, eip8037_create_tx_to_prefunded_account_has_no_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + pre[create_address].balance = 1; + + expect.gas_used = 24'000; + expect.state_gas = 0; + expect.post[create_address] = {.nonce = 1, .balance = 1}; +} + +TEST_F(state_transition, eip8037_create_tx_out_of_gas_on_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + tx.gas_limit = 60'000; // Intrinsic gas leaves less than NEW_ACCOUNT_STATE_GAS. + + expect.status = EVMC_OUT_OF_GAS; + expect.gas_used = tx.gas_limit; + expect.state_gas = 0; + expect.post[compute_create_address(Sender, pre[Sender].nonce)].exists = false; +} + +TEST_F(state_transition, eip8037_nested_create_revert_refills_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; + pre[To] = {.code = mstore(0, push(revert(0, 0))) + create().input(27, 5) + OP_STOP}; + + expect.state_gas = 0; + expect.post[To].nonce = 1; +} + +namespace +{ +/// The intrinsic plus the CALL's execution gas: its seven arguments, the warm call, the +/// cold-account surcharge and the value transfer. CALL_VALUE is ACCOUNT_WRITE + CALL_STIPEND +/// and a light failure returns the stipend unspent, so only ACCOUNT_WRITE remains (EIP-8038). +/// The NEW_ACCOUNT state charge is refilled, so it does not appear here. +constexpr int64_t CALL_LIGHTFAIL_EXECUTION_GAS = + 15'000 + 7 * instr::gas_costs[EVMC_AMSTERDAM][OP_PUSH1] + + instr::gas_costs[EVMC_AMSTERDAM][OP_CALL] + + instr::additional_cold_account_access(EVMC_AMSTERDAM) + instr::ACCOUNT_WRITE; +} // namespace + +TEST_F(state_transition, eip8037_call_value_lightfail_new_account_charge_refilled) +{ + // A value-CALL charges NEW_ACCOUNT for an absent target before the sender-balance check. + // The light failure creates no account, so the charge is refilled and the net state-gas is + // zero — matching the baseline below, which differs only in the target existing. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto TARGET = 0xbeef_address; // Absent from `pre`. + + pre[To] = {.code = call(TARGET).value(1).gas(0xffff) + OP_STOP}; // To cannot pay the value. + + expect.status = EVMC_SUCCESS; // To STOPs after the light failure. + expect.post[To].exists = true; + expect.post[TARGET].exists = false; + expect.gas_used = CALL_LIGHTFAIL_EXECUTION_GAS; + expect.state_gas = 0; +} + +TEST_F(state_transition, eip8037_call_value_lightfail_existing_account_baseline) +{ + // The baseline for the case above: an existing target is never charged, so both the execution + // gas and the state-gas must come out identical. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto TARGET = 0xbeef_address; + + pre[To] = {.code = call(TARGET).value(1).gas(0xffff) + OP_STOP}; + pre[TARGET] = {.nonce = 1, .code = bytecode{OP_STOP}}; + + expect.status = EVMC_SUCCESS; + expect.post[To].exists = true; + expect.post[TARGET] = {.nonce = 1}; + expect.gas_used = CALL_LIGHTFAIL_EXECUTION_GAS; + expect.state_gas = 0; +} + +TEST_F(state_transition, eip8037_value_to_zero_balance_precompile_pays_new_account) +{ + // Funding a zero-balance precompile materializes a state account, so it pays NEW_ACCOUNT + // (EIP-161). The reservoir is empty below the cap, so the charge spills into execution gas + // and the precompile runs on what is left. + rev = EVMC_AMSTERDAM; + tx.to = 0x04_address; // Identity, absent from `pre`. + tx.value = 1; + + constexpr int64_t IDENTITY_BASE_COST = 15; + + expect.status = EVMC_SUCCESS; + expect.post[*tx.to].balance = 1; + expect.gas_used = 21'000 + IDENTITY_BASE_COST + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_new_account_pays_new_account) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; // Absent from pre. + tx.value = 1; + tx.gas_limit = 21'000 + NEW_ACCOUNT_STATE_GAS; // Exact successful boundary. + + expect.post[To].balance = 1; + expect.gas_used = tx.gas_limit; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_existing_empty_account_pays_new_account) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; + tx.value = 1; + pre[To] = {}; + + expect.post[To].balance = 1; + expect.gas_used = 21'000 + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_new_account_uses_reservoir_then_execution_gas) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; // Absent from pre. + tx.value = 1; + tx.gas_limit = MAX_TX_GAS_LIMIT + NEW_ACCOUNT_STATE_GAS / 2; + block.gas_limit = tx.gas_limit; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + expect.post[To].balance = 1; + expect.gas_used = 21'000 + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_new_account_out_of_gas) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; // Absent from pre. + tx.value = 1; + tx.gas_limit = 21'000 + NEW_ACCOUNT_STATE_GAS - 1; + + expect.status = EVMC_OUT_OF_GAS; + expect.gas_used = tx.gas_limit; + expect.state_gas = 0; + expect.post[To].exists = false; +} + +TEST_F(state_transition, eip8037_value_to_new_precompile_failure_refunds_new_account) +{ + rev = EVMC_AMSTERDAM; + tx.to = 0x04_address; // Identity, absent from pre. + tx.value = 1; + tx.gas_limit = 21'000 + NEW_ACCOUNT_STATE_GAS + 14; // Identity requires 15 gas. + + expect.status = EVMC_OUT_OF_GAS; + expect.gas_used = tx.gas_limit; + expect.state_gas = 0; + expect.post[*tx.to].exists = false; +} + +TEST_F(state_transition, eip8037_sstore_slot_allocated_and_cleared_in_one_tx) +{ + // Allocating a slot and clearing it in the same transaction (0 -> 1 -> 0) refills the + // allocation charge, leaving the net state-gas at zero. + rev = EVMC_AMSTERDAM; + tx.to = To; + pre[To] = {.code = sstore(1, 1) + sstore(1, 0)}; + + // Intrinsic, four PUSHes, the cold allocation, the warm clear; the clear's refund is capped + // at a fifth of the pre-refund gas. + constexpr auto PRE_REFUND = 15'000 + 12 + 12'100 + 100; + expect.gas_used = PRE_REFUND - PRE_REFUND / 5; + expect.block_gas_used = PRE_REFUND; + expect.state_gas = 0; + expect.post[To].exists = true; +} + +TEST_F(state_transition, eip8037_sstore_slot_cleared_in_a_child_frame) +{ + // A slot allocated in one frame and cleared in a deeper one refills more state-gas than the + // child was given, so the child returns a bigger reservoir than it received. The credit must + // reach the `gas_left` that funded the spilled allocation charge. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto CLEARER = 0xdead_address; + pre[CLEARER] = {.code = sstore(1, 0)}; + pre[To] = {.code = sstore(1, 1) + delegatecall(CLEARER).gas(0xffff) + OP_STOP}; + + // Intrinsic, ten PUSHes, the cold allocation, the cold DELEGATECALL, the warm clear; the + // clear's refund is capped at a fifth of the pre-refund gas. + constexpr auto PRE_REFUND = 15'000 + 30 + 12'100 + 3000 + 100; + expect.gas_used = PRE_REFUND - PRE_REFUND / 5; + expect.block_gas_used = PRE_REFUND; + expect.state_gas = 0; + expect.post[To].exists = true; + expect.post[CLEARER].exists = true; +} + +TEST_F(state_transition, eip8037_reverted_child_keeps_the_slot_allocation_charged) +{ + // A child clearing a slot its caller allocated refills more state-gas than it was given, + // leaving its reservoir above its own budget. Reverting must restore that budget rather than + // credit the refill, so the allocation stays charged. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto CLEARER = 0xdead_address; + pre[CLEARER] = {.code = sstore(1, 0) + revert(0, 0)}; + pre[To] = {.code = sstore(1, 1) + delegatecall(CLEARER).gas(0xffff) + OP_STOP}; + + // Intrinsic, twelve PUSHes, the cold allocation and its state charge, the cold DELEGATECALL, + // the reverted warm clear. The clear's refund dies with the frame. + expect.gas_used = 15'000 + 36 + 12'100 + STORAGE_SET_STATE_GAS + 3000 + 100; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = STORAGE_SET_STATE_GAS; + expect.post[To].exists = true; + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; // The child's clear is rolled back. + expect.post[CLEARER].exists = true; +} + +namespace +{ +/// The code deposit of a maximum-size contract, split into its two components (EIP-8037). +constexpr auto DEPOSIT_CODE_WORDS = MAX_CODE_SIZE_AMSTERDAM / 32; +constexpr auto DEPOSIT_EXECUTION = 6 * DEPOSIT_CODE_WORDS; +constexpr int64_t DEPOSIT_STATE = int64_t{MAX_CODE_SIZE_AMSTERDAM} * COST_PER_STATE_BYTE; + +/// Gas limit whose excess over the cap covers the deposit's state component outright. +constexpr auto DEPOSIT_TX_GAS = MAX_TX_GAS_LIMIT + DEPOSIT_STATE + 1'000'000; + +/// Cap leaving the initcode frame mid-window: enough for CREATE and the memory the returned code +/// needs, plus half the execution component. The CREATE price is the only term a reprice has +/// moved, so it comes from the cost table rather than being pinned. +/// DEPOSIT_MEMORY mirrors the expansion formula in check_memory(); a change there shifts the +/// window rather than failing here. +constexpr auto DEPOSIT_MEMORY = + 3 * DEPOSIT_CODE_WORDS + DEPOSIT_CODE_WORDS * DEPOSIT_CODE_WORDS / 512; +constexpr auto DEPOSIT_GAS_CAP = + instr::gas_costs[EVMC_AMSTERDAM][OP_CREATE] + DEPOSIT_MEMORY + DEPOSIT_EXECUTION / 2; + +constexpr auto DEPOSIT_CREATOR = 0xbbbb_address; + +/// Code deploying MAX_CODE_SIZE_AMSTERDAM zero bytes through a nested CREATE. +bytecode deposit_creator_code() +{ + const auto initcode = ret(0, MAX_CODE_SIZE_AMSTERDAM); + return mstore(0, push(initcode)) + create().input(32 - initcode.size(), initcode.size()); +} +} // namespace + +TEST_F(state_transition, eip8037_code_deposit_out_of_execution_gas_with_a_full_reservoir) +{ + // The code deposit splits into an execution and a state component. A reservoir covering the + // state component must not let the deposit through when the execution component is + // unaffordable. + rev = EVMC_AMSTERDAM; + tx.gas_limit = DEPOSIT_TX_GAS; + block.gas_limit = tx.gas_limit; + tx.to = To; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + 1; + pre[DEPOSIT_CREATOR] = {.code = deposit_creator_code()}; + pre[To] = {.code = call(DEPOSIT_CREATOR).gas(DEPOSIT_GAS_CAP) + OP_STOP}; + + expect.state_gas = 0; // The refused deposit charges none, and the CREATE's is refilled. + expect.post[To].exists = true; + expect.post[DEPOSIT_CREATOR].nonce = pre[DEPOSIT_CREATOR].nonce + 1; // Bumped by the CREATE. + expect.post[compute_create_address(DEPOSIT_CREATOR, pre[DEPOSIT_CREATOR].nonce)].exists = false; +} + +TEST_F(state_transition, eip8037_code_deposit_execution_gas_boundary) +{ + // The same deposit one execution component richer succeeds, pinning the case above to the + // execution gas rather than to anything else the CREATE pays for. + rev = EVMC_AMSTERDAM; + tx.gas_limit = DEPOSIT_TX_GAS; + block.gas_limit = tx.gas_limit; + tx.to = To; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + 1; + pre[DEPOSIT_CREATOR] = {.code = deposit_creator_code()}; + pre[To] = {.code = call(DEPOSIT_CREATOR).gas(DEPOSIT_GAS_CAP + DEPOSIT_EXECUTION) + OP_STOP}; + + expect.state_gas = DEPOSIT_STATE + NEW_ACCOUNT_STATE_GAS; // Deposit plus the new account. + expect.post[To].exists = true; + expect.post[DEPOSIT_CREATOR].nonce = pre[DEPOSIT_CREATOR].nonce + 1; + expect.post[compute_create_address(DEPOSIT_CREATOR, pre[DEPOSIT_CREATOR].nonce)].code = + bytes(MAX_CODE_SIZE_AMSTERDAM, 0x00); +} diff --git a/evmone/test/unittests/state_transition_touch_test.cpp b/evmone/test/unittests/state_transition_touch_test.cpp index 0bac989..aa2ead3 100644 --- a/evmone/test/unittests/state_transition_touch_test.cpp +++ b/evmone/test/unittests/state_transition_touch_test.cpp @@ -21,6 +21,7 @@ TEST_F(state_transition, touch_empty_sd) expect.post[*tx.to].exists = true; expect.post[EMPTY].exists = false; + expect.post[EMPTY].in_diff = true; // Pre-existing account, the sweep is a real deletion. } TEST_F(state_transition, touch_empty_tw) @@ -111,6 +112,24 @@ TEST_F(state_transition, touch_revert_nonexistent_istanbul) expect.post[EMPTY].exists = false; } +TEST_F(state_transition, touch_revert_cold_access_nonexistent) +{ + // Accessing a non-existent account warms it up without creating it (EIP-2929). + // Reverting the accessing frame must leave no state diff entry. + rev = EVMC_BERLIN; + block.base_fee = 0; + static constexpr auto NONEXISTENT = 0x4e_address; + + tx.type = Transaction::Type::legacy; + tx.to = To; + pre[*tx.to] = {.code = push(NONEXISTENT) + OP_BALANCE + OP_POP + revert(0, 0)}; + + expect.status = EVMC_REVERT; + expect.post[*tx.to].exists = true; + expect.post[NONEXISTENT].exists = false; + expect.post[NONEXISTENT].in_diff = false; +} + TEST_F(state_transition, touch_revert_nonexistent_tw) { rev = EVMC_TANGERINE_WHISTLE; // no touching @@ -307,3 +326,54 @@ TEST_F(state_transition, touch_revert_storage_only) expect.post[STORAGE_ONLY].exists = true; expect.post[STORAGE_ONLY].storage[0x01_bytes32] = 0x01_bytes32; } + +TEST_F(state_transition, access_nonexistent) +{ + // Accessing an account which does not exist warms it up (EIP-2929) without creating it, + // so it does not appear in the state diff. + static constexpr auto ABSENT = 0xab5e17_address; + + tx.to = To; + pre[*tx.to] = {.code = push(ABSENT) + OP_BALANCE + OP_POP}; + + expect.post[*tx.to].exists = true; + expect.post[ABSENT].exists = false; + expect.post[ABSENT].in_diff = false; +} + +TEST_F(state_transition, touch_nonexistent_after_reverted_create2) +{ + // The CREATE2 warms the never-existing address and its creation reverts. Then a zero-value + // call touches the address, so it ends empty and is erased. + const auto initcode = revert(0, 0); + const auto created = compute_create2_address(To, {}, initcode); + + tx.to = To; + pre[To] = {.code = mstore(0, push(initcode)) + + create2().input(32 - initcode.size(), initcode.size()) + OP_POP + + call(created)}; + + expect.post[To].exists = true; + expect.post[created].exists = false; + // FIXME: A no-op deletion of an account which has never existed (#1718). + expect.post[created].in_diff = true; +} + +TEST_F(state_transition, touch_empty_after_reverted_create2) +{ + // The CREATE2 over a pre-existing empty account reverts. Then a zero-value call touches the + // account, so it ends empty and is deleted (EIP-161). Empty accounts exist before EIP-7523. + rev = EVMC_LONDON; + const auto initcode = revert(0, 0); + const auto created = compute_create2_address(To, {}, initcode); + + tx.to = To; + pre[To] = {.code = mstore(0, push(initcode)) + + create2().input(32 - initcode.size(), initcode.size()) + OP_POP + + call(created)}; + pre[created] = {}; + + expect.post[To].exists = true; + expect.post[created].exists = false; + expect.post[created].in_diff = true; +} diff --git a/evmone/test/unittests/state_transition_tx_test.cpp b/evmone/test/unittests/state_transition_tx_test.cpp index b0d9d6a..719fb8d 100644 --- a/evmone/test/unittests/state_transition_tx_test.cpp +++ b/evmone/test/unittests/state_transition_tx_test.cpp @@ -111,6 +111,30 @@ TEST_F(state_transition, tx_blob_gas_price) expect.status = EVMC_SUCCESS; } +TEST_F(state_transition, invalid_tx_blob_max_fee_overflow) +{ + // With one blob, GAS_PER_BLOB * max_blob_gas_price is 2^17 * 2^239 = 2^256, which wraps to 0 + // in uint256 and would make the blob fee vanish from the affordability check. + rev = EVMC_CANCUN; + tx.type = Transaction::Type::blob; + tx.to = To; + tx.gas_limit = 25000; + tx.max_gas_price = block.base_fee; + tx.max_priority_gas_price = 0; + tx.nonce = 1; + tx.blob_hashes.emplace_back( + 0x0100000000000000000000000000000000000000000000000000000000000000_bytes32); + tx.max_blob_gas_price = intx::uint256{1} << 239; + + block.excess_blob_gas = 0; + block.blob_base_fee = 1; + block.blob_gas_used = 786432; + + pre[tx.sender].balance = tx.gas_limit * tx.max_gas_price; + + expect.tx_error = INSUFFICIENT_ACCOUNT_FUNDS; +} + TEST_F(state_transition, empty_coinbase_fee_0_sd) { rev = EVMC_SPURIOUS_DRAGON; @@ -198,7 +222,7 @@ TEST_F(state_transition, tx_data_floor_amsterdam_exec_0) tx.data = "0001"_hex; static constexpr auto MIN_GAS = 64 * 2; - expect.gas_used = 21000 + MIN_GAS; + expect.gas_used = 15000 + MIN_GAS; } TEST_F(state_transition, tx_data_floor_amsterdam_exec_below_floor) @@ -211,7 +235,7 @@ TEST_F(state_transition, tx_data_floor_amsterdam_exec_below_floor) static constexpr auto MIN_GAS = 64 * 2; pre[To] = {.code = (MIN_GAS - DATA_GAS - 1) * OP_JUMPDEST}; - expect.gas_used = 21000 + MIN_GAS; + expect.gas_used = 15000 + MIN_GAS; expect.post[To].exists = true; } @@ -225,7 +249,7 @@ TEST_F(state_transition, tx_data_floor_amsterdam_exec_at_floor) static constexpr auto MIN_GAS = 64 * 2; pre[To] = {.code = (MIN_GAS - DATA_GAS) * OP_JUMPDEST}; - expect.gas_used = 21000 + MIN_GAS; + expect.gas_used = 15000 + MIN_GAS; expect.post[To].exists = true; } @@ -239,7 +263,7 @@ TEST_F(state_transition, tx_data_floor_amsterdam_exec_above_floor) static constexpr auto MIN_GAS = 64 * 2; pre[To] = {.code = (MIN_GAS - DATA_GAS + 1) * OP_JUMPDEST}; - expect.gas_used = 21000 + MIN_GAS + 1; + expect.gas_used = 15000 + MIN_GAS + 1; expect.post[To].exists = true; } @@ -251,7 +275,7 @@ TEST_F(state_transition, tx_data_floor_amsterdam_zero_bytes) tx.data = "0000"_hex; static constexpr auto MIN_GAS = 64 * 2; - expect.gas_used = 21000 + MIN_GAS; + expect.gas_used = 15000 + MIN_GAS; } TEST_F(state_transition, tx_data_floor_osaka_uses_eip7623) @@ -268,11 +292,12 @@ TEST_F(state_transition, tx_data_floor_osaka_uses_eip7623) TEST_F(state_transition, access_list_cost_amsterdam) { // EIP-7981: 1280 gas (64*20) per address, 2048 gas (64*32) per storage key. + // EIP-8038: the per-entry prices become 2900 and 2000. rev = EVMC_AMSTERDAM; tx.to = To; tx.access_list = {{To, {0x01_bytes32}}}; - // intrinsic = 21000 + 2400 + 1900 + 1280 + 2048 = 28628 - expect.gas_used = 28628; + // intrinsic = 15000 + 2900 + 2000 + 1280 + 2048 = 23228 + expect.gas_used = 23228; } TEST_F(state_transition, access_list_cost_osaka_unchanged) @@ -303,19 +328,19 @@ TEST_F(state_transition, access_list_floor_amsterdam) tx.to = To; tx.data = bytes(100, 0x00); tx.access_list = {{To, {}}}; - // intrinsic = 21000 + 100*4 + 2400 + 1280 = 25080 - // floor = 21000 + 64*(100 + 20) = 28680 (dominates) - expect.gas_used = 28680; + // intrinsic = 15000 + 100*4 + 2900 + 1280 = 19580 + // floor = 15000 + 64*(100 + 20) = 22680 (dominates) + expect.gas_used = 22680; } TEST_F(state_transition, invalid_access_list_amsterdam_gas_limit_below_floor) { - // EIP-7981: gas limit must cover the floor (28680) — pre-7981 intrinsic (23800) is not enough. + // EIP-7981: gas limit must cover the floor (22680) — the intrinsic cost (19580) is not enough. rev = EVMC_AMSTERDAM; tx.to = To; tx.data = bytes(100, 0x00); tx.access_list = {{To, {}}}; - tx.gas_limit = 28679; + tx.gas_limit = 22679; expect.tx_error = INTRINSIC_GAS_TOO_LOW; } diff --git a/evmone/test/unittests/state_tx_test.cpp b/evmone/test/unittests/state_tx_test.cpp index 1db2b90..72e0c16 100644 --- a/evmone/test/unittests/state_tx_test.cpp +++ b/evmone/test/unittests/state_tx_test.cpp @@ -26,17 +26,17 @@ TEST(state_tx, validate_nonce) const TestState state{{tx.sender, {.nonce = 1, .balance = 1'000'000}}}; ASSERT_FALSE(holds_alternative( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0))); + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0))); tx.nonce = 0; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0)) .message(), "TransactionException.NONCE_MISMATCH_TOO_LOW"); tx.nonce = 2; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0)) .message(), "TransactionException.NONCE_MISMATCH_TOO_HIGH"); } @@ -54,19 +54,19 @@ TEST(state_tx, validate_sender) const TestState state{{tx.sender, {}}}; ASSERT_FALSE(holds_alternative( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0))); + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0))); block.base_fee = 1; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0, 0)) .message(), "TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS"); tx.max_gas_price = block.base_fee; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0, 0)) .message(), "TransactionException.INSUFFICIENT_ACCOUNT_FUNDS"); } @@ -92,17 +92,17 @@ TEST(state_tx, validate_blob_tx) const auto blob_gas_limit = static_cast(max_blob_gas_per_block(get_blob_params(EVMC_CANCUN))); EXPECT_EQ(std::get(validate_transaction( - state, block, tx, EVMC_SHANGHAI, block.gas_limit, blob_gas_limit)), + state, block, tx, EVMC_SHANGHAI, block.gas_limit, 0, blob_gas_limit)), make_error_code(ErrorCode::TYPE_NOT_SUPPORTED)); EXPECT_EQ(std::get(validate_transaction(state, block, tx, EVMC_CANCUN, - block.gas_limit, blob_gas_limit)) + block.gas_limit, 0, blob_gas_limit)) .message(), make_error_code(ErrorCode::CREATE_BLOB_TX).message()); tx.to = 0x01_address; EXPECT_EQ(std::get(validate_transaction( - state, block, tx, EVMC_CANCUN, block.gas_limit, blob_gas_limit)), + state, block, tx, EVMC_CANCUN, block.gas_limit, 0, blob_gas_limit)), make_error_code(ErrorCode::EMPTY_BLOB_HASHES_LIST)); for (uint8_t i = 0; i < 6; ++i) @@ -114,7 +114,7 @@ TEST(state_tx, validate_blob_tx) const auto expect_error = [&](int64_t g) { return std::get( - validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, g)); + validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, 0, g)); }; EXPECT_EQ(expect_error(blob_gas_limit), @@ -130,7 +130,7 @@ TEST(state_tx, validate_blob_tx) expect_error(blob_gas_limit - 1), make_error_code(ErrorCode::BLOB_GAS_LIMIT_EXCEEDED)); EXPECT_EQ(std::get(validate_transaction(state, block, tx, EVMC_CANCUN, - block.gas_limit, blob_gas_limit)) + block.gas_limit, 0, blob_gas_limit)) .execution_gas_limit, 39000); @@ -157,7 +157,8 @@ TEST(state_tx, validate_eof_create_transaction) for (int r = EVMC_CANCUN; r <= EVMC_MAX_REVISION; ++r) { const auto rev = static_cast(r); - const auto res = validate_transaction(state, block, tx, rev, block.gas_limit, 0); + const auto res = + validate_transaction(state, block, tx, rev, block.gas_limit, block.gas_limit, 0); EXPECT_FALSE(holds_alternative(res)); } } @@ -179,7 +180,7 @@ TEST(state_tx, validate_tx_data_cost) const TestState state{{tx.sender, {.balance = 1'000'000}}}; const auto get_props = [&](evmc_revision rev) { - const auto res = validate_transaction(state, block, tx, rev, block.gas_limit, 0); + const auto res = validate_transaction(state, block, tx, rev, block.gas_limit, 0, 0); EXPECT_TRUE(holds_alternative(res)); if (holds_alternative(res)) return get(res); @@ -233,14 +234,14 @@ TEST(state_tx, max_blob_count) // Should be valid EXPECT_FALSE(holds_alternative( - validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, blob_gas_limit))); + validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, 0, blob_gas_limit))); // Add one more blob to exceed the limit tx.blob_hashes.emplace_back( 0x01000000000000000000000000000000000000000000000000000000000000FF_bytes32); EXPECT_EQ(std::get(validate_transaction( - state, block, tx, EVMC_CANCUN, block.gas_limit, blob_gas_limit)), + state, block, tx, EVMC_CANCUN, block.gas_limit, 0, blob_gas_limit)), make_error_code(ErrorCode::BLOB_GAS_LIMIT_EXCEEDED)); } @@ -251,6 +252,6 @@ TEST(state_tx, max_gas_limit_exceeded) const TestState state; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_OSAKA, block.gas_limit, 0)), + validate_transaction(state, block, tx, EVMC_OSAKA, block.gas_limit, 0, 0)), make_error_code(ErrorCode::GAS_LIMIT_EXCEEDS_MAXIMUM)); } diff --git a/evmone/test/unittests/statediff_export_test.cpp b/evmone/test/unittests/statediff_export_test.cpp new file mode 100644 index 0000000..e98f7db --- /dev/null +++ b/evmone/test/unittests/statediff_export_test.cpp @@ -0,0 +1,57 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +using namespace evmone; +using namespace evmone::state; +using namespace evmc::literals; + +TEST(statediff_export, state_diff_empty) +{ + const StateDiff diff; + EXPECT_EQ(test::to_json(diff).dump(), R"({"deletedAccounts":[],"modifiedAccounts":{}})"); +} + +TEST(statediff_export, state_diff_modified_account_with_code_and_storage) +{ + StateDiff diff; + diff.modified_accounts.push_back({.addr = 0x01_address, + .nonce = 1, + .balance = 2, + .code = bytes{0xfe}, + .modified_storage = {{0x03_bytes32, 0x04_bytes32}}}); + + const auto j = test::to_json(diff); + const auto& j_acc = j["modifiedAccounts"]["0x0000000000000000000000000000000000000001"]; + EXPECT_EQ(j_acc["nonce"], "0x1"); + EXPECT_EQ(j_acc["balance"], "0x2"); + EXPECT_EQ(j_acc["code"], "0xfe"); + EXPECT_EQ(j_acc["modifiedStorage"] + ["0x0000000000000000000000000000000000000000000000000000000000000003"], + "0x0000000000000000000000000000000000000000000000000000000000000004"); + EXPECT_EQ(j["deletedAccounts"], json::json::array()); +} + +TEST(statediff_export, state_diff_modified_account_without_code) +{ + StateDiff diff; + diff.modified_accounts.push_back({.addr = 0x02_address, .nonce = 0, .balance = 0}); + + const auto j = test::to_json(diff); + EXPECT_FALSE( + j["modifiedAccounts"]["0x0000000000000000000000000000000000000002"].contains("code")); +} + +TEST(statediff_export, state_diff_deleted_account) +{ + StateDiff diff; + diff.deleted_accounts.push_back(0x03_address); + + const auto j = test::to_json(diff); + EXPECT_EQ(j["modifiedAccounts"], json::json::object()); + ASSERT_EQ(j["deletedAccounts"].size(), 1); + EXPECT_EQ(j["deletedAccounts"][0], "0x0000000000000000000000000000000000000003"); +} diff --git a/evmone/test/unittests/statetest_runner_test.cpp b/evmone/test/unittests/statetest_runner_test.cpp new file mode 100644 index 0000000..a6acfda --- /dev/null +++ b/evmone/test/unittests/statetest_runner_test.cpp @@ -0,0 +1,278 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Tests of what the state test runner reports when a fixture does not hold. Those paths run only +/// when evmone disagrees with a fixture, which a green EEST run never does. + +#include +#include +#include +#include +#include + +#include + +using namespace evmone; +using namespace evmone::test; + +namespace +{ +/// A transaction whose nonce is ahead of the sender's, in a fixture that expects it to succeed. +constexpr std::string_view NONCE_TOO_HIGH = R"({"invalid_nonce": { + "env": { + "currentBaseFee": "0x0a", + "currentCoinbase": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "currentDifficulty": "0x020000", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000020000", + "currentTimestamp": "0x03e8" + }, + "post": {"Shanghai": [{ + "hash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "indexes": {"data": 0, "gas": 0, "value": 0}, + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }]}, + "pre": {}, + "transaction": { + "data": ["0x"], + "gasLimit": ["0x00"], + "gasPrice": "0x0a", + "nonce": "0x01", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "0x00", + "value": ["0x00"] + } +}})"; + +/// A value transfer which succeeds, spending less than its gas limit and emitting no logs. The +/// coinbase is the recipient and collects no priority fee, so only two accounts end up in the +/// post state. +constexpr std::string_view VALID_TX = R"({"valid_tx": { + "env": { + "currentBaseFee": "0x0a", + "currentCoinbase": "0x0000000000000000000000000000000000000000", + "currentDifficulty": "0x020000", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01", + "currentTimestamp": "0x03e8" + }, + "post": {"London": [{ + "hash": "0x71322a2754548ef6d3b540e9b6b858379787c2d1f31bc68205b70902c1d6155f", + "indexes": {"data": 0, "gas": 0, "value": 0}, + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }]}, + "pre": {"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "nonce": "0x00", "balance": "0x0de0b6b3a7640000", "code": "0x", "storage": {} + }}, + "transaction": { + "data": ["0x"], + "gasLimit": ["0x061a80"], + "gasPrice": "0x0a", + "nonce": "0x00", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "0x0000000000000000000000000000000000000000", + "value": ["0x01"] + } +}})"; + +/// A contract creation whose init code is INVALID: the transaction is valid, but its execution +/// fails and consumes the whole gas limit. Nothing is created, so only the sender ends up in the +/// post state. +constexpr std::string_view FAILING_TX = R"({"failing_tx": { + "env": { + "currentBaseFee": "0x0a", + "currentCoinbase": "0x0000000000000000000000000000000000000000", + "currentDifficulty": "0x020000", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01", + "currentTimestamp": "0x03e8" + }, + "post": {"London": [{ + "hash": "0x994280467b9327f616db6328828df403c3493f32ca409a86e7ce6a415a7be488", + "indexes": {"data": 0, "gas": 0, "value": 0}, + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }]}, + "pre": {"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "nonce": "0x00", "balance": "0x0de0b6b3a7640000", "code": "0x", "storage": {} + }}, + "transaction": { + "data": ["0xfe"], + "gasLimit": ["0x061a80"], + "gasPrice": "0x0a", + "nonce": "0x00", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "", + "value": ["0x00"] + } +}})"; + +/// A case given as raw txbytes, which the runner decodes and recovers the sender of, in a fixture +/// that expects it to succeed. The `transaction` beside it is what the loader wants, not what runs. +std::string txbytes_case(std::string_view txbytes) +{ + return R"({"txbytes_case": { + "env": { + "currentCoinbase": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "currentGasLimit": "0xff112233445566", + "currentNumber": "0x01", + "currentTimestamp": "0x03e8" + }, + "post": {"Shanghai": [{ + "hash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "indexes": {"data": 0, "gas": 0, "value": 0}, + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "txbytes": ")" + + std::string{txbytes} + R"(" + }]}, + "pre": {}, + "transaction": { + "data": ["0x"], "gasLimit": ["0x5208"], "gasPrice": "0x0a", "nonce": "0x00", + "value": ["0x00"] + } + }})"; +} + +/// A legacy transaction whose signature `s` is the secp256k1 group order itself, where every +/// valid one is below it. +constexpr auto TXBYTES_INVALID_SIGNATURE = + "0xeb800a8252088080801b01a0fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"; + +/// The failures and the reported output of one run. +struct Run +{ + std::vector failures; + std::string output; +}; + +Run run(std::string_view fixture, bool trace_summary = false, bool state_diff = false) +{ + std::istringstream input{std::string{fixture}}; + const auto tests = load_state_tests(input); + + std::ostringstream output; + std::vector failures; + TestReport report{[&](const Failure& failure) { failures.push_back(failure); }}; + evmc::VM vm{evmc_create_evmone()}; + for (const auto& t : tests) + run_state_test(t, vm, + {.output = output, .trace_summary = trace_summary, .state_diff = state_diff}, report); + return {std::move(failures), std::move(output).str()}; +} +} // namespace + +TEST(statetest_runner, unexpected_invalid_transactions) +{ + const auto failures = run(NONCE_TOO_HIGH).failures; + + ASSERT_EQ(failures.size(), 1u); + EXPECT_EQ(failures[0].test, "invalid_nonce"); + EXPECT_EQ(failures[0].where, "Shanghai/0"); + EXPECT_EQ(failures[0].what, "transaction validity"); + EXPECT_EQ(failures[0].detail, + "unexpected invalid transaction: TransactionException.NONCE_MISMATCH_TOO_HIGH"); +} + +TEST(statetest_runner, not_rejected_transaction) +{ + // The transaction is valid, but the fixture expects it to be rejected. + auto fixture = json::json::parse(VALID_TX); + fixture["valid_tx"]["post"]["London"][0]["expectException"] = + "TransactionException.NONCE_MISMATCH_TOO_HIGH"; + + const auto failures = run(fixture.dump()).failures; + + ASSERT_EQ(failures.size(), 1u); // It abandons the case, so no state root check follows. + EXPECT_EQ(failures[0].what, "transaction validity"); + EXPECT_EQ(failures[0].detail, "unexpected valid transaction"); +} + +TEST(statetest_runner, trace_summary) +{ + const auto [failures, output] = run(VALID_TX, true); + + EXPECT_THAT(failures, testing::IsEmpty()); + EXPECT_EQ(output, + R"({"pass":true,"gasUsed":"0x5208","logsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","stateRoot":"0x71322a2754548ef6d3b540e9b6b858379787c2d1f31bc68205b70902c1d6155f"})" + "\n"); +} + +TEST(statetest_runner, trace_summary_rejected_transaction) +{ + // A rejected transaction has no receipt, so the summary reports only what the state test + // compares: the hashes, which for a rejection are those of the unchanged pre-state. + const auto [failures, output] = run(NONCE_TOO_HIGH, true); + + EXPECT_EQ(failures.size(), 1u); + EXPECT_EQ(output, + R"({"logsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","stateRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"})" + "\n"); +} + +TEST(statetest_runner, trace_summary_failed_transaction) +{ + const auto [failures, output] = run(FAILING_TX, true); + + EXPECT_THAT(failures, testing::IsEmpty()); + EXPECT_EQ(output, + R"({"pass":false,"error":"invalid instruction","gasUsed":"0x61a80","logsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","stateRoot":"0x994280467b9327f616db6328828df403c3493f32ca409a86e7ce6a415a7be488"})" + "\n"); +} + +TEST(statetest_runner, state_diff) +{ + // --state-diff implies --trace-summary, so the diff rides on the same JSON object as the + // summary rather than replacing it. + const auto [failures, output] = run(VALID_TX, true, true); + + EXPECT_THAT(failures, testing::IsEmpty()); + EXPECT_EQ(output, + R"({"pass":true,"gasUsed":"0x5208","logsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","stateRoot":"0x71322a2754548ef6d3b540e9b6b858379787c2d1f31bc68205b70902c1d6155f","stateDiff":{"deletedAccounts":[],"modifiedAccounts":{"0x0000000000000000000000000000000000000000":{"balance":"0x1","modifiedStorage":{},"nonce":"0x0"},"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b":{"balance":"0xde0b6b3a760cbaf","modifiedStorage":{},"nonce":"0x1"}}}})" + "\n"); +} + +TEST(statetest_runner, state_diff_rejected_transaction) +{ + // A rejected transaction has no receipt, so its diff is empty rather than omitted. + const auto [failures, output] = run(NONCE_TOO_HIGH, true, true); + + EXPECT_EQ(failures.size(), 1u); + EXPECT_EQ(output, + R"({"logsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","stateRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateDiff":{"deletedAccounts":[],"modifiedAccounts":{}}})" + "\n"); +} + +TEST(statetest_runner, trace_summary_output_is_valid_json) +{ + // Verifies the output is proper JSON, regardless of which fields it holds: the pinned tests + // above catch a wrong value, but not e.g. a trailing comma left over from a conditionally + // printed field. + for (const std::string_view fixture : {VALID_TX, NONCE_TOO_HIGH, FAILING_TX}) + { + for (const bool state_diff : {false, true}) + { + json::json parsed; + EXPECT_NO_THROW(parsed = json::json::parse(run(fixture, true, state_diff).output)); + EXPECT_TRUE(parsed.is_object()); + } + } +} + +TEST(statetest_runner, txbytes_invalid_signature) +{ + const auto failures = run(txbytes_case(TXBYTES_INVALID_SIGNATURE)).failures; + + ASSERT_EQ(failures.size(), 1u); + EXPECT_EQ(failures[0].detail, + "unexpected invalid transaction: TransactionException.INVALID_SIGNATURE_VRS"); +} + +TEST(statetest_runner, txbytes_invalid_encoding) +{ + // An empty list, where a legacy transaction is a list of nine items. + const auto failures = run(txbytes_case("0xc0")).failures; + + ASSERT_EQ(failures.size(), 1u); + EXPECT_EQ(failures[0].detail, "unexpected invalid transaction: invalid transaction encoding"); +} diff --git a/evmone/test/unittests/test_collector_test.cpp b/evmone/test/unittests/test_collector_test.cpp new file mode 100644 index 0000000..637953a --- /dev/null +++ b/evmone/test/unittests/test_collector_test.cpp @@ -0,0 +1,78 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +using namespace evmone::test; +namespace fs = std::filesystem; + +namespace +{ +/// A collection as collect_test_files() would return it, with a sibling directory whose name +/// begins with another one's. +std::vector collected() +{ + return { + "root/bc4895/a.json", + "root/bc4895/nested/c.json", + "root/bc4895-withdrawals/b.json", + "root/top.json", + }; +} + +std::vector names(const std::vector& files) +{ + std::vector result; + std::ranges::copy( + files | std::views::transform([](const auto& p) { return p.filename().string(); }), + std::back_inserter(result)); + return result; +} +} // namespace + +TEST(test_collector, ignore_nothing) +{ + const std::vector all{"a.json", "c.json", "b.json", "top.json"}; + + auto files = collected(); + ignore_test_files(files, "root", {}); + EXPECT_EQ(names(files), all); + + // An empty path, which an unset variable expands to, must not drop everything. Neither must + // ".", which names the search root: pytest also collects it all for --ignore of the root. + const std::vector ignored{"", ".", "./"}; + ignore_test_files(files, "root", ignored); + EXPECT_EQ(names(files), all); +} + +TEST(test_collector, ignore_directory) +{ + // The sibling shares the prefix as text, but not as a path component. + auto files = collected(); + const std::vector ignored{"bc4895"}; + ignore_test_files(files, "root", ignored); + EXPECT_EQ(names(files), (std::vector{"b.json", "top.json"})); +} + +TEST(test_collector, ignore_directory_other_spellings) +{ + for (const auto& spelling : {"bc4895/", "./bc4895"}) + { + auto files = collected(); + const std::vector ignored{spelling}; + ignore_test_files(files, "root", ignored); + EXPECT_EQ(names(files), (std::vector{"b.json", "top.json"})) << spelling; + } +} + +TEST(test_collector, ignore_files) +{ + auto files = collected(); + const std::vector ignored{"bc4895/nested/c.json", "top.json"}; + ignore_test_files(files, "root", ignored); + EXPECT_EQ(names(files), (std::vector{"a.json", "b.json"})); +} diff --git a/evmone/test/unittests/test_driver_test.cpp b/evmone/test/unittests/test_driver_test.cpp new file mode 100644 index 0000000..07d1a54 --- /dev/null +++ b/evmone/test/unittests/test_driver_test.cpp @@ -0,0 +1,211 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +using namespace evmone::test; + +namespace +{ +/// The exit code and the report of one run. +struct Run +{ + int exit_code = -1; + std::string output; +}; + +Run run(std::span cases, const RunOptions& options = {}) +{ + std::ostringstream out; + const auto exit_code = run_tests(cases, out, options); + return {exit_code, std::move(out).str()}; +} + +/// A test which runs one fixture, as a file holding one does. +TestCase one(const std::string& name, std::function run) +{ + return {name, [name, run = std::move(run)] { return std::vector{run_one(name, run)}; }}; +} + +/// A test whose fixtures are handed their outcomes, running nothing. +TestCase holding(std::string name, std::initializer_list outcomes) +{ + std::vector results; + for (const auto outcome : outcomes) + results.push_back({name + "::case", outcome, "the reason", {}}); + return {std::move(name), [results = std::move(results)] { return results; }}; +} +} // namespace + +TEST(test_driver, nothing_collected) +{ + const auto [exit_code, output] = run({}); + EXPECT_EQ(NOTHING_VERIFIED, 5); // pytest's value, not just whatever we declared. + EXPECT_EQ(exit_code, NOTHING_VERIFIED); + EXPECT_NE(output.find("collected 0 files"), std::string::npos); +} + +TEST(test_driver, collect_only_lists_without_running) +{ + bool ran = false; + const std::vector cases{one("a name", [&ran](TestReport&) { ran = true; })}; + + const auto [exit_code, output] = run(cases, {.collect_only = true}); + EXPECT_FALSE(ran); + EXPECT_EQ(exit_code, SUCCESS); + EXPECT_EQ(output, "a name\n"); +} + +TEST(test_driver, collect_only_nothing_collected) +{ + const auto [exit_code, output] = run({}, {.collect_only = true}); + EXPECT_EQ(exit_code, NOTHING_VERIFIED); + EXPECT_EQ(output, ""); +} + +TEST(test_driver, exception_fails_only_its_own_test) +{ + bool last_ran = false; + const std::vector cases{ + one("ok", [](TestReport&) {}), + one("throws", [](TestReport&) { throw std::runtime_error{"the reason"}; }), + one("unknown", [](TestReport&) { throw 42; }), // NOLINT(hicpp-exception-baseclass) + one("last", [&last_ran](TestReport&) { last_ran = true; }), + }; + + const auto [exit_code, output] = run(cases); + EXPECT_TRUE(last_ran); + EXPECT_EQ(exit_code, 1); + EXPECT_NE(output.find("2 failed, 2 passed"), std::string::npos); + // The reason a test threw belongs in the summary, not only in the failure block. + EXPECT_NE(output.find("FAILED throws - exception: the reason"), std::string::npos); +} + +TEST(test_driver, unsupported_feature_skips) +{ + const std::vector cases{ + one("ok", [](TestReport&) {}), + one("skipped", [](TestReport&) { throw UnsupportedTestFeature{"no support for it"}; }), + }; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, 0); // A skip does not fail the run. + EXPECT_NE(output.find("1 passed, 1 skipped"), std::string::npos); + EXPECT_NE(output.find("SKIPPED skipped - no support for it"), std::string::npos); +} + +TEST(test_driver, everything_skipped_verifies_nothing) +{ + const std::vector cases{ + one("skipped", [](TestReport&) { throw UnsupportedTestFeature{"no support for it"}; })}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, NOTHING_VERIFIED); + EXPECT_NE(output.find("0 passed, 1 skipped"), std::string::npos); +} + +TEST(test_driver, summary_names_the_check_which_failed) +{ + const std::vector cases{ + one("mismatch", [](TestReport& report) { report.check_eq("a value", 1, 2); })}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, 1); + EXPECT_NE(output.find("FAILED mismatch - a value"), std::string::npos); +} + +TEST(test_driver, failure_outranks_a_later_exception) +{ + const std::vector cases{one("both", [](TestReport& report) { + report.check_eq("a value", 1, 2); + throw std::runtime_error{"gave up afterwards"}; + })}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, TESTS_FAILED); + // The summary names the check which failed, not the exception which ended the test. + EXPECT_NE(output.find("FAILED both - a value"), std::string::npos); +} + +TEST(test_driver, failure_outranks_a_later_skip) +{ + const std::vector cases{one("both", [](TestReport& report) { + report.check_eq("a value", 1, 2); + throw UnsupportedTestFeature{"gave up afterwards"}; + })}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, 1); + EXPECT_NE(output.find("1 failed"), std::string::npos); + // The summary names the check which failed, not what the test then gave up on. + EXPECT_NE(output.find("FAILED both - a value"), std::string::npos); +} + +TEST(test_driver, a_file_counts_once_however_many_fixtures_it_holds) +{ + const std::vector cases{ + holding("a file", {Outcome::passed, Outcome::passed, Outcome::passed})}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, SUCCESS); + EXPECT_NE(output.find("collected 1 file"), std::string::npos); + EXPECT_NE(output.find("1 passed"), std::string::npos); +} + +TEST(test_driver, a_declined_fixture_is_named_though_its_file_passed) +{ + const std::vector cases{holding("a file", {Outcome::passed, Outcome::skipped})}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, SUCCESS); + // The file's own verdict says nothing about what it declined, so the fixture is named. + EXPECT_NE(output.find("1 passed in"), std::string::npos); + EXPECT_NE(output.find("SKIPPED a file::case - the reason"), std::string::npos); +} + +TEST(test_driver, one_failed_fixture_fails_its_file) +{ + const std::vector cases{ + holding("a file", {Outcome::passed, Outcome::failed, Outcome::skipped})}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, TESTS_FAILED); + EXPECT_NE(output.find("1 failed, 0 passed"), std::string::npos); +} + +TEST(test_driver, a_file_is_skipped_only_when_nothing_in_it_ran) +{ + const std::vector cases{holding("a file", {Outcome::skipped, Outcome::skipped})}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, NOTHING_VERIFIED); + EXPECT_NE(output.find("0 passed, 1 skipped"), std::string::npos); +} + +TEST(test_driver, a_file_whose_fixtures_were_all_filtered_out_verifies_nothing) +{ + // A filter selecting nothing leaves no result at all: not a pass, and not a skip either. + const std::vector cases{holding("a file", {})}; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, NOTHING_VERIFIED); + EXPECT_NE(output.find("0 passed, 1 deselected in"), std::string::npos); + EXPECT_EQ(output.find("skipped"), std::string::npos); + // Deselected files are not listed. + EXPECT_EQ(output.find("short test summary info"), std::string::npos); +} + +TEST(test_driver, deselected_is_counted_apart_from_skipped) +{ + const std::vector cases{ + holding("ran", {Outcome::passed}), + holding("declined", {Outcome::skipped}), + holding("filtered out", {}), + }; + + const auto [exit_code, output] = run(cases); + EXPECT_EQ(exit_code, SUCCESS); + EXPECT_NE(output.find("1 passed, 1 skipped, 1 deselected in"), std::string::npos); +} diff --git a/evmone/test/unittests/test_report_test.cpp b/evmone/test/unittests/test_report_test.cpp new file mode 100644 index 0000000..526f589 --- /dev/null +++ b/evmone/test/unittests/test_report_test.cpp @@ -0,0 +1,253 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Tests of the failure report the fixture runners write into: what a check records, and how a +/// recorded failure is rendered. The rendering is the runners' entire output, and a green fixture +/// run never reaches it. + +#include +#include +#include +#include +#include +#include + +using namespace evmone; +using namespace evmone::test; +using namespace evmc::literals; +using evmc::bytes; + +namespace +{ +/// A report whose sink keeps every failure, so a test can look at what was reported. +class Recorded +{ + std::vector m_failures; + +public: + TestReport report{[this](const Failure& failure) { m_failures.push_back(failure); }}; + + Recorded() = default; + Recorded(Recorded&&) = delete; // The sink holds `this`. + + [[nodiscard]] const std::vector& failures() const noexcept { return m_failures; } + + /// The failures as the runners print them. + [[nodiscard]] std::string render() const + { + std::ostringstream out; + for (const auto& failure : m_failures) + out << failure << '\n'; + return std::move(out).str(); + } +}; +} // namespace + +TEST(test_report, no_failures) +{ + Recorded recorded; + recorded.report.start_case("t"); + EXPECT_TRUE(recorded.report.check_eq("state root", 1, 1)); + EXPECT_TRUE(recorded.report.check(true, "rejection reason", "a", "b")); + EXPECT_TRUE(recorded.failures().empty()); +} + +TEST(test_report, check_eq_reports_both_values) +{ + Recorded recorded; + recorded.report.start_case("t"); + const auto in_case = recorded.report.at("Prague", '/', 0); + + EXPECT_FALSE(recorded.report.check_eq("state root", 0x01_bytes32, 0x02_bytes32)); + + ASSERT_EQ(recorded.failures().size(), 1u); + EXPECT_EQ(recorded.render(), + "t:\n" + " Prague/0:\n" + " state root:\n" + " actual 0x0000000000000000000000000000000000000000000000000000000000000001\n" + " expected 0x0000000000000000000000000000000000000000000000000000000000000002\n"); +} + +TEST(test_report, check_reports_both_values_without_equality) +{ + // The rejection-reason checks compare through is_expected_tx_exception(), not ==. + Recorded recorded; + recorded.report.start_case("t"); + EXPECT_FALSE(recorded.report.check(false, "rejection reason", "GOT", "WANTED")); + EXPECT_EQ(recorded.render(), + "t:\n" + " rejection reason:\n" + " actual GOT\n" + " expected WANTED\n"); +} + +TEST(test_report, what_stays_at_the_same_level_without_a_place) +{ + Recorded recorded; + recorded.report.start_case("t"); + recorded.report.fail("block validity", "expected the block to be invalid"); + EXPECT_EQ(recorded.render(), + "t:\n" + " block validity:\n" + " expected the block to be invalid\n"); +} + +TEST(test_report, at_scopes_nest_and_restore) +{ + Recorded recorded; + recorded.report.start_case("t"); + { + const auto outer = recorded.report.at("Prague"); + { + const auto inner = recorded.report.at(3); + recorded.report.fail("inner", "d"); + } + recorded.report.fail("outer", "d"); + } + recorded.report.fail("case", "d"); + + ASSERT_EQ(recorded.failures().size(), 3u); + EXPECT_EQ(recorded.failures()[0].where, "Prague/3"); + EXPECT_EQ(recorded.failures()[1].where, "Prague"); + EXPECT_EQ(recorded.failures()[2].where, ""); +} + +TEST(test_report, extra_detail_extends_the_failure) +{ + Recorded recorded; + recorded.report.start_case("t"); + ASSERT_FALSE(recorded.report.check_eq( + "post state root", 1, 2, [] { return std::string{"Result state:\nan account"}; })); + + EXPECT_EQ(recorded.render(), + "t:\n" + " post state root:\n" + " actual 1\n" + " expected 2\n" + " Result state:\n" + " an account\n"); +} + +TEST(test_report, byte_sequences_render_as_hex) +{ + Recorded recorded; + recorded.report.start_case("t"); + recorded.report.check_eq("bytes", bytes{0xb0, 0xb1}, bytes{0xb2}); + recorded.report.check_eq("address", 0xaa_address, 0xbb_address); + + ASSERT_EQ(recorded.failures().size(), 2u); + EXPECT_EQ(recorded.failures()[0].detail, "actual 0xb0b1\nexpected 0xb2"); + EXPECT_EQ(recorded.failures()[1].detail, + "actual 0x00000000000000000000000000000000000000aa\n" + "expected 0x00000000000000000000000000000000000000bb"); +} + +TEST(test_report, concat_renders_mixed_values) +{ + EXPECT_EQ(concat("n=", 42, ' ', 0x01_bytes32), + "n=42 0x0000000000000000000000000000000000000000000000000000000000000001"); + EXPECT_EQ(concat(intx::uint256{255}), "0xff"); +} + +TEST(test_report, error_codes_render_as_their_message) +{ + const auto ec = std::make_error_code(std::errc::invalid_argument); + EXPECT_EQ(concat(ec), ec.message()); + EXPECT_NE(ec.message(), "generic:22"); // What streaming it directly would produce. +} + +TEST(test_report, bytes_render_as_numbers_and_chars_as_text) +{ + EXPECT_EQ(concat(uint8_t{65}), "65"); + EXPECT_EQ(concat('/'), "/"); +} + +TEST(test_report, blank_detail_lines_carry_no_indent) +{ + Recorded recorded; + recorded.report.start_case("t"); + ASSERT_FALSE(recorded.report.check_eq("post state root", 1, 2, + [] { return std::string{"Result state:\naccount\n\nExpected state:\naccount\n"}; })); + + EXPECT_EQ(recorded.render(), + "t:\n" + " post state root:\n" + " actual 1\n" + " expected 2\n" + " Result state:\n" + " account\n" + "\n" + " Expected state:\n" + " account\n" + "\n"); +} + +TEST(test_report, a_place_that_renders_empty_adds_no_separator) +{ + Recorded recorded; + recorded.report.start_case("t"); + const auto outer = recorded.report.at("Prague"); + { + const auto inner = recorded.report.at(std::string{}); + recorded.report.fail("what"); + } + recorded.report.fail("after"); + + ASSERT_EQ(recorded.failures().size(), 2u); + EXPECT_EQ(recorded.failures()[0].where, "Prague"); + EXPECT_EQ(recorded.failures()[1].where, "Prague"); +} + +TEST(test_report, extra_detail_is_not_formatted_when_the_check_holds) +{ + Recorded recorded; + recorded.report.start_case("t"); + int calls = 0; + EXPECT_TRUE(recorded.report.check_eq("root", 1, 1, [&] { + ++calls; + return std::string{"expensive"}; + })); + EXPECT_EQ(calls, 0); +} + +TEST(test_report, the_sink_sees_each_failure_complete_as_it_is_recorded) +{ + Recorded recorded; + recorded.report.start_case("t"); + + recorded.report.check_eq("root", 1, 2, [] { return std::string{"dump"}; }); + // Delivered during the run rather than at the end of it, and already carrying its detail. + ASSERT_EQ(recorded.failures().size(), 1u); + EXPECT_EQ(recorded.failures()[0].detail, "actual 1\nexpected 2\ndump"); + + recorded.report.fail("other"); + ASSERT_EQ(recorded.failures().size(), 2u); + EXPECT_EQ(recorded.failures()[1].what, "other"); +} + +TEST(test_report, check_with_only_a_detail_reports_no_columns) +{ + // A structural check has nothing to put in an actual/expected pair. + Recorded recorded; + recorded.report.start_case("t"); + EXPECT_TRUE(recorded.report.check(true, "block RLP", "not a list")); + EXPECT_FALSE(recorded.report.check(false, "block RLP", "not a list")); + + EXPECT_EQ(recorded.render(), + "t:\n" + " block RLP:\n" + " not a list\n"); +} + +TEST(test_report, a_failure_without_detail_ends_after_what) +{ + Recorded recorded; + recorded.report.start_case("t"); + recorded.report.fail("block RLP"); + + EXPECT_EQ(recorded.render(), + "t:\n" + " block RLP\n"); +} diff --git a/evmone/test/unittests/tooling_t8n_test.cpp b/evmone/test/unittests/tooling_t8n_test.cpp index c43b151..793ec05 100644 --- a/evmone/test/unittests/tooling_t8n_test.cpp +++ b/evmone/test/unittests/tooling_t8n_test.cpp @@ -4,11 +4,13 @@ #include #include +#include #include #include using namespace evmone; using namespace testing; +using nlohmann::json; namespace { @@ -36,8 +38,8 @@ constexpr auto ALLOC_JSON = R"({ })"; // Single legacy CREATE transaction; init code is `PUSH1 0x01 PUSH0 RETURN`, -// which deploys a one-byte runtime `0x01`. Three opcodes => three trace lines. -// Matches test/integration/t8n/cancun_create_tx/txs.json[0]; the tx hash is +// which deploys a one-byte runtime `0x00`. Three opcodes => three trace lines. +// Matches test/integration/evmone-cli/t8n/cancun_create_tx/txs.json[0]; the tx hash is // well-known and used below. constexpr auto TX_JSON = R"([{ "to": null, @@ -52,8 +54,209 @@ constexpr auto TX_JSON = R"([{ "r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae", "s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc" }])"; + +/// Runs t8n over the given pre-state and transactions, and returns the result JSON. +std::string run_t8n(std::string_view alloc_json, std::string_view txs_json, evmc_revision rev, + std::string_view env_json = ENV_JSON) +{ + evmc::VM vm{evmc_create_evmone()}; + + std::istringstream env{std::string{env_json}}; + std::istringstream alloc{std::string{alloc_json}}; + std::istringstream txs{std::string{txs_json}}; + std::ostringstream out_result; + + tooling::T8NArgs args; + args.rev = rev; + args.chain_id = 1; + args.alloc = &alloc; + args.env = &env; + args.txs = &txs; + args.out_result = &out_result; + + tooling::t8n(vm, args); + return out_result.str(); +} + +/// Legacy transaction calling CALLEE. t8n takes `sender` from the JSON and only checks `hash` +/// when present, so the signature is never recovered. +constexpr auto TX_TO_CALLEE = R"([{ + "to": "0x000000000000000000000000000000000000c0de", + "input": "0x", + "gas": "0x186a0", + "nonce": "0x0", + "value": "0x0", + "gasPrice": "0x32", + "chainId": "0x1", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "v": "0x1b", + "r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae", + "s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc" +}])"; + +/// Runs TX_TO_CALLEE against a callee deployed with the given code. +std::string run_call_to(std::string_view callee_code, evmc_revision rev) +{ + const auto alloc = R"({ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "", "nonce": "0x00", "balance": "0x02540be400" + }, + "0x000000000000000000000000000000000000c0de": { + "code": ")" + + std::string{callee_code} + + R"(", "nonce": "0x00", "balance": "0x00" + } + })"; + return run_t8n(alloc, TX_TO_CALLEE, rev); +} + +/// A block env carrying the parent's fee market, which is what a base fee is computed from. +constexpr auto ENV_WITH_PARENT_JSON = R"({ + "currentCoinbase": "0x8888f1f195afa192cfee860698584c030f4c9db1", + "currentNumber": "0x01", + "currentTimestamp": "0x54c99069", + "currentGasLimit": "15000000", + "parentBaseFee": "7", + "parentGasLimit": "5000000", + "parentGasUsed": "5000000" +})"; + +/// Runs t8n over an empty state with no transaction, for what the block env alone decides. +std::string run_t8n_env(std::string_view env_json, evmc_revision rev) +{ + evmc::VM vm{evmc_create_evmone()}; + + std::istringstream env{std::string{env_json}}; + std::istringstream alloc{"{}"}; + std::istringstream txs{"[]"}; + std::ostringstream out_result; + + tooling::T8NArgs args; + args.rev = rev; + args.alloc = &alloc; + args.env = &env; + args.txs = &txs; + args.out_result = &out_result; + + tooling::t8n(vm, args); + return out_result.str(); +} + +/// ALLOC_JSON's sender beside the beacon-roots contract: without its code the block-start system +/// call is skipped (EIP-4788). +constexpr auto ALLOC_WITH_BEACON_ROOTS_JSON = R"({ + "0x000f3df6d732807ef1319fb7b8bb8522d0beac02": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", + "nonce": "0x01", + "balance": "0x00" + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "", + "nonce": "0x00", + "balance": "0x02540be400" + } +})"; + +/// Stubs of the request contracts, each returning nothing. Without their code the block-end +/// system calls fail the block instead (EIP-7002, EIP-7251, EIP-8282). +constexpr auto ALLOC_WITH_REQUEST_STUBS_JSON = R"({ + "0x00000961ef480eb55e80d19ad83579a64c007002": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + }, + "0x0000bbddc7ce488642fb579f8b00f3a590007251": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + }, + "0x0000bff46984e3725691fa540a8c7589300d8282": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + }, + "0x000064d678505ad48f8ccb093bc65613800e8282": { + "code": "0x00", + "nonce": "0x01", + "balance": "0x00" + } +})"; } // namespace +TEST(tooling_t8n, base_fee_is_computed_from_the_parent_block) +{ + // The parent used twice its gas target, so the fee rises by max(7 / 8, 1) = 1 (EIP-1559). + const auto result = json::parse(run_t8n_env(ENV_WITH_PARENT_JSON, EVMC_LONDON)); + EXPECT_EQ(result.at("currentBaseFee"), "0x8"); +} + +TEST(tooling_t8n, no_base_fee_before_london) +{ + const auto result = json::parse(run_t8n_env(ENV_WITH_PARENT_JSON, EVMC_BERLIN)); + EXPECT_TRUE(result.contains("gasUsed")); // Not an empty result which says nothing. + EXPECT_FALSE(result.contains("currentBaseFee")); +} + +TEST(tooling_t8n, blob_transaction_creating_a_contract_is_rejected) +{ + // A blob transaction has no create form (EIP-4844). This one is valid in every other respect, + // so the missing `to` is its only fault. + static constexpr auto BLOB_CREATE_TX = R"([{ + "input": "0x", + "gas": "0x186a0", + "nonce": "0x0", + "value": "0x0", + "v": "0x0", + "r": "0xfc12b67159a3567f8bdbc49e0be369a2e20e09d57a51c41310543a4128409464", + "s": "0x2de0cfe5495c4f58ff60645ceda0afd67a4c90a70bc89fe207269435b35e5b67", + "maxFeePerGas": "0x32", + "maxPriorityFeePerGas": "0x2", + "maxFeePerBlobGas": "0xa", + "blobVersionedHashes": [ + "0x01a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + ], + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + }])"; + + const auto result = json::parse(run_t8n(ALLOC_JSON, BLOB_CREATE_TX, EVMC_CANCUN)); + EXPECT_EQ(result.at("receipts"), json::array()); + ASSERT_EQ(result.at("rejected").size(), 1u); + EXPECT_EQ( + result.at("rejected")[0].at("error"), "TransactionException.TYPE_3_TX_CONTRACT_CREATION"); +} + +TEST(tooling_t8n, a_block_requesting_nothing_reports_the_empty_requests_hash) +{ + const auto result = json::parse(run_t8n(ALLOC_WITH_REQUEST_STUBS_JSON, "[]", EVMC_PRAGUE)); + EXPECT_FALSE(result.contains("blockException")); + EXPECT_EQ(result.at("requests"), json::array()); + // sha256 of nothing at all (EIP-7685). + EXPECT_EQ(result.at("requestsHash"), + "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); +} + +TEST(tooling_t8n, block_access_list_over_the_gas_limit_fails_the_block) +{ + // A zero gas limit allows no block access list items, but the block-end system calls access + // the request contracts (EIP-7928). + constexpr auto env = R"({ + "currentCoinbase": "0x8888f1f195afa192cfee860698584c030f4c9db1", + "currentNumber": "0x01", + "currentTimestamp": "0x54c99069", + "currentGasLimit": "0x0", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000000" + })"; + const auto result = + json::parse(run_t8n(ALLOC_WITH_REQUEST_STUBS_JSON, "[]", EVMC_AMSTERDAM, env)); + EXPECT_EQ(result.at("blockException"), "BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED"); + + // Without the request contracts the block fails on the requests, which are processed first, + // although the beacon root write exceeds the limit too. + const auto no_requests = + json::parse(run_t8n(ALLOC_WITH_BEACON_ROOTS_JSON, "[]", EVMC_AMSTERDAM, env)); + EXPECT_EQ(no_requests.at("blockException"), "BlockException.SYSTEM_CONTRACT_EMPTY"); +} + TEST(tooling_t8n, no_inputs_no_outputs) { // Smoke: t8n() with everything left at defaults must not throw or crash. @@ -85,6 +288,34 @@ TEST(tooling_t8n, result_written_to_out_streams) EXPECT_THAT(out_alloc.str(), Eq("{}")); } +TEST(tooling_t8n, out_alloc_reports_the_beacon_root_write_and_the_created_account) +{ + evmc::VM vm{evmc_create_evmone()}; + + std::istringstream env{ENV_JSON}; + std::istringstream alloc{ALLOC_WITH_BEACON_ROOTS_JSON}; + std::istringstream txs{TX_JSON}; + std::ostringstream out_alloc; + + tooling::T8NArgs args; + args.rev = EVMC_CANCUN; + args.chain_id = 1; + args.alloc = &alloc; + args.env = &env; + args.txs = &txs; + args.out_alloc = &out_alloc; + + tooling::t8n(vm, args); + + const auto post = json::parse(out_alloc.str()); + // Slot timestamp % 8191 holds the timestamp (EIP-4788). + EXPECT_EQ(post.at("0x000f3df6d732807ef1319fb7b8bb8522d0beac02") + .at("storage") + .at("0x00000000000000000000000000000000000000000000000000000000000016ca"), + "0x0000000000000000000000000000000000000000000000000000000054c99069"); + EXPECT_EQ(post.at("0x6295ee1b4f6dd65047762f924ecd367c17eabf8f").at("code"), "0x00"); +} + TEST(tooling_t8n, open_trace_called_per_tx) { evmc::VM vm{evmc_create_evmone()}; @@ -149,31 +380,13 @@ TEST(tooling_t8n, out_body_is_hex_rlp_of_transactions) TEST(tooling_t8n, pre_byzantium_sets_receipt_post_state) { - evmc::VM vm{evmc_create_evmone()}; - - // Pre-Byzantium receipts include the post-state root via receipt.post_state. - // The TX_JSON fixture uses PUSH0 in its init code, so the inner CREATE fails - // at Homestead, but the outer tx still produces a TransactionReceipt that - // exercises the `rev < EVMC_BYZANTIUM` branch in t8n(). - std::istringstream env{ENV_JSON}; - std::istringstream alloc{ALLOC_JSON}; - std::istringstream txs{TX_JSON}; - std::ostringstream out_result; + // The TX_JSON fixture uses PUSH0 in its init code, so the inner CREATE fails at Homestead, + // but the outer tx still produces a receipt, which carries the post-state root instead of + // the EIP-658 status. + const auto result = run_t8n(ALLOC_JSON, TX_JSON, EVMC_HOMESTEAD); - tooling::T8NArgs args; - args.rev = EVMC_HOMESTEAD; - args.chain_id = 1; - args.alloc = &alloc; - args.env = &env; - args.txs = &txs; - args.out_result = &out_result; - - tooling::t8n(vm, args); - - // The "receipts" array is initialized empty on every txs-present run; the - // distinguishing signal that a receipt was actually produced (i.e., the - // tx wasn't classified as rejected) is the presence of transactionHash. - EXPECT_THAT(out_result.str(), HasSubstr("\"transactionHash\"")); + EXPECT_THAT(result, HasSubstr("\"transactionHash\"")); + EXPECT_THAT(result, HasSubstr("\"root\": \"0x")); } TEST(tooling_t8n, mismatched_tx_hash_throws) @@ -287,3 +500,54 @@ TEST(tooling_t8n, max_v) EXPECT_NO_THROW(tooling::t8n(vm, args)); EXPECT_THAT(out_result.str(), HasSubstr("\"transactionHash\"")); } + +TEST(tooling_t8n, receipt_reports_emitted_logs) +{ + // MSTORE8(0, 0xaa); LOG1(offset=0, size=1, topic=0x42). + const auto result = run_call_to("0x60aa600053604260016000a100", EVMC_SHANGHAI); + + EXPECT_THAT(result, HasSubstr("\"address\": \"0x000000000000000000000000000000000000c0de\"")); + EXPECT_THAT(result, + HasSubstr("\"0x0000000000000000000000000000000000000000000000000000000000000042\"")); + EXPECT_THAT(result, HasSubstr("\"data\": \"0xaa\"")); +} + +TEST(tooling_t8n, receipt_status_reports_failure) +{ + // The callee is the INVALID instruction, so the transaction fails. + EXPECT_THAT(run_call_to("0xfe", EVMC_SHANGHAI), HasSubstr("\"status\": \"0x0\"")); + EXPECT_THAT(run_call_to("0x00", EVMC_SHANGHAI), HasSubstr("\"status\": \"0x1\"")); +} + +TEST(tooling_t8n, block_gas_used_is_pre_refund_from_amsterdam) +{ + // The block gas is the pre-refund transaction gas from Amsterdam on (EIP-7778), while a + // receipt reports what the sender paid. The callee clears a storage slot, so the transaction + // earns a refund and the two must differ; before Amsterdam they must stay equal. + static constexpr auto ALLOC_REFUNDING_CALLEE = R"({ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "", "nonce": "0x00", "balance": "0x02540be400" + }, + "0x000000000000000000000000000000000000c0de": { + "code": "0x5f5f55", "nonce": "0x00", "balance": "0x00", + "storage": {"0x00": "0x01"} + } + })"; + + const auto run = [](evmc_revision rev) { + const auto j = json::parse(run_t8n(ALLOC_REFUNDING_CALLEE, TX_TO_CALLEE, rev)); + const auto hex_value = [](const json& v) { + return std::stoll(v.get(), nullptr, 16); + }; + return std::pair{ + hex_value(j.at("gasUsed")), hex_value(j.at("receipts").at(0).at("cumulativeGasUsed"))}; + }; + + const auto [osaka_block_gas, osaka_cumulative] = run(EVMC_OSAKA); + EXPECT_EQ(osaka_block_gas, osaka_cumulative); + + // The difference is the refund for clearing the slot, capped at 1/5 of the gas used + // (EIP-3529): the repriced refund exceeds that cap. + const auto [block_gas, cumulative] = run(EVMC_AMSTERDAM); + EXPECT_EQ(block_gas - cumulative, block_gas / 5); +} diff --git a/evmone/test/utils/CMakeLists.txt b/evmone/test/utils/CMakeLists.txt index 3ed2a3c..99657a6 100644 --- a/evmone/test/utils/CMakeLists.txt +++ b/evmone/test/utils/CMakeLists.txt @@ -20,6 +20,7 @@ target_sources( block_transition.cpp blockchaintest.hpp blockchaintest_loader.cpp + blockchaintest_runner.cpp bytecode.hpp error_matching.hpp error_matching.cpp @@ -36,8 +37,15 @@ target_sources( statetest_export.cpp statetest_loader.cpp statetest_logs_hash.cpp + statetest_runner.cpp t8n.hpp t8n.cpp + test_collector.hpp + test_collector.cpp + test_driver.hpp + test_driver.cpp + test_report.hpp + test_report.cpp test_state.hpp test_state.cpp utils.hpp diff --git a/evmone/test/utils/block_transition.cpp b/evmone/test/utils/block_transition.cpp index c8a36a7..ba0e55b 100644 --- a/evmone/test/utils/block_transition.cpp +++ b/evmone/test/utils/block_transition.cpp @@ -4,6 +4,8 @@ #include "block_transition.hpp" #include +#include +#include #include #include #include @@ -42,15 +44,25 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: vm.set_option("trace", "1"); // This actually appends a new tracer on each set_option(). TestState block_state(state); + + // The block access list (EIP-7928) records the reads and the state changes at their block + // access index. + state::BalBuilder bal_builder; + const auto apply = [&](size_t index, const state::StateDiff& diff) { + if (rev >= EVMC_AMSTERDAM) + bal_builder.record_diff(static_cast(index), diff, state); + block_state.apply(diff); + }; + if (!opts.skip_system_calls) - system_call_block_start(block_state, block, block_hashes, rev, vm); + apply(0, state::system_call_block_start(block_state, block, block_hashes, rev, vm)); std::vector rejected_txs; std::vector receipts; int64_t block_gas_left = block.gas_limit; + int64_t block_state_gas_left = block.gas_limit; int64_t cumulative_gas_used = 0; - int64_t block_gas_used = 0; auto blob_gas_left = blob_gas_limit; for (size_t i = 0; i < txs.size(); ++i) @@ -62,32 +74,37 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: if (trace_enabled) trace_guard.emplace(std::clog, opts.open_trace(i, computed_tx_hash).rdbuf()); - auto res = transition( - block_state, block, block_hashes, tx, rev, vm, block_gas_left, blob_gas_left); - - if (holds_alternative(res)) + const auto tx_props_or_err = state::validate_transaction( + block_state, block, tx, rev, block_gas_left, block_state_gas_left, blob_gas_left); + if (std::holds_alternative(tx_props_or_err)) { - rejected_txs.push_back({computed_tx_hash, i, std::get(res)}); + rejected_txs.push_back( + {computed_tx_hash, i, std::get(tx_props_or_err)}); } else { - auto& receipt = get(res); + auto receipt = state::transition(block_state, block, block_hashes, tx, rev, vm, + std::get(tx_props_or_err)); + apply(i + 1, receipt.state_diff); cumulative_gas_used += receipt.gas_used; receipt.cumulative_gas_used = cumulative_gas_used; if (rev < EVMC_BYZANTIUM) receipt.post_state = state::mpt_hash(block_state); - // Block gas accounting, refunds excluded (EIP-7778). - const auto block_tx_gas = - (rev >= EVMC_AMSTERDAM) ? receipt.gas_used + receipt.gas_refund : receipt.gas_used; - block_gas_used += block_tx_gas; - block_gas_left -= block_tx_gas; + // Block gas accounting, with EIP-7778 applied in transition(). The execution + // dimension is the block gas less the state one (EIP-8037). + block_gas_left -= receipt.block_gas_used - receipt.state_gas_used; + block_state_gas_left -= receipt.state_gas_used; blob_gas_left -= static_cast(tx.blob_gas_used()); receipts.emplace_back(std::move(receipt)); } } + // The withdrawals share the last index with the block-end system calls (EIP-7928). + apply(txs.size() + 1, state::finalize(block_state, rev, block.coinbase, opts.block_reward, + block.ommers, block.withdrawals)); + std::vector requests; std::error_code requests_error; if (!opts.skip_system_calls) @@ -101,20 +118,25 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: } if (!requests_error) { - auto block_end = system_call_block_end(block_state, block, block_hashes, rev, vm); + auto block_end = + state::system_call_block_end(block_state, block, block_hashes, rev, vm); if (const auto* ec = std::get_if(&block_end)) requests_error = *ec; else - std::ranges::move(std::get>(block_end), - std::back_inserter(requests)); + { + auto& rr = std::get(block_end); + apply(txs.size() + 1, rr.state_diff); + std::ranges::move(rr.requests, std::back_inserter(requests)); + } } } - finalize(block_state, rev, block.coinbase, opts.block_reward, block.ommers, block.withdrawals); - + // Both counters start at block.gas_limit, so this is max(execution used, state used): + // the block's gas used is its bottleneck dimension (EIP-8037). + const auto block_gas_used = block.gas_limit - std::min(block_gas_left, block_state_gas_left); const auto bloom = compute_bloom_filter(receipts); return {std::move(receipts), std::move(rejected_txs), std::move(requests), requests_error, - block_gas_used, bloom, blob_gas_left, std::move(block_state)}; + block_gas_used, bloom, blob_gas_left, std::move(block_state), bal_builder.build()}; } } // namespace evmone::test diff --git a/evmone/test/utils/block_transition.hpp b/evmone/test/utils/block_transition.hpp index c95628a..d052ba5 100644 --- a/evmone/test/utils/block_transition.hpp +++ b/evmone/test/utils/block_transition.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -56,11 +57,12 @@ struct TransitionResult state::BloomFilter bloom; ///< Logs bloom over all accepted transactions. int64_t blob_gas_left = 0; ///< Blob gas remaining out of the budget passed in. TestState block_state; ///< State after applying the block. + state::BlockAccessList block_access_list; ///< Empty before Amsterdam (EIP-7928). }; /// Applies a block of transactions to a copy of @p state: block-start system call, the -/// transactions, request collection, block-end system call, and finalization. The system calls -/// and request collection are skipped when `opts.skip_system_calls` is set. +/// transactions, finalization (incl. the withdrawals), request collection and block-end system +/// call. The system calls and request collection are skipped when `opts.skip_system_calls` is set. /// /// Shared block-transition core for the blockchain test runner and the t8n tool. It performs /// no validation/assertions and produces no output; callers interpret the returned result. diff --git a/evmone/test/utils/blockchaintest.hpp b/evmone/test/utils/blockchaintest.hpp index 88f3356..8f455e8 100644 --- a/evmone/test/utils/blockchaintest.hpp +++ b/evmone/test/utils/blockchaintest.hpp @@ -4,21 +4,20 @@ #pragma once #include +#include #include #include #include #include +#include #include #include #include +namespace json = nlohmann; + namespace evmone::test { -struct UnsupportedTestFeature : std::runtime_error -{ - using runtime_error::runtime_error; -}; - // https://ethereum.org/en/developers/docs/blocks/ struct BlockHeader { @@ -27,14 +26,14 @@ struct BlockHeader hash256 state_root; hash256 receipts_root; state::BloomFilter logs_bloom; - int64_t difficulty; + int64_t difficulty = 0; bytes32 prev_randao; - int64_t block_number; - int64_t gas_limit; - int64_t gas_used; - int64_t timestamp; + int64_t block_number = 0; + int64_t gas_limit = 0; + int64_t gas_used = 0; + int64_t timestamp = 0; bytes extra_data; - uint64_t base_fee_per_gas; + uint64_t base_fee_per_gas = 0; hash256 hash; hash256 transactions_root; hash256 withdrawal_root; @@ -42,7 +41,8 @@ struct BlockHeader std::optional blob_gas_used; std::optional excess_blob_gas; hash256 requests_hash; - std::optional slot_number; ///< EIP-7843 — absent before Amsterdam. + std::optional block_access_list_hash; ///< Since EIP-7928. + std::optional slot_number; ///< Since EIP-7843. }; struct TestBlock @@ -77,4 +77,10 @@ struct BlockchainTest }; std::vector load_blockchain_tests(std::istream& input); + +/// Builds the test named @p name in a fixture file from its JSON value @p j. +BlockchainTest make_blockchain_test(const std::string& name, const json::json& j); + +/// Execute the blockchain @p test using the @p vm, recording what does not match into @p report. +void run_blockchain_test(const BlockchainTest& test, evmc::VM& vm, TestReport& report); } // namespace evmone::test diff --git a/evmone/test/utils/blockchaintest_loader.cpp b/evmone/test/utils/blockchaintest_loader.cpp index aed3203..73aa40f 100644 --- a/evmone/test/utils/blockchaintest_loader.cpp +++ b/evmone/test/utils/blockchaintest_loader.cpp @@ -35,6 +35,7 @@ BlockHeader from_json(const json::json& j) .blob_gas_used = load_optional(j, "blobGasUsed"), .excess_blob_gas = load_optional(j, "excessBlobGas"), .requests_hash = load_or(j, "requestsHash", {}), + .block_access_list_hash = load_optional(j, "blockAccessListHash"), .slot_number = load_optional(j, "slotNumber"), }; } @@ -118,9 +119,7 @@ static TestBlock load_test_block( return tb; } -namespace -{ -BlockchainTest load_blockchain_test_case(const std::string& name, const json::json& j) +BlockchainTest make_blockchain_test(const std::string& name, const json::json& j) { using namespace state; @@ -175,12 +174,11 @@ BlockchainTest load_blockchain_test_case(const std::string& name, const json::js return bt; } -} // namespace static void from_json(const json::json& j, std::vector& o) { for (const auto& elem_it : j.items()) - o.emplace_back(load_blockchain_test_case(elem_it.key(), elem_it.value())); + o.emplace_back(make_blockchain_test(elem_it.key(), elem_it.value())); } std::vector load_blockchain_tests(std::istream& input) diff --git a/evmone/test/utils/blockchaintest_runner.cpp b/evmone/test/utils/blockchaintest_runner.cpp new file mode 100644 index 0000000..6eb0c8e --- /dev/null +++ b/evmone/test/utils/blockchaintest_runner.cpp @@ -0,0 +1,538 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2023 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace evmone::test +{ + +/// The CL gossip protocol constraint of the maximum block size (EIP-7934). +constexpr size_t MAX_BLOCK_SIZE = 10 * 1024 * 1024; +/// The safety margin for beacon block content (EIP-7934). +constexpr size_t SAFETY_MARGIN = 2 * 1024 * 1024; +/// The maximum EL block size when RLP encoded (EIP-7934). +constexpr size_t MAX_RLP_BLOCK_SIZE = MAX_BLOCK_SIZE - SAFETY_MARGIN; + +namespace +{ +/// Validates block-level validity unrelated to individual transactions. +/// +/// Returns an empty error_code if the block is valid, otherwise the specific validation error. +std::error_code validate_block(evmc_revision rev, state::BlobParams blob_params, + const TestBlock& test_block, const BlockHeader* parent_header, bool parent_has_ommers) noexcept +{ + using namespace state; + + // Fail if parent header was not found: the block references a parent that is neither the + // genesis nor any previously-accepted block (an unknown or rejected parent). + if (parent_header == nullptr) + return make_error_code(UNKNOWN_PARENT); + + if (test_block.block_info.number != parent_header->block_number + 1) + return make_error_code(INVALID_BLOCK_NUMBER); + + if (test_block.block_info.gas_used > test_block.block_info.gas_limit) + return make_error_code(INCORRECT_BLOCK_FORMAT); + + // Some tests have gas limit at INT64_MAX, so we cast to uint64_t to avoid overflow. + const auto parent_header_gas_limit_u64 = static_cast(parent_header->gas_limit); + const auto test_block_gas_limit_u64 = static_cast(test_block.block_info.gas_limit); + if (test_block_gas_limit_u64 >= + parent_header_gas_limit_u64 + parent_header_gas_limit_u64 / 1024) + return make_error_code(INVALID_GASLIMIT); + if (test_block_gas_limit_u64 <= + parent_header_gas_limit_u64 - parent_header_gas_limit_u64 / 1024) + return make_error_code(INVALID_GASLIMIT); + + // Block gas limit minimum from Yellow Paper. + if (test_block.block_info.gas_limit < 5000) + return make_error_code(INVALID_GASLIMIT); + + // FIXME: Some tests have timestamp not fitting into int64_t, type has to be uint64_t. + if (static_cast(test_block.block_info.timestamp) <= + static_cast(parent_header->timestamp)) + return make_error_code(INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT); + + if (test_block.block_info.difficulty != + calculate_difficulty(parent_header->difficulty, parent_has_ommers, parent_header->timestamp, + test_block.block_info.timestamp, test_block.block_info.number, rev)) + return make_error_code(INCORRECT_BLOCK_FORMAT); + + if (rev >= EVMC_PARIS && !test_block.block_info.ommers.empty()) + return make_error_code(INCORRECT_BLOCK_FORMAT); + + for (const auto& ommer : test_block.block_info.ommers) + { + // Check that ommer block number difference with current block is within allowed range. + // https://github.com/ethereum/execution-specs/blob/ee73be5c4d83a2e3c358bd14990878002e52ba9e/src/ethereum/gray_glacier/fork.py#L623 + if (ommer.delta < 1 || ommer.delta > 6) + return make_error_code(INCORRECT_BLOCK_FORMAT); + } + + if (test_block.block_info.extra_data.size() > 32) + return make_error_code(INCORRECT_BLOCK_FORMAT); + + if (rev >= EVMC_LONDON) + { + const auto calculated_base_fee = calc_base_fee( + parent_header->gas_limit, parent_header->gas_used, parent_header->base_fee_per_gas); + if (test_block.block_info.base_fee != calculated_base_fee) + return make_error_code(INVALID_BASEFEE_PER_GAS); + } + + if (rev >= EVMC_CANCUN) + { + // `excess_blob_gas` and `blob_gas_used` mandatory after Cancun and invalid before. + if (!test_block.block_info.excess_blob_gas.has_value() || + !test_block.block_info.blob_gas_used.has_value()) + return make_error_code(INCORRECT_BLOCK_FORMAT); + + // Check that the excess blob gas was updated correctly. + // According to EIP-7918 current blocks params (`rev`) should be used for parent base fee + // calculation. + const auto parent_blob_base_fee = + compute_blob_gas_price(blob_params, parent_header->excess_blob_gas.value_or(0)); + if (*test_block.block_info.excess_blob_gas != + calc_excess_blob_gas(rev, blob_params, parent_header->blob_gas_used.value_or(0), + parent_header->excess_blob_gas.value_or(0), parent_header->base_fee_per_gas, + parent_blob_base_fee)) + return make_error_code(INCORRECT_EXCESS_BLOB_GAS); + } + else + { + if (test_block.block_info.excess_blob_gas.has_value() || + test_block.block_info.blob_gas_used.has_value()) + return make_error_code(INCORRECT_BLOCK_FORMAT); + } + + // `slot_number` is mandatory from Amsterdam and invalid before (EIP-7843). + if (test_block.block_info.slot_number.has_value() != (rev >= EVMC_AMSTERDAM)) + return make_error_code(INCORRECT_BLOCK_FORMAT); + + // Block is invalid if some of the withdrawal fields failed to be parsed. + if (!test_block.withdrawals_parse_success) + return make_error_code(INCORRECT_BLOCK_FORMAT); + + if (rev >= EVMC_OSAKA && test_block.rlp.size() > MAX_RLP_BLOCK_SIZE) + return make_error_code(RLP_BLOCK_LIMIT_EXCEEDED); + + // The block access list hash is required from Amsterdam (EIP-7928). Before, the field is not + // in the header the block hash commits to. + const auto has_bal_hash = test_block.expected_block_header.block_access_list_hash.has_value(); + if (rev >= EVMC_AMSTERDAM && !has_bal_hash) + return make_error_code(INVALID_BAL_HASH); + if (rev < EVMC_AMSTERDAM && has_bal_hash) + return make_error_code(INVALID_BLOCK_HASH); + + return {}; +} + +/// Checks the transaction codec against a block's own serialization: every transaction in it must +/// decode, and encode back to the very same bytes. +void check_transactions_round_trip(bytes_view block_rlp, TestReport& report) +{ + bytes_view body; // A block is [header, transactions, ...]. + if (!report.check(rlp::take_list_payload(block_rlp, body), "block RLP", "not a list")) + return; + if (!report.check(block_rlp.empty(), "block RLP", "trailing bytes after the block")) + return; + bytes_view block_header; // Skipped over. + if (!report.check( + rlp::take_list_payload(body, block_header), "block RLP", "the header is not a list")) + return; + bytes_view txs; + if (!report.check( + rlp::take_list_payload(body, txs), "block RLP", "the transactions are not a list")) + return; + + while (!txs.empty()) + { + const auto item = txs; + rlp::Header h; + // decode_header() advances txs to the item's payload. + if (!report.check( + rlp::decode_header(txs, h), "block RLP", "a transaction has no RLP header")) + return; + const auto header_size = item.size() - txs.size(); + txs.remove_prefix(h.payload_length); + + // A legacy transaction is an RLP list here, a typed one an RLP string wrapping the + // EIP-2718 envelope; the envelope alone is the transaction. + const auto tx_bytes = h.is_list ? item.substr(0, header_size + h.payload_length) : + item.substr(header_size, h.payload_length); + + const auto tx = state::decode_transaction(tx_bytes); + if (!tx.has_value()) + { + report.fail("transaction decoding", hex(tx_bytes)); + return; + } + report.check_eq("transaction re-encoding", rlp::encode(*tx), tx_bytes); + } +} + +std::optional mining_reward(evmc_revision rev) noexcept +{ + if (rev < EVMC_BYZANTIUM) + return 5'000000000'000000000; + if (rev < EVMC_PETERSBURG) + return 3'000000000'000000000; + if (rev < EVMC_PARIS) + return 2'000000000'000000000; + return std::nullopt; +} + +std::string print_state(const TestState& s) +{ + std::stringstream out; + + for (const auto& [key, acc] : s) + { + out << key << " : \n"; + out << "\tnonce : " << acc.nonce << "\n"; + out << "\tbalance : " << hex0x(acc.balance) << "\n"; + out << "\tcode : " << hex0x(acc.code) << "\n"; + + if (!acc.storage.empty()) + { + out << "\tstorage : \n"; + for (const auto& [s_key, val] : acc.storage) + { + if (!is_zero(val)) // Skip 0 values. + out << "\t\t" << s_key << " : " << hex0x(val) << "\n"; + } + } + } + + return out.str(); +} +} // namespace + +void run_blockchain_test(const BlockchainTest& test, evmc::VM& vm, TestReport& report) +{ + const auto rev_schedule = to_rev_schedule(test.network); + report.start_case(test.name); + // The network names the whole schedule, so a fork transition shows at the test level and a + // block needs only its index. The block number would not do: an invalid block does not + // advance it, so two of them can share one. + const auto in_test = report.at(test.network); + + // Validate the genesis block header. + report.check_eq("genesis block number", test.genesis_block_header.block_number, 0); + report.check_eq("genesis gas used", test.genesis_block_header.gas_used, 0); + report.check_eq("genesis transactions root", test.genesis_block_header.transactions_root, + state::EMPTY_MPT_HASH); + report.check_eq( + "genesis receipts root", test.genesis_block_header.receipts_root, state::EMPTY_MPT_HASH); + report.check_eq("genesis withdrawals root", test.genesis_block_header.withdrawal_root, + rev_schedule.get_revision(test.genesis_block_header.timestamp) >= EVMC_SHANGHAI ? + state::EMPTY_MPT_HASH : + bytes32{}); + report.check_eq("genesis logs bloom", bytes_view{test.genesis_block_header.logs_bloom}, + bytes_view{state::BloomFilter{}}); + + TestBlockHashes block_hashes{ + {test.genesis_block_header.block_number, test.genesis_block_header.hash}}; + + struct BlockData + { + const BlockHeader* header; + bool has_ommers = false; + TestState post_state; + intx::uint256 total_difficulty; + }; + std::unordered_map block_data{ + {{test.genesis_block_header.hash, {&test.genesis_block_header, false, test.pre_state, + test.genesis_block_header.difficulty}}}}; + const auto* canonical_state = &test.pre_state; + hash256 canonical_state_root; // Skip pre-state root hash computation (maybe not needed). + auto canonical_tip_hash = test.genesis_block_header.hash; + intx::uint256 max_total_difficulty = test.genesis_block_header.difficulty; + + for (size_t i = 0; i < test.test_blocks.size(); ++i) + { + const auto& test_block = test.test_blocks[i]; + const auto& bi = test_block.block_info; + + const auto parent_data_it = block_data.find(test_block.block_info.parent_hash); + const auto* parent_header = + parent_data_it != block_data.end() ? parent_data_it->second.header : nullptr; + const auto parent_has_ommers = + parent_data_it != block_data.end() && parent_data_it->second.has_ommers; + + const auto rev = rev_schedule.get_revision(bi.timestamp); + const auto blob_params = get_blob_params(test.network, test.blob_schedule, bi.timestamp); + const auto blob_gas_limit = + static_cast(state::max_blob_gas_per_block(blob_params)); + + const auto in_block = report.at(i); + + // Invalid blocks are skipped: they may carry transactions that do not even decode. + if (test_block.expected_exception.empty()) + check_transactions_round_trip(test_block.rlp, report); + + const auto block_error = + validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers); + + if (test_block.expected_exception.empty()) + { + if (block_error) + { + report.fail( + "block validity", "expected the block to be valid: " + block_error.message()); + return; + } + + // Block being valid guarantees its parent was found. + assert(parent_data_it != block_data.end()); + const auto& pre_state = parent_data_it->second.post_state; + + auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, rev, + blob_gas_limit, {.block_reward = mining_reward(rev)}); + + if (res.requests_error) + { + report.fail("requests", res.requests_error.message()); + return; + } + + block_hashes[test_block.expected_block_header.block_number] = + test_block.expected_block_header.hash; + const auto [inserted_it, _] = block_data.insert({test_block.block_info.hash, + { + .header = &test_block.expected_block_header, + .has_ommers = !test_block.block_info.ommers.empty(), + .post_state = std::move(res.block_state), + .total_difficulty = + parent_data_it->second.total_difficulty + test_block.block_info.difficulty, + }}); + + const auto state_root = state::mpt_hash(inserted_it->second.post_state); + + if (inserted_it->second.total_difficulty >= max_total_difficulty) + { + canonical_state = &inserted_it->second.post_state; + canonical_state_root = state_root; + canonical_tip_hash = test_block.expected_block_header.hash; + max_total_difficulty = inserted_it->second.total_difficulty; + } + + if (!res.rejected.empty()) + { + report.fail("transactions in a valid block", + "invalid transaction: " + res.rejected.front().error.message()); + } + + report.check_eq("blob gas used", blob_gas_limit - res.blob_gas_left, + static_cast(bi.blob_gas_used.value_or(0))); + report.check_eq("state root", state_root, test_block.expected_block_header.state_root); + + if (rev >= EVMC_SHANGHAI) + { + report.check_eq("withdrawals root", + state::mpt_hash(test_block.block_info.withdrawals), + test_block.expected_block_header.withdrawal_root); + } + + report.check_eq("transactions root", state::mpt_hash(test_block.transactions), + test_block.expected_block_header.transactions_root); + report.check_eq("receipts root", state::mpt_hash(res.receipts), + test_block.expected_block_header.receipts_root); + if (rev >= EVMC_PRAGUE) + { + report.check_eq("requests hash", calculate_requests_hash(res.requests), + test_block.expected_block_header.requests_hash); + } + report.check_eq("gas used", res.gas_used, test_block.expected_block_header.gas_used); + report.check_eq("logs bloom", bytes_view{res.bloom}, + bytes_view{test_block.expected_block_header.logs_bloom}); + if (rev >= EVMC_AMSTERDAM) + { + report.check_eq("block access list hash", res.block_access_list.hash(), + *test_block.expected_block_header.block_access_list_hash); + report.check( + !res.block_access_list.exceeds_gas_limit(static_cast(bi.gas_limit)), + "block access list", "too many items for the block gas limit"); + } + } + else + { + if (block_error) + { + // Block correctly rejected at validation; verify the reason matches the + // fixture's expected exception. + report.check( + is_expected_block_exception(block_error, test_block.expected_exception), + "block rejection reason", block_error, test_block.expected_exception); + continue; + } + + // Block being valid guarantees its parent was found. + assert(parent_data_it != block_data.end()); + const auto& pre_state = parent_data_it->second.post_state; + + // Legacy fixtures name the broken rule in vocabulary evmone does not speak + // (InvalidStateRoot, TooManyUncles); only the spec names can be compared. + const auto names_spec_exception = + test_block.expected_exception.find("Exception.") != std::string::npos; + + // TODO: The transaction senders come from the fixture instead of being recovered + // from the signatures, so evmone never sees the signature the test broke. Such a + // transaction executes as the sender the fixture names and the block is rejected + // by whatever rule that sender happens to break, or by its state root alone. + const auto sender_not_recovered = contains_any( + test_block.expected_exception, "TransactionException.INVALID_SIGNATURE_VRS"); + + const auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, + rev, blob_gas_limit, {.block_reward = mining_reward(rev)}); + if (!res.rejected.empty()) + { + // A transaction was rejected: the fixture must name that reason, not merely + // some rejection. + const auto& rejected = res.rejected.front(); + if (names_spec_exception && !sender_not_recovered) + { + report.check( + is_expected_tx_exception(rejected.error, test_block.expected_exception), + "transaction rejection reason", rejected.error, + test_block.expected_exception); + } + continue; + } + if (res.requests_error) + { + if (!sender_not_recovered) + { + report.check(is_expected_block_exception( + res.requests_error, test_block.expected_exception), + "block rejection reason", res.requests_error, + test_block.expected_exception); + } + continue; + } + // The block executed, so it is invalid only if it computes something other than + // its header claims. Each difference below is the symptom of one BlockException: + // a block failing a check other than the one the fixture names breaks a different + // rule than the test is about. + // TODO: Of the ommers only the count and the distance to their nephew are + // validated, not the ommer headers themselves, so a fixture that breaks an + // ommer's gas limit, number or timestamp reaches execution and lands here. + const auto ommers_not_validated = !test_block.block_info.ommers.empty(); + + // Asserts the fixture names one of @p names, the exceptions the check that just + // fired is the symptom of. Silent where the reason cannot be compared. + const auto expect_fixture_names = [&](std::string_view names) { + if (!names_spec_exception || ommers_not_validated || sender_not_recovered) + return; + report.check(contains_any(test_block.expected_exception, names), + "block rejection reason", names, test_block.expected_exception); + }; + + if (blob_gas_limit - res.blob_gas_left != + static_cast(bi.blob_gas_used.value_or(0))) + { + expect_fixture_names( + "BlockException.INCORRECT_BLOB_GAS_USED|" + "BlockException.BLOB_GAS_USED_ABOVE_LIMIT"); + continue; + } + + if (state::mpt_hash(res.block_state) != test_block.expected_block_header.state_root) + { + expect_fixture_names("BlockException.INVALID_STATE_ROOT"); + continue; + } + + if (rev >= EVMC_SHANGHAI && state::mpt_hash(test_block.block_info.withdrawals) != + test_block.expected_block_header.withdrawal_root) + { + expect_fixture_names("BlockException.INVALID_WITHDRAWALS_ROOT"); + continue; + } + if (state::mpt_hash(test_block.transactions) != + test_block.expected_block_header.transactions_root) + { + expect_fixture_names("BlockException.INVALID_TRANSACTIONS_ROOT"); + continue; + } + if (state::mpt_hash(res.receipts) != test_block.expected_block_header.receipts_root) + { + expect_fixture_names("BlockException.INVALID_RECEIPTS_ROOT"); + continue; + } + if (rev >= EVMC_PRAGUE && calculate_requests_hash(res.requests) != + test_block.expected_block_header.requests_hash) + { + expect_fixture_names("BlockException.INVALID_REQUESTS"); + continue; + } + if (res.gas_used != test_block.expected_block_header.gas_used) + { + expect_fixture_names( + "BlockException.INVALID_GAS_USED|" + "BlockException.GAS_USED_OVERFLOW"); + continue; + } + if (bytes_view{res.bloom} != bytes_view{test_block.expected_block_header.logs_bloom}) + { + expect_fixture_names("BlockException.INVALID_LOG_BLOOM"); + continue; + } + if (rev >= EVMC_AMSTERDAM) + { + if (res.block_access_list.exceeds_gas_limit(static_cast(bi.gas_limit))) + { + expect_fixture_names("BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED"); + continue; + } + if (res.block_access_list.hash() != + *test_block.expected_block_header.block_access_list_hash) + { + // The BAL is not decoded, only recomputed and compared, so a structurally + // malformed one (out-of-order or duplicate entries) also lands here. + expect_fixture_names( + "BlockException.INVALID_BAL_HASH|" + "BlockException.INVALID_BLOCK_ACCESS_LIST|" + "BlockException.INCORRECT_BLOCK_FORMAT"); + continue; + } + } + + report.fail("block validity", "expected the block to be invalid"); + } + } + report.check_eq("canonical chain tip", canonical_tip_hash, test.expectation.last_block_hash); + + const auto expected_post_hash = + std::holds_alternative(test.expectation.post_state) ? + state::mpt_hash(std::get(test.expectation.post_state)) : + std::get(test.expectation.post_state); + + // Get the final state hash. In case none blocks have been applied, compute genesis one. + const auto canonical_post_hash = + canonical_state_root ? canonical_state_root : state::mpt_hash(test.pre_state); + // The state dumps are a callable so that formatting the whole state only happens once + // the roots are already known to differ. + report.check_eq("post state root", canonical_post_hash, expected_post_hash, [&] { + return "Result state:\n" + print_state(*canonical_state) + + (std::holds_alternative(test.expectation.post_state) ? + "\n\nExpected state:\n" + + print_state(std::get(test.expectation.post_state)) : + ""); + }); +} + +} // namespace evmone::test diff --git a/evmone/test/utils/error_matching.cpp b/evmone/test/utils/error_matching.cpp index b239ad0..f710690 100644 --- a/evmone/test/utils/error_matching.cpp +++ b/evmone/test/utils/error_matching.cpp @@ -43,6 +43,7 @@ constexpr AlternativeExceptions ALTERNATIVE_TX_EXCEPTIONS[]{ // decode_transaction() reports one code for every malformed encoding, so this accepts more // than the v rule; narrowing it needs the decoder to report the v domain separately. {state::INVALID_ENCODING, "TransactionException.INVALID_SIGNATURE_VRS"}, + }; /// The same, for the rules evmone checks on the block rather than the transaction. diff --git a/evmone/test/utils/mpt_hash.cpp b/evmone/test/utils/mpt_hash.cpp index 0c4ff40..e84efe3 100644 --- a/evmone/test/utils/mpt_hash.cpp +++ b/evmone/test/utils/mpt_hash.cpp @@ -30,7 +30,7 @@ hash256 mpt_hash(const test::TestState& state) for (const auto& [addr, acc] : state) { trie.insert(keccak256(addr), - rlp::encode_tuple(acc.nonce, acc.balance, mpt_hash(acc.storage), keccak256(acc.code))); + rlp::encode_tuple(acc.nonce, acc.balance, mpt_hash(acc.storage), acc.get_code_hash())); } return trie.hash(); } diff --git a/evmone/test/utils/statetest.hpp b/evmone/test/utils/statetest.hpp index c9d071e..232562a 100644 --- a/evmone/test/utils/statetest.hpp +++ b/evmone/test/utils/statetest.hpp @@ -4,11 +4,14 @@ #pragma once #include "blob_schedule.hpp" +#include "utils.hpp" #include #include #include #include +#include #include +#include #include namespace json = nlohmann; @@ -144,6 +147,12 @@ T load_or(const json::json& j, std::string_view key, T default_value) /// Exports the State (accounts) to JSON format (aka pre/post/alloc state). json::json to_json(const TestState& state); +/// Exports a transaction log to JSON format (as in a receipt's log list). +json::json to_json(const state::Log& log); + +/// Exports a StateDiff to JSON. +json::json to_json(const state::StateDiff& diff); + /// Export the state test to JSON format. json::json to_state_test(std::string_view test_name, const state::BlockInfo& block, state::Transaction& tx, const TestState& pre, evmc_revision rev, @@ -151,32 +160,33 @@ json::json to_state_test(std::string_view test_name, const state::BlockInfo& blo std::vector load_state_tests(std::istream& input); +/// Builds the test named @p name in a fixture file from its JSON value @p j. +StateTransitionTest make_state_test(const std::string& name, const json::json& j); + /// Validates the invariants of the Ethereum state (e.g. no zero-value storage entries). /// Throws std::invalid_argument exception. void validate_state(const TestState& state, evmc_revision rev); -/// Execute the state @p test using the @p vm. -/// -/// @param trace_summary Output execution summary to the default trace stream. -void run_state_test(const StateTransitionTest& test, evmc::VM& vm, bool trace_summary); +/// What run_state_test() reports besides the failures, and where. +struct StateTestOptions +{ + /// The stream the enabled reports are written to. + std::ostream& output; + + /// Report each case's execution summary. + bool trace_summary = false; + + /// Report each case's execution summary and state diff. + bool state_diff = false; +}; + +/// Execute the state @p test using the @p vm, recording what does not match into @p report. +void run_state_test(const StateTransitionTest& test, evmc::VM& vm, const StateTestOptions& options, + TestReport& report); /// Computes the hash of the RLP-encoded list of transaction logs. /// This method is only used in tests. hash256 logs_hash(const std::vector& logs); - -/// Converts an integer to hex string representation with 0x prefix. -/// -/// This handles also builtin types like uint64_t. Not optimal but works for now. -inline std::string hex0x(const intx::uint256& v) -{ - return "0x" + intx::hex(v); -} - -/// Encodes bytes as hex with 0x prefix. -inline std::string hex0x(bytes_view v) -{ - return "0x" + evmc::hex(v); -} } // namespace evmone::test inline std::ostream& operator<<(std::ostream& out, const evmone::address& a) diff --git a/evmone/test/utils/statetest_export.cpp b/evmone/test/utils/statetest_export.cpp index 2ab1041..ef90da7 100644 --- a/evmone/test/utils/statetest_export.cpp +++ b/evmone/test/utils/statetest_export.cpp @@ -44,6 +44,41 @@ json::json to_json(const TestState& state) return j; } +json::json to_json(const state::Log& log) +{ + json::json j; + j["address"] = hex0x(log.addr); + auto& j_topics = j["topics"] = json::json::array(); + for (const auto& topic : log.topics) + j_topics.push_back(hex0x(topic)); + j["data"] = hex0x(log.data); + return j; +} + +json::json to_json(const state::StateDiff& diff) +{ + json::json j; + auto& j_modified = j["modifiedAccounts"] = json::json::object(); + for (const auto& m : diff.modified_accounts) + { + auto& j_acc = j_modified[hex0x(m.addr)]; + j_acc["nonce"] = hex0x(m.nonce); + j_acc["balance"] = hex0x(m.balance); + if (m.code.has_value()) + j_acc["code"] = hex0x(bytes_view(m.code->data(), m.code->size())); + + auto& j_storage = j_acc["modifiedStorage"] = json::json::object(); + for (const auto& [key, val] : m.modified_storage) + j_storage[hex0x(key)] = hex0x(val); + } + + auto& j_deleted = j["deletedAccounts"] = json::json::array(); + for (const auto& addr : diff.deleted_accounts) + j_deleted.emplace_back(hex0x(addr)); + + return j; +} + json::json to_state_test(std::string_view test_name, const state::BlockInfo& block, state::Transaction& tx, const TestState& pre, evmc_revision rev, const std::variant& res, const TestState& post) diff --git a/evmone/test/utils/statetest_loader.cpp b/evmone/test/utils/statetest_loader.cpp index 118fc9f..ec49636 100644 --- a/evmone/test/utils/statetest_loader.cpp +++ b/evmone/test/utils/statetest_loader.cpp @@ -319,6 +319,7 @@ TestState from_json(const json::json& j) o[from_json
(j_addr)] = {.nonce = from_json(j_acc.at("nonce")), .balance = from_json(j_acc.at("balance")), .code = from_json(j_acc.at("code"))}; + acc.code_hash = keccak256(acc.code); if (const auto storage_it = j_acc.find("storage"); storage_it != j_acc.end()) { @@ -506,11 +507,14 @@ static void from_json(const json::json& j_t, StateTransitionTest& o) static void from_json(const json::json& j, std::vector& o) { for (const auto& elem_it : j.items()) - { - auto test = elem_it.value().get(); - test.name = elem_it.key(); - o.emplace_back(std::move(test)); - } + o.emplace_back(make_state_test(elem_it.key(), elem_it.value())); +} + +StateTransitionTest make_state_test(const std::string& name, const json::json& j) +{ + auto test = j.get(); + test.name = name; + return test; } std::vector load_state_tests(std::istream& input) diff --git a/evmone/test/statetest/statetest_runner.cpp b/evmone/test/utils/statetest_runner.cpp similarity index 58% rename from evmone/test/statetest/statetest_runner.cpp rename to evmone/test/utils/statetest_runner.cpp index 8d2ea43..97054bd 100644 --- a/evmone/test/statetest/statetest_runner.cpp +++ b/evmone/test/utils/statetest_runner.cpp @@ -2,24 +2,26 @@ // Copyright 2022 The evmone Authors. // SPDX-License-Identifier: Apache-2.0 -#include #include #include #include #include #include +#include +#include namespace evmone::test { -void run_state_test(const StateTransitionTest& test, evmc::VM& vm, bool trace_summary) +void run_state_test(const StateTransitionTest& test, evmc::VM& vm, const StateTestOptions& options, + TestReport& report) { - SCOPED_TRACE(test.name); + report.start_case(test.name); for (const auto& [rev, cases, block] : test.cases) { validate_state(test.pre_state, rev); for (size_t case_index = 0; case_index != cases.size(); ++case_index) { - SCOPED_TRACE(std::string{evmc::to_string(rev)} + '/' + std::to_string(case_index)); + const auto in_case = report.at(evmc::to_string(rev), '/', case_index); // if (rev != EVMC_FRONTIER) // continue; // if (case_index != 3) @@ -41,7 +43,7 @@ void run_state_test(const StateTransitionTest& test, evmc::VM& vm, bool trace_su else { // Decoding is the inverse of encoding: what decoded must encode back exactly. - EXPECT_EQ(rlp::encode(*tx), *expected.txbytes); + report.check_eq("transaction re-encoding", rlp::encode(*tx), *expected.txbytes); // Recover the signer, as a node does, instead of taking it from JSON. const auto sender = state::recover_sender(*tx, *expected.txbytes); @@ -59,6 +61,7 @@ void run_state_test(const StateTransitionTest& test, evmc::VM& vm, bool trace_su const auto res = error ? error : transition(state, block, test.block_hashes, *tx, rev, vm, block.gas_limit, + block.gas_limit, static_cast(state::max_blob_gas_per_block(blob_params))); if (holds_alternative(res)) @@ -70,43 +73,62 @@ void run_state_test(const StateTransitionTest& test, evmc::VM& vm, bool trace_su const auto state_root = state::mpt_hash(state); - if (trace_summary) + const auto* const receipt = get_if(&res); + const auto logs_hash = + receipt != nullptr ? test::logs_hash(receipt->logs) : test::logs_hash({}); + + if (options.trace_summary || options.state_diff) { - std::clog << '{'; + auto& out = options.output; + out << '{'; if (holds_alternative(res)) // if tx valid { const auto& r = get(res); if (r.status == EVMC_SUCCESS) - std::clog << R"("pass":true)"; + out << R"("pass":true)"; else - std::clog << R"("pass":false,"error":")" << r.status << '"'; - std::clog << R"(,"gasUsed":"0x)" << std::hex << r.gas_used << R"(",)"; + out << R"("pass":false,"error":")" << r.status << '"'; + out << R"(,"gasUsed":")" << hex0x(r.gas_used) << R"(",)"; + } + out << R"("logsHash":")" << hex0x(logs_hash) << R"(",)"; + out << R"("stateRoot":")" << hex0x(state_root) << '"'; + if (options.state_diff) + { + out << R"(,"stateDiff":)"; + const auto& diff = holds_alternative(res) ? + get(res).state_diff : + state::StateDiff{}; + out << to_json(diff).dump(); } - std::clog << R"("stateRoot":"0x)" << hex(state_root) << "\"}\n"; + out << "}\n"; } if (!expected.exception.empty()) { - ASSERT_FALSE(holds_alternative(res)) - << "unexpected valid transaction"; + if (holds_alternative(res)) + { + report.fail("transaction validity", "unexpected valid transaction"); + return; + } // The transaction must be rejected for the reason the fixture states, not merely // rejected: a wrong reason is a wrong implementation of the rule being tested. const auto& reason = get(res); - EXPECT_TRUE(is_expected_tx_exception(reason, expected.exception)) - << "transaction rejected as \"" << reason.message() << "\", expected " - << expected.exception; - - EXPECT_EQ(logs_hash(std::vector()), expected.logs_hash); + report.check(is_expected_tx_exception(reason, expected.exception), + "transaction rejection reason", reason, expected.exception); } else { - ASSERT_TRUE(holds_alternative(res)) - << "unexpected invalid transaction: " << get(res).message(); - EXPECT_EQ(logs_hash(get(res).logs), expected.logs_hash); + if (!holds_alternative(res)) + { + report.fail("transaction validity", + "unexpected invalid transaction: " + get(res).message()); + return; + } } - EXPECT_EQ(state_root, expected.state_hash); + report.check_eq("logs hash", logs_hash, expected.logs_hash); + report.check_eq("state root", state_root, expected.state_hash); } } } diff --git a/evmone/test/utils/t8n.cpp b/evmone/test/utils/t8n.cpp index dbf8a08..55564ef 100644 --- a/evmone/test/utils/t8n.cpp +++ b/evmone/test/utils/t8n.cpp @@ -115,8 +115,10 @@ void t8n(evmc::VM& vm, const T8NArgs& args) } } + // No block reward from Paris (EIP-3675). A reward of 0 would still touch the coinbase, + // which would then be read (EIP-7928). auto res = apply_block(state, vm, block, block_hashes, txs, rev, blob_gas_limit, - {.block_reward = args.block_reward, + {.block_reward = rev < EVMC_PARIS ? args.block_reward : std::nullopt, .skip_system_calls = args.pre_state_only, .open_trace = args.open_trace}); @@ -149,9 +151,14 @@ void t8n(evmc::VM& vm, const T8NArgs& args) j_receipt["blockHash"] = hex0x(bytes32{}); j_receipt["contractAddress"] = hex0x(address{}); j_receipt["logsBloom"] = hex0x(receipt.logs_bloom_filter); - j_receipt["logs"] = JSON::array(); // FIXME: Add to_json - j_receipt["root"] = ""; - j_receipt["status"] = "0x1"; + auto& j_logs = j_receipt["logs"] = JSON::array(); + for (const auto& log : receipt.logs) + j_logs.push_back(to_json(log)); + // A pre-Byzantium receipt is keyed on the post-state root instead of the + // EIP-658 status. Emit both, as go-ethereum does: the root takes precedence. + j_receipt["root"] = + receipt.post_state.has_value() ? hex0x(*receipt.post_state) : ""; + j_receipt["status"] = hex0x(uint64_t{receipt.status == EVMC_SUCCESS}); j_receipt["transactionIndex"] = hex0x(i); transactions.emplace_back(std::move(txs[i])); } @@ -164,15 +171,23 @@ void t8n(evmc::VM& vm, const T8NArgs& args) requests = std::move(res.requests); receipts = std::move(res.receipts); - // Block gas used reported as the cumulative transaction gas (refunds excluded), - // preserving the prior t8n output. - gas_used = receipts.empty() ? 0 : receipts.back().cumulative_gas_used; + gas_used = res.gas_used; bloom = res.bloom; blob_gas_left = res.blob_gas_left; post_state = std::move(res.block_state); j_result["logsHash"] = hex0x(logs_hash(txs_logs)); j_result["stateRoot"] = hex0x(state::mpt_hash(post_state)); + + if (!args.pre_state_only && rev >= EVMC_AMSTERDAM) + { + j_result["blockAccessList"] = hex0x(res.block_access_list.rlp_encode()); + j_result["blockAccessListHash"] = hex0x(res.block_access_list.hash()); + // The requests are processed first, so their failure takes precedence. + if (!res.requests_error && + res.block_access_list.exceeds_gas_limit(static_cast(block.gas_limit))) + j_result["blockException"] = "BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED"; + } } else post_state = state; diff --git a/evmone/test/utils/test_collector.cpp b/evmone/test/utils/test_collector.cpp new file mode 100644 index 0000000..eded561 --- /dev/null +++ b/evmone/test/utils/test_collector.cpp @@ -0,0 +1,80 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "test_collector.hpp" +#include +#include + +namespace evmone::test +{ +namespace fs = std::filesystem; + +std::vector collect_test_files(const fs::path& root) +{ + static constexpr auto is_test_file = [](const fs::directory_entry& entry) { + return entry.is_regular_file() && entry.path().extension() == ".json" && + entry.path().filename() != "index.json"; + }; + // TODO(gcc-12): Pipe the temporary in directly. Adapting one needs owning_view, which C++20 + // has but gcc-11's libstdc++ does not implement. + const fs::recursive_directory_iterator entries{root}; + + // TODO(C++23): std::ranges::to() replaces the vector and the copy. + std::vector files; + std::ranges::copy(entries | std::views::filter(is_test_file) | + std::views::transform(&fs::directory_entry::path), + std::back_inserter(files)); + std::ranges::sort(files); + return files; +} + +void ignore_test_files( + std::vector& files, const fs::path& root, std::span ignored) +{ + // Whether the path begins with every component of the prefix. + static constexpr auto is_under = [](const fs::path& path, const fs::path& prefix) { + // "./B" has to name what "B" names, and a trailing separator, which tab completion adds, + // is an empty final component of its own. + auto p = prefix.lexically_normal(); + if (p.filename().empty()) + p = p.parent_path(); + // An empty prefix, which an unset variable expands to, names nothing rather than + // everything. + return !p.empty() && std::ranges::mismatch(p, path).in1 == p.end(); + }; + + std::erase_if(files, [&root, ignored](const fs::path& file) { + const auto relative = file.lexically_relative(root); + return std::ranges::any_of( + ignored, [&relative](const fs::path& prefix) { return is_under(relative, prefix); }); + }); +} + +void collect_tests( + std::vector& cases, const fs::path& root, const RunOptions& options, evmc::VM& vm) +{ + // A file named directly is its own collection; the ignored paths are relative to the + // directory holding it, as they are to a directory named directly. + const auto is_dir = is_directory(root); + auto files = is_dir ? collect_test_files(root) : std::vector{root}; + ignore_test_files(files, is_dir ? root : root.parent_path(), options.ignored); + + cases.reserve(cases.size() + files.size()); + for (const auto& path : files) + { + // Loaded when the test runs: loading a whole tree up front costs far more. + cases.push_back( + {path.string(), [path, &options, &vm] { return run_fixture_file(path, options, vm); }}); + } +} + +int test( + evmc::VM& vm, std::span roots, const RunOptions& options, std::ostream& out) +{ + std::vector cases; + for (const auto& root : roots) + collect_tests(cases, root, options, vm); + return run_tests(cases, out, options); +} +} // namespace evmone::test diff --git a/evmone/test/utils/test_collector.hpp b/evmone/test/utils/test_collector.hpp new file mode 100644 index 0000000..6ecd1e4 --- /dev/null +++ b/evmone/test/utils/test_collector.hpp @@ -0,0 +1,33 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +namespace evmone::test +{ +/// The JSON test files under @p root, sorted, so a run visits them in a stable order. +/// "index.json" is skipped: those are lists of tests generated by other tools, not tests. +[[nodiscard]] std::vector collect_test_files( + const std::filesystem::path& root); + +/// Drops from @p files everything under one of the @p ignored paths, which are relative to +/// @p root. Whole path components are matched, so ignoring "bc4895" keeps "bc4895-withdrawals". +void ignore_test_files(std::vector& files, const std::filesystem::path& root, + std::span ignored); + +/// Adds to @p cases one test per fixture file under @p root, which is that file itself when it +/// is not a directory. The tests hold @p options and @p vm by reference, so both must outlive +/// them. +void collect_tests(std::vector& cases, const std::filesystem::path& root, + const RunOptions& options, evmc::VM& vm); + +/// Runs the Ethereum tests collected under every path in @p roots, reporting to @p out. +/// Returns the process exit code. +[[nodiscard]] int test(evmc::VM& vm, std::span roots, + const RunOptions& options, std::ostream& out); +} // namespace evmone::test diff --git a/evmone/test/utils/test_driver.cpp b/evmone/test/utils/test_driver.cpp new file mode 100644 index 0000000..41c43a1 --- /dev/null +++ b/evmone/test/utils/test_driver.cpp @@ -0,0 +1,336 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "test_driver.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CODSPEED_ENABLED +#include +#include +#endif + +namespace evmone::test +{ +namespace fs = std::filesystem; + +namespace +{ +/// The report is laid out like pytest's. +constexpr int LINE_WIDTH = 72; +constexpr int PROGRESS_WIDTH = 60; + +void banner(std::ostream& out, std::string_view title, char fill = '=') +{ + const auto padding = LINE_WIDTH - static_cast(title.size()) - 2; + const auto left = std::max(padding / 2, 1); + out << std::string(static_cast(left), fill) << ' ' << title << ' ' + << std::string(static_cast(std::max(padding - left, 1)), fill) << '\n'; +} + +/// A file with something to report: how it counts, and every fixture of it which did not pass. +struct Note +{ + std::string name; + Outcome outcome; + std::vector results; +}; + +/// One progress character per test, wrapped, each line ending in the percentage done. +class Progress +{ + std::ostream& m_out; + size_t m_total; + size_t m_done = 0; + int m_column = 0; + +public: + Progress(std::ostream& out, size_t total) noexcept : m_out{out}, m_total{total} {} + + void advance(Outcome outcome) + { + m_out << static_cast(outcome); + ++m_done; + if (++m_column != PROGRESS_WIDTH && m_done != m_total) + return; + + m_out << std::string(static_cast(PROGRESS_WIDTH - m_column), ' ') << " [" + << std::setw(3) << m_done * 100 / m_total << "%]\n" + << std::flush; + m_column = 0; + } +}; + +/// What this tool makes of one fixture. +enum class Format +{ + state_test, + blockchain_test, + unsupported, ///< A fixture, in a format this tool does not run. + not_a_test, ///< Not a fixture at all. +}; + +Format classify(const json::json& fixture) +{ + if (const auto info = fixture.find("_info"); info != fixture.end()) + { + if (const auto format = info->find("fixture-format"); format != info->end()) + { + if (*format == "state_test") + return Format::state_test; + if (*format == "blockchain_test") + return Format::blockchain_test; + return Format::unsupported; + } + } + // Nothing declares the format: a hand-written or pre-EEST file, or an "_info" without one. + // Each shape is named by the state it starts from and what is applied to it, never by what + // it expects, so a fixture whose expectations are missing is still a test and is run. + // Anything else is not a test at all, as EEST's shared pre-allocation is not. + if (fixture.contains("pre") && fixture.contains("blocks")) + return Format::blockchain_test; + if (fixture.contains("pre") && fixture.contains("transaction")) + return Format::state_test; + return Format::not_a_test; +} + +/// Parses the fixture file at @p path. Throws UnsupportedTestFeature for a file with no +/// fixture in it: EEST keeps its shared pre-allocation and an index beside the fixtures, and +/// neither is a test. +json::json load_fixture_file(const fs::path& path) +{ + std::ifstream f{path}; + const auto contents = json::json::parse(f); + // A document which is not an object holds none either: items() would walk an array by + // index, naming its elements "0", "1", ... + if (!contents.is_object() || std::ranges::none_of(contents.items(), [](const auto& i) { + return classify(i.value()) != Format::not_a_test; + })) + throw UnsupportedTestFeature{"not a test"}; + return contents; +} + +/// Runs one fixture of a fixture file. +void run_fixture(const std::string& name, const json::json& fixture, const RunOptions& options, + evmc::VM& vm, TestReport& report) +{ + // The runners rename theirs; this names a failure to load, or a fixture which is not one. + report.start_case(name); + switch (classify(fixture)) + { + case Format::state_test: + run_state_test(make_state_test(name, fixture), vm, + {.output = std::clog, + .trace_summary = options.trace_summary, + .state_diff = options.state_diff}, + report); + break; + case Format::blockchain_test: + { + const auto test = make_blockchain_test(name, fixture); +#ifdef CODSPEED_ENABLED + codspeed::CodSpeed::getInstance(); // Sets up the hooks and the integration name, once. + measurement_start(); +#endif + run_blockchain_test(test, vm, report); +#ifdef CODSPEED_ENABLED + measurement_stop(); + measurement_set_executed_benchmark(name); // The fixture's name is the benchmark's URI. +#endif + break; + } + case Format::unsupported: + throw UnsupportedTestFeature{ + "unsupported fixture format: " + fixture.at("_info").at("fixture-format").dump()}; + case Format::not_a_test: + // The rest of the file holds fixtures, so this one is broken. + report.fail("not a test"); + break; + } +} + +} // namespace + +Result run_one(std::string name, const std::function& run) +{ + Result result{.name = std::move(name)}; + TestReport report{[&result](const Failure& failure) { result.failures.push_back(failure); }}; + report.start_case(result.name); + + try + { + run(report); + } + catch (const UnsupportedTestFeature& ex) + { + result.outcome = Outcome::skipped; + result.reason = ex.what(); + } + catch (const std::exception& ex) + { + // One unloadable fixture in a tree of thousands fails its own test, not the run. + report.fail(concat("exception: ", ex.what())); + } + catch (...) + { + report.fail("exception not derived from std::exception"); + } + + // The failures are in the order they happened, so the first is what the test is reported + // as: one recorded before the run gave up outranks it. + if (!result.failures.empty()) + { + result.outcome = Outcome::failed; + result.reason = result.failures.front().what; + } + return result; +} + +int run_tests(std::span cases, std::ostream& out, const RunOptions& options) +{ + if (options.collect_only) + { + for (const auto& test : cases) + out << test.name << '\n'; + return cases.empty() ? NOTHING_VERIFIED : SUCCESS; + } + + const auto started = std::chrono::steady_clock::now(); + + banner(out, "test session starts"); + out << "collected " << cases.size() << (cases.size() == 1 ? " file\n\n" : " files\n\n"); + + // Held until the run ends, as pytest holds them, so nothing interleaves. + std::vector notes; + Progress row{out, cases.size()}; + size_t failed = 0; + size_t skipped = 0; + size_t deselected = 0; + size_t passed = 0; + + for (const auto& test : cases) + { + out << std::flush; + + auto results = test.run(); + // A test writes its own output, an EVM trace above all, to another stream. + std::clog << std::flush; + + // The file counts once, for the worst its fixtures reached. It is skipped only when + // nothing in it ran at all, so one fixture running is enough to give it a verdict. + // No result at all means the filter emptied the file: deselected, not skipped. + static constexpr auto is = [](Outcome outcome) { + return [outcome](const Result& result) { return result.outcome == outcome; }; + }; + auto outcome = Outcome::passed; + if (results.empty()) + outcome = Outcome::deselected; + else if (std::ranges::any_of(results, is(Outcome::failed))) + outcome = Outcome::failed; + else if (std::ranges::none_of(results, is(Outcome::passed))) + outcome = Outcome::skipped; + + switch (outcome) + { + case Outcome::passed: + ++passed; + break; + case Outcome::failed: + ++failed; + break; + case Outcome::skipped: + ++skipped; + break; + case Outcome::deselected: + ++deselected; + break; + } + + // Every fixture which did not pass is named, including one declined by a file which + // passed on the fixtures beside it. Otherwise it would vanish from a green run. + std::erase_if(results, is(Outcome::passed)); + if (!results.empty()) + notes.push_back({test.name, outcome, std::move(results)}); + + if (options.progress) + row.advance(outcome); + } + + if (failed != 0) + { + out << '\n'; + banner(out, "FAILURES"); + for (const auto& note : notes) + { + if (note.outcome != Outcome::failed) + continue; + banner(out, note.name, '_'); + for (const auto& result : note.results) + { + for (const auto& failure : result.failures) + out << failure << '\n'; + } + } + } + + if (!notes.empty()) + { + out << '\n'; + banner(out, "short test summary info"); + for (const auto& note : notes) + { + for (const auto& result : note.results) + { + // Only a passed result has no reason, and those were erased above. + assert(!result.reason.empty()); + out << (result.outcome == Outcome::failed ? "FAILED " : "SKIPPED ") << result.name + << " - " << result.reason << '\n'; + } + } + } + + const std::chrono::duration elapsed = std::chrono::steady_clock::now() - started; + std::ostringstream summary; + if (failed != 0) + summary << failed << " failed, "; + summary << passed << " passed"; + if (skipped != 0) + summary << ", " << skipped << " skipped"; + if (deselected != 0) + summary << ", " << deselected << " deselected"; + summary << " in " << std::fixed << std::setprecision(2) << elapsed.count() << "s"; + out << '\n'; + banner(out, std::move(summary).str()); + + if (failed != 0) + return TESTS_FAILED; + return passed == 0 ? NOTHING_VERIFIED : SUCCESS; +} + +std::vector run_fixture_file(const fs::path& path, const RunOptions& options, evmc::VM& vm) +{ + json::json contents; + if (auto loaded = + run_one(path.string(), [&](TestReport&) { contents = load_fixture_file(path); }); + loaded.outcome != Outcome::passed) + return {std::move(loaded)}; + + std::vector results; + for (const auto& [name, fixture] : contents.items()) + { + if (!options.selects(name)) + continue; + results.push_back(run_one(path.string() + "::" + name, + [&](TestReport& report) { run_fixture(name, fixture, options, vm, report); })); + } + return results; +} + +} // namespace evmone::test diff --git a/evmone/test/utils/test_driver.hpp b/evmone/test/utils/test_driver.hpp new file mode 100644 index 0000000..49a5b27 --- /dev/null +++ b/evmone/test/utils/test_driver.hpp @@ -0,0 +1,105 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace evmone::test +{ +/// Nothing failed and something passed. +constexpr int SUCCESS = 0; + +/// At least one test failed. +constexpr int TESTS_FAILED = 1; + +/// No test passed, whether none was collected or every one was skipped. pytest returns this +/// value for the first of those; a test skipped has verified no more than a missing one. +constexpr int NOTHING_VERIFIED = 5; + +/// The verdict on a fixture or on a file, spelled as the file's progress character. +enum class Outcome : char +{ + passed = '.', + failed = 'F', + skipped = 's', + deselected = 'd', ///< A file whose fixtures were all filtered out. +}; + +/// What running one fixture produced. +struct Result +{ + /// The fixture, as "::", or the file alone when it never got as far as one. + std::string name; + + Outcome outcome = Outcome::passed; + + /// Why it did not pass. Empty when it did. + std::string reason; + + std::vector failures; +}; + +/// A single test: its name and how to run it. +struct TestCase +{ + std::string name; + + /// Executes the test, returning what each of its fixtures produced. A test which never got + /// as far as a fixture returns the one result which says so, rather than throwing. + std::function()> run; +}; + +/// Runs @p run under a report of its own and says what it produced. What the run recorded +/// outranks how it ended: an exception is the reason only when nothing failed before it threw. +[[nodiscard]] Result run_one(std::string name, const std::function& run); + +/// What to collect, what to run of it, and what to report. +struct RunOptions +{ + /// List the tests instead of running them. + bool collect_only = false; + + /// Mark each test with a progress character. A progress line has no terminating newline, + /// so anything a test prints itself would continue it. + bool progress = true; + + /// Run only the fixtures whose name contains this. + std::optional name_filter; + + /// Paths, relative to a test directory, not to collect tests from. + std::vector ignored; + + /// Report each state test's execution summary on the trace stream. + /// Blockchain tests have no summary to report. + bool trace_summary = false; + + /// The same as above, but additionally reports the state diff. + /// Blockchain tests have no summary to report. + bool state_diff = false; + + /// Whether the name filter, if any, keeps the fixture called @p name. + [[nodiscard]] bool selects(const std::string& name) const noexcept + { + return !name_filter.has_value() || name.find(*name_filter) != std::string::npos; + } +}; + +/// Runs @p cases, reports to @p out and returns the process exit code. +/// +/// The failures are printed once the run ends, so a run killed part-way reports only how far it +/// got. +[[nodiscard]] int run_tests( + std::span cases, std::ostream& out, const RunOptions& options = {}); + +/// Runs every selected fixture of one fixture file, which together are one test, and returns +/// what each produced. A file which holds no fixture, or does not parse, is the one result. +[[nodiscard]] std::vector run_fixture_file( + const std::filesystem::path& path, const RunOptions& options, evmc::VM& vm); + +} // namespace evmone::test diff --git a/evmone/test/utils/test_report.cpp b/evmone/test/utils/test_report.cpp new file mode 100644 index 0000000..1c24a73 --- /dev/null +++ b/evmone/test/utils/test_report.cpp @@ -0,0 +1,37 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "test_report.hpp" +#include + +namespace evmone::test +{ +std::ostream& operator<<(std::ostream& out, const Failure& failure) +{ + // A failure nests: the test, the place in it, the check, the values. The test name is a line + // of its own because fixture names run to ~300 characters. The columns are fixed, so a check + // with no place to report at leaves the second rung empty rather than shifting. + out << failure.test << ":\n"; + if (!failure.where.empty()) + out << " " << failure.where << ":\n"; + out << " " << failure.what; + + if (failure.detail.empty()) + return out; + + out << ':'; + const std::string_view detail{failure.detail}; + for (size_t pos = 0;;) + { + const auto end = detail.find('\n', pos); + const auto line = detail.substr(pos, end - pos); + out << '\n'; + if (!line.empty()) // A blank line separating blocks stays blank, not six spaces. + out << " " << line; + if (end == std::string_view::npos) + return out; + pos = end + 1; + } +} +} // namespace evmone::test diff --git a/evmone/test/utils/test_report.hpp b/evmone/test/utils/test_report.hpp new file mode 100644 index 0000000..fd3cfa0 --- /dev/null +++ b/evmone/test/utils/test_report.hpp @@ -0,0 +1,177 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace evmone::test +{ +/// A test cannot be run at all. Skipped rather than failed. +struct UnsupportedTestFeature : std::runtime_error +{ + using runtime_error::runtime_error; +}; + +namespace detail +{ +/// Writes @p value the way a report shows it: byte sequences as hex, error codes as their +/// message, everything else the way it streams itself. +template +void stream(std::ostream& out, const T& value) +{ + // A byte buffer must arrive as a bytes_view: both bytes_view and the stream operators + // take their length from a terminating zero, so a raw pointer or array either reports the + // wrong length or reads past the end. Text is the one pointer worth streaming as it is. + using Pointee = std::remove_cv_t>>; + static_assert(!std::is_pointer_v> || std::same_as, + "pass bytes_view{...} rather than a raw pointer or array"); + + if constexpr (std::convertible_to) + out << hex0x(bytes_view{value}); + else if constexpr (std::same_as) + out << hex0x(value); + else if constexpr (std::same_as) + out << value.message(); // Not the ":" it streams as. + else if constexpr (std::integral && sizeof(T) == 1 && !std::same_as) + out << +value; // A byte is a number in a report, not a character. + else + out << value; +} +} // namespace detail + +/// Joins @p parts into a string the way a report shows them. +template +[[nodiscard]] std::string concat(const Ts&... parts) +{ + std::ostringstream out; + (detail::stream(out, parts), ...); + return std::move(out).str(); +} + +/// A check that did not hold. +struct Failure +{ + /// The test case it fired in: the fixture's top-level name. + std::string test; + /// Where in that case, as built by TestReport::at(), e.g. "Prague/0". + std::string where; + /// What was checked, named in the terms the fixture uses. + std::string what; + /// The values that differ and anything further worth saying. + std::string detail; +}; + +/// Records what did not hold in one test case. Each failure goes to the sink as it happens. +/// Holds no global state, so reports are independent of each other. +class TestReport +{ +public: + /// Called with each failure as it is recorded, and the only place a failure goes. + using Sink = std::function; + + explicit TestReport(Sink sink) : m_sink{std::move(sink)} + { + assert(m_sink && "a report without a sink discards every failure"); + } + + /// Restores the place its at() extended. + class [[nodiscard]] Scope + { + TestReport& m_report; + size_t m_restore_length; + + friend class TestReport; + Scope(TestReport& report, size_t restore_length) noexcept + : m_report{report}, m_restore_length{restore_length} + {} + + public: + Scope(Scope&&) = delete; + ~Scope() { m_report.m_where.resize(m_restore_length); } + }; + + /// Names the test case the failures that follow belong to, until the next call. + void start_case(std::string name) { m_test = std::move(name); } + + /// Extends the place within the test case with @p position, until the scope ends. + /// Nested calls read as a path: at(1) inside at("Prague") is "Prague/1". + [[nodiscard]] Scope at(const auto&... position) + { + const auto restore_length = m_where.size(); + // The suffix is built whole before m_where is touched, so a place that renders empty + // adds no separator and a throw part-way through leaves nothing behind. + auto suffix = concat(position...); + if (!m_where.empty() && !suffix.empty()) + suffix.insert(suffix.begin(), '/'); + m_where += suffix; + return Scope{*this, restore_length}; // A prvalue: Scope is not movable. + } + + /// Records that @p what did not hold. + void fail(std::string_view what, std::string detail = {}) + { + m_sink(Failure{m_test, m_where, std::string{what}, std::move(detail)}); + } + + /// Records a failure with @p detail, unless @p ok holds. Returns @p ok. For a check that + /// has nothing to put in two columns, such as a structure that did not decode. + /// + /// @p detail is an ordinary argument, so a caller whose detail costs anything to build + /// needs the explicit `if` and fail() instead. + bool check(bool ok, std::string_view what, std::string detail = {}) + { + if (ok) + return true; + + fail(what, std::move(detail)); + return false; + } + + /// Records a failure showing what was produced against what the fixture expects, unless + /// @p ok holds. Returns @p ok, so a caller can skip what only makes sense once it does not. + /// + /// @p more, if given, is invoked only when the check fails, for detail too expensive to + /// format otherwise. It runs before the failure is recorded, so the failure is complete the + /// first time anything sees it. + bool check(bool ok, std::string_view what, const auto& actual, const auto& expected, + const std::invocable auto&... more) + { + if (ok) + return true; + + auto detail = concat("actual ", actual, "\nexpected ", expected); + (detail.append("\n").append(more()), ...); + fail(what, std::move(detail)); + return false; + } + + /// The same, for the common case of the two having to be equal. + bool check_eq(std::string_view what, const auto& actual, const auto& expected, + const std::invocable auto&... more) + { + return check(actual == expected, what, actual, expected, more...); + } + +private: + Sink m_sink; + std::string m_test; + std::string m_where; +}; + +/// Writes @p failure the way pytest reports one: the test it fired in and where, then what was +/// checked and its detail. The C++ location of the check is deliberately absent — the name of the +/// check identifies it, and the fixture is what the reader is debugging. +std::ostream& operator<<(std::ostream& out, const Failure& failure); +} // namespace evmone::test diff --git a/evmone/test/utils/test_state.cpp b/evmone/test/utils/test_state.cpp index 0d3ac7a..7bae223 100644 --- a/evmone/test/utils/test_state.cpp +++ b/evmone/test/utils/test_state.cpp @@ -7,6 +7,13 @@ namespace evmone::test { +bytes32 TestAccount::get_code_hash() const noexcept +{ + if (code_hash.has_value()) + return *code_hash; + return code.empty() ? state::Account::EMPTY_CODE_HASH : keccak256(code); +} + std::optional TestState::get_account(const address& addr) const noexcept { const auto it = find(addr); @@ -14,8 +21,7 @@ std::optional TestState::get_account(const address& a return std::nullopt; const auto& acc = it->second; - // TODO: Cache code hash for MTP root hash calculation? - return Account{acc.nonce, acc.balance, keccak256(acc.code), !acc.storage.empty()}; + return Account{acc.nonce, acc.balance, acc.get_code_hash(), !acc.storage.empty()}; } bytes TestState::get_account_code(const address& addr) const noexcept @@ -35,7 +41,10 @@ void TestState::apply(const state::StateDiff& diff) a.nonce = m.nonce; a.balance = m.balance; if (m.code.has_value()) + { a.code = *m.code; // TODO: Consider taking rvalue ref to avoid code copy. + a.code_hash = keccak256(a.code); + } for (const auto& [k, v] : m.modified_storage) { if (v) @@ -73,10 +82,10 @@ bytes32 TestBlockHashes::get_block_hash(int64_t block_number) const noexcept [[nodiscard]] std::variant transition(TestState& state, const state::BlockInfo& block, const state::BlockHashes& block_hashes, const state::Transaction& tx, evmc_revision rev, evmc::VM& vm, int64_t block_gas_left, - int64_t blob_gas_left) + int64_t block_state_gas_left, int64_t blob_gas_left) { - const auto tx_props_or_error = - state::validate_transaction(state, block, tx, rev, block_gas_left, blob_gas_left); + const auto tx_props_or_error = state::validate_transaction( + state, block, tx, rev, block_gas_left, block_state_gas_left, blob_gas_left); if (const auto err = get_if(&tx_props_or_error)) return *err; diff --git a/evmone/test/utils/test_state.hpp b/evmone/test/utils/test_state.hpp index 54330c7..59db632 100644 --- a/evmone/test/utils/test_state.hpp +++ b/evmone/test/utils/test_state.hpp @@ -39,7 +39,16 @@ struct TestAccount std::map storage; bytes code; - bool operator==(const TestAccount&) const noexcept = default; + /// The cached hash of the code. If absent, the hash is computed from the code when needed. + std::optional code_hash; + + [[nodiscard]] bytes32 get_code_hash() const noexcept; + + /// Compares the accounts ignoring the cached code hash. + bool operator==(const TestAccount& o) const noexcept + { + return nonce == o.nonce && balance == o.balance && storage == o.storage && code == o.code; + } }; /// Ethereum State representation for tests. @@ -72,7 +81,7 @@ class TestBlockHashes : public state::BlockHashes, public std::unordered_map transition(TestState& state, const state::BlockInfo& block, const state::BlockHashes& block_hashes, const state::Transaction& tx, evmc_revision rev, evmc::VM& vm, int64_t block_gas_left, - int64_t blob_gas_left); + int64_t block_state_gas_left, int64_t blob_gas_left); /// Wrapping of state::finalize() which operates on TestState. void finalize(TestState& state, evmc_revision rev, const address& coinbase, diff --git a/evmone/test/utils/utils.hpp b/evmone/test/utils/utils.hpp index 25aac32..8917a01 100644 --- a/evmone/test/utils/utils.hpp +++ b/evmone/test/utils/utils.hpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace evmone::test @@ -40,6 +41,20 @@ evmc_revision to_rev(std::string_view s); /// Translates tests fork name to the EVM revision schedule. RevisionSchedule to_rev_schedule(std::string_view s); +/// Converts an integer to hex string representation with 0x prefix. +/// +/// This handles also builtin types like uint64_t. Not optimal but works for now. +inline std::string hex0x(const intx::uint256& v) +{ + return "0x" + intx::hex(v); +} + +/// Encodes bytes as hex with 0x prefix. +inline std::string hex0x(bytes_view v) +{ + return "0x" + evmc::hex(v); +} + /// Converts a string to bytes by casting individual characters. inline bytes to_bytes(std::string_view s) { diff --git a/evmone/tools/evmone/main.cpp b/evmone/tools/evmone/main.cpp index 82e4c1f..8278066 100644 --- a/evmone/tools/evmone/main.cpp +++ b/evmone/tools/evmone/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -151,6 +152,48 @@ int exec_t8n_cmd(evmc::VM& vm, const T8nOptions& opts) evmone::tooling::t8n(vm, args); return 0; } + +const CLI::App& setup_test_cmd( + CLI::App& app, std::vector& paths, evmone::test::RunOptions& opts) +{ + auto& cmd = *app.add_subcommand("test", "Run Ethereum tests")->fallthrough(); + cmd.add_option("path", paths, + "Test file or directory. Every fixture file is one test: under a directory, each " + ".json file except index.json.") + ->required() + ->check(CLI::ExistingPath); + cmd.add_option("-k", opts.name_filter, "Run only the fixtures whose name contains this."); + cmd.add_option("--ignore", opts.ignored, + "Path not to collect tests from, relative to each directory given, or to the " + "directory holding a file given. May be given more than once. Whole path components " + "are matched, so --ignore bc4895 keeps bc4895-withdrawals.") + // Without this the option is variadic and swallows the positional paths after it. + ->allow_extra_args(false); + cmd.add_flag( + "--collect-only", opts.collect_only, "List each collected test, one per line, and exit."); + cmd.add_flag("--trace-summary", opts.trace_summary, + "Report each state test's execution summary, as --trace also does, on the trace stream. " + "Blockchain tests have no summary to report."); + cmd.add_flag("--state-diff", opts.state_diff, + "Report each state test's execution summary (same as `--trace-summary`) and transaction's " + "state diff. Blockchain tests have no summary to report."); + return cmd; +} + +int exec_test_cmd(evmc::VM& vm, std::span paths, evmone::test::RunOptions opts, + bool trace, bool histogram) +{ + // main() has switched the tracer on already. Its line per instruction is worth + // unsynchronising the streams for, and anything it writes per test would run into the + // progress row, as a summary would. + if (trace) + std::ios::sync_with_stdio(false); + opts.trace_summary |= trace; + opts.progress = !(opts.trace_summary || opts.state_diff || histogram); + + return evmone::test::test(vm, paths, opts, std::cout); +} + } // namespace int main(int argc, const char* const* argv) noexcept @@ -173,6 +216,7 @@ int main(int argc, const char* const* argv) noexcept VM vm{evmc_create_evmone()}; CLI::App app{"evmone EVM tool"}; + app.require_subcommand(0, 1); // Forbid multiple subcommands: run would hijack the rest. app.set_version_flag( "--version", [&vm] { return std::string{vm.name()} + " " + vm.version(); }); app.add_flag("--trace", trace, "Enable execution trace"); @@ -197,6 +241,10 @@ int main(int argc, const char* const* argv) noexcept T8nOptions t8n_opts; const auto& t8n_cmd = setup_t8n_cmd(app, t8n_opts); + std::vector test_paths; + evmone::test::RunOptions test_opts; + const auto& test_cmd = setup_test_cmd(app, test_paths, test_opts); + try { app.parse(argc, argv); @@ -217,6 +265,9 @@ int main(int argc, const char* const* argv) noexcept if (t8n_cmd) return exec_t8n_cmd(vm, t8n_opts); + if (test_cmd) + return exec_test_cmd(vm, test_paths, test_opts, trace, histogram); + return 0; } catch (const CLI::ParseError& e)