Skip to content

Upgrade evmone and use full transaction execution - #16916

Draft
chfast wants to merge 5 commits into
argotorg:developfrom
chfast:evmone-abi18
Draft

Upgrade evmone and use full transaction execution#16916
chfast wants to merge 5 commits into
argotorg:developfrom
chfast:evmone-abi18

Conversation

@chfast

@chfast chfast commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Upgrades the test suite to evmone 0.23.0 / EVMC ABI 18, starts consuming evmone from the build system instead of vendoring its headers, and adds an opt-in path that runs semantic tests through evmone::state::transition() — evmone's real state library — instead of the hand-written mock host in test/EVMHost.cpp.

The first two parts are meant to land as-is. The third is a prototype behind a flag that is off by default; it is included so the remaining gap to real Mainnet semantics is visible and measurable rather than hypothetical.

1. EVMC 13 → 18 (test:, ci: commits)

evmone 0.23.0 reports EVMC ABI 18, which the previous vendored headers (ABI 13) refuse to load, so this is a required upgrade rather than an optional one. The breaking changes and how they are handled:

EVMC 18 change Handling
VM computes the CREATE/CREATE2 address, passes it in msg.recipient EVMHost no longer derives it; derivation moves to EVMHost::computeCreateAddress for top-level creations
evmc_result::create_address removed Callers read the address from the message they built
evmc_message::create2_salt removed The host no longer needs it
get_nonce() added to the host interface Exposed from MockedHost; the VM reads it live to derive CREATE addresses
VM capability queries removed EVM1 capability checks dropped
EVMC_CONSTANTINOPLE removed constantinople maps to Petersburg (see caveat below)

Because the VM now derives CREATE addresses from get_nonce(sender), created accounts store nonce 1 (EIP-161) and test accounts start at nonce 1. That keeps every derived contract address byte-identical to before.

Verification: the ported suite reproduces the pre-port baseline exactly across nine EVM versions (osaka, osaka --optimize, and semanticTests at constantinople, petersburg, berlin, london, cancun, prague, amsterdam) — same test-case counts, same assertion counts, zero failures, with --enforce-gas-cost enabled at the default version. No test expectation was changed anywhere in this PR.

2. evmone via FetchContent (build: commits)

cmake/EvmoneDependency.cmake fetches evmone 0.23.0 and intx 0.15.0 from hash-pinned release archives, so the version is pinned by the build system rather than by documentation. EVMC headers now come from evmone's own tree; the eight vendored copies under test/evmc/ are deleted.

test/evmc/ keeps exactly two files: loader.c and loader.h. As of 0.23.0 evmone's evmc/lib builds only the evmc, evmc_cpp and mocked_host targets — the EVMC loader is gone upstream — and EVMHost::getVM needs evmc_load_and_configure for the user-facing --vm flag. Those two files are ABI-agnostic (they read only vm->abi_version), so they need no per-version maintenance. --vm and ETH_EVMONE continue to work against an external EVMC VM; with no path given, the linked-in evmone is used.

Two notes for reviewers:

  • cmake_minimum_required rises to 3.25, the version this design needs (3.24 for CMAKE_FIND_PACKAGE_REDIRECTS_DIR/OVERRIDE_FIND_PACKAGE, 3.25 for block(SCOPE_FOR VARIABLES)). scripts/ci/install_and_check_minimum_requirements.sh and docs/installing-solidity.rst are updated in step. This drops Ubuntu 22.04, which ships CMake 3.22.
  • Warnings are disabled for the fetched targets only. Solidity's -Werror propagates into evmone's sources while evmone's own -Wno-attributes=clang::/msvc:: suppressions do not, because they sit inside cable_configure_compiler()'s if(PROJECT_IS_TOP_LEVEL) guard. Without this a default (PEDANTIC=ON) configure fails outright. Solidity's own targets keep -Werror.

3. --use-evmone-state (prototype, off by default)

