diff --git a/.ci/run_test_suite.sh b/.ci/run_test_suite.sh index db6814c67..eb28f1a7b 100644 --- a/.ci/run_test_suite.sh +++ b/.ci/run_test_suite.sh @@ -26,6 +26,10 @@ INPUT_FORMAT=${INPUT_FORMAT,,} CMAKE_OPTIONS="-DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TARGET" +if [[ ${CMAKE_BUILD_TARGET} != "Release" && ${RUN_MODE} != "interpreter" && ${INPUT_FORMAT} == "evm" ]]; then + CMAKE_OPTIONS="$CMAKE_OPTIONS -DZEN_ENABLE_SPDLOG=ON -DZEN_ENABLE_JIT_LOGGING=ON" +fi + if [ "${ENABLE_ASAN:-false}" = true ]; then CMAKE_OPTIONS="$CMAKE_OPTIONS -DZEN_ENABLE_ASAN=ON" fi @@ -201,7 +205,7 @@ for STACK_TYPE in ${STACK_TYPES[@]}; do --output-summary "$BENCHMARK_SUMMARY_FILE" \ --lib ./libdtvmapi.so \ --mode "$BENCHMARK_MODE" \ - --benchmark-dir test/evm-benchmarks/benchmarks + --benchmark-dir test/evm-benchmarks/benchmarks $OPCODE_BENCH_DIR elif [ -n "$BENCHMARK_BASELINE_LIB" ]; then # No cache -- run baseline benchmarks with the pre-built # baseline library, then run current benchmarks and compare. @@ -212,7 +216,7 @@ for STACK_TYPE in ${STACK_TYPES[@]}; do --save-baseline "$SAVE_PATH" \ --lib ./libdtvmapi.so \ --mode "$BENCHMARK_MODE" \ - --benchmark-dir test/evm-benchmarks/benchmarks + --benchmark-dir test/evm-benchmarks/benchmarks $OPCODE_BENCH_DIR echo "Running current benchmarks with PR library..." cp ../build/lib/libdtvmapi.so ./libdtvmapi.so @@ -222,15 +226,15 @@ for STACK_TYPE in ${STACK_TYPES[@]}; do --output-summary "$BENCHMARK_SUMMARY_FILE" \ --lib ./libdtvmapi.so \ --mode "$BENCHMARK_MODE" \ - --benchmark-dir test/evm-benchmarks/benchmarks + --benchmark-dir test/evm-benchmarks/benchmarks $OPCODE_BENCH_DIR elif [ -n "$BENCHMARK_SAVE_BASELINE" ]; then echo "Saving performance baseline..." python3 check_performance_regression.py \ - --save-baseline "$BENCHMARK_SAVE_BASELINE" \ + --save-baseline "$ ENCHMARK_SAVE_BASELINE" \ --output-summary "$BENCHMARK_SUMMARY_FILE" \ --lib ./libdtvmapi.so \ --mode "$BENCHMARK_MODE" \ - --benchmark-dir test/evm-benchmarks/benchmarks + --benchmark-dir test/evm-benchmarks/benchmarks $OPCODE_BENCH_DIR elif [ -n "$BENCHMARK_BASELINE_FILE" ]; then echo "Checking performance regression against baseline..." python3 check_performance_regression.py \ @@ -239,7 +243,7 @@ for STACK_TYPE in ${STACK_TYPES[@]}; do --output-summary "$BENCHMARK_SUMMARY_FILE" \ --lib ./libdtvmapi.so \ --mode "$BENCHMARK_MODE" \ - --benchmark-dir test/evm-benchmarks/benchmarks + --benchmark-dir test/evm-benchmarks/benchmarks $OPCODE_BENCH_DIR else echo "Running benchmark suite without comparison..." python3 check_performance_regression.py \ @@ -247,7 +251,7 @@ for STACK_TYPE in ${STACK_TYPES[@]}; do --output-summary "$BENCHMARK_SUMMARY_FILE" \ --lib ./libdtvmapi.so \ --mode "$BENCHMARK_MODE" \ - --benchmark-dir test/evm-benchmarks/benchmarks + --benchmark-dir test/evm-benchmarks/benchmarks $OPCODE_BENCH_DIR cat benchmark_results.json fi diff --git a/.github/workflows/dtvm_evm_test_x86.yml b/.github/workflows/dtvm_evm_test_x86.yml index 55acbc280..5c38bc0fc 100644 --- a/.github/workflows/dtvm_evm_test_x86.yml +++ b/.github/workflows/dtvm_evm_test_x86.yml @@ -218,7 +218,6 @@ jobs: export ENABLE_GAS_REGISTER=true bash .ci/run_test_suite.sh - build_test_release_evmone_unittests_on_x86: name: Test DTVM-EVM multipass and interpreter using evmone unit tests in release mode on x86-64 runs-on: ubuntu-latest diff --git a/benchmarks/evm_contract_benchmark.cpp b/benchmarks/evm_contract_benchmark.cpp new file mode 100644 index 000000000..c890378ae --- /dev/null +++ b/benchmarks/evm_contract_benchmark.cpp @@ -0,0 +1,301 @@ +// Copyright (C) 2025 the DTVM authors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include "host/evm/crypto.h" +#include "tests/solidity_test_helpers.h" +#include "utils/evm.h" +#include +#include + +using namespace zen::evm_test_utils; +using namespace zen::utils; + +// Custom EVMC Host that uses evmc::VM for execution, allowing recursive CALLs +class EVMCBenchmarkHost : public evmc::MockedHost { + evmc::VM* Vm = nullptr; + evmc_revision Rev = EVMC_CANCUN; + +public: + void SetVm(evmc::VM* VmParam) { Vm = VmParam; } + void SetRevision(evmc_revision RevParam) { Rev = RevParam; } + + evmc::Result call(const evmc_message& Msg) noexcept override { + // Record the access + if (recorded_account_accesses.empty()) + recorded_account_accesses.reserve(200); + if (recorded_account_accesses.size() < 200) + recorded_account_accesses.emplace_back(Msg.recipient); + + if (Msg.kind == EVMC_CREATE || Msg.kind == EVMC_CREATE2) { + evmc::address NewAddress; + if (Msg.kind == EVMC_CREATE) { + NewAddress = computeCreateAddress(Msg.sender, accounts[Msg.sender].nonce++); + } else { + accounts[Msg.sender].nonce++; // nonce is incremented for both CREATE and CREATE2 + + std::vector InitCode(Msg.input_data, Msg.input_data + Msg.input_size); + std::vector InitCodeHash = zen::host::evm::crypto::keccak256(InitCode); + + std::vector Buffer; + Buffer.reserve(1 + sizeof(Msg.sender.bytes) + sizeof(Msg.create2_salt.bytes) + InitCodeHash.size()); + Buffer.push_back(0xff); + Buffer.insert(Buffer.end(), std::begin(Msg.sender.bytes), std::end(Msg.sender.bytes)); + Buffer.insert(Buffer.end(), std::begin(Msg.create2_salt.bytes), std::end(Msg.create2_salt.bytes)); + Buffer.insert(Buffer.end(), InitCodeHash.begin(), InitCodeHash.end()); + + std::vector FinalHash = zen::host::evm::crypto::keccak256(Buffer); + std::copy_n(FinalHash.end() - sizeof(NewAddress.bytes), sizeof(NewAddress.bytes), NewAddress.bytes); + } + + // Create a new message for execution with the computed recipient + evmc_message ExecMsg = Msg; + ExecMsg.recipient = NewAddress; + ExecMsg.input_data = nullptr; + ExecMsg.input_size = 0; + + // For CREATE, we execute the init code + evmc::Result Result = Vm->execute(*this, Rev, ExecMsg, Msg.input_data, Msg.input_size); + if (Result.status_code == EVMC_SUCCESS && Result.output_size > 0) { + // Save the deployed code + auto& Account = accounts[NewAddress]; + Account.code = evmc::bytes(Result.output_data, Result.output_size); + } + Result.create_address = NewAddress; + return Result; + } else { + // Normal CALL + auto It = accounts.find(Msg.recipient); + if (It == accounts.end() || It->second.code.empty()) { + // No code, just transfer value and return success + return evmc::Result{EVMC_SUCCESS, Msg.gas, 0, nullptr, 0}; + } + const auto& Code = It->second.code; + return Vm->execute(*this, Rev, Msg, Code.data(), Code.size()); + } + } +}; + +static std::unique_ptr GlobalVm; +static evmc_revision GlobalRev = EVMC_CANCUN; + +static std::map SetupHostFromContractTest(EVMCBenchmarkHost& Host, const SolidityContractTestData& ContractTest, uint64_t GasLimit) { + evmc::address Deployer = parseAddress("1000000000000000000000000000000000000000"); + auto& DeployerAcc = Host.accounts[Deployer]; + DeployerAcc.nonce = 0; + DeployerAcc.set_balance(100000000000ULL); + + // Precompute all contract addresses so forward references work + std::map ResolvedAddresses; + for (size_t I = 0; I < ContractTest.DeployContracts.size(); ++I) { + ResolvedAddresses[ContractTest.DeployContracts[I]] = + computeCreateAddress(Deployer, I); + } + + std::map DeployedAddresses; + + for (const std::string& Name : ContractTest.DeployContracts) { + auto It = ContractTest.ContractDataMap.find(Name); + if (It == ContractTest.ContractDataMap.end()) { + throw std::runtime_error("Contract data not found for: " + Name); + } + + std::vector> CtorArgs; + auto ArgsIt = ContractTest.ConstructorArgs.find(Name); + if (ArgsIt != ContractTest.ConstructorArgs.end()) { + CtorArgs = ArgsIt->second; + } + + std::string DeployHex = It->second.DeployBytecode + encodeConstructorParams(CtorArgs, ResolvedAddresses); + auto DeployBytecode = fromHex(DeployHex); + if (!DeployBytecode) { + throw std::runtime_error("Invalid hex for deployment of " + Name); + } + + evmc::address ContractAddr = computeCreateAddress(Deployer, Host.accounts[Deployer].nonce); + + evmc_message Msg = {}; + Msg.kind = EVMC_CREATE; + Msg.gas = GasLimit; + Msg.recipient = ContractAddr; + Msg.sender = Deployer; + Msg.input_data = DeployBytecode->data(); + Msg.input_size = DeployBytecode->size(); + Msg.depth = 0; + + evmc::Result Res = GlobalVm->execute(Host, GlobalRev, Msg, DeployBytecode->data(), DeployBytecode->size()); + if (Res.status_code != EVMC_SUCCESS) { + throw std::runtime_error("Deploy failed for " + Name + " status: " + std::to_string(Res.status_code)); + } + + if (Res.output_size > 0) { + Host.accounts[ContractAddr].code = evmc::bytes(Res.output_data, Res.output_size); + } + + Host.accounts[Deployer].nonce++; + DeployedAddresses[Name] = ContractAddr; + } + return DeployedAddresses; +} + +// Helper function to build calldata from a test case +static std::vector BuildCalldata(const SolidityTestCase& Tc, + const std::map& Addrs) { + // Currently, SolidityTestCase does not expose typed arguments; rely on raw calldata. + (void)Addrs; // unused until dynamic argument encoding is wired through SolidityTestCase + if (!Tc.Calldata.empty()) { + auto Opt = fromHex(Tc.Calldata); + if (Opt) return *Opt; + } + return {}; +} + +class ContractBenchmark { +public: + SolidityContractTestData ContractTest; + SolidityTestCase TestCase; + std::unique_ptr Host; + evmc::address ContractAddress; + std::vector Calldata; + std::map DeployedAddresses; + + ContractBenchmark(const SolidityContractTestData& TestData, const SolidityTestCase& CaseData) + : ContractTest(TestData), TestCase(CaseData) {} + + void SetUp() { + Host = std::make_unique(); + Host->SetVm(GlobalVm.get()); + Host->SetRevision(GlobalRev); + + DeployedAddresses = SetupHostFromContractTest(*Host, ContractTest, 0xFFFFFFFFFFFF); + + evmc::address Deployer = parseAddress("1000000000000000000000000000000000000000"); + + // Run setup test cases (name starts with "setup_") + for (const auto& SetupTc : ContractTest.TestCases) { + if (SetupTc.Name.rfind("setup_", 0) != 0) continue; + auto SetupCd = BuildCalldata(SetupTc, DeployedAddresses); + if (SetupCd.empty()) continue; + + evmc::address Target = DeployedAddresses[SetupTc.Contract]; + evmc_message SetupMsg = {}; + SetupMsg.kind = EVMC_CALL; + SetupMsg.gas = 0xFFFFFFFFFFFF; + SetupMsg.recipient = Target; + SetupMsg.sender = Deployer; + SetupMsg.input_data = SetupCd.data(); + SetupMsg.input_size = SetupCd.size(); + GlobalVm->execute(*Host, GlobalRev, SetupMsg, + Host->accounts[Target].code.data(), + Host->accounts[Target].code.size()); + } + + ContractAddress = DeployedAddresses[TestCase.Contract]; + Calldata = BuildCalldata(TestCase, DeployedAddresses); + } + + void TearDown() { + Host.reset(); + } +}; + +static void RegisterBenchmarks() { + std::filesystem::path TestsRoot = "tests/evm_solidity"; + if (!std::filesystem::exists(TestsRoot)) { + TestsRoot = "../tests/evm_solidity"; + } + + std::vector Categories = {"defi", "erc20_bench", "nft", "dao", "layer2"}; + + for (const auto& Cat : Categories) { + std::filesystem::path CatDir = TestsRoot / Cat; + if (!std::filesystem::exists(CatDir)) continue; + + ContractDirectoryInfo DirInfo = checkCaseDirectory(CatDir); + if (!std::filesystem::exists(DirInfo.SolcJsonFile) || !std::filesystem::exists(DirInfo.CasesFile)) continue; + + SolidityContractTestData ContractTest; + parseContractJson(DirInfo.SolcJsonFile, ContractTest.ContractDataMap); + parseTestCaseJson(DirInfo.CasesFile, ContractTest); + + for (const auto& Tc : ContractTest.TestCases) { + if (Tc.Name.rfind("setup_", 0) == 0) continue; + + std::string BenchName = Cat + "/" + Tc.Name; + + // We use lambda to capture test data + benchmark::RegisterBenchmark(BenchName.c_str(), [ContractTest, Tc](benchmark::State& State) { + ContractBenchmark Fixture(ContractTest, Tc); + Fixture.SetUp(); + + evmc_message Msg = {}; + Msg.kind = EVMC_CALL; + Msg.gas = 0xFFFFFFFFFFFF; + Msg.recipient = Fixture.ContractAddress; + Msg.sender = parseAddress("1000000000000000000000000000000000000000"); + Msg.input_data = Fixture.Calldata.data(); + Msg.input_size = Fixture.Calldata.size(); + + // Warm-up: trigger JIT compilation outside timed loop + { + // Save host state so warm-up does not affect benchmark iterations + auto SavedAccounts = Fixture.Host->accounts; + + const auto& Code = Fixture.Host->accounts[Fixture.ContractAddress].code; + auto Warmup = GlobalVm->execute(*Fixture.Host, GlobalRev, Msg, Code.data(), Code.size()); + benchmark::DoNotOptimize(Warmup); + + // Restore host state to ensure clean, reproducible benchmark runs + Fixture.Host->accounts = std::move(SavedAccounts); + } + + for (auto _ : State) { + const auto& Code = Fixture.Host->accounts[Fixture.ContractAddress].code; + evmc::Result Res = GlobalVm->execute(*Fixture.Host, GlobalRev, Msg, Code.data(), Code.size()); + benchmark::DoNotOptimize(Res); + } + + Fixture.TearDown(); + })->Unit(benchmark::kMicrosecond); + } + } +} + +int main(int Argc, char** Argv) { + // Parse our custom args before benchmark::Initialize + std::string VmConfig; + + std::vector BArgv; + BArgv.push_back(Argv[0]); + + for (int I = 1; I < Argc; ++I) { + std::string Arg = Argv[I]; + if (Arg == "--vm" && I + 1 < Argc) { + VmConfig = Argv[++I]; + } else { + BArgv.push_back(Argv[I]); + } + } + + int BArgc = static_cast(BArgv.size()); + benchmark::Initialize(&BArgc, BArgv.data()); + + if (!VmConfig.empty()) { + evmc_loader_error_code Ec; + evmc::VM Vm{evmc_load_and_configure(VmConfig.c_str(), &Ec)}; + if (Ec != EVMC_LOADER_SUCCESS) { + std::cerr << "Failed to load EVMC VM from " << VmConfig << "\n"; + return 1; + } + GlobalVm = std::make_unique(std::move(Vm)); + } else { + std::cerr << "Usage: " << Argv[0] << " --vm [benchmark_options]\n"; + return 1; + } + + RegisterBenchmarks(); + return benchmark::RunSpecifiedBenchmarks(); +} diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index fc639044d..fbd184a25 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -596,7 +596,7 @@ template class EVMByteCodeVisitor { Ip++; PC++; } - if (PC > RunStartPC && !InDeadCode) { + if (PC > RunStartPC) { Builder.meterOpcodeRange(RunStartPC, PC); } handleEndBlock(); diff --git a/src/compiler/evm_frontend/evm_imported.cpp b/src/compiler/evm_frontend/evm_imported.cpp index 4a9aae27b..46687dbd5 100644 --- a/src/compiler/evm_frontend/evm_imported.cpp +++ b/src/compiler/evm_frontend/evm_imported.cpp @@ -794,10 +794,13 @@ const uint8_t *evmHandleCreateInternal(zen::runtime::EVMInstance *Instance, // Track subcall refund (may be negative) Instance->addGasRefund(Result.gas_refund); - std::vector ReturnData(Result.output_data, - Result.output_data + Result.output_size); - Instance->setReturnData(std::move(ReturnData)); - + if (Result.status_code == EVMC_REVERT) { + std::vector ReturnData(Result.output_data, + Result.output_data + Result.output_size); + Instance->setReturnData(std::move(ReturnData)); + } else { + Instance->setReturnData({}); + } if (Result.status_code == EVMC_SUCCESS) { static thread_local uint8_t PaddedAddress[32] = {0}; memcpy(PaddedAddress + 12, Result.create_address.bytes, 20); diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 40f1e4ffb..e43ce5b9a 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -2713,18 +2713,6 @@ EVMMirBuilder::handleMLoad(Operand AddrComponents) { Operand Bytes32Op(MemPtr, EVMType::BYTES32); Operand Result = convertBytes32ToU256Operand(Bytes32Op); - - // Pin loaded values into local variables so the backend cannot reschedule - // the memory reads past later function calls (e.g. CODECOPY / MSTORE) that - // may modify the same memory region. Without this, an MLOAD result that - // stays on the EVM stack across a memory-writing opcode could observe the - // *new* contents instead of the value at the time of the MLOAD. - U256Inst Parts = extractU256Operand(Result); - for (int I = 0; I < static_cast(EVM_ELEMENTS_COUNT); ++I) { - Parts[I] = protectUnsafeValue(Parts[I], I64Type); - } - Result = Operand(Parts, EVMType::UINT256); - #ifdef ZEN_ENABLE_EVM_GAS_REGISTER reloadGasFromMemory(); #endif @@ -3152,6 +3140,9 @@ void EVMMirBuilder::handleRevert(Operand OffsetOp, Operand SizeOp) { MBasicBlock *PostRevertBB = createBasicBlock(); setInsertBlock(PostRevertBB); +#ifdef ZEN_ENABLE_EVM_GAS_REGISTER + reloadGasFromMemory(); +#endif } void EVMMirBuilder::handleInvalid() { diff --git a/src/evm/evm_cache.cpp b/src/evm/evm_cache.cpp index eb0ee4d04..d24d174d6 100644 --- a/src/evm/evm_cache.cpp +++ b/src/evm/evm_cache.cpp @@ -1117,7 +1117,7 @@ static bool buildGasChunksSPP(const zen::common::Byte *Code, size_t CodeSize, continue; } GasChunkEnd[Blocks[Id].Start] = Blocks[Id].End; - GasChunkCost[Blocks[Id].Start] = Blocks[Id].Cost; + GasChunkCost[Blocks[Id].Start] = Metering[Id]; } return true; diff --git a/src/evm/interpreter.cpp b/src/evm/interpreter.cpp index 95f6bc4c0..c98d649da 100644 --- a/src/evm/interpreter.cpp +++ b/src/evm/interpreter.cpp @@ -765,15 +765,6 @@ void BaseInterpreter::interpret() { Byte OpcodeByte = Code[Frame->Pc]; evmc_opcode Op = static_cast(OpcodeByte); - const uint8_t OpcodeU8 = static_cast(OpcodeByte); - - if (NamesTable[OpcodeU8] == NULL) { - Context.setStatus(EVMC_UNDEFINED_INSTRUCTION); - if (handleExecutionStatus(Frame, Context)) { - return; - } - break; - } switch (Op) { case evmc_opcode::OP_STOP: diff --git a/src/tests/evm_fallback_execution_tests.cpp b/src/tests/evm_fallback_execution_tests.cpp index 31223a1ab..bdb7533f9 100644 --- a/src/tests/evm_fallback_execution_tests.cpp +++ b/src/tests/evm_fallback_execution_tests.cpp @@ -157,8 +157,8 @@ TEST_F(EVMFallbackExecutionTest, MultipleFallbackTriggers) { evmc_result Result = executeBytecode(Bytecode); - // 0xEE is not a valid EVM opcode, so it should be undefined - EXPECT_EQ(Result.status_code, EVMC_UNDEFINED_INSTRUCTION); + // Should handle multiple fallbacks + EXPECT_EQ(Result.status_code, EVMC_INVALID_INSTRUCTION); // Verify gas consumption EXPECT_LT(Result.gas_left, 1000000); diff --git a/src/tests/solidity_contract_tests.cpp b/src/tests/solidity_contract_tests.cpp index 1b329f4f1..e1486e258 100644 --- a/src/tests/solidity_contract_tests.cpp +++ b/src/tests/solidity_contract_tests.cpp @@ -128,6 +128,14 @@ evmc_status_code executeSingleContractTest(const RuntimeConfig &Config, std::map DeployedContracts; std::map DeployedAddresses; + // Precompute addresses for all contracts (needed for constructor args that + // reference not-yet-deployed contracts) + std::map ResolvedAddresses; + for (size_t I = 0; I < ContractTest.DeployContracts.size(); ++I) { + ResolvedAddresses[ContractTest.DeployContracts[I]] = + TestEnv.MockedHost->computeCreateAddress(TestEnv.DeployerAddr, I); + } + // Step 1: Deploy all specified contracts for (const std::string &NowContractName : ContractTest.DeployContracts) { auto ContractIt = ContractTest.ContractDataMap.find(NowContractName); @@ -143,7 +151,7 @@ evmc_status_code executeSingleContractTest(const RuntimeConfig &Config, try { DeployedContract Deployed = deployContract(TestEnv, NowContractName, ContractData, Ctorargs, - DeployedAddresses, GasLimit); + ResolvedAddresses, GasLimit); DeployedContracts[NowContractName] = Deployed; DeployedAddresses[NowContractName] = Deployed.Address; @@ -164,13 +172,20 @@ evmc_status_code executeSingleContractTest(const RuntimeConfig &Config, << std::endl; return EVMC_FAILURE; } - if (TestCase.Calldata.empty()) { + + std::string CalldataToUse; + if (!TestCase.Args.empty() && !TestCase.Function.empty()) { + CalldataToUse = computeFunctionSelector(TestCase.Function) + + encodeConstructorParams(TestCase.Args, DeployedAddresses); + } else if (!TestCase.Calldata.empty()) { + CalldataToUse = TestCase.Calldata; + } else { throw getError(ErrorCode::InvalidRawData); } const auto &Contract = InstanceIt->second; evmc::Result CallResult = - executeContractCall(TestEnv, Contract, TestCase.Calldata, GasLimit); + executeContractCall(TestEnv, Contract, CalldataToUse, GasLimit); if (checkResult(TestCase, CallResult) != EVMC_SUCCESS) { AllCasePassed = false; } diff --git a/src/tests/solidity_test_helpers.cpp b/src/tests/solidity_test_helpers.cpp index 24d9e8462..d6d1e0f67 100644 --- a/src/tests/solidity_test_helpers.cpp +++ b/src/tests/solidity_test_helpers.cpp @@ -238,9 +238,19 @@ void parseTestCasesFromJson(const rapidjson::Document &Doc, Test.Function = TestCase["function"].GetString(); } + if (TestCase.HasMember("args") && TestCase["args"].IsArray()) { + for (const auto &Arg : TestCase["args"].GetArray()) { + if (Arg.HasMember("type") && Arg["type"].IsString() && + Arg.HasMember("value") && Arg["value"].IsString()) { + Test.Args.emplace_back(Arg["type"].GetString(), + Arg["value"].GetString()); + } + } + } + if (TestCase.HasMember("calldata") && TestCase["calldata"].IsString()) { Test.Calldata = TestCase["calldata"].GetString(); - } else if (!Test.Function.empty()) { + } else if (!Test.Function.empty() && Test.Args.empty()) { std::string FunctionSelector = computeFunctionSelector(Test.Function); if (!FunctionSelector.empty()) { Test.Calldata = FunctionSelector; @@ -248,7 +258,7 @@ void parseTestCasesFromJson(const rapidjson::Document &Doc, // Skip test case if can't compute calldata continue; } - } else { + } else if (Test.Function.empty() && Test.Calldata.empty()) { // Skip test case if neither calldata nor function provided continue; } diff --git a/src/tests/solidity_test_helpers.h b/src/tests/solidity_test_helpers.h index dbd13ed98..085f81aec 100644 --- a/src/tests/solidity_test_helpers.h +++ b/src/tests/solidity_test_helpers.h @@ -21,6 +21,7 @@ struct SolidityTestCase { std::string Expected; std::string Contract; std::string Calldata; + std::vector> Args; }; // contract.json structures diff --git a/tests/evm_solidity/dao/DAOWrapper.sol b/tests/evm_solidity/dao/DAOWrapper.sol new file mode 100644 index 000000000..d348353bd --- /dev/null +++ b/tests/evm_solidity/dao/DAOWrapper.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./SimpleGovernor.sol"; +import "./MultiSigWallet.sol"; + +contract DAOWrapper { + SimpleGovernor public governor; + MultiSigWallet public wallet; + + constructor(address _governor, address payable _wallet) { + governor = SimpleGovernor(_governor); + wallet = MultiSigWallet(_wallet); + } + + function testGovernor() public returns (bool) { + uint256 pid = governor.propose("Upgrade system"); + governor.vote(pid, true); + return governor.execute(pid); + } + + function testMultiSig() public returns (bool) { + uint256 txId = wallet.submitTransaction(address(0x456), 0, "0x"); + wallet.confirmTransaction(txId); + return wallet.executeTransaction(txId); + } +} diff --git a/tests/evm_solidity/dao/MultiSigWallet.sol b/tests/evm_solidity/dao/MultiSigWallet.sol new file mode 100644 index 000000000..6cac60c48 --- /dev/null +++ b/tests/evm_solidity/dao/MultiSigWallet.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MultiSigWallet { + event Deposit(address indexed sender, uint256 amount, uint256 balance); + event SubmitTransaction(address indexed owner, uint256 indexed txIndex, address indexed to, uint256 value, bytes data); + event ConfirmTransaction(address indexed owner, uint256 indexed txIndex); + event RevokeConfirmation(address indexed owner, uint256 indexed txIndex); + event ExecuteTransaction(address indexed owner, uint256 indexed txIndex); + + address[] public owners; + mapping(address => bool) public isOwner; + uint256 public numConfirmationsRequired; + + struct Transaction { + address to; + uint256 value; + bytes data; + bool executed; + uint256 numConfirmations; + } + + mapping(uint256 => mapping(address => bool)) public isConfirmed; + Transaction[] public transactions; + + constructor(address owner1, address owner2, uint256 _numConfirmationsRequired) { + require(owner1 != address(0) && owner2 != address(0), "invalid owner"); + require(_numConfirmationsRequired > 0 && _numConfirmationsRequired <= 2, "invalid number of required confirmations"); + + isOwner[owner1] = true; + isOwner[owner2] = true; + owners.push(owner1); + owners.push(owner2); + + numConfirmationsRequired = _numConfirmationsRequired; + } + + receive() external payable { + emit Deposit(msg.sender, msg.value, address(this).balance); + } + + function submitTransaction(address _to, uint256 _value, bytes memory _data) public returns (uint256) { + require(isOwner[msg.sender], "not owner"); + uint256 txIndex = transactions.length; + + transactions.push(Transaction({ + to: _to, + value: _value, + data: _data, + executed: false, + numConfirmations: 0 + })); + + emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data); + return txIndex; + } + + function confirmTransaction(uint256 _txIndex) public { + require(isOwner[msg.sender], "not owner"); + require(_txIndex < transactions.length, "tx does not exist"); + require(!transactions[_txIndex].executed, "tx already executed"); + require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed"); + + Transaction storage transaction = transactions[_txIndex]; + transaction.numConfirmations += 1; + isConfirmed[_txIndex][msg.sender] = true; + + emit ConfirmTransaction(msg.sender, _txIndex); + } + + function executeTransaction(uint256 _txIndex) public returns (bool) { + require(isOwner[msg.sender], "not owner"); + require(_txIndex < transactions.length, "tx does not exist"); + require(!transactions[_txIndex].executed, "tx already executed"); + require(transactions[_txIndex].numConfirmations >= numConfirmationsRequired, "cannot execute tx"); + + Transaction storage transaction = transactions[_txIndex]; + transaction.executed = true; + + // Simulate execution + // (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data); + // require(success, "tx failed"); + + emit ExecuteTransaction(msg.sender, _txIndex); + return true; + } +} diff --git a/tests/evm_solidity/dao/SimpleGovernor.sol b/tests/evm_solidity/dao/SimpleGovernor.sol new file mode 100644 index 000000000..05fc93892 --- /dev/null +++ b/tests/evm_solidity/dao/SimpleGovernor.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract SimpleGovernor { + struct Proposal { + uint256 id; + address proposer; + string description; + uint256 forVotes; + uint256 againstVotes; + bool executed; + mapping(address => bool) hasVoted; + } + + uint256 public proposalCount; + mapping(uint256 => Proposal) public proposals; + + function propose(string memory description) public returns (uint256) { + proposalCount++; + Proposal storage p = proposals[proposalCount]; + p.id = proposalCount; + p.proposer = msg.sender; + p.description = description; + return proposalCount; + } + + function vote(uint256 proposalId, bool support) public { + Proposal storage p = proposals[proposalId]; + require(p.id != 0, "Proposal does not exist"); + require(!p.hasVoted[msg.sender], "Already voted"); + require(!p.executed, "Already executed"); + + p.hasVoted[msg.sender] = true; + if (support) { + p.forVotes++; + } else { + p.againstVotes++; + } + } + + function execute(uint256 proposalId) public returns (bool) { + Proposal storage p = proposals[proposalId]; + require(p.id != 0, "Proposal does not exist"); + require(!p.executed, "Already executed"); + require(p.forVotes > p.againstVotes, "Proposal failed"); + + p.executed = true; + return true; + } +} diff --git a/tests/evm_solidity/dao/test_cases.json b/tests/evm_solidity/dao/test_cases.json new file mode 100644 index 000000000..3785a94ea --- /dev/null +++ b/tests/evm_solidity/dao/test_cases.json @@ -0,0 +1,34 @@ +{ + "skip": false, + "main_contract": "DAOWrapper", + "deploy_contracts": [ + "SimpleGovernor", + "MultiSigWallet", + "DAOWrapper" + ], + "constructor_args": { + "MultiSigWallet": [ + {"type": "address", "value": "0x1000000000000000000000000000000000000000"}, + {"type": "address", "value": "DAOWrapper"}, + {"type": "uint256", "value": "1"} + ], + "DAOWrapper": [ + {"type": "address", "value": "SimpleGovernor"}, + {"type": "address", "value": "MultiSigWallet"} + ] + }, + "test_cases": [ + { + "name": "test_governor", + "function": "testGovernor()", + "calldata": "634a9562", + "expected": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "test_multisig", + "function": "testMultiSig()", + "calldata": "158f00cf", + "expected": "0000000000000000000000000000000000000000000000000000000000000001" + } + ] +} \ No newline at end of file diff --git a/tests/evm_solidity/defi/DeFiWrapper.sol b/tests/evm_solidity/defi/DeFiWrapper.sol new file mode 100644 index 000000000..9d59ea32d --- /dev/null +++ b/tests/evm_solidity/defi/DeFiWrapper.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./SimpleDEX.sol"; +import "./LendingPool.sol"; + +contract DeFiWrapper { + SimpleDEX public dex; + LendingPool public pool; + + constructor(address _dex, address _pool) { + dex = SimpleDEX(_dex); + pool = LendingPool(_pool); + } + + function testDexSwap() public returns (uint256) { + dex.init(1000000, 1000000); + return dex.swapAForB(1000); // expect 996 + } + + function testLending() public returns (uint256) { + pool.deposit(address(this), 5000); + pool.borrow(address(this), 2000); + pool.repay(address(this), 1000); + return pool.getUtilization(); // (1000 * 100) / 5000 = 20 + } +} diff --git a/tests/evm_solidity/defi/LendingPool.sol b/tests/evm_solidity/defi/LendingPool.sol new file mode 100644 index 000000000..56921dbe1 --- /dev/null +++ b/tests/evm_solidity/defi/LendingPool.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract LendingPool { + mapping(address => uint256) public deposits; + mapping(address => uint256) public borrows; + + uint256 public totalDeposits; + uint256 public totalBorrows; + + function deposit(address user, uint256 amount) public { + deposits[user] += amount; + totalDeposits += amount; + } + + function borrow(address user, uint256 amount) public { + require(deposits[user] * 2 >= borrows[user] + amount, "Insufficient collateral"); + borrows[user] += amount; + totalBorrows += amount; + } + + function repay(address user, uint256 amount) public { + require(borrows[user] >= amount, "Repaying more than borrowed"); + borrows[user] -= amount; + totalBorrows -= amount; + } + + function getUtilization() public view returns (uint256) { + if (totalDeposits == 0) return 0; + return (totalBorrows * 100) / totalDeposits; + } +} diff --git a/tests/evm_solidity/defi/SimpleDEX.sol b/tests/evm_solidity/defi/SimpleDEX.sol new file mode 100644 index 000000000..1becf6b59 --- /dev/null +++ b/tests/evm_solidity/defi/SimpleDEX.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract SimpleDEX { + uint256 public reserveA; + uint256 public reserveB; + + function init(uint256 _amountA, uint256 _amountB) public { + require(reserveA == 0 && reserveB == 0, "Already initialized"); + reserveA = _amountA; + reserveB = _amountB; + } + + function swapAForB(uint256 amountAIn) public returns (uint256 amountBOut) { + require(amountAIn > 0, "Invalid amount"); + + uint256 amountInWithFee = amountAIn * 997; + uint256 numerator = amountInWithFee * reserveB; + uint256 denominator = (reserveA * 1000) + amountInWithFee; + amountBOut = numerator / denominator; + + reserveA += amountAIn; + reserveB -= amountBOut; + } + + function swapBForA(uint256 amountBIn) public returns (uint256 amountAOut) { + require(amountBIn > 0, "Invalid amount"); + + uint256 amountInWithFee = amountBIn * 997; + uint256 numerator = amountInWithFee * reserveA; + uint256 denominator = (reserveB * 1000) + amountInWithFee; + amountAOut = numerator / denominator; + + reserveB += amountBIn; + reserveA -= amountAOut; + } + + function getReserves() public view returns (uint256, uint256) { + return (reserveA, reserveB); + } +} diff --git a/tests/evm_solidity/defi/test_cases.json b/tests/evm_solidity/defi/test_cases.json new file mode 100644 index 000000000..80f4d3a42 --- /dev/null +++ b/tests/evm_solidity/defi/test_cases.json @@ -0,0 +1,29 @@ +{ + "skip": false, + "main_contract": "DeFiWrapper", + "deploy_contracts": [ + "SimpleDEX", + "LendingPool", + "DeFiWrapper" + ], + "constructor_args": { + "DeFiWrapper": [ + {"type": "address", "value": "SimpleDEX"}, + {"type": "address", "value": "LendingPool"} + ] + }, + "test_cases": [ + { + "name": "test_dex_swap", + "function": "testDexSwap()", + "calldata": "5412f010", + "expected": "00000000000000000000000000000000000000000000000000000000000003e4" + }, + { + "name": "test_lending", + "function": "testLending()", + "calldata": "db3b1fdd", + "expected": "0000000000000000000000000000000000000000000000000000000000000014" + } + ] +} \ No newline at end of file diff --git a/tests/evm_solidity/erc20_bench/ERC20BenchWrapper.sol b/tests/evm_solidity/erc20_bench/ERC20BenchWrapper.sol new file mode 100644 index 000000000..7b00a4912 --- /dev/null +++ b/tests/evm_solidity/erc20_bench/ERC20BenchWrapper.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./PausableBurnableERC20.sol"; +import "./FeeOnTransferERC20.sol"; + +contract ERC20BenchWrapper { + PausableBurnableERC20 public pbToken; + FeeOnTransferERC20 public fotToken; + + constructor(address _pbToken, address _fotToken) { + pbToken = PausableBurnableERC20(_pbToken); + fotToken = FeeOnTransferERC20(_fotToken); + } + + function testPausableBurnable() public returns (uint256) { + pbToken.transfer(address(0x123), 1000); + pbToken.burn(500); + return pbToken.totalSupply(); // 1000000 - 500 = 999500 + } + + function testFeeOnTransfer() public returns (uint256) { + fotToken.transfer(address(0x456), 1000); + // Fee is 5% of 1000 = 50. Receiver (this contract) gets 50. + // address(0x456) gets 950. + return fotToken.balanceOf(address(0x456)); // 950 + } +} diff --git a/tests/evm_solidity/erc20_bench/FeeOnTransferERC20.sol b/tests/evm_solidity/erc20_bench/FeeOnTransferERC20.sol new file mode 100644 index 000000000..f9328c6d9 --- /dev/null +++ b/tests/evm_solidity/erc20_bench/FeeOnTransferERC20.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract FeeOnTransferERC20 { + mapping(address => uint256) public balanceOf; + uint256 public totalSupply; + uint256 public feePercentage; // e.g. 5 for 5% + address public feeReceiver; + + constructor(uint256 _initialSupply, uint256 _feePercentage, address _feeReceiver) { + totalSupply = _initialSupply; + balanceOf[msg.sender] = _initialSupply; + feePercentage = _feePercentage; + feeReceiver = _feeReceiver; + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(balanceOf[msg.sender] >= amount, "ERC20: transfer amount exceeds balance"); + + uint256 fee = (amount * feePercentage) / 100; + uint256 amountAfterFee = amount - fee; + + balanceOf[msg.sender] -= amount; + balanceOf[feeReceiver] += fee; + balanceOf[to] += amountAfterFee; + + return true; + } +} diff --git a/tests/evm_solidity/erc20_bench/PausableBurnableERC20.sol b/tests/evm_solidity/erc20_bench/PausableBurnableERC20.sol new file mode 100644 index 000000000..127ed880d --- /dev/null +++ b/tests/evm_solidity/erc20_bench/PausableBurnableERC20.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract PausableBurnableERC20 { + mapping(address => uint256) public balanceOf; + uint256 public totalSupply; + bool public paused; + address public owner; + + constructor(uint256 _initialSupply) { + owner = msg.sender; + totalSupply = _initialSupply; + balanceOf[msg.sender] = _initialSupply; + } + + modifier whenNotPaused() { + require(!paused, "Pausable: paused"); + _; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Ownable: caller is not the owner"); + _; + } + + function pause() public onlyOwner { + paused = true; + } + + function unpause() public onlyOwner { + paused = false; + } + + function transfer(address to, uint256 amount) public whenNotPaused returns (bool) { + require(balanceOf[msg.sender] >= amount, "ERC20: transfer amount exceeds balance"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + return true; + } + + function burn(uint256 amount) public whenNotPaused { + require(balanceOf[msg.sender] >= amount, "ERC20: burn amount exceeds balance"); + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + } +} diff --git a/tests/evm_solidity/erc20_bench/test_cases.json b/tests/evm_solidity/erc20_bench/test_cases.json new file mode 100644 index 000000000..97084720b --- /dev/null +++ b/tests/evm_solidity/erc20_bench/test_cases.json @@ -0,0 +1,51 @@ +{ + "skip": false, + "main_contract": "ERC20BenchWrapper", + "deploy_contracts": ["PausableBurnableERC20", "FeeOnTransferERC20", "ERC20BenchWrapper"], + "constructor_args": { + "PausableBurnableERC20": [{"type": "uint256", "value": "1000000"}], + "FeeOnTransferERC20": [ + {"type": "uint256", "value": "1000000"}, + {"type": "uint256", "value": "5"}, + {"type": "address", "value": "0x1000000000000000000000000000000000000000"} + ], + "ERC20BenchWrapper": [ + {"type": "address", "value": "PausableBurnableERC20"}, + {"type": "address", "value": "FeeOnTransferERC20"} + ] + }, + "test_cases": [ + { + "name": "setup_fund_pb", + "function": "transfer(address,uint256)", + "contract": "PausableBurnableERC20", + "expected": "0000000000000000000000000000000000000000000000000000000000000001", + "args": [ + {"type": "address", "value": "ERC20BenchWrapper"}, + {"type": "uint256", "value": "1000000"} + ] + }, + { + "name": "setup_fund_fot", + "function": "transfer(address,uint256)", + "contract": "FeeOnTransferERC20", + "expected": "0000000000000000000000000000000000000000000000000000000000000001", + "args": [ + {"type": "address", "value": "ERC20BenchWrapper"}, + {"type": "uint256", "value": "1000000"} + ] + }, + { + "name": "test_pausable_burnable", + "function": "testPausableBurnable()", + "calldata": "bae4cf7e", + "expected": "00000000000000000000000000000000000000000000000000000000000f404c" + }, + { + "name": "test_fee_on_transfer", + "function": "testFeeOnTransfer()", + "calldata": "cd0685ca", + "expected": "00000000000000000000000000000000000000000000000000000000000003b6" + } + ] +} diff --git a/tests/evm_solidity/layer2/Layer2Wrapper.sol b/tests/evm_solidity/layer2/Layer2Wrapper.sol new file mode 100644 index 000000000..020ef5aef --- /dev/null +++ b/tests/evm_solidity/layer2/Layer2Wrapper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./RollupState.sol"; +import "./MerkleProofVerifier.sol"; + +contract Layer2Wrapper { + RollupState public rollup; + MerkleProofVerifier public verifier; + + constructor(address _rollup, address _verifier) { + rollup = RollupState(_rollup); + verifier = MerkleProofVerifier(_verifier); + } + + function testRollup() public returns (uint256) { + bytes memory batchData = new bytes(64); + // Fill with some dummy data + for (uint i = 0; i < 64; i++) { + batchData[i] = bytes1(uint8(i)); + } + bytes32 newRoot = keccak256("new_root"); + rollup.commitBatch(newRoot, batchData); + return rollup.batchHeight(); // 1 + } + + function testMerkle() public view returns (bool) { + bytes32 leaf = keccak256("leaf"); + bytes32 sibling = keccak256("sibling"); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = sibling; + + bytes32 root; + if (leaf <= sibling) { + root = keccak256(abi.encodePacked(leaf, sibling)); + } else { + root = keccak256(abi.encodePacked(sibling, leaf)); + } + + return verifier.verify(proof, root, leaf); // true + } +} diff --git a/tests/evm_solidity/layer2/MerkleProofVerifier.sol b/tests/evm_solidity/layer2/MerkleProofVerifier.sol new file mode 100644 index 000000000..6433b40ff --- /dev/null +++ b/tests/evm_solidity/layer2/MerkleProofVerifier.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MerkleProofVerifier { + function verify( + bytes32[] memory proof, + bytes32 root, + bytes32 leaf + ) public pure returns (bool) { + bytes32 computedHash = leaf; + + for (uint256 i = 0; i < proof.length; i++) { + bytes32 proofElement = proof[i]; + + if (computedHash <= proofElement) { + // Hash(current computed hash + current element of the proof) + computedHash = keccak256(abi.encodePacked(computedHash, proofElement)); + } else { + // Hash(current element of the proof + current computed hash) + computedHash = keccak256(abi.encodePacked(proofElement, computedHash)); + } + } + + return computedHash == root; + } +} diff --git a/tests/evm_solidity/layer2/RollupState.sol b/tests/evm_solidity/layer2/RollupState.sol new file mode 100644 index 000000000..c5ee6257b --- /dev/null +++ b/tests/evm_solidity/layer2/RollupState.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract RollupState { + bytes32 public stateRoot; + uint256 public batchHeight; + + event StateBatchCommitted(uint256 indexed batchIndex, bytes32 stateRoot); + + function commitBatch(bytes32 _newStateRoot, bytes calldata _batchData) public { + require(_batchData.length > 0, "Empty batch"); + + // Simulate processing batch data + uint256 txCount = _batchData.length / 32; + bytes32 computedRoot = stateRoot; + + for (uint256 i = 0; i < txCount; i++) { + bytes32 txHash; + assembly { + txHash := calldataload(add(_batchData.offset, mul(i, 32))) + } + computedRoot = keccak256(abi.encodePacked(computedRoot, txHash)); + } + + stateRoot = _newStateRoot; + batchHeight++; + emit StateBatchCommitted(batchHeight, stateRoot); + } +} diff --git a/tests/evm_solidity/layer2/test_cases.json b/tests/evm_solidity/layer2/test_cases.json new file mode 100644 index 000000000..d101fb610 --- /dev/null +++ b/tests/evm_solidity/layer2/test_cases.json @@ -0,0 +1,29 @@ +{ + "skip": false, + "main_contract": "Layer2Wrapper", + "deploy_contracts": [ + "RollupState", + "MerkleProofVerifier", + "Layer2Wrapper" + ], + "constructor_args": { + "Layer2Wrapper": [ + {"type": "address", "value": "RollupState"}, + {"type": "address", "value": "MerkleProofVerifier"} + ] + }, + "test_cases": [ + { + "name": "test_rollup", + "function": "testRollup()", + "calldata": "9d47753e", + "expected": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "test_merkle", + "function": "testMerkle()", + "calldata": "4d748da2", + "expected": "0000000000000000000000000000000000000000000000000000000000000001" + } + ] +} \ No newline at end of file diff --git a/tests/evm_solidity/nft/ERC721Enumerable.sol b/tests/evm_solidity/nft/ERC721Enumerable.sol new file mode 100644 index 000000000..5a3f42d8b --- /dev/null +++ b/tests/evm_solidity/nft/ERC721Enumerable.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ERC721Enumerable { + mapping(uint256 => address) private _owners; + mapping(address => uint256) private _balances; + + // Enumerable mappings + mapping(address => mapping(uint256 => uint256)) private _ownedTokens; + mapping(uint256 => uint256) private _ownedTokensIndex; + uint256[] private _allTokens; + mapping(uint256 => uint256) private _allTokensIndex; + + function mint(address to, uint256 tokenId) public { + require(to != address(0), "Mint to zero address"); + require(_owners[tokenId] == address(0), "Token already minted"); + + _balances[to] += 1; + _owners[tokenId] = to; + + _addTokenToOwnerEnumeration(to, tokenId); + _addTokenToAllTokensEnumeration(tokenId); + } + + function burn(uint256 tokenId) public { + address owner = _owners[tokenId]; + require(owner != address(0), "Token does not exist"); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + _removeTokenFromOwnerEnumeration(owner, tokenId); + _removeTokenFromAllTokensEnumeration(tokenId); + } + + function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { + uint256 length = _balances[to] - 1; + _ownedTokens[to][length] = tokenId; + _ownedTokensIndex[tokenId] = length; + } + + function _addTokenToAllTokensEnumeration(uint256 tokenId) private { + _allTokensIndex[tokenId] = _allTokens.length; + _allTokens.push(tokenId); + } + + function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private { + uint256 lastTokenIndex = _balances[from]; + uint256 tokenIndex = _ownedTokensIndex[tokenId]; + + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = _ownedTokens[from][lastTokenIndex]; + _ownedTokens[from][tokenIndex] = lastTokenId; + _ownedTokensIndex[lastTokenId] = tokenIndex; + } + + delete _ownedTokensIndex[tokenId]; + delete _ownedTokens[from][lastTokenIndex]; + } + + function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private { + uint256 lastTokenIndex = _allTokens.length - 1; + uint256 tokenIndex = _allTokensIndex[tokenId]; + + uint256 lastTokenId = _allTokens[lastTokenIndex]; + + _allTokens[tokenIndex] = lastTokenId; + _allTokensIndex[lastTokenId] = tokenIndex; + + delete _allTokensIndex[tokenId]; + _allTokens.pop(); + } + + function totalSupply() public view returns (uint256) { + return _allTokens.length; + } +} diff --git a/tests/evm_solidity/nft/NFTWrapper.sol b/tests/evm_solidity/nft/NFTWrapper.sol new file mode 100644 index 000000000..062a0d877 --- /dev/null +++ b/tests/evm_solidity/nft/NFTWrapper.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./ERC721Enumerable.sol"; +import "./OnChainMetadataNFT.sol"; + +contract NFTWrapper { + ERC721Enumerable public enumNFT; + OnChainMetadataNFT public metaNFT; + + constructor(address _enumNFT, address _metaNFT) { + enumNFT = ERC721Enumerable(_enumNFT); + metaNFT = OnChainMetadataNFT(_metaNFT); + } + + function testEnumerableMintBurn() public returns (uint256) { + enumNFT.mint(address(this), 1); + enumNFT.mint(address(this), 2); + enumNFT.burn(1); + return enumNFT.totalSupply(); // should be 1 + } + + function testMetadata() public returns (string memory) { + metaNFT.mint(address(this), 1, "TestNFT", "A test NFT"); + return metaNFT.tokenURI(1); + } +} diff --git a/tests/evm_solidity/nft/OnChainMetadataNFT.sol b/tests/evm_solidity/nft/OnChainMetadataNFT.sol new file mode 100644 index 000000000..88fc1a24e --- /dev/null +++ b/tests/evm_solidity/nft/OnChainMetadataNFT.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract OnChainMetadataNFT { + mapping(uint256 => address) public owners; + mapping(uint256 => string) public tokenNames; + mapping(uint256 => string) public tokenDescriptions; + + function mint(address to, uint256 tokenId, string memory name, string memory desc) public { + owners[tokenId] = to; + tokenNames[tokenId] = name; + tokenDescriptions[tokenId] = desc; + } + + function tokenURI(uint256 tokenId) public view returns (string memory) { + require(owners[tokenId] != address(0), "Token does not exist"); + string memory name = tokenNames[tokenId]; + string memory desc = tokenDescriptions[tokenId]; + + // Simulating JSON metadata construction + string memory json = string(abi.encodePacked( + '{"name": "', name, '", "description": "', desc, '"}' + )); + + return string(abi.encodePacked("data:application/json;base64,", _encodeBase64(bytes(json)))); + } + + function _encodeBase64(bytes memory data) internal pure returns (string memory) { + if (data.length == 0) return ""; + + string memory table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + uint256 encodedLen = 4 * ((data.length + 2) / 3); + string memory result = new string(encodedLen); + + // OpenZeppelin Base64.sol style - see contracts/utils/Base64.sol + assembly { + let tablePtr := add(table, 1) + let resultPtr := add(result, 0x20) + let dataPtr := data + let endPtr := add(data, mload(data)) + let afterPtr := add(endPtr, 0x20) + let afterCache := mload(afterPtr) + mstore(afterPtr, 0x00) + + for {} lt(dataPtr, endPtr) {} { + dataPtr := add(dataPtr, 3) + let input := mload(dataPtr) + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + resultPtr := add(resultPtr, 1) + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + resultPtr := add(resultPtr, 1) + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) + resultPtr := add(resultPtr, 1) + mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) + resultPtr := add(resultPtr, 1) + } + + mstore(afterPtr, afterCache) + + switch mod(mload(data), 3) + case 1 { + mstore8(sub(resultPtr, 1), 0x3d) + mstore8(sub(resultPtr, 2), 0x3d) + } + case 2 { + mstore8(sub(resultPtr, 1), 0x3d) + } + mstore(result, encodedLen) + } + + return result; + } +} diff --git a/tests/evm_solidity/nft/test_cases.json b/tests/evm_solidity/nft/test_cases.json new file mode 100644 index 000000000..6c1dbf83b --- /dev/null +++ b/tests/evm_solidity/nft/test_cases.json @@ -0,0 +1,29 @@ +{ + "skip": false, + "main_contract": "NFTWrapper", + "deploy_contracts": [ + "ERC721Enumerable", + "OnChainMetadataNFT", + "NFTWrapper" + ], + "constructor_args": { + "NFTWrapper": [ + {"type": "address", "value": "ERC721Enumerable"}, + {"type": "address", "value": "OnChainMetadataNFT"} + ] + }, + "test_cases": [ + { + "name": "test_enumerable_mint_burn", + "function": "testEnumerableMintBurn()", + "calldata": "7c7fa45e", + "expected": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "test_metadata", + "function": "testMetadata()", + "calldata": "5ad7ef92", + "expected": "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005d646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c65794a755957316c496a6f67496c526c6333524f526c51694c4341695a47567a59334a7063485270623234694f694169515342305a584e304945354756434a39000000" + } + ] +} \ No newline at end of file diff --git a/tools/check_performance_regression.py b/tools/check_performance_regression.py index d20525ab4..cc01b26ee 100755 --- a/tools/check_performance_regression.py +++ b/tools/check_performance_regression.py @@ -79,7 +79,8 @@ def run_benchmark( cmd.extend(extra_args) if not any(arg.startswith("--benchmark_filter") for arg in cmd): - cmd.append("--benchmark_filter=external/total/*") + # We include external/total/* for standard benchmarks and synthetic benchmarks + cmd.append("--benchmark_filter=external/total/.*") print(f"Running: {' '.join(cmd)}") print(f"Environment: EVMONE_EXTERNAL_OPTIONS={env['EVMONE_EXTERNAL_OPTIONS']}") diff --git a/tools/generate_opcode_benchmarks.py b/tools/generate_opcode_benchmarks.py new file mode 100755 index 000000000..8497286a3 --- /dev/null +++ b/tools/generate_opcode_benchmarks.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +""" +Opcode Microbenchmark Generator for EVM + +This script generates EVM bytecode test cases specifically designed to measure +the execution cost of individual opcodes. It uses data-dependency chaining +and stack-balanced loop bodies to ensure: +1. No stack underflows or overflows during execution. +2. The JIT compiler (in DTVM) cannot optimize away the loops via dead-code elimination. + +The output is formatted as Ethereum State Test JSONs, which can be directly +loaded and benchmarked by `evmone-bench`. +""" + +import json +import os +import argparse +from typing import Dict, List, Any + +# EVM Opcode hex values +OP_STOP = "00" +OP_ADD = "01" +OP_MUL = "02" +OP_SUB = "03" +OP_DIV = "04" +OP_SDIV = "05" +OP_MOD = "06" +OP_SMOD = "07" +OP_ADDMOD = "08" +OP_MULMOD = "09" +OP_EXP = "0a" +OP_SIGNEXTEND = "0b" + +OP_LT = "10" +OP_GT = "11" +OP_SLT = "12" +OP_SGT = "13" +OP_EQ = "14" +OP_ISZERO = "15" +OP_AND = "16" +OP_OR = "17" +OP_XOR = "18" +OP_NOT = "19" +OP_BYTE = "1a" +OP_SHL = "1b" +OP_SHR = "1c" +OP_SAR = "1d" + +OP_SHA3 = "20" + +OP_ADDRESS = "30" +OP_BALANCE = "31" +OP_ORIGIN = "32" +OP_CALLER = "33" +OP_CALLVALUE = "34" +OP_CALLDATALOAD = "35" +OP_CALLDATASIZE = "36" +OP_CALLDATACOPY = "37" +OP_CODESIZE = "38" +OP_CODECOPY = "39" +OP_GASPRICE = "3a" +OP_EXTCODESIZE = "3b" +OP_EXTCODECOPY = "3c" +OP_RETURNDATASIZE = "3d" +OP_RETURNDATACOPY = "3e" +OP_EXTCODEHASH = "3f" + +OP_BLOCKHASH = "40" +OP_COINBASE = "41" +OP_TIMESTAMP = "42" +OP_NUMBER = "43" +OP_DIFFICULTY = "44" +OP_GASLIMIT = "45" +OP_CHAINID = "46" +OP_SELFBALANCE = "47" +OP_BASEFEE = "48" + +OP_POP = "50" +OP_MLOAD = "51" +OP_MSTORE = "52" +OP_MSTORE8 = "53" +OP_SLOAD = "54" +OP_SSTORE = "55" +OP_JUMP = "56" +OP_JUMPI = "57" +OP_PC = "58" +OP_MSIZE = "59" +OP_GAS = "5a" +OP_JUMPDEST = "5b" + +OP_PUSH1 = "60" +OP_PUSH2 = "61" +OP_PUSH32 = "7f" + +OP_DUP1 = "80" +OP_DUP2 = "81" +OP_DUP3 = "82" + +OP_SWAP1 = "90" +OP_SWAP2 = "91" + +OP_RETURN = "f3" +OP_REVERT = "fd" + + +def build_nullary_op(opcode: str, iterations: int) -> str: + """ + Template for opcodes that take 0 inputs and push 1 output (e.g. PC, GAS, ADDRESS). + We initialize an accumulator (0x00) and then loop: ADD. + This creates a data dependency chain. + """ + setup = OP_PUSH1 + "00" # Initial accumulator + loop_body = opcode + OP_ADD + end = OP_PUSH1 + "00" + OP_MSTORE + OP_PUSH1 + "20" + OP_PUSH1 + "00" + OP_RETURN + return setup + (loop_body * iterations) + end + + +def build_unary_op(opcode: str, iterations: int) -> str: + """ + Template for opcodes that take 1 input and push 1 output (e.g. NOT, ISZERO). + We initialize an accumulator (0x01) and then loop: . + Since it takes 1 and leaves 1, the result just keeps feeding back into the opcode. + """ + setup = OP_PUSH1 + "01" # Initial accumulator + loop_body = opcode + end = OP_PUSH1 + "00" + OP_MSTORE + OP_PUSH1 + "20" + OP_PUSH1 + "00" + OP_RETURN + return setup + (loop_body * iterations) + end + + +def build_binary_op(opcode: str, iterations: int) -> str: + """ + Template for opcodes that take 2 inputs and push 1 output (e.g. ADD, MUL, SHL). + Setup: PUSH1 0x01 (Constant), PUSH1 0x01 (Accumulator). + Loop: DUP2 . This duplicates the constant, then applies on (constant, accumulator). + The result becomes the new accumulator. + """ + setup = OP_PUSH1 + "01" + OP_PUSH1 + "01" + loop_body = OP_DUP2 + opcode + end = OP_PUSH1 + "00" + OP_MSTORE + OP_PUSH1 + "20" + OP_PUSH1 + "00" + OP_RETURN + return setup + (loop_body * iterations) + end + + +def build_ternary_op(opcode: str, iterations: int) -> str: + """ + Template for opcodes that take 3 inputs and push 1 output (e.g. ADDMOD, MULMOD). + Setup: PUSH1 0x07 (Modulus), PUSH1 0x01 (Operand), PUSH1 0x01 (Accumulator). + Loop: DUP3 DUP3 . Duplicates the modulus and operand, applying on + (modulus, operand, accumulator). The result becomes the new accumulator. + """ + setup = OP_PUSH1 + "07" + OP_PUSH1 + "01" + OP_PUSH1 + "01" + loop_body = OP_DUP3 + OP_DUP3 + opcode + end = OP_PUSH1 + "00" + OP_MSTORE + OP_PUSH1 + "20" + OP_PUSH1 + "00" + OP_RETURN + return setup + (loop_body * iterations) + end + + +def build_memory_op_mload(iterations: int) -> str: + """ + Template for MLOAD. + We need pointer chasing to defeat DCE. + Setup: Store 0x00 at memory address 0x00. Push 0x00 (pointer). + Loop: MLOAD (reads 0x00 from addr 0x00, which is the new pointer). + """ + # memory[0x00] = 0x00; stack.push(0x00) + setup = OP_PUSH1 + "00" + OP_PUSH1 + "00" + OP_MSTORE + OP_PUSH1 + "00" + loop_body = OP_MLOAD + end = OP_PUSH1 + "00" + OP_MSTORE + OP_PUSH1 + "20" + OP_PUSH1 + "00" + OP_RETURN + return setup + (loop_body * iterations) + end + + +def build_memory_op_mstore(iterations: int) -> str: + """ + Template for MSTORE. + Setup: PUSH1 0x00 (Address), PUSH1 0x01 (Value). + Loop: DUP2 DUP2 MSTORE (duplicate addr and value, then store). + Note: MSTORE doesn't produce an output on stack, so we just keep DUPing. + Actually, DUP2 DUP2 MSTORE consumes 2 items and pushes 0, so DUP2 DUP2 perfectly offsets it. + To create a data dependency, we can increment the value: DUP2 DUP2 MSTORE PUSH1 0x01 ADD. + """ + setup = OP_PUSH1 + "00" + OP_PUSH1 + "00" # Addr, Value + loop_body = OP_DUP2 + OP_DUP2 + OP_MSTORE + OP_PUSH1 + "01" + OP_ADD + end = OP_PUSH1 + "00" + OP_MSTORE + OP_PUSH1 + "20" + OP_PUSH1 + "00" + OP_RETURN + return setup + (loop_body * iterations) + end + + +def generate_state_test(name: str, bytecode: str) -> Dict[str, Any]: + """Wraps a bytecode payload in a standard Ethereum State Test JSON format.""" + address = "0x00000000000000000000000000000000000000aa" + + return { + name: { + "env": { + "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty": "0x020000", + "currentGasLimit": "0x7fffffffffffffff", + "currentNumber": "0x01", + "currentTimestamp": "0x03e8", + "currentBaseFee": "0x0a" + }, + "pre": { + address: { + "balance": "0x00", + "code": "0x" + bytecode, + "nonce": "0x00", + "storage": {} + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x0de0b6b3a7640000", + "code": "0x", + "nonce": "0x00", + "storage": {} + } + }, + "transaction": { + "data": ["0x"], + "gasLimit": ["0x7fffffffffffffff"], + "gasPrice": "0x0a", + "nonce": "0x00", + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + "to": address, + "value": ["0x00"] + }, + "post": { + "Cancun": [ + { + "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "indexes": {"data": 0, "gas": 0, "value": 0}, + "logs": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + } + } + + +def main(): + parser = argparse.ArgumentParser(description="Generate EVM opcode microbenchmarks") + parser.add_argument("--outdir", required=True, help="Output directory for generated JSON files") + parser.add_argument("--iterations", type=int, default=10000, help="Number of loop iterations per opcode (default 10000)") + args = parser.parse_args() + + os.makedirs(args.outdir, exist_ok=True) + + # Opcode mappings + benchmarks = { + # Nullary ops + "PC": build_nullary_op(OP_PC, args.iterations), + "GAS": build_nullary_op(OP_GAS, args.iterations), + "ADDRESS": build_nullary_op(OP_ADDRESS, args.iterations), + "CALLER": build_nullary_op(OP_CALLER, args.iterations), + "ORIGIN": build_nullary_op(OP_ORIGIN, args.iterations), + "CODESIZE": build_nullary_op(OP_CODESIZE, args.iterations), + "CALLDATASIZE": build_nullary_op(OP_CALLDATASIZE, args.iterations), + "RETURNDATASIZE": build_nullary_op(OP_RETURNDATASIZE, args.iterations), + "COINBASE": build_nullary_op(OP_COINBASE, args.iterations), + "TIMESTAMP": build_nullary_op(OP_TIMESTAMP, args.iterations), + "NUMBER": build_nullary_op(OP_NUMBER, args.iterations), + "DIFFICULTY": build_nullary_op(OP_DIFFICULTY, args.iterations), + "GASLIMIT": build_nullary_op(OP_GASLIMIT, args.iterations), + "CHAINID": build_nullary_op(OP_CHAINID, args.iterations), + "BASEFEE": build_nullary_op(OP_BASEFEE, args.iterations), + + # Unary ops + "ISZERO": build_unary_op(OP_ISZERO, args.iterations), + "NOT": build_unary_op(OP_NOT, args.iterations), + + # Binary ops + "ADD": build_binary_op(OP_ADD, args.iterations), + "MUL": build_binary_op(OP_MUL, args.iterations), + "SUB": build_binary_op(OP_SUB, args.iterations), + "DIV": build_binary_op(OP_DIV, args.iterations), + "SDIV": build_binary_op(OP_SDIV, args.iterations), + "MOD": build_binary_op(OP_MOD, args.iterations), + "SMOD": build_binary_op(OP_SMOD, args.iterations), + "EXP": build_binary_op(OP_EXP, args.iterations), + "SIGNEXTEND": build_binary_op(OP_SIGNEXTEND, args.iterations), + "LT": build_binary_op(OP_LT, args.iterations), + "GT": build_binary_op(OP_GT, args.iterations), + "SLT": build_binary_op(OP_SLT, args.iterations), + "SGT": build_binary_op(OP_SGT, args.iterations), + "EQ": build_binary_op(OP_EQ, args.iterations), + "AND": build_binary_op(OP_AND, args.iterations), + "OR": build_binary_op(OP_OR, args.iterations), + "XOR": build_binary_op(OP_XOR, args.iterations), + "BYTE": build_binary_op(OP_BYTE, args.iterations), + "SHL": build_binary_op(OP_SHL, args.iterations), + "SHR": build_binary_op(OP_SHR, args.iterations), + "SAR": build_binary_op(OP_SAR, args.iterations), + + # Ternary ops + "ADDMOD": build_ternary_op(OP_ADDMOD, args.iterations), + "MULMOD": build_ternary_op(OP_MULMOD, args.iterations), + + # Memory ops + "MLOAD": build_memory_op_mload(args.iterations), + "MSTORE": build_memory_op_mstore(args.iterations), + } + + count = 0 + for name, bytecode in benchmarks.items(): + test_case = generate_state_test(f"opcode_{name}", bytecode) + outpath = os.path.join(args.outdir, f"opcode_{name}.json") + with open(outpath, "w") as f: + json.dump(test_case, f, indent=4) + count += 1 + + print(f"Successfully generated {count} opcode benchmark test cases in '{args.outdir}'.") + +if __name__ == "__main__": + main() diff --git a/tools/solc_batch_compile.sh b/tools/solc_batch_compile.sh index 2e0745fbc..280d630f2 100755 --- a/tools/solc_batch_compile.sh +++ b/tools/solc_batch_compile.sh @@ -50,20 +50,23 @@ for dir in "$BASE_DIR"/*/; do # Get the directory name (without path) dirname=$(basename "$dir") - # Check if the corresponding .sol file exists - sol_file="$dir$dirname.sol" + # Check if any .sol files exist in the directory + shopt -s nullglob + sol_files=("$dir"*.sol) + shopt -u nullglob + json_file="$dir$dirname.json" - if [ -f "$sol_file" ]; then - echo "Compiling $sol_file..." - # Compile the Solidity file and format JSON - if solc --evm-version cancun --combined-json abi,bin,bin-runtime "$dir$dirname.sol" | jq --indent 2 '.' > "$dir$dirname.json"; then - echo "✓ Successfully compiled $dirname.sol to $dirname.json (cancun EVM)" + if [ ${#sol_files[@]} -gt 0 ]; then + echo "Compiling ${#sol_files[@]} Solidity files in $dir..." + # Compile the Solidity files and format JSON + if solc --evm-version cancun --combined-json abi,bin,bin-runtime "${sol_files[@]}" | jq --indent 2 '.' > "$json_file"; then + echo "✓ Successfully compiled files in $dir to $dirname.json (cancun EVM)" else - echo "✗ Failed to compile $dirname.sol" + echo "✗ Failed to compile files in $dir" fi else - echo "Warning: $sol_file not found, skipping directory $dirname" + echo "Warning: No .sol files found in $dir, skipping" fi fi done