From 74b58f9f16db787b84a992c5a3ff4d6c66d0dcec Mon Sep 17 00:00:00 2001 From: abmcar Date: Thu, 11 Jun 2026 15:47:27 +0800 Subject: [PATCH 1/3] test: consolidate multipass differential suites into one target Move the interpreter-vs-multipass differential tests into a dedicated gtest target, evmDifferentialTests, in its own self-contained source file. It holds 40 fixture-based cases (RangeNarrowing, ConstShiftPruning, SubWrapU64) plus the 4-test adversarial-matrix harness, all sharing one executor that forces synchronous multipass compilation so the JIT run is non-vacuous. Three in-flight optimization PRs each appended a near-identical differential suite to src/tests/evm_interp_tests.cpp, guaranteeing merge conflicts; a fourth added the matrix harness there too. A single dedicated home removes both the duplication and the conflicts. The differential invariant holds on plain main, so the suite passes independently of those optimizations. Co-Authored-By: Claude Fable 5 --- .../README.md | 73 +++ src/tests/CMakeLists.txt | 18 + src/tests/evm_differential_tests.cpp | 451 ++++++++++++++++++ tests/evm_asm/iszero_calldatasize.easm | 9 + tests/evm_asm/iszero_calldatasize.expected | 8 + tests/evm_asm/iszero_dyn_highsparse.easm | 13 + tests/evm_asm/iszero_dyn_highsparse.expected | 8 + tests/evm_asm/iszero_dyn_u64_nonzero.easm | 15 + tests/evm_asm/iszero_dyn_u64_nonzero.expected | 8 + ...umpi_iszero_fused_nottaken_highsparse.easm | 30 ++ ..._iszero_fused_nottaken_highsparse.expected | 8 + tests/evm_asm/jumpi_iszero_fused_taken.easm | 44 ++ .../evm_asm/jumpi_iszero_fused_taken.expected | 8 + tests/evm_asm/jumpi_iszero_iszero_fused.easm | 42 ++ .../jumpi_iszero_iszero_fused.expected | 8 + tests/evm_asm/jumpi_u64_cond_nottaken.easm | 29 ++ .../evm_asm/jumpi_u64_cond_nottaken.expected | 8 + tests/evm_asm/jumpi_u64_cond_taken.easm | 40 ++ tests/evm_asm/jumpi_u64_cond_taken.expected | 8 + tests/evm_asm/or_dyn_u64_u64.easm | 25 + tests/evm_asm/or_dyn_u64_u64.expected | 8 + tests/evm_asm/or_dyn_u64_wide.easm | 25 + tests/evm_asm/or_dyn_u64_wide.expected | 8 + tests/evm_asm/sar_const64_pos.easm | 13 + tests/evm_asm/sar_const64_pos.expected | 8 + tests/evm_asm/sar_const8_neg.easm | 14 + tests/evm_asm/sar_const8_neg.expected | 8 + tests/evm_asm/sgt_const_vs_dyn.easm | 15 + tests/evm_asm/sgt_const_vs_dyn.expected | 8 + .../evm_asm/sgt_dyn_highsparse_vs_const.easm | 15 + .../sgt_dyn_highsparse_vs_const.expected | 8 + tests/evm_asm/sgt_dyn_msb64_vs_const.easm | 15 + tests/evm_asm/sgt_dyn_msb64_vs_const.expected | 8 + tests/evm_asm/sgt_dyn_neg_vs_const.easm | 14 + tests/evm_asm/sgt_dyn_neg_vs_const.expected | 8 + tests/evm_asm/shl_const136_u64val.easm | 19 + tests/evm_asm/shl_const136_u64val.expected | 8 + tests/evm_asm/shl_const200_u64val.easm | 17 + tests/evm_asm/shl_const200_u64val.expected | 8 + tests/evm_asm/shl_const256_dyn.easm | 13 + tests/evm_asm/shl_const256_dyn.expected | 8 + tests/evm_asm/shl_const4_dyn.easm | 14 + tests/evm_asm/shl_const4_dyn.expected | 8 + tests/evm_asm/shl_const96_dyn.easm | 14 + tests/evm_asm/shl_const96_dyn.expected | 8 + tests/evm_asm/shl_const_highlimb_dyn.easm | 15 + tests/evm_asm/shl_const_highlimb_dyn.expected | 8 + tests/evm_asm/shl_dyn_amount.easm | 18 + tests/evm_asm/shl_dyn_amount.expected | 8 + tests/evm_asm/shr_const256_dyn.easm | 13 + tests/evm_asm/shr_const256_dyn.expected | 8 + tests/evm_asm/shr_const4_dyn.easm | 13 + tests/evm_asm/shr_const4_dyn.expected | 8 + tests/evm_asm/shr_const72_dyn.easm | 13 + tests/evm_asm/shr_const72_dyn.expected | 8 + tests/evm_asm/shr_const8_u64val.easm | 17 + tests/evm_asm/shr_const8_u64val.expected | 8 + tests/evm_asm/slt_const_vs_dyn.easm | 15 + tests/evm_asm/slt_const_vs_dyn.expected | 8 + tests/evm_asm/slt_dyn_eq_const.easm | 14 + tests/evm_asm/slt_dyn_eq_const.expected | 8 + .../evm_asm/slt_dyn_highsparse_vs_const.easm | 15 + .../slt_dyn_highsparse_vs_const.expected | 8 + tests/evm_asm/slt_dyn_msb64_vs_const.easm | 16 + tests/evm_asm/slt_dyn_msb64_vs_const.expected | 8 + tests/evm_asm/slt_dyn_neg_vs_const.easm | 15 + tests/evm_asm/slt_dyn_neg_vs_const.expected | 8 + tests/evm_asm/sub_u64_pair_equal.easm | 23 + tests/evm_asm/sub_u64_pair_equal.expected | 8 + tests/evm_asm/sub_u64_pair_nounderflow.easm | 25 + .../evm_asm/sub_u64_pair_nounderflow.expected | 8 + tests/evm_asm/sub_u64_pair_underflow.easm | 24 + tests/evm_asm/sub_u64_pair_underflow.expected | 8 + tests/evm_asm/sub_u64_pair_wrap_boundary.easm | 25 + .../sub_u64_pair_wrap_boundary.expected | 8 + tests/evm_asm/sub_u64_pair_zero_rhs_dyn.easm | 24 + .../sub_u64_pair_zero_rhs_dyn.expected | 8 + tests/evm_asm/sub_wide_u64_control.easm | 23 + tests/evm_asm/sub_wide_u64_control.expected | 8 + tests/evm_asm/xor_dyn_u64_u64.easm | 25 + tests/evm_asm/xor_dyn_u64_u64.expected | 8 + tests/evm_asm/xor_dyn_u64_wide.easm | 24 + tests/evm_asm/xor_dyn_u64_wide.expected | 8 + 83 files changed, 1654 insertions(+) create mode 100644 docs/changes/2026-06-11-evm-differential-suite/README.md create mode 100644 src/tests/evm_differential_tests.cpp create mode 100644 tests/evm_asm/iszero_calldatasize.easm create mode 100644 tests/evm_asm/iszero_calldatasize.expected create mode 100644 tests/evm_asm/iszero_dyn_highsparse.easm create mode 100644 tests/evm_asm/iszero_dyn_highsparse.expected create mode 100644 tests/evm_asm/iszero_dyn_u64_nonzero.easm create mode 100644 tests/evm_asm/iszero_dyn_u64_nonzero.expected create mode 100644 tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.easm create mode 100644 tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.expected create mode 100644 tests/evm_asm/jumpi_iszero_fused_taken.easm create mode 100644 tests/evm_asm/jumpi_iszero_fused_taken.expected create mode 100644 tests/evm_asm/jumpi_iszero_iszero_fused.easm create mode 100644 tests/evm_asm/jumpi_iszero_iszero_fused.expected create mode 100644 tests/evm_asm/jumpi_u64_cond_nottaken.easm create mode 100644 tests/evm_asm/jumpi_u64_cond_nottaken.expected create mode 100644 tests/evm_asm/jumpi_u64_cond_taken.easm create mode 100644 tests/evm_asm/jumpi_u64_cond_taken.expected create mode 100644 tests/evm_asm/or_dyn_u64_u64.easm create mode 100644 tests/evm_asm/or_dyn_u64_u64.expected create mode 100644 tests/evm_asm/or_dyn_u64_wide.easm create mode 100644 tests/evm_asm/or_dyn_u64_wide.expected create mode 100644 tests/evm_asm/sar_const64_pos.easm create mode 100644 tests/evm_asm/sar_const64_pos.expected create mode 100644 tests/evm_asm/sar_const8_neg.easm create mode 100644 tests/evm_asm/sar_const8_neg.expected create mode 100644 tests/evm_asm/sgt_const_vs_dyn.easm create mode 100644 tests/evm_asm/sgt_const_vs_dyn.expected create mode 100644 tests/evm_asm/sgt_dyn_highsparse_vs_const.easm create mode 100644 tests/evm_asm/sgt_dyn_highsparse_vs_const.expected create mode 100644 tests/evm_asm/sgt_dyn_msb64_vs_const.easm create mode 100644 tests/evm_asm/sgt_dyn_msb64_vs_const.expected create mode 100644 tests/evm_asm/sgt_dyn_neg_vs_const.easm create mode 100644 tests/evm_asm/sgt_dyn_neg_vs_const.expected create mode 100644 tests/evm_asm/shl_const136_u64val.easm create mode 100644 tests/evm_asm/shl_const136_u64val.expected create mode 100644 tests/evm_asm/shl_const200_u64val.easm create mode 100644 tests/evm_asm/shl_const200_u64val.expected create mode 100644 tests/evm_asm/shl_const256_dyn.easm create mode 100644 tests/evm_asm/shl_const256_dyn.expected create mode 100644 tests/evm_asm/shl_const4_dyn.easm create mode 100644 tests/evm_asm/shl_const4_dyn.expected create mode 100644 tests/evm_asm/shl_const96_dyn.easm create mode 100644 tests/evm_asm/shl_const96_dyn.expected create mode 100644 tests/evm_asm/shl_const_highlimb_dyn.easm create mode 100644 tests/evm_asm/shl_const_highlimb_dyn.expected create mode 100644 tests/evm_asm/shl_dyn_amount.easm create mode 100644 tests/evm_asm/shl_dyn_amount.expected create mode 100644 tests/evm_asm/shr_const256_dyn.easm create mode 100644 tests/evm_asm/shr_const256_dyn.expected create mode 100644 tests/evm_asm/shr_const4_dyn.easm create mode 100644 tests/evm_asm/shr_const4_dyn.expected create mode 100644 tests/evm_asm/shr_const72_dyn.easm create mode 100644 tests/evm_asm/shr_const72_dyn.expected create mode 100644 tests/evm_asm/shr_const8_u64val.easm create mode 100644 tests/evm_asm/shr_const8_u64val.expected create mode 100644 tests/evm_asm/slt_const_vs_dyn.easm create mode 100644 tests/evm_asm/slt_const_vs_dyn.expected create mode 100644 tests/evm_asm/slt_dyn_eq_const.easm create mode 100644 tests/evm_asm/slt_dyn_eq_const.expected create mode 100644 tests/evm_asm/slt_dyn_highsparse_vs_const.easm create mode 100644 tests/evm_asm/slt_dyn_highsparse_vs_const.expected create mode 100644 tests/evm_asm/slt_dyn_msb64_vs_const.easm create mode 100644 tests/evm_asm/slt_dyn_msb64_vs_const.expected create mode 100644 tests/evm_asm/slt_dyn_neg_vs_const.easm create mode 100644 tests/evm_asm/slt_dyn_neg_vs_const.expected create mode 100644 tests/evm_asm/sub_u64_pair_equal.easm create mode 100644 tests/evm_asm/sub_u64_pair_equal.expected create mode 100644 tests/evm_asm/sub_u64_pair_nounderflow.easm create mode 100644 tests/evm_asm/sub_u64_pair_nounderflow.expected create mode 100644 tests/evm_asm/sub_u64_pair_underflow.easm create mode 100644 tests/evm_asm/sub_u64_pair_underflow.expected create mode 100644 tests/evm_asm/sub_u64_pair_wrap_boundary.easm create mode 100644 tests/evm_asm/sub_u64_pair_wrap_boundary.expected create mode 100644 tests/evm_asm/sub_u64_pair_zero_rhs_dyn.easm create mode 100644 tests/evm_asm/sub_u64_pair_zero_rhs_dyn.expected create mode 100644 tests/evm_asm/sub_wide_u64_control.easm create mode 100644 tests/evm_asm/sub_wide_u64_control.expected create mode 100644 tests/evm_asm/xor_dyn_u64_u64.easm create mode 100644 tests/evm_asm/xor_dyn_u64_u64.expected create mode 100644 tests/evm_asm/xor_dyn_u64_wide.easm create mode 100644 tests/evm_asm/xor_dyn_u64_wide.expected diff --git a/docs/changes/2026-06-11-evm-differential-suite/README.md b/docs/changes/2026-06-11-evm-differential-suite/README.md new file mode 100644 index 000000000..cd6d71b2e --- /dev/null +++ b/docs/changes/2026-06-11-evm-differential-suite/README.md @@ -0,0 +1,73 @@ +# Change: Consolidate EVM interp-vs-multipass differential suites into one target + +- **Status**: Implemented +- **Date**: 2026-06-11 +- **Tier**: Light + +## Overview + +Add a dedicated gtest target, `evmDifferentialTests` +(`src/tests/evm_differential_tests.cpp`), that consolidates the +interpreter-vs-multipass differential tests into a single self-contained file. + +The suite has two parts: + +- **Fixture-based differential cases (40)** — one parameterized fixture loads a + hex bytecode from `tests/evm_asm/`, runs it through both the interpreter and + the multipass JIT, and asserts identical status and output. The cases are + grouped into `RangeNarrowing` (21), `ConstShiftPruning` (13), and + `SubWrapU64` (6), each feeding a dynamic operand that drives a value-range + lowering path. +- **Adversarial-matrix harness (4)** — `EVMRangeDifferential` builds bytecode + programmatically and sweeps an 11-value operand matrix (value-range + boundaries plus high-sparse words) across 20 binary opcodes, the three shifts + on limb-boundary amounts, and the AND-u64 narrow fast path, asserting the JIT + agrees with the interpreter on every pair. + +A single executor underpins both parts. It forces synchronous multipass +compilation (`DisableMultipassMultithread`) so the JIT side actually executes +compiled code, and asserts `JITCompiled` under `ZEN_ENABLE_JIT` so a multipass +run that fell back to the interpreter cannot pass vacuously. + +## Motivation + +Differential JIT tests do not belong in the interpreter test file. Three +in-flight optimization PRs each appended a near-identical copy-pasted +differential suite plus its fixtures to `src/tests/evm_interp_tests.cpp`, and a +fourth added the programmatic matrix harness to the same file. Every one of +these would conflict with the others on merge. + +A single dedicated home removes both problems: the copy-pasted suites collapse +into one parameterized fixture with one executor, and the shared file no longer +forces a merge conflict between the optimization PRs. + +The differential property — interpreter output equals multipass output, and +multipass actually JIT-compiles — holds on plain `main`, independent of the +optimization PRs the fixtures were written alongside. The suite therefore +guards an invariant that is true before and after those optimizations merge. + +## Impact + +- **New file**: `src/tests/evm_differential_tests.cpp` (test-only). +- **New fixtures**: 40 `.easm`/`.expected` pairs under `tests/evm_asm/`. +- **Build**: `src/tests/CMakeLists.txt` registers the `evmDifferentialTests` + target inside the existing `ZEN_ENABLE_MULTIPASS_JIT` blocks. +- **Runtime / codegen**: none. `libdtvmapi` is unaffected. +- `src/tests/evm_interp_tests.cpp` is not modified. + +## Verification + +- `evmDifferentialTests`: **44 / 44 pass** (40 fixture + 4 matrix) on + upstream/main with no optimization PR applied. +- `ctest` (`SPEC_TESTS_ARGS="-m multipass --format evm --enable-evm-gas"`): all + tests pass. `evmInterpTests` now runs the 40 new fixtures through its golden + sample test as well. +- `tools/format.sh check`: pass. +- No new compiler warnings for the new file. + +## Checklist + +- [x] Implementation complete +- [x] Tests added/updated +- [ ] Module specs in `docs/modules/` updated (if affected) +- [x] Build and tests pass diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 0e14b39ee..63c8a8870 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -68,6 +68,7 @@ if(ZEN_ENABLE_SPEC_TEST) if(ZEN_ENABLE_MULTIPASS_JIT) add_executable(evmJitFrontendTests evm_jit_frontend_tests.cpp) add_executable(evmRangeAnalyzerTests evm_range_analyzer_tests.cpp) + add_executable(evmDifferentialTests evm_differential_tests.cpp) endif() add_executable( solidityContractTests evm_precompiles.hpp solidity_contract_tests.cpp @@ -110,6 +111,7 @@ if(ZEN_ENABLE_SPEC_TEST) if(ZEN_ENABLE_MULTIPASS_JIT) target_compile_options(evmJitFrontendTests PRIVATE -fsanitize=address) target_compile_options(evmRangeAnalyzerTests PRIVATE -fsanitize=address) + target_compile_options(evmDifferentialTests PRIVATE -fsanitize=address) endif() target_compile_options(solidityContractTests PRIVATE -fsanitize=address) target_compile_options(mptCompareCpp PRIVATE -fsanitize=address) @@ -149,6 +151,11 @@ if(ZEN_ENABLE_SPEC_TEST) PRIVATE compiler dtvmcore gtest_main -fsanitize=address PUBLIC ${GTEST_BOTH_LIBRARIES} ) + target_link_libraries( + evmDifferentialTests + PRIVATE dtvmcore gtest_main -fsanitize=address + PUBLIC ${GTEST_BOTH_LIBRARIES} + ) endif() target_link_libraries( solidityContractTests @@ -222,6 +229,11 @@ if(ZEN_ENABLE_SPEC_TEST) -static-libasan PUBLIC ${GTEST_BOTH_LIBRARIES} ) + target_link_libraries( + evmDifferentialTests + PRIVATE dtvmcore gtest_main -fsanitize=address -static-libasan + PUBLIC ${GTEST_BOTH_LIBRARIES} + ) endif() target_link_libraries( solidityContractTests @@ -301,6 +313,11 @@ if(ZEN_ENABLE_SPEC_TEST) PRIVATE compiler dtvmcore gtest_main PUBLIC ${GTEST_BOTH_LIBRARIES} ) + target_link_libraries( + evmDifferentialTests + PRIVATE dtvmcore gtest_main + PUBLIC ${GTEST_BOTH_LIBRARIES} + ) endif() target_link_libraries( solidityContractTests @@ -351,6 +368,7 @@ if(ZEN_ENABLE_SPEC_TEST) if(ZEN_ENABLE_MULTIPASS_JIT) add_test(NAME evmJitFrontendTests COMMAND evmJitFrontendTests) add_test(NAME evmRangeAnalyzerTests COMMAND evmRangeAnalyzerTests) + add_test(NAME evmDifferentialTests COMMAND evmDifferentialTests) endif() add_test( NAME solidityContractTests diff --git a/src/tests/evm_differential_tests.cpp b/src/tests/evm_differential_tests.cpp new file mode 100644 index 000000000..9efd7cf0f --- /dev/null +++ b/src/tests/evm_differential_tests.cpp @@ -0,0 +1,451 @@ +// Copyright (C) 2025 the DTVM authors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include "evm/evm.h" +#include "evm_test_host.hpp" +#include "runtime/evm_module.h" +#include "utils/evm.h" +#include "zetaengine.h" + +using namespace zen; +using namespace zen::evm; +using namespace zen::runtime; + +#ifdef ZEN_ENABLE_MULTIPASS_JIT + +namespace { + +std::filesystem::path getEvmAsmDirPath() { + return std::filesystem::path(__FILE__).parent_path() / + std::filesystem::path("../../tests/evm_asm"); +} + +struct EVMExecutionResult { + evmc_status_code Status = EVMC_INTERNAL_ERROR; + std::string OutputHex; + bool JITCompiled = false; +}; + +// Run `Bytecode` in `Mode`, forcing synchronous multipass compilation so the +// multipass run actually executes JIT code. The default async/multithread +// config can fall back to the interpreter for a single call, which would make +// a differential test vacuous; DisableMultipassMultithread compiles before the +// run so the JIT side is non-vacuous. +EVMExecutionResult runEvmBytecode(const std::string &Label, + const std::vector &Bytecode, + common::RunMode Mode, + const std::vector &CallData = {}) { + EVMExecutionResult Empty; + + RuntimeConfig Config; + Config.Mode = Mode; + Config.DisableMultipassMultithread = true; // compile before run -> JIT used + + auto MockedHost = std::make_unique(); + MockedHost->tx_context.tx_origin = zen::evm::DEFAULT_DEPLOYER_ADDRESS; + auto RT = Runtime::newEVMRuntime(Config, MockedHost.get()); + if (!RT) { + ADD_FAILURE() << "runtime create failed: " << Label; + return Empty; + } + MockedHost->setRuntime(RT.get()); + + auto ModRet = RT->loadEVMModule(Label, Bytecode.data(), Bytecode.size()); + if (!ModRet) { + ADD_FAILURE() << "module load failed: " << Label; + return Empty; + } + EVMModule *Mod = *ModRet; + + Isolation *Iso = RT->createManagedIsolation(); + if (!Iso) { + ADD_FAILURE() << "isolation create failed: " << Label; + return Empty; + } + + const uint64_t GasLimit = 0xFFFF'FFFF'FFFF - zen::evm::BASIC_EXECUTION_COST; + auto InstRet = Iso->createEVMInstance(*Mod, GasLimit); + if (!InstRet) { + ADD_FAILURE() << "instance create failed: " << Label; + return Empty; + } + EVMInstance *Inst = *InstRet; + Inst->setRevision(evmc_revision::EVMC_OSAKA); + + evmc_message Msg = { + .kind = EVMC_CALL, + .flags = 0u, + .depth = 0, + .gas = static_cast(GasLimit), + .recipient = {}, + .sender = zen::evm::DEFAULT_DEPLOYER_ADDRESS, + .input_data = CallData.empty() ? nullptr : CallData.data(), + .input_size = CallData.size(), + .value = {}, + .create2_salt = {}, + .code_address = {}, + .code = reinterpret_cast(Mod->Code), + .code_size = Mod->CodeSize, + }; + + evmc::Result RawResult; + EVMExecutionResult Exec; +#ifdef ZEN_ENABLE_JIT + Exec.JITCompiled = Mod->getJITCode() != nullptr && Mod->getJITCodeSize() > 0; +#endif + EXPECT_NO_THROW({ RT->callEVMMain(*Inst, Msg, RawResult); }); + Exec.Status = RawResult.status_code; + Exec.OutputHex = + zen::utils::toHex(RawResult.output_data, RawResult.output_size); + return Exec; +} + +EVMExecutionResult +runEvmBytecodeFile(const std::string &FilePath, common::RunMode Mode, + const std::vector &CallData = {}) { + EVMExecutionResult Empty; + + std::ifstream Fin(FilePath); + if (!Fin.is_open()) { + ADD_FAILURE() << "Failed to open test file: " << FilePath; + return Empty; + } + + std::string Hex; + Fin >> Hex; + zen::utils::trimString(Hex); + auto BytecodeBuf = zen::utils::fromHex(Hex); + if (!BytecodeBuf) { + ADD_FAILURE() << "Failed to convert hex to bytecode: " << FilePath; + return Empty; + } + + return runEvmBytecode(FilePath, *BytecodeBuf, Mode, CallData); +} + +// Run `Bytecode` through interpreter and multipass, assert the multipass JIT +// compiled (so the comparison is non-vacuous) and that status + output agree. +// Returns false on divergence so matrix callers can stop flooding output. +bool expectInterpMatchesMultipass(const std::string &Label, + const std::vector &Bytecode, + const std::vector &CallData) { + auto Interp = runEvmBytecode(Label + "_interp", Bytecode, + common::RunMode::InterpMode, CallData); + auto Multi = runEvmBytecode(Label + "_multipass", Bytecode, + common::RunMode::MultipassMode, CallData); +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(Multi.JITCompiled) << "multipass did not JIT-compile: " << Label; +#endif + EXPECT_EQ(Multi.Status, Interp.Status) << "status diverged: " << Label; + EXPECT_EQ(Multi.OutputHex, Interp.OutputHex) << "output diverged: " << Label; + return Multi.Status == Interp.Status && Multi.OutputHex == Interp.OutputHex; +} + +// Fixture-file variant of the assertion above: load a hex fixture, run both +// engines, and require the multipass JIT output to match the interpreter (the +// full-width ground truth) for the named stem. +void expectFixtureInterpMatchesMultipass(const std::string &Stem) { + const auto FilePath = (getEvmAsmDirPath() / (Stem + ".evm.hex")).string(); + + auto Interp = runEvmBytecodeFile(FilePath, common::RunMode::InterpMode); + auto Multi = runEvmBytecodeFile(FilePath, common::RunMode::MultipassMode); + +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(Multi.JITCompiled) << "Multipass JIT should compile " << Stem; +#endif + + EXPECT_EQ(Interp.Status, EVMC_SUCCESS) + << "Interpreter did not succeed for " << Stem; + EXPECT_EQ(Multi.Status, Interp.Status) + << "Multipass status diverged from interpreter for " << Stem; + EXPECT_EQ(Multi.OutputHex, Interp.OutputHex) + << "Multipass output diverged from interpreter for " << Stem; +} + +std::string fixtureTestName(const testing::TestParamInfo &Info) { + return Info.param; +} + +} // namespace + +// =========================================================================== +// Fixture-based differential suite. +// +// Each fixture feeds a dynamic (analyzer-unprovable) operand through an opcode +// whose multipass lowering has a value-range fast path, then asserts the JIT +// output matches the interpreter (the full-width reference) exactly. A +// divergence means a narrowed fold produced a wrong value, not merely a slower +// one. The fixtures hold on the plain lowering as well as the narrowed one, so +// they guard the invariant independently of whether the fast paths are present. +// =========================================================================== + +class EVMFixtureDifferentialTest + : public ::testing::TestWithParam {}; + +TEST_P(EVMFixtureDifferentialTest, InterpMatchesMultipass) { + expectFixtureInterpMatchesMultipass(GetParam()); +} + +// Range-narrowed lowering paths: the range-narrowed ISZERO/JUMPI folds, +// U64-tagged OR/XOR, and the signed-compare (SLT/SGT) u64-const fast paths in +// the multipass lowering. Each fixture feeds a dynamic operand so the narrowed +// path is actually taken. +INSTANTIATE_TEST_SUITE_P( + RangeNarrowing, EVMFixtureDifferentialTest, + ::testing::Values("iszero_dyn_u64_nonzero", "iszero_dyn_highsparse", + "iszero_calldatasize", "jumpi_iszero_fused_taken", + "jumpi_iszero_fused_nottaken_highsparse", + "jumpi_iszero_iszero_fused", "jumpi_u64_cond_taken", + "jumpi_u64_cond_nottaken", "or_dyn_u64_u64", + "xor_dyn_u64_u64", "or_dyn_u64_wide", "xor_dyn_u64_wide", + "slt_dyn_neg_vs_const", "slt_dyn_highsparse_vs_const", + "slt_dyn_msb64_vs_const", "slt_dyn_eq_const", + "sgt_dyn_neg_vs_const", "sgt_dyn_highsparse_vs_const", + "sgt_dyn_msb64_vs_const", "slt_const_vs_dyn", + "sgt_const_vs_dyn"), + fixtureTestName); + +// Const-shift guard pruning and range-aware source-limb pruning in handleShift. +// Each fixture must yield identical output in the interpreter and the multipass +// JIT, and the multipass module must JIT. +INSTANTIATE_TEST_SUITE_P( + ConstShiftPruning, EVMFixtureDifferentialTest, + ::testing::Values("shl_const4_dyn", "shl_const96_dyn", + "shl_const136_u64val", "shl_const200_u64val", + "shl_const256_dyn", "shl_const_highlimb_dyn", + "shr_const4_dyn", "shr_const72_dyn", "shr_const8_u64val", + "shr_const256_dyn", "sar_const8_neg", "sar_const64_pos", + "shl_dyn_amount"), + fixtureTestName); + +// Range-based u64 SUB fast path. Each stem exercises (a - b) mod 2^256 with +// both operands range-proven u64 (except the wide control), including the +// adversarial underflow cases where the upper 192 bits sign-fill to all-ones. +// Interpreter and multipass JIT must agree. +INSTANTIATE_TEST_SUITE_P( + SubWrapU64, EVMFixtureDifferentialTest, + ::testing::Values("sub_u64_pair_nounderflow", "sub_u64_pair_underflow", + "sub_u64_pair_equal", "sub_u64_pair_wrap_boundary", + "sub_u64_pair_zero_rhs_dyn", "sub_wide_u64_control"), + fixtureTestName); + +// =========================================================================== +// Value-range lowering differential matrix harness. +// +// A multipass value-range fast path that emits a single- or double-limb result +// MUST be bit-identical to the full 4-limb path for every input the range tag +// claims to cover. A too-narrow tag silently miscompiles only on operands with +// non-zero high limbs, which ordinary corpora rarely carry. Each test drives an +// opcode with an adversarial operand matrix (value-range boundaries + +// high-sparse {0,x,0,0}/{0,0,0,x}) and asserts the multipass JIT agrees +// bit-for-bit with the interpreter (the full-width reference). +// +// Coverage split (the operands decide which lowering path fires): +// - BinaryOpsMatchInterpreterOnAdversarialOperands feeds two CALLDATALOAD +// operands, which are dynamic and U256-range, so they do NOT enter the +// bothFitU64 / AND-narrow fast paths: this test gates the FULL-width +// 4-limb lowering (the #487-class high-limb-corruption surface). It sweeps +// 20 binary opcodes (arithmetic, comparison, bitwise, BYTE, SIGNEXTEND, +// and the three shifts). EXP is excluded because its gas cost scales with +// the exponent byte length, so high-limb operands would trip out-of-gas +// instead of exposing a value-range divergence. +// - ShiftAmountsMatchInterpreterOnLimbBoundaries sweeps SHL/SHR/SAR with +// shift amounts in the limb-crossing region 2..255. The 11-value operand +// matrix, used as a shift amount, only realizes {0, 1, >=2^64}, so the +// dynamic-shift cross-limb carry/offset logic is unreachable from the +// binary-op sweep above; this test covers it. +// - The AndU64Mask* tests construct a provably-U64 operand (AND with a u64 +// constant) and feed it directly / through a bothFitU64 ADD, so they gate +// the actual narrowing fast paths. +// =========================================================================== + +namespace { + +// Build a big-endian 32-byte word holding `Val` in 64-bit limb `Limb` +// (limb 0 = least significant). OR several together for multi-limb operands. +std::vector matrixLimb(int Limb, uint64_t Val) { + std::vector W(32, 0); + const int Base = 24 - Limb * 8; // limb 0 -> bytes [24..31] + for (int I = 0; I < 8; ++I) { + W[Base + 7 - I] = static_cast((Val >> (8 * I)) & 0xff); + } + return W; +} + +std::vector matrixOr(std::vector A, + const std::vector &B) { + for (size_t I = 0; I < A.size(); ++I) { + A[I] |= B[I]; + } + return A; +} + +// Adversarial operand matrix: each entry is a 32-byte big-endian u256. +std::vector> matrixOperands() { + const uint64_t Max = 0xFFFFFFFFFFFFFFFFull; + std::vector> Ops; + Ops.push_back(matrixLimb(0, 0)); // 0 + Ops.push_back(matrixLimb(0, 1)); // 1 + Ops.push_back(matrixLimb(0, Max)); // 2^64 - 1 (U64 boundary) + Ops.push_back(matrixLimb(1, 1)); // 2^64 + Ops.push_back(matrixOr(matrixLimb(0, Max), matrixLimb(1, Max))); // U128-1 + Ops.push_back(matrixLimb(2, 1)); // 2^128 (U128 boundary) + Ops.push_back(matrixLimb(3, 1)); // 2^192 (high-sparse {0,0,0,x}) + Ops.push_back(matrixOr(matrixLimb(3, 1), matrixLimb(0, 5))); // 2^192+5 + Ops.push_back(matrixLimb(1, 5)); // high-sparse {0,x,0,0} + Ops.push_back(matrixLimb(3, 0x8000000000000000ull)); // 2^255 (sign bit) + Ops.push_back( + matrixOr(matrixOr(matrixLimb(0, Max), matrixLimb(1, Max)), + matrixOr(matrixLimb(2, Max), matrixLimb(3, Max)))); // 2^256-1 + return Ops; +} + +std::vector matrixCalldata(const std::vector &A, + const std::vector &B) { + std::vector CD; + CD.insert(CD.end(), A.begin(), A.end()); + CD.insert(CD.end(), B.begin(), B.end()); + return CD; +} + +// "a = calldata[0:32]; b = calldata[32:64]; OP". After the two pushes, b is on +// top of the stack, so EVM evaluates `b OP a` (e.g. SUB = b - a; for shifts the +// shift amount is b). The exact convention is irrelevant to a differential test +// — both engines run identical bytecode — and the nested A*B sweep feeds every +// matrix value into both slots regardless. +std::vector matrixBinOp(uint8_t Op) { + return {0x60, 0x00, 0x35, // PUSH1 0 CALLDATALOAD -> a + 0x60, 0x20, 0x35, // PUSH1 0x20 CALLDATALOAD -> b + Op, // b OP a + 0x60, 0x00, 0x52, // PUSH1 0 MSTORE + 0x60, 0x20, 0x60, 0x00, 0xf3}; // RETURN(0, 32) +} + +} // namespace + +// Differential over every binary opcode whose lowering touches limbs. Operands +// are dynamic CALLDATALOAD (U256-range), so this gates the FULL-width 4-limb +// path, not the narrow fast paths (those are gated by the AndU64Mask* tests). +// On the first divergent (op, operand) pair the whole test returns, so a single +// regression reports one op and skips the rest — intentional output-flood +// control, not full per-op isolation. +// +// EXP (0x0a) is intentionally excluded: its gas cost scales with the byte +// length of the exponent, so the high-limb operands in this matrix (2^192, +// 2^255, 2^256-1) would charge enormous dynamic gas and obscure a pure +// value-range divergence behind out-of-gas noise. Shift amounts, which need +// the limb-crossing region 2..255, are swept separately by +// ShiftAmountsMatchInterpreterOnLimbBoundaries below. +TEST(EVMRangeDifferential, BinaryOpsMatchInterpreterOnAdversarialOperands) { + struct OpCase { + uint8_t Op; + const char *Name; + }; + const OpCase Cases[] = { + {0x01, "ADD"}, {0x02, "MUL"}, {0x03, "SUB"}, {0x04, "DIV"}, + {0x05, "SDIV"}, {0x06, "MOD"}, {0x07, "SMOD"}, {0x0b, "SIGNEXTEND"}, + {0x10, "LT"}, {0x11, "GT"}, {0x12, "SLT"}, {0x13, "SGT"}, + {0x14, "EQ"}, {0x16, "AND"}, {0x17, "OR"}, {0x18, "XOR"}, + {0x1a, "BYTE"}, {0x1b, "SHL"}, {0x1c, "SHR"}, {0x1d, "SAR"}, + }; + const auto Operands = matrixOperands(); + for (const auto &C : Cases) { + const auto Bytecode = matrixBinOp(C.Op); + for (const auto &A : Operands) { + for (const auto &B : Operands) { + if (!expectInterpMatchesMultipass(C.Name, Bytecode, + matrixCalldata(A, B))) { + return; // one divergence is enough; avoid output flood + } + } + } + } +} + +// Sweep SHL/SHR/SAR with shift amounts that land inside the limb-crossing +// region 2..255. The 11-value operand matrix, when fed as a shift amount, only +// realizes {0, 1, >=2^64}; it never produces an amount in 2..255, so the +// dynamic-shift lowering's cross-limb carry/offset logic (which moves bits +// across 64-bit limb boundaries at amounts 64/128/192 and within a limb at the +// rest) is never exercised by BinaryOpsMatchInterpreterOnAdversarialOperands. +// +// matrixBinOp puts the shift amount in slot b (calldata[32:64], the stack top +// SHL/SHR/SAR pops first) and the shifted value in slot a (calldata[0:32]). +// Both are CALLDATALOAD, so the full-width dynamic shift path fires. The +// shifted value sweeps the full adversarial matrix; the amount sweeps the +// boundary set below, covering both the on-limb-boundary amounts (64, 128, 192) +// and the in-limb amounts (2, 7, 31, 63, 65, 127, ...). +TEST(EVMRangeDifferential, ShiftAmountsMatchInterpreterOnLimbBoundaries) { + struct OpCase { + uint8_t Op; + const char *Name; + }; + const OpCase Cases[] = { + {0x1b, "SHL"}, + {0x1c, "SHR"}, + {0x1d, "SAR"}, + }; + const uint64_t Amounts[] = {2, 7, 8, 31, 63, 64, 65, 127, + 128, 129, 136, 191, 192, 200, 255}; + const auto Values = matrixOperands(); + for (const auto &C : Cases) { + const auto Bytecode = matrixBinOp(C.Op); + for (const auto &Value : Values) { + for (uint64_t Amount : Amounts) { + const auto AmountWord = matrixLimb(0, Amount); + if (!expectInterpMatchesMultipass(C.Name, Bytecode, + matrixCalldata(Value, AmountWord))) { + return; // one divergence is enough; avoid output flood + } + } + } + } +} + +// Directly exercise the AND-with-u64-constant fast path (CONST_U64): it tags +// the result U64 and MUST zero limbs[1..3]. Returning the result directly (not +// through a later narrow op that would discard the high limbs) is what makes a +// too-narrow / too-wide limb bug observable. Feeding high-sparse calldata, the +// masked result must equal the interpreter's. +// a = calldata[0:32]; MSTORE(0, a AND 0xFFFFFFFFFFFFFFFF); RETURN 32. +TEST(EVMRangeDifferential, AndU64MaskMatchesInterpreter) { + const std::vector Bytecode = { + 0x60, 0x00, 0x35, // CALLDATALOAD a + 0x67, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // PUSH8 u64 mask + 0x16, // AND -> m (U64) + 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3}; // RETURN(0,32) + const auto Operands = matrixOperands(); + for (const auto &A : Operands) { + if (!expectInterpMatchesMultipass("and_u64_mask", Bytecode, + matrixCalldata(A, A))) { + return; + } + } +} + +// Exercise a narrow-result CONSUMER: AND with a u64 constant produces a U64 +// value that feeds a self-ADD on the bothFitU64 narrow path. The summed result +// is returned; for any high-sparse input the narrowed two-limb sum must match +// the interpreter's full-width sum. +// a = calldata[0:32]; m = a AND 0xFFFFFFFFFFFFFFFF; MSTORE(0, m + m); RETURN. +TEST(EVMRangeDifferential, AndU64MaskThenNarrowAddMatchesInterpreter) { + const std::vector Bytecode = { + 0x60, 0x00, 0x35, // CALLDATALOAD a + 0x67, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // PUSH8 u64 mask + 0x16, // AND -> m (U64) + 0x80, // DUP1 + 0x01, // ADD m + m (narrow) + 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3}; // RETURN(0,32) + const auto Operands = matrixOperands(); + for (const auto &A : Operands) { + if (!expectInterpMatchesMultipass("and_u64_mask_then_add", Bytecode, + matrixCalldata(A, A))) { + return; + } + } +} + +#endif diff --git a/tests/evm_asm/iszero_calldatasize.easm b/tests/evm_asm/iszero_calldatasize.easm new file mode 100644 index 000000000..0e367008b --- /dev/null +++ b/tests/evm_asm/iszero_calldatasize.easm @@ -0,0 +1,9 @@ +// CALLDATASIZE -> ISZERO -> 1 (harness passes empty calldata, so size = 0). +// CALLDATASIZE yields a U64-tagged dynamic value. +CALLDATASIZE +ISZERO +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/iszero_calldatasize.expected b/tests/evm_asm/iszero_calldatasize.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/iszero_calldatasize.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/iszero_dyn_highsparse.easm b/tests/evm_asm/iszero_dyn_highsparse.easm new file mode 100644 index 000000000..9577e0a0d --- /dev/null +++ b/tests/evm_asm/iszero_dyn_highsparse.easm @@ -0,0 +1,13 @@ +// dyn(2^192) -> ISZERO -> 0. 2^192 lives entirely in the top limb (limb3); +// a wrong limb0-only fold would see 0 and wrongly return 1. +PUSH32 0x0000000000000001000000000000000000000000000000000000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +ISZERO +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/iszero_dyn_highsparse.expected b/tests/evm_asm/iszero_dyn_highsparse.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/iszero_dyn_highsparse.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/iszero_dyn_u64_nonzero.easm b/tests/evm_asm/iszero_dyn_u64_nonzero.easm new file mode 100644 index 000000000..4933eb250 --- /dev/null +++ b/tests/evm_asm/iszero_dyn_u64_nonzero.easm @@ -0,0 +1,15 @@ +// dyn(0x2a) AND 0xFF -> U64-tagged nonzero -> ISZERO -> 0. +// MLOAD makes 0x2a dynamic (untracked); AND 0xFF tags it U64. +PUSH32 0x000000000000000000000000000000000000000000000000000000000000002a +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0x00FF +AND +ISZERO +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/iszero_dyn_u64_nonzero.expected b/tests/evm_asm/iszero_dyn_u64_nonzero.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/iszero_dyn_u64_nonzero.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.easm b/tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.easm new file mode 100644 index 000000000..666c7470b --- /dev/null +++ b/tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.easm @@ -0,0 +1,30 @@ +// dyn(2^192) -> ISZERO (-> 0) -> JUMPI NOT taken -> fallthrough returns 0xad. +// Adversarial: 2^192 lives entirely in the top limb. A wrong limb0-only fold +// of the base would see 0, compute ISZERO -> 1, and wrongly take the branch +// (returning 0x01). The deferred-zero-test fold must OR all four limbs. +// +// PC math identical to jumpi_iszero_fused_taken (PUSH32 same size); dest=0x35. +// 0x27 ISZERO -> cond = 0 (2^192 is nonzero) -> JUMPI not taken -> fallthrough. +PUSH32 0x0000000000000001000000000000000000000000000000000000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +ISZERO +PUSH1 0x35 +JUMPI +// fallthrough: return 0xad +PUSH1 0xad +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN +// jump target PC = 0x35 (not reached): return 0x01 +JUMPDEST +PUSH1 0x01 +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.expected b/tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.expected new file mode 100644 index 000000000..7b756e9d2 --- /dev/null +++ b/tests/evm_asm/jumpi_iszero_fused_nottaken_highsparse.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '00000000000000000000000000000000000000000000000000000000000000AD' +storage: {} +transient_storage: {} +return: '00000000000000000000000000000000000000000000000000000000000000AD' +events: [] diff --git a/tests/evm_asm/jumpi_iszero_fused_taken.easm b/tests/evm_asm/jumpi_iszero_fused_taken.easm new file mode 100644 index 000000000..dba0b9e85 --- /dev/null +++ b/tests/evm_asm/jumpi_iszero_fused_taken.easm @@ -0,0 +1,44 @@ +// dyn(0) -> ISZERO (-> 1) -> JUMPI taken -> return 0x01. +// Exercises the deferred-zero-test fusion in handleJumpI: ISZERO is not +// materialized; JUMPI folds "cond != 0" directly against the base limbs. +// Fallthrough would return 0xad; the taken JUMPDEST returns 0x01. +// +// PC math (PUSH1=2, PUSH32=33, bare op=1): +// 0x00 PUSH32 0x00..00 -> 0x21 +// 0x21 PUSH1 0x00 -> 0x23 +// 0x23 MSTORE -> 0x24 +// 0x24 PUSH1 0x00 -> 0x26 +// 0x26 MLOAD -> 0x27 ; dyn(0) +// 0x27 ISZERO -> 0x28 ; cond = 1 +// 0x28 PUSH1 0x35 (dest) -> 0x2A +// 0x2A JUMPI -> 0x2B ; taken +// 0x2B PUSH1 0xad -> 0x2D ; fallthrough block start +// 0x2D PUSH1 0x00 -> 0x2F +// 0x2F MSTORE -> 0x30 +// 0x30 PUSH1 0x20 -> 0x32 +// 0x32 PUSH1 0x00 -> 0x34 +// 0x34 RETURN -> 0x35 +// 0x35 JUMPDEST (dest) -> 0x36 ; taken block start +PUSH32 0x0000000000000000000000000000000000000000000000000000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +ISZERO +PUSH1 0x35 +JUMPI +// fallthrough: return 0xad +PUSH1 0xad +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN +// taken target PC = 0x35: return 0x01 +JUMPDEST +PUSH1 0x01 +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/jumpi_iszero_fused_taken.expected b/tests/evm_asm/jumpi_iszero_fused_taken.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/jumpi_iszero_fused_taken.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/jumpi_iszero_iszero_fused.easm b/tests/evm_asm/jumpi_iszero_iszero_fused.easm new file mode 100644 index 000000000..ee0adae58 --- /dev/null +++ b/tests/evm_asm/jumpi_iszero_iszero_fused.easm @@ -0,0 +1,42 @@ +// dyn(2^128) -> ISZERO -> ISZERO (-> 1) -> JUMPI taken -> return 0x01. +// The double ISZERO exercises the negation-flip path: the first ISZERO defers +// a ZERO_TEST_EQ, the second flips it to ZERO_TEST_NE without materializing, +// and JUMPI folds "cond != 0" with predicate ICMP_NE over the base limbs. +// 2^128 is nonzero -> ISZERO=0 -> ISZERO=1 -> taken. +// +// PC math (one extra ISZERO vs the single-fold case shifts dest to 0x36): +// 0x00 PUSH32 (2^128) -> 0x21 +// 0x21 PUSH1 0x00 -> 0x23 +// 0x23 MSTORE -> 0x24 +// 0x24 PUSH1 0x00 -> 0x26 +// 0x26 MLOAD -> 0x27 +// 0x27 ISZERO -> 0x28 +// 0x28 ISZERO -> 0x29 +// 0x29 PUSH1 0x36 (dest) -> 0x2B +// 0x2B JUMPI -> 0x2C +// 0x2C..0x35 fallthrough block (return 0xad) +// 0x36 JUMPDEST (dest) +PUSH32 0x0000000000000000000000000000000100000000000000000000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +ISZERO +ISZERO +PUSH1 0x36 +JUMPI +// fallthrough: return 0xad +PUSH1 0xad +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN +// taken target PC = 0x36: return 0x01 +JUMPDEST +PUSH1 0x01 +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/jumpi_iszero_iszero_fused.expected b/tests/evm_asm/jumpi_iszero_iszero_fused.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/jumpi_iszero_iszero_fused.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/jumpi_u64_cond_nottaken.easm b/tests/evm_asm/jumpi_u64_cond_nottaken.easm new file mode 100644 index 000000000..295ab18fc --- /dev/null +++ b/tests/evm_asm/jumpi_u64_cond_nottaken.easm @@ -0,0 +1,29 @@ +// dyn(0x100) AND 0xFF (-> 0x00, U64-tagged) -> JUMPI NOT taken -> 0xad. +// cond carries a U64 range tag (FoldLimbs = 1). 0x100 AND 0xFF = 0 -> not taken +// -> fallthrough returns 0xad. +// +// PC math identical to jumpi_u64_cond_taken; dest = 0x38 (not reached). +PUSH32 0x0000000000000000000000000000000000000000000000000000000000000100 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0x00FF +AND +PUSH1 0x38 +JUMPI +// fallthrough: return 0xad +PUSH1 0xad +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN +// jump target PC = 0x38 (not reached): return 0x01 +JUMPDEST +PUSH1 0x01 +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/jumpi_u64_cond_nottaken.expected b/tests/evm_asm/jumpi_u64_cond_nottaken.expected new file mode 100644 index 000000000..7b756e9d2 --- /dev/null +++ b/tests/evm_asm/jumpi_u64_cond_nottaken.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '00000000000000000000000000000000000000000000000000000000000000AD' +storage: {} +transient_storage: {} +return: '00000000000000000000000000000000000000000000000000000000000000AD' +events: [] diff --git a/tests/evm_asm/jumpi_u64_cond_taken.easm b/tests/evm_asm/jumpi_u64_cond_taken.easm new file mode 100644 index 000000000..480b69b44 --- /dev/null +++ b/tests/evm_asm/jumpi_u64_cond_taken.easm @@ -0,0 +1,40 @@ +// dyn(0xAB) AND 0xFF (-> 0xAB, U64-tagged) -> JUMPI taken -> return 0x01. +// cond carries a U64 range tag, so handleJumpI's non-deferred path ORs only +// limb0 (FoldLimbs = 1). cond = 0xAB is nonzero -> taken. +// +// PC math (PUSH2=3 for the mask): +// 0x00 PUSH32 (0xAB) -> 0x21 +// 0x21 PUSH1 0x00 -> 0x23 +// 0x23 MSTORE -> 0x24 +// 0x24 PUSH1 0x00 -> 0x26 +// 0x26 MLOAD -> 0x27 +// 0x27 PUSH2 0x00FF -> 0x2A +// 0x2A AND -> 0x2B ; cond = 0xAB (U64) +// 0x2B PUSH1 0x38 (dest) -> 0x2D +// 0x2D JUMPI -> 0x2E +// 0x2E..0x37 fallthrough block (return 0xad) +// 0x38 JUMPDEST (dest) +PUSH32 0x00000000000000000000000000000000000000000000000000000000000000ab +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0x00FF +AND +PUSH1 0x38 +JUMPI +// fallthrough: return 0xad +PUSH1 0xad +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN +// taken target PC = 0x38: return 0x01 +JUMPDEST +PUSH1 0x01 +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/jumpi_u64_cond_taken.expected b/tests/evm_asm/jumpi_u64_cond_taken.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/jumpi_u64_cond_taken.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/or_dyn_u64_u64.easm b/tests/evm_asm/or_dyn_u64_u64.easm new file mode 100644 index 000000000..f02c7685c --- /dev/null +++ b/tests/evm_asm/or_dyn_u64_u64.easm @@ -0,0 +1,25 @@ +// (dyn(0xAAAA) AND 0xF0F0) OR (dyn(0x5555) AND 0x0F0F) -> 0xA5A5. +// Both operands are U64-tagged dynamics, exercising the both-fit-U64 OR path. +// dyn(0xAAAA) AND 0xF0F0 = 0xA0A0 +PUSH32 0x000000000000000000000000000000000000000000000000000000000000aaaa +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0xF0F0 +AND +// dyn(0x5555) AND 0x0F0F = 0x0505 +PUSH32 0x0000000000000000000000000000000000000000000000000000000000005555 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0x0F0F +AND +// 0xA0A0 OR 0x0505 = 0xA5A5 +OR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/or_dyn_u64_u64.expected b/tests/evm_asm/or_dyn_u64_u64.expected new file mode 100644 index 000000000..110d2b23d --- /dev/null +++ b/tests/evm_asm/or_dyn_u64_u64.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '000000000000000000000000000000000000000000000000000000000000A5A5' +storage: {} +transient_storage: {} +return: '000000000000000000000000000000000000000000000000000000000000A5A5' +events: [] diff --git a/tests/evm_asm/or_dyn_u64_wide.easm b/tests/evm_asm/or_dyn_u64_wide.easm new file mode 100644 index 000000000..845ad841b --- /dev/null +++ b/tests/evm_asm/or_dyn_u64_wide.easm @@ -0,0 +1,25 @@ +// dyn(2^192 + 0xDEAD) OR (dyn(0x1234) AND 0xFFFF) -> 2^192 + 0xDEBD. +// Adversarial for upper-limb passthrough: the wide side is untracked (U256), +// the other side is U64-tagged. If the one-side-U64 path zeroed the wide +// upper limbs, the 2^192 bit would vanish. limb0: 0xDEAD | 0x1234 = 0xDEBD. +// wide = 2^192 + 0xDEAD (top limb set + low limb) +PUSH32 0x000000000000000100000000000000000000000000000000000000000000dead +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +// narrow = dyn(0x1234) AND 0xFFFF = 0x1234 (U64-tagged) +PUSH32 0x0000000000000000000000000000000000000000000000000000000000001234 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0xFFFF +AND +// OR -> 2^192 + 0xDEBD +OR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/or_dyn_u64_wide.expected b/tests/evm_asm/or_dyn_u64_wide.expected new file mode 100644 index 000000000..88fb257d4 --- /dev/null +++ b/tests/evm_asm/or_dyn_u64_wide.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '000000000000000100000000000000000000000000000000000000000000DEBD' +storage: {} +transient_storage: {} +return: '000000000000000100000000000000000000000000000000000000000000DEBD' +events: [] diff --git a/tests/evm_asm/sar_const64_pos.easm b/tests/evm_asm/sar_const64_pos.easm new file mode 100644 index 000000000..f2b50f72f --- /dev/null +++ b/tests/evm_asm/sar_const64_pos.easm @@ -0,0 +1,13 @@ +// dyn(2^192) SAR 64 -> 2^128 (positive value, CompShift=1, ShiftMod=0) +PUSH32 0x0000000000000001000000000000000000000000000000000000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x40 +SAR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sar_const64_pos.expected b/tests/evm_asm/sar_const64_pos.expected new file mode 100644 index 000000000..e35ab5d39 --- /dev/null +++ b/tests/evm_asm/sar_const64_pos.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000100000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000100000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/sar_const8_neg.easm b/tests/evm_asm/sar_const8_neg.easm new file mode 100644 index 000000000..3bd988ab9 --- /dev/null +++ b/tests/evm_asm/sar_const8_neg.easm @@ -0,0 +1,14 @@ +// dyn(-1) SAR 8 -> all ones (sign-fill intact; const path only drops the +// IsLargeShift guard, never source limbs) +PUSH32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x08 +SAR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sar_const8_neg.expected b/tests/evm_asm/sar_const8_neg.expected new file mode 100644 index 000000000..6e85c7e3d --- /dev/null +++ b/tests/evm_asm/sar_const8_neg.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' +storage: {} +transient_storage: {} +return: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' +events: [] diff --git a/tests/evm_asm/sgt_const_vs_dyn.easm b/tests/evm_asm/sgt_const_vs_dyn.easm new file mode 100644 index 000000000..dc257d3f0 --- /dev/null +++ b/tests/evm_asm/sgt_const_vs_dyn.easm @@ -0,0 +1,15 @@ +// const 0 >s dyn(-1) -> 1. Exercises the swapped dispatch: a const LHS routes +// SGT to the SLT helper (0 >s x <=> x s const 0xFFFFFFFFFFFFFFFF -> 1. 2^128 is non-negative with an +// upper limb (limb2) set, so a > b regardless of b's limb0 magnitude -> 1. +// Push const RHS (max u64) first, dynamic LHS (2^128) on top. +PUSH8 0xFFFFFFFFFFFFFFFF +PUSH32 0x0000000000000000000000000000000100000000000000000000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SGT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sgt_dyn_highsparse_vs_const.expected b/tests/evm_asm/sgt_dyn_highsparse_vs_const.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/sgt_dyn_highsparse_vs_const.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/sgt_dyn_msb64_vs_const.easm b/tests/evm_asm/sgt_dyn_msb64_vs_const.easm new file mode 100644 index 000000000..4efe9df43 --- /dev/null +++ b/tests/evm_asm/sgt_dyn_msb64_vs_const.easm @@ -0,0 +1,15 @@ +// dyn(0x9000000000000000) >s const 0x8000000000000000 -> 1. Both are +// non-negative signed-256 (only limb0 set), so an UNSIGNED limb0 compare gives +// 0x9000... > 0x8000... = 1. Push const RHS first, dynamic LHS on top. +PUSH8 0x8000000000000000 +PUSH32 0x0000000000000000000000000000000000000000000000009000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SGT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sgt_dyn_msb64_vs_const.expected b/tests/evm_asm/sgt_dyn_msb64_vs_const.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/sgt_dyn_msb64_vs_const.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/sgt_dyn_neg_vs_const.easm b/tests/evm_asm/sgt_dyn_neg_vs_const.easm new file mode 100644 index 000000000..2f50efbe4 --- /dev/null +++ b/tests/evm_asm/sgt_dyn_neg_vs_const.easm @@ -0,0 +1,14 @@ +// dyn(-1) >s const 0 -> 0. SGT pops top as LHS (a), so push const RHS (b=0) +// first, dynamic LHS (a=-1) on top: a >s b = (-1 >s 0) = 0 since a is negative. +PUSH1 0x00 +PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SGT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sgt_dyn_neg_vs_const.expected b/tests/evm_asm/sgt_dyn_neg_vs_const.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/sgt_dyn_neg_vs_const.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/shl_const136_u64val.easm b/tests/evm_asm/shl_const136_u64val.easm new file mode 100644 index 000000000..60945a7c1 --- /dev/null +++ b/tests/evm_asm/shl_const136_u64val.easm @@ -0,0 +1,19 @@ +// (dyn AND 0xFFFFFFFFFFFFFFFF) << 136 +// the AND mask gives the value a provable U64 range (LiveLimbs=1), so the SHL +// const path (CompShift=2, ShiftMod=8) drives result limb 3 down the +// carry-only branch: SrcIdx=1 is a dead shifted term but its carry term reads +// the live limb Value[0] (Value[0] >> 56), so limb 3 is one shift, no OR. +PUSH32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0x88 +SHL +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shl_const136_u64val.expected b/tests/evm_asm/shl_const136_u64val.expected new file mode 100644 index 000000000..724941cb6 --- /dev/null +++ b/tests/evm_asm/shl_const136_u64val.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '00000000000000FFFFFFFFFFFFFFFF0000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '00000000000000FFFFFFFFFFFFFFFF0000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/shl_const200_u64val.easm b/tests/evm_asm/shl_const200_u64val.easm new file mode 100644 index 000000000..f1d9e2c7a --- /dev/null +++ b/tests/evm_asm/shl_const200_u64val.easm @@ -0,0 +1,17 @@ +// (dyn AND 0xFFFFFFFFFFFFFFFF) << 200 +// the AND mask gives the value a provable U64 range, so the SHL const path +// (CompShift=3, ShiftMod=8) prunes all source limbs except limb0 +PUSH32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0xC8 +SHL +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shl_const200_u64val.expected b/tests/evm_asm/shl_const200_u64val.expected new file mode 100644 index 000000000..dfa328f42 --- /dev/null +++ b/tests/evm_asm/shl_const200_u64val.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: 'FFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: 'FFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/shl_const256_dyn.easm b/tests/evm_asm/shl_const256_dyn.easm new file mode 100644 index 000000000..b92efe61d --- /dev/null +++ b/tests/evm_asm/shl_const256_dyn.easm @@ -0,0 +1,13 @@ +// dyn value << 256 -> 0 (static large-shift fold for a constant amount >= 256) +PUSH32 0x112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0x0100 +SHL +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shl_const256_dyn.expected b/tests/evm_asm/shl_const256_dyn.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/shl_const256_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/shl_const4_dyn.easm b/tests/evm_asm/shl_const4_dyn.easm new file mode 100644 index 000000000..037bcb696 --- /dev/null +++ b/tests/evm_asm/shl_const4_dyn.easm @@ -0,0 +1,14 @@ +// dyn(0x1122..FF00) << 4 +// value is laundered through memory so the shift amount stays the only constant +PUSH32 0x112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x04 +SHL +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shl_const4_dyn.expected b/tests/evm_asm/shl_const4_dyn.expected new file mode 100644 index 000000000..749df0ad8 --- /dev/null +++ b/tests/evm_asm/shl_const4_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '12233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF000' +storage: {} +transient_storage: {} +return: '12233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF000' +events: [] diff --git a/tests/evm_asm/shl_const96_dyn.easm b/tests/evm_asm/shl_const96_dyn.easm new file mode 100644 index 000000000..eb09e0e25 --- /dev/null +++ b/tests/evm_asm/shl_const96_dyn.easm @@ -0,0 +1,14 @@ +// dyn value with all four 64-bit limbs nonzero << 96 +// exercises the cross-limb carry terms in the SHL const path +PUSH32 0x1111111111111111222222222222222233333333333333334444444444444444 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x60 +SHL +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shl_const96_dyn.expected b/tests/evm_asm/shl_const96_dyn.expected new file mode 100644 index 000000000..1fcb3e612 --- /dev/null +++ b/tests/evm_asm/shl_const96_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '2222222233333333333333334444444444444444000000000000000000000000' +storage: {} +transient_storage: {} +return: '2222222233333333333333334444444444444444000000000000000000000000' +events: [] diff --git a/tests/evm_asm/shl_const_highlimb_dyn.easm b/tests/evm_asm/shl_const_highlimb_dyn.easm new file mode 100644 index 000000000..b7357cb87 --- /dev/null +++ b/tests/evm_asm/shl_const_highlimb_dyn.easm @@ -0,0 +1,15 @@ +// dyn value << 2^64 -> 0 +// the shift amount has limb0 == 0 but an upper limb set, so getConstShiftAmount +// only sees a zero low limb; the static large-shift fold resolves it to zero +PUSH32 0x112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH32 0x0000000000000000000000000000000000000000000000010000000000000000 +SHL +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shl_const_highlimb_dyn.expected b/tests/evm_asm/shl_const_highlimb_dyn.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/shl_const_highlimb_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/shl_dyn_amount.easm b/tests/evm_asm/shl_dyn_amount.easm new file mode 100644 index 000000000..8047b5e47 --- /dev/null +++ b/tests/evm_asm/shl_dyn_amount.easm @@ -0,0 +1,18 @@ +// dyn value << dyn(4): both operands laundered through memory so the shift +// amount is non-constant, exercising the dynamic-amount path +PUSH32 0x00000000000000000000000000000000000000000000000000000000DEADBEEF +PUSH1 0x00 +MSTORE +PUSH1 0x04 +PUSH1 0x20 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x20 +MLOAD +SHL +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shl_dyn_amount.expected b/tests/evm_asm/shl_dyn_amount.expected new file mode 100644 index 000000000..e26df8eb4 --- /dev/null +++ b/tests/evm_asm/shl_dyn_amount.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000DEADBEEF0' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000DEADBEEF0' +events: [] diff --git a/tests/evm_asm/shr_const256_dyn.easm b/tests/evm_asm/shr_const256_dyn.easm new file mode 100644 index 000000000..dd278b132 --- /dev/null +++ b/tests/evm_asm/shr_const256_dyn.easm @@ -0,0 +1,13 @@ +// dyn value >> 256 -> 0 (static large-shift fold for a constant amount >= 256) +PUSH32 0x112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0x0100 +SHR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shr_const256_dyn.expected b/tests/evm_asm/shr_const256_dyn.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/shr_const256_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/shr_const4_dyn.easm b/tests/evm_asm/shr_const4_dyn.easm new file mode 100644 index 000000000..f4c5fe3dd --- /dev/null +++ b/tests/evm_asm/shr_const4_dyn.easm @@ -0,0 +1,13 @@ +// dyn(0x1122..FF00) >> 4 (logical right shift) +PUSH32 0x112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x04 +SHR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shr_const4_dyn.expected b/tests/evm_asm/shr_const4_dyn.expected new file mode 100644 index 000000000..e47633357 --- /dev/null +++ b/tests/evm_asm/shr_const4_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF0' +storage: {} +transient_storage: {} +return: '0112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF0' +events: [] diff --git a/tests/evm_asm/shr_const72_dyn.easm b/tests/evm_asm/shr_const72_dyn.easm new file mode 100644 index 000000000..7ac3a5ca9 --- /dev/null +++ b/tests/evm_asm/shr_const72_dyn.easm @@ -0,0 +1,13 @@ +// dyn value with all four limbs nonzero >> 72 (CompShift=1, ShiftMod=8, cross-limb) +PUSH32 0x1111111111111111222222222222222233333333333333334444444444444444 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x48 +SHR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shr_const72_dyn.expected b/tests/evm_asm/shr_const72_dyn.expected new file mode 100644 index 000000000..9007a5bea --- /dev/null +++ b/tests/evm_asm/shr_const72_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000001111111111111111222222222222222233333333333333' +storage: {} +transient_storage: {} +return: '0000000000000000001111111111111111222222222222222233333333333333' +events: [] diff --git a/tests/evm_asm/shr_const8_u64val.easm b/tests/evm_asm/shr_const8_u64val.easm new file mode 100644 index 000000000..6848e22de --- /dev/null +++ b/tests/evm_asm/shr_const8_u64val.easm @@ -0,0 +1,17 @@ +// (dyn AND 0xFFFFFFFFFFFFFFFF) >> 8 +// the AND mask makes the value provably U64, so only limb0 is live and the +// SHR const path prunes the carry-in from limb1 +PUSH32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0x08 +SHR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/shr_const8_u64val.expected b/tests/evm_asm/shr_const8_u64val.expected new file mode 100644 index 000000000..1f435d31a --- /dev/null +++ b/tests/evm_asm/shr_const8_u64val.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '00000000000000000000000000000000000000000000000000FFFFFFFFFFFFFF' +storage: {} +transient_storage: {} +return: '00000000000000000000000000000000000000000000000000FFFFFFFFFFFFFF' +events: [] diff --git a/tests/evm_asm/slt_const_vs_dyn.easm b/tests/evm_asm/slt_const_vs_dyn.easm new file mode 100644 index 000000000..1906139d2 --- /dev/null +++ b/tests/evm_asm/slt_const_vs_dyn.easm @@ -0,0 +1,15 @@ +// const 5 0. Exercises the swapped dispatch: a const LHS routes +// SLT to the SGT helper (5 x >s 5; dyn(-1) >s 5 is false). +// SLT pops top as LHS, so make the dynamic RHS first, then PUSH 5 on top. +PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH1 0x05 +SLT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/slt_const_vs_dyn.expected b/tests/evm_asm/slt_const_vs_dyn.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/slt_const_vs_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/slt_dyn_eq_const.easm b/tests/evm_asm/slt_dyn_eq_const.easm new file mode 100644 index 000000000..0035a7282 --- /dev/null +++ b/tests/evm_asm/slt_dyn_eq_const.easm @@ -0,0 +1,14 @@ +// dyn(5) 0. Equal values are not strictly less -> 0. +// Boundary check on the limb0 ULT compare (5 < 5 is false). +PUSH1 0x05 +PUSH32 0x0000000000000000000000000000000000000000000000000000000000000005 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SLT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/slt_dyn_eq_const.expected b/tests/evm_asm/slt_dyn_eq_const.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/slt_dyn_eq_const.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/slt_dyn_highsparse_vs_const.easm b/tests/evm_asm/slt_dyn_highsparse_vs_const.easm new file mode 100644 index 000000000..810eea482 --- /dev/null +++ b/tests/evm_asm/slt_dyn_highsparse_vs_const.easm @@ -0,0 +1,15 @@ +// dyn(2^192) 0. 2^192 is non-negative (bit255 clear) and has an +// upper limb set, so a > b -> false. Push const RHS (5) first, dynamic LHS on +// top. Exercises the upper-limb HasUpper select in the SLT helper. +PUSH1 0x05 +PUSH32 0x0000000000000001000000000000000000000000000000000000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SLT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/slt_dyn_highsparse_vs_const.expected b/tests/evm_asm/slt_dyn_highsparse_vs_const.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/slt_dyn_highsparse_vs_const.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/slt_dyn_msb64_vs_const.easm b/tests/evm_asm/slt_dyn_msb64_vs_const.easm new file mode 100644 index 000000000..3c3f40988 --- /dev/null +++ b/tests/evm_asm/slt_dyn_msb64_vs_const.easm @@ -0,0 +1,16 @@ +// dyn(0x8000000000000000) 1. Both values are +// non-negative signed-256 (only limb0 set, bit255 clear), so the helper does +// an UNSIGNED limb0 compare: 0x8000... < 0x9000... = 1. Hard gate: an +// erroneous SIGNED i64 limb0 compare would read limb0 as negative. +PUSH8 0x9000000000000000 +PUSH32 0x0000000000000000000000000000000000000000000000008000000000000000 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SLT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/slt_dyn_msb64_vs_const.expected b/tests/evm_asm/slt_dyn_msb64_vs_const.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/slt_dyn_msb64_vs_const.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/slt_dyn_neg_vs_const.easm b/tests/evm_asm/slt_dyn_neg_vs_const.easm new file mode 100644 index 000000000..976c00154 --- /dev/null +++ b/tests/evm_asm/slt_dyn_neg_vs_const.easm @@ -0,0 +1,15 @@ +// dyn(-1) 1. SLT pops top as LHS (a), so push const RHS (b=5) +// first, then the dynamic LHS (a=-1) on top: a negative -> true. +PUSH1 0x05 +PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SLT +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/slt_dyn_neg_vs_const.expected b/tests/evm_asm/slt_dyn_neg_vs_const.expected new file mode 100644 index 000000000..312a8953f --- /dev/null +++ b/tests/evm_asm/slt_dyn_neg_vs_const.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000001' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000001' +events: [] diff --git a/tests/evm_asm/sub_u64_pair_equal.easm b/tests/evm_asm/sub_u64_pair_equal.easm new file mode 100644 index 000000000..790cf74d1 --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_equal.easm @@ -0,0 +1,23 @@ +// dyn-u64(7) - dyn-u64(7) = 0 +// Equal operands: no borrow, limb0 = 0, upper limbs zero. +// Push b = 7 first, then a = 7; SUB computes a - b. +PUSH1 0x07 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0x07 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +SUB +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sub_u64_pair_equal.expected b/tests/evm_asm/sub_u64_pair_equal.expected new file mode 100644 index 000000000..481e245e2 --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_equal.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000000000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000000000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/sub_u64_pair_nounderflow.easm b/tests/evm_asm/sub_u64_pair_nounderflow.easm new file mode 100644 index 000000000..902dd7f44 --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_nounderflow.easm @@ -0,0 +1,25 @@ +// dyn-u64(100) - dyn-u64(58) = 42 +// Both operands are dynamic (MLOAD round-trip) and range-proven u64 (AND mask), +// so the SUB range-based u64 fast path applies. No underflow: result limb0 = 42, +// upper limbs zero. +// Push b = 58 first (second from top), then a = 100 (top); SUB computes a - b. +PUSH1 0x3a +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0x64 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +SUB +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sub_u64_pair_nounderflow.expected b/tests/evm_asm/sub_u64_pair_nounderflow.expected new file mode 100644 index 000000000..d7ed191dd --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_nounderflow.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '000000000000000000000000000000000000000000000000000000000000002A' +storage: {} +transient_storage: {} +return: '000000000000000000000000000000000000000000000000000000000000002A' +events: [] diff --git a/tests/evm_asm/sub_u64_pair_underflow.easm b/tests/evm_asm/sub_u64_pair_underflow.easm new file mode 100644 index 000000000..1cedc1531 --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_underflow.easm @@ -0,0 +1,24 @@ +// dyn-u64(5) - dyn-u64(7) = 2^256 - 2 (adversarial underflow case) +// 5 < 7, so borrow = 1: limb0 = wrapping_sub(5, 7) = 2^64 - 2 = 0xFFFFFFFFFFFFFFFE, +// and the upper 192 bits sign-fill to all-ones (0 - 1 = 0xFFFFFFFFFFFFFFFF). +// Push b = 7 first, then a = 5; SUB computes a - b. +PUSH1 0x07 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0x05 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +SUB +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sub_u64_pair_underflow.expected b/tests/evm_asm/sub_u64_pair_underflow.expected new file mode 100644 index 000000000..ebacdd6cf --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_underflow.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE' +storage: {} +transient_storage: {} +return: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE' +events: [] diff --git a/tests/evm_asm/sub_u64_pair_wrap_boundary.easm b/tests/evm_asm/sub_u64_pair_wrap_boundary.easm new file mode 100644 index 000000000..ec4ecb364 --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_wrap_boundary.easm @@ -0,0 +1,25 @@ +// dyn-u64(0) - dyn-u64(0xFFFFFFFFFFFFFFFF) = 2^256 - 2^64 + 1 +// 0 < 0xFFFFFFFFFFFFFFFF, so borrow = 1: limb0 = wrapping_sub(0, 0xFFFFFFFFFFFFFFFF) +// = 1, and the upper 192 bits sign-fill to all-ones. +// Word = FFFF..FFFF (48 hex F) followed by 0000000000000001. +// Push b = 0xFFFFFFFFFFFFFFFF first, then a = 0; SUB computes a - b. +PUSH8 0xFFFFFFFFFFFFFFFF +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0x00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +SUB +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sub_u64_pair_wrap_boundary.expected b/tests/evm_asm/sub_u64_pair_wrap_boundary.expected new file mode 100644 index 000000000..0d21e0b4c --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_wrap_boundary.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000001' +storage: {} +transient_storage: {} +return: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000001' +events: [] diff --git a/tests/evm_asm/sub_u64_pair_zero_rhs_dyn.easm b/tests/evm_asm/sub_u64_pair_zero_rhs_dyn.easm new file mode 100644 index 000000000..176bb33e2 --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_zero_rhs_dyn.easm @@ -0,0 +1,24 @@ +// dyn-u64(42) - dyn-u64(0) = 42 +// The subtrahend is a dynamic zero (MLOAD round-trip), so it does NOT take the +// const zero-identity shortcut; it exercises the range-based u64 fast path. +// Push b = 0 first, then a = 42; SUB computes a - b. +PUSH1 0x00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH1 0x2a +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +SUB +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sub_u64_pair_zero_rhs_dyn.expected b/tests/evm_asm/sub_u64_pair_zero_rhs_dyn.expected new file mode 100644 index 000000000..d7ed191dd --- /dev/null +++ b/tests/evm_asm/sub_u64_pair_zero_rhs_dyn.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '000000000000000000000000000000000000000000000000000000000000002A' +storage: {} +transient_storage: {} +return: '000000000000000000000000000000000000000000000000000000000000002A' +events: [] diff --git a/tests/evm_asm/sub_wide_u64_control.easm b/tests/evm_asm/sub_wide_u64_control.easm new file mode 100644 index 000000000..973b53d32 --- /dev/null +++ b/tests/evm_asm/sub_wide_u64_control.easm @@ -0,0 +1,23 @@ +// dyn-U256(2^192+5) - dyn-u64(5) = 2^192 (regression control) +// The minuend is a dynamic wide U256 (no u64 mask), so only one operand is +// range-proven u64. The range-based u64 fast path must NOT fire; the generic +// 4-limb SBB path computes the result. +// Push b = 5 (dyn-u64) first, then a = 2^192+5 (dyn-U256); SUB computes a - b. +PUSH1 0x05 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH8 0xFFFFFFFFFFFFFFFF +AND +PUSH32 0x0000000000000001000000000000000000000000000000000000000000000005 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +SUB +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/sub_wide_u64_control.expected b/tests/evm_asm/sub_wide_u64_control.expected new file mode 100644 index 000000000..153a1d85d --- /dev/null +++ b/tests/evm_asm/sub_wide_u64_control.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '0000000000000001000000000000000000000000000000000000000000000000' +storage: {} +transient_storage: {} +return: '0000000000000001000000000000000000000000000000000000000000000000' +events: [] diff --git a/tests/evm_asm/xor_dyn_u64_u64.easm b/tests/evm_asm/xor_dyn_u64_u64.easm new file mode 100644 index 000000000..a43018e8f --- /dev/null +++ b/tests/evm_asm/xor_dyn_u64_u64.easm @@ -0,0 +1,25 @@ +// (dyn(0xAAAA) AND 0xFFFF) XOR (dyn(0x00FF) AND 0xFFFF) -> 0xAA55. +// Both operands are U64-tagged dynamics, exercising the both-fit-U64 XOR path. +// dyn(0xAAAA) AND 0xFFFF = 0xAAAA +PUSH32 0x000000000000000000000000000000000000000000000000000000000000aaaa +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0xFFFF +AND +// dyn(0x00FF) AND 0xFFFF = 0x00FF +PUSH32 0x00000000000000000000000000000000000000000000000000000000000000ff +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0xFFFF +AND +// 0xAAAA XOR 0x00FF = 0xAA55 +XOR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/xor_dyn_u64_u64.expected b/tests/evm_asm/xor_dyn_u64_u64.expected new file mode 100644 index 000000000..8bfc3209c --- /dev/null +++ b/tests/evm_asm/xor_dyn_u64_u64.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '000000000000000000000000000000000000000000000000000000000000AA55' +storage: {} +transient_storage: {} +return: '000000000000000000000000000000000000000000000000000000000000AA55' +events: [] diff --git a/tests/evm_asm/xor_dyn_u64_wide.easm b/tests/evm_asm/xor_dyn_u64_wide.easm new file mode 100644 index 000000000..2e767a22b --- /dev/null +++ b/tests/evm_asm/xor_dyn_u64_wide.easm @@ -0,0 +1,24 @@ +// dyn(2^192 + 0xFF00) XOR (dyn(0x00FF) AND 0xFFFF) -> 2^192 + 0xFFFF. +// Adversarial for upper-limb passthrough on XOR: the wide upper limb must +// pass through (XOR with implicit 0). limb0: 0xFF00 ^ 0x00FF = 0xFFFF. +// wide = 2^192 + 0xFF00 +PUSH32 0x000000000000000100000000000000000000000000000000000000000000ff00 +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +// narrow = dyn(0x00FF) AND 0xFFFF = 0x00FF (U64-tagged) +PUSH32 0x00000000000000000000000000000000000000000000000000000000000000ff +PUSH1 0x00 +MSTORE +PUSH1 0x00 +MLOAD +PUSH2 0xFFFF +AND +// XOR -> 2^192 + 0xFFFF +XOR +PUSH1 0x00 +MSTORE +PUSH1 0x20 +PUSH1 0x00 +RETURN diff --git a/tests/evm_asm/xor_dyn_u64_wide.expected b/tests/evm_asm/xor_dyn_u64_wide.expected new file mode 100644 index 000000000..eee020191 --- /dev/null +++ b/tests/evm_asm/xor_dyn_u64_wide.expected @@ -0,0 +1,8 @@ +status: success +error_code: 0 +stack: [] +memory: '000000000000000100000000000000000000000000000000000000000000FFFF' +storage: {} +transient_storage: {} +return: '000000000000000100000000000000000000000000000000000000000000FFFF' +events: [] From b86b9c97e986370598f9e96a70071ccb6e2e8432 Mon Sep 17 00:00:00 2001 From: abmcar Date: Thu, 11 Jun 2026 16:32:35 +0800 Subject: [PATCH 2/3] test: address review comments on the differential suite Add explicit standard-library includes instead of relying on transitive ones, and make the matrix harness assert the interpreter run succeeds so a shared failure (e.g. out-of-gas in both engines) cannot pass the differential check vacuously. Co-Authored-By: Claude Fable 5 --- src/tests/evm_differential_tests.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/tests/evm_differential_tests.cpp b/src/tests/evm_differential_tests.cpp index 9efd7cf0f..803eb81b1 100644 --- a/src/tests/evm_differential_tests.cpp +++ b/src/tests/evm_differential_tests.cpp @@ -1,8 +1,12 @@ // Copyright (C) 2025 the DTVM authors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +#include #include #include #include +#include +#include +#include #include "evm/evm.h" #include "evm_test_host.hpp" @@ -126,9 +130,10 @@ runEvmBytecodeFile(const std::string &FilePath, common::RunMode Mode, return runEvmBytecode(FilePath, *BytecodeBuf, Mode, CallData); } -// Run `Bytecode` through interpreter and multipass, assert the multipass JIT -// compiled (so the comparison is non-vacuous) and that status + output agree. -// Returns false on divergence so matrix callers can stop flooding output. +// Run `Bytecode` through interpreter and multipass, assert the interpreter +// run succeeds (a shared failure such as out-of-gas would make the comparison +// vacuous), the multipass JIT compiled, and that status + output agree. +// Returns false on any violation so matrix callers can stop flooding output. bool expectInterpMatchesMultipass(const std::string &Label, const std::vector &Bytecode, const std::vector &CallData) { @@ -139,9 +144,12 @@ bool expectInterpMatchesMultipass(const std::string &Label, #ifdef ZEN_ENABLE_JIT EXPECT_TRUE(Multi.JITCompiled) << "multipass did not JIT-compile: " << Label; #endif + EXPECT_EQ(Interp.Status, EVMC_SUCCESS) + << "interpreter did not succeed: " << Label; EXPECT_EQ(Multi.Status, Interp.Status) << "status diverged: " << Label; EXPECT_EQ(Multi.OutputHex, Interp.OutputHex) << "output diverged: " << Label; - return Multi.Status == Interp.Status && Multi.OutputHex == Interp.OutputHex; + return Interp.Status == EVMC_SUCCESS && Multi.Status == Interp.Status && + Multi.OutputHex == Interp.OutputHex; } // Fixture-file variant of the assertion above: load a hex fixture, run both From 188cc748d48b9d6662b3ee40299d92154784b8a9 Mon Sep 17 00:00:00 2001 From: abmcar <52450271+abmcar@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:33:04 +0800 Subject: [PATCH 3/3] docs: tighten differential-suite change doc (lead summary, backed counts) Align the doc with the technical-writing rule: describe internal labels by behavior, remove who-reviewed and process narrative, and keep every count, flag, code anchor, and measurement. No code or runtime change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Cen5bPpPEgkSkcxWWTSY7d --- .../README.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/changes/2026-06-11-evm-differential-suite/README.md b/docs/changes/2026-06-11-evm-differential-suite/README.md index cd6d71b2e..fac887349 100644 --- a/docs/changes/2026-06-11-evm-differential-suite/README.md +++ b/docs/changes/2026-06-11-evm-differential-suite/README.md @@ -6,9 +6,11 @@ ## Overview -Add a dedicated gtest target, `evmDifferentialTests` -(`src/tests/evm_differential_tests.cpp`), that consolidates the -interpreter-vs-multipass differential tests into a single self-contained file. +Consolidate the interpreter-vs-multipass differential tests into one test-only +gtest target, `evmDifferentialTests` +(`src/tests/evm_differential_tests.cpp`); the suite passes 44/44 on +upstream/main with no runtime or codegen change. The target collects the tests +into a single self-contained file. The suite has two parts: @@ -31,20 +33,19 @@ run that fell back to the interpreter cannot pass vacuously. ## Motivation -Differential JIT tests do not belong in the interpreter test file. Three -in-flight optimization PRs each appended a near-identical copy-pasted +The differential JIT tests previously lived in the interpreter test file. Three +in-flight optimization PRs each appended a near-identical copy of the differential suite plus its fixtures to `src/tests/evm_interp_tests.cpp`, and a fourth added the programmatic matrix harness to the same file. Every one of these would conflict with the others on merge. -A single dedicated home removes both problems: the copy-pasted suites collapse +A single target removes both problems: the copied suites collapse into one parameterized fixture with one executor, and the shared file no longer forces a merge conflict between the optimization PRs. -The differential property — interpreter output equals multipass output, and -multipass actually JIT-compiles — holds on plain `main`, independent of the -optimization PRs the fixtures were written alongside. The suite therefore -guards an invariant that is true before and after those optimizations merge. +The invariant holds on plain `main`, independent of the optimization PRs the +fixtures were written alongside, so the suite guards it before and after those +optimizations merge. ## Impact @@ -59,9 +60,9 @@ guards an invariant that is true before and after those optimizations merge. - `evmDifferentialTests`: **44 / 44 pass** (40 fixture + 4 matrix) on upstream/main with no optimization PR applied. -- `ctest` (`SPEC_TESTS_ARGS="-m multipass --format evm --enable-evm-gas"`): all - tests pass. `evmInterpTests` now runs the 40 new fixtures through its golden - sample test as well. +- `ctest` (`SPEC_TESTS_ARGS="-m multipass --format evm --enable-evm-gas"`): the + suite passes with no new failures. `evmInterpTests` now runs the 40 new + fixtures through its golden sample test as well. - `tools/format.sh check`: pass. - No new compiler warnings for the new file.