./build/test/soltest -- --no-smt --use-evmone-state

With the flag set, ExecutionFramework builds an evmone::state::Transaction and calls validate_transaction() + transition(), applying the returned StateDiff back to an in-memory evmone::state::StateView. That replaces the mock host, its simplified state model, and its ~1000 lines of hand-written precompiles with evmone's real ones. The flag is read in exactly one place and selects a backend — nothing else changes.

What works

  • Default path is unaffected: 8195/8195 test cases, 50188/50188 assertions, zero failures, with --enforce-gas-cost.
  • End-to-end deployment, state-changing calls and view calls run through real transaction execution, including real intrinsic gas, EIP-161/2929/6780, and evmone's precompiles.
  • Contract addresses match, so the nonce model agrees between the two paths.

What is expected to fail

Excluding SolidityAuctionRegistrar (see below):

Cases passing Failing Skipped
--use-evmone-state 8159 / 8195 28 8
--use-evmone-state --enforce-gas-cost 8101 / 8195 86 8

(The 8 skipped are version-gated fixtures that do not run at the default EVM version; they are skipped on the default path too.)

The 28 break down as:

  • 13 × GasMeterTests / GasCostTests — these assert that solc's static gas estimate is an upper bound on actual gas. It no longer is, because the estimator does not model EIP-7623's calldata floor.
  • 4 × blobhash (builtinFunctions, inlineAssembly, state, state/uncalled) — the driver does not populate tx.blob_hashes.
  • 3 × EXTCODEHASH/balance on precompile accounts (various/codehash, codehash_assembly, codebalance_assembly) — the mock host pre-seeds precompile accounts with a balance and codehash; evmone does not. Precompile computation is unaffected.
  • 1 × state/tx_originevmone::state::Transaction has no separate origin field; origin comes from tx.sender.
  • 1 × EIP-170 (operators/userDefined/all_possible_user_defined_value_types_with_operators) — deposits ~38 KB of code, over the 0x6000 limit that evmone enforces and the mock host never checked.
  • 1 × validate_transaction rejection (isoltestTesting/account, INSUFFICIENT_ACCOUNT_FUNDS) — gas is really debited from the sender's balance here.
  • 5 × remaining storage-layout and gas-expectation mismatches (array/array_storage_index_access, array_storage_push_pop, storageLayoutSpecifier/dynamic_array_storage_end, mapping_storage_end, variables/transient_state_address_variable_members).

The extra 58 under --enforce-gas-cost are gas-value expectations: 88 fixtures carry a // gas … code: line, and real gas differs for the EIP-7623/7825/170 reasons above.

Every one of these is a case where the state path is applying semantics the mock host never modelled. None was worked around, and no expectation was regenerated.

Known caveats

  • An unfiltered --use-evmone-state run aborts. SolidityAuctionRegistrar/auction_simple reaches directly into m_evmcHost, which is null on the state path, and dies with a memory access violation. This is Solidity test code, not evmone. Until it is adapted, exclude it: -t '!SolidityAuctionRegistrar'.
  • Do not combine --use-evmone-state with pre-Osaka --evm-version yet. The driver's block gas limit (20M) is below the uncapped InitialGas (100M), so GAS_ALLOWANCE_EXCEEDED follows. Correct block-level validation, but the prototype does not yet lower gas to suit.
  • Return data is captured by replaying evmone's pre-call setup, because TransactionReceipt carries none. The replica is cross-checked against the official run's status, gas used and gas refund on every call; a divergence in output alone would not be detected, and the code says so.

Required follow-up before CI can pass

The four buildpack-deps images must be rebuilt and republished, and their digests re-pinned in .circleci/config.yml (lines 17, 21, 25, 29). Those images still carry evmone 0.22.0, so Linux jobs would load an ABI-13 library against ABI-18 headers. The Dockerfiles and their LABEL version values are bumped in this PR, which is what triggers the rebuild; the digest update cannot be done from a source checkout.

Caveat worth flagging explicitly

constantinople now maps to EVMC_PETERSBURG. Petersburg uses the legacy SSTORE schedule where Constantinople used EIP-1283 net metering, so this is an approximation. No fixture asserts gas at that version, so the difference is untested. solc's own code generation is unaffected — it is driven by langutil::EVMVersion, not evmc_revision.

The ossfuzz targets were updated for the new API but could not be compiled in the development environment (no libprotobuf-mutator, no clang++); they were syntax-checked only.

@axic

axic commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

The key interesting piece, beyond easier upgrades, is that precompiles come fully implemented by evmone, and no need to manually add precompile expection cases in EVMHost.

chfast added 4 commits August 19, 2026 14:54
Refresh the vendored EVMC headers from evmone 0.23.0 (ABI 13 -> 18) and adapt
the test host to the new interface:

- The VM now computes the CREATE/CREATE2 address and passes it in msg.recipient,
  so EVMHost no longer derives it and evmc_result::create_address is gone. The
  RLP/keccak derivation moves to EVMHost::computeCreateAddress, which callers
  use for top-level creations where no VM frame exists to compute it.
- The Host exposes get_nonce(), which the VM reads live to derive that address.
  Created accounts therefore store nonce 1 (EIP-161) and test accounts start at
  nonce 1, which keeps every derived address byte-identical to the previous
  behaviour. destination.nonce is set before execution, not after: evmone reads
  get_nonce(sender) during a nested CREATE, so setting it later would give a
  contract creating from inside its own constructor a stale nonce of 0.
- VM capability queries no longer exist in EVMC; drop the EVM1 checks.
- EVMC_CONSTANTINOPLE was removed. Map constantinople to Petersburg, its
  closest surviving revision. Note this is an approximation: Petersburg uses the
  legacy SSTORE schedule where Constantinople used EIP-1283 net metering, and
  no fixture asserts gas at that version, so the difference is untested. solc's
  own code generation is unaffected, being driven by langutil::EVMVersion rather
  than evmc_revision.

One behaviour change, deliberate: m_contractAddress is now set for every
creation attempt, where previously evmc_result::create_address was left as the
zero address on the early failure paths (tx-gas underflow, insufficient
balance, CREATE2 collision) and on code-deposit out-of-gas. The address is
computed before the call and is observable regardless of outcome.

Also fixes an RLP length-prefix bug carried over from the old derivation: a
two-byte nonce needs the 0x82 short-string prefix, not 0xb9. Unreachable in
practice, but the helper is now shared. EVMHostTest covers the encoder against
evmone's own compute_create_address vectors, including nonces above 255.

(cherry picked from commit 4c6783c)
Update every pinned reference to evmone so the tree matches the headers the
test host now compiles against:

- .circleci/osx_install_dependencies.sh, with a new sha256 for the 0.23.0
  darwin-arm64 asset.
- scripts/install_evmone.ps1 and the download links in test/Common.h.
- The four buildpack-deps Dockerfiles. Two download the release tarball (new
  sha256 for the linux-x86_64 asset); two build from the git tag. These are not
  separable from the source bump the way the digest re-pin is: leaving them at
  0.22.0 would have the next image rebuild ship an ABI-13 library against
  ABI-18 headers.
- Their LABEL version values, incremented as scripts/docker/buildpack-deps
  requires. Without this, scripts/ci/docker_upgrade.sh hard-errors on a
  Dockerfile diff carrying no version bump and no image is built at all.

test/evmc/README.md now records that the headers come from evmone 0.23.0, and
that loader.c/loader.h can no longer be refreshed from upstream: as of 0.23.0
evmone's evmc/lib builds only the evmc, evmc_cpp and mocked_host targets, so
the EVMC loader is gone. The copies kept here are ABI-agnostic -- they read
only vm->abi_version -- so they need no per-version maintenance. Also drops a
long-stale instruction to delete tooling.hpp and instructions.h, which have not
shipped in evmc/include/evmc for several versions.

REQUIRED FOLLOW-UP, not doable from a source checkout: the buildpack-deps
images must be rebuilt and republished, and their digests re-pinned in
.circleci/config.yml (lines 17, 21, 25 and 29). Linux CI cannot pass until
then, because those images still carry evmone 0.22.0.

(cherry picked from commit 8b3e8a9)
Fetch evmone 0.23.0 and intx 0.15.0 from hash-pinned release archives and take
EVMC from evmone's own tree, so the version is pinned by the build system
rather than by documentation. cmake/EvmoneDependency.cmake owns this.

evmone resolves intx through Hunter, which cannot run from a CMake subproject:
HunterGate() injects CMAKE_TOOLCHAIN_FILE, which CMake reads only at the first
top-level project() call, and it actively refuses to run once a project is
already named. Disabling Hunter makes hunter_add_package() a no-op instead of
an error, and EVMONE_INTX_DIR -- evmone's own escape hatch -- supplies intx via
add_subdirectory().

test/evmc/ keeps only loader.c and loader.h, because evmone 0.23.0 ships no
EVMC loader and EVMHost::getVM needs evmc_load_and_configure for the
user-facing --vm flag and ETH_EVMONE. Every other header is deleted; exactly
one <test/evmc/...> include survives. With evmone linked in, an empty VM path
now selects the built-in evmone, so --vm and ETH_EVMONE remain available for
running against any other EVMC VM.

Solidity's local tweak to the vendored mocked_host.hpp -- MockedAccount::storage
as std::map, for deterministic EVMHostPrinter output (PR argotorg#11094) -- cannot
survive taking that header from upstream. EVMHostPrinter now sorts storage at
the point of output instead, so upstream's unordered_map is safe to consume, and
EVMHostTest covers the ordering directly.

Building evmone in-tree rather than downloading it needs three accommodations:

- Solidity's PEDANTIC block adds -Wall -Wextra -Werror -pedantic at top level
  and those propagate into evmone's sources, while evmone's own compensating
  options never run: they sit inside cable_configure_compiler(), which returns
  early when PROJECT_IS_TOP_LEVEL is false. Warnings are disabled for the
  fetched targets, and their interface include directories are marked SYSTEM so
  consumers get -isystem. The latter matters because Solidity compiles
  test/evmc/loader.c itself, and that C translation unit includes evmone's
  <evmc/evmc.h>, where EVMC 18 declares `enum evmc_access_status : bool` -- C23
  syntax, which is a hard error under the C17 default most toolchains still use.
  Solidity's own targets keep -Werror.

- BUILD_SHARED_LIBS and HUNTER_ENABLED are generic, third-party-owned names, so
  forcing them into the cache overwrote the user's setting permanently: a
  configure with -DBUILD_SHARED_LIBS=ON came out of the cache as OFF, and any
  later reconfigure that did not re-pass it silently turned every Solidity
  library static. They are shadowed with plain set() inside
  block(SCOPE_FOR VARIABLES), which evmone still reads but which writes nothing
  to the cache. The evmone-specific EVMONE_* names stay forced, having no other
  consumer.

- The CMake features this needs -- CMAKE_FIND_PACKAGE_REDIRECTS_DIR and
  OVERRIDE_FIND_PACKAGE (3.24), block(SCOPE_FOR VARIABLES) (3.25) -- raise the
  floor, but only for building the tests. The requirement therefore lives in
  test/CMakeLists.txt rather than at the top level, so `cmake -DTESTS=0` still
  builds with the project's own 3.13 floor. That matters for the Emscripten
  build, whose image ships CMake 3.16 and which never configures the test
  directory. scripts/ci/install_and_check_minimum_requirements.sh installs 3.25
  accordingly; note its download URL also needed the lowercase -linux-x86_64
  spelling, which CMake switched to in 3.20.

  Because the 3.28 EXCLUDE_FROM_ALL keyword for FetchContent_Declare is above
  that floor, evmone is populated and added with the documented pre-3.28
  equivalent, add_subdirectory(... EXCLUDE_FROM_ALL), keeping evmone's tools out
  of the default build. cmake_policy(SET CMP0169 OLD) is guarded with
  if(POLICY CMP0169), since CMP0169 is a 3.30 addition and setting an unknown
  policy is a hard configure error rather than a no-op.
evmone's state library lives in test/state and is built only under
EVMONE_TOOLS, which also pulls in test/utils and tools/. Because Hunter is
disabled here, hunter_add_package() is a no-op and each following
find_package(... CONFIG REQUIRED) must be satisfied some other way. There are
two, and they need opposite treatments:

- nlohmann_json is already provided by Solidity itself, via
  add_subdirectory(deps/nlohmann-json). Declaring it through FetchContent with
  OVERRIDE_FIND_PACKAGE would add that same source tree a second time, which is
  a duplicate-target error. An empty config in CMAKE_FIND_PACKAGE_REDIRECTS_DIR
  satisfies find_package() without creating anything, since the target exists.
- CLI11, required by tools/evmone, has no counterpart in Solidity, so
  OVERRIDE_FIND_PACKAGE is exactly right. Pinned to v2.5.0, matching evmone's
  own Hunter config.

Nothing under tools/ joins the default build; it is nested inside evmone's
EXCLUDE_FROM_ALL subtree.

(cherry picked from commit 2131dc3)
Add a prototype execution path that runs semantic tests through evmone's state
library instead of Solidity's hand-written mock host, so tests can be exercised
against real Mainnet semantics: genuine intrinsic gas, EIP-161/2929/6780, and
evmone's own precompiles rather than the mock's hard-coded answers.

EVMState provides an in-memory evmone::state::StateView and BlockHashes over an
ordered account map; EVMTransactionDriver builds an evmone::state::Transaction,
calls validate_transaction() and transition(), and applies the returned
StateDiff back. Selected by --use-evmone-state, which defaults off: the flag is
read in exactly one place, choosing a backend and nothing else, so the EVMHost
path is untouched. isoltest compiles ExecutionFramework.cpp too, so it links
evmone::state and builds the driver's sources alongside soltest's.

Details worth knowing:

- Transaction::sender is a plain field, so no signing is needed, and ecrecover
  works without libsecp256k1 via evmone's built-in implementation.
- EIP-7825 caps transaction gas at 0x1000000, far below this framework's
  InitialGas. The cap is applied only from Osaka onward, matching evmone's own
  revision check, so pre-Osaka runs are not silently over-constrained.
- TransactionReceipt carries no return data, so output is captured by replaying
  evmone's pre-call setup. That replica is cross-checked against the official
  run's status, gas used and gas refund on every call; a divergence in output
  alone cannot be detected, and the comment says so rather than implying
  otherwise.
- applyDiff erases storage slots whose new value is zero, matching evmone's own
  reference applier. Retaining them would leave an all-zero account reporting
  has_storage, which feeds Account::has_initial_storage and makes
  is_create_collision() reject a later CREATE2 to that address.
- Code-deposit gas is reconstructed from the StateDiff by summing the size of
  each changed code entry. This deliberately yields zero for a failed creation,
  where the mock host instead charges for result.output_size -- revert-reason
  bytes, not deployed code -- and accumulates it into a counter it never rolls
  back. evmone's behaviour is the correct one here.

This is a prototype, not a replacement: with the flag on, a substantial number
of fixtures still differ, dominated by EIP-7623's calldata floor, EIP-170's
deposited-code limit and the EIP-7825 cap -- all real semantics the mock host
never modelled. Deleting EVMHost is follow-up work this unblocks, not part of
this change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants