From 44509f892113c32b9979f3755138d026ec79ebd0 Mon Sep 17 00:00:00 2001 From: starwarfan <519125404@qq.com> Date: Thu, 9 Apr 2026 17:12:01 +0800 Subject: [PATCH 1/7] fix: fix building on newer gcc --- src/platform/platform.h | 1 + src/utils/logging.h | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/platform/platform.h b/src/platform/platform.h index a8b2c4f25..d299c7291 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include diff --git a/src/utils/logging.h b/src/utils/logging.h index 58df7f2d7..423037546 100644 --- a/src/utils/logging.h +++ b/src/utils/logging.h @@ -6,7 +6,9 @@ #include "common/defines.h" #include "platform/platform.h" +#include #include +#include #ifdef ZEN_ENABLE_SPDLOG namespace spdlog { From 8f05551cd251e0acb0c6316e459edc3c91384383 Mon Sep 17 00:00:00 2001 From: starwarfan <519125404@qq.com> Date: Thu, 7 May 2026 10:13:54 +0800 Subject: [PATCH 2/7] fix(evm): stabilize multipass JIT for long Silkworm sync runs - Fix EVMCallThreadState::setJITTraces inverted Inst guard (was no-op for all real instances); align trap backtrace behavior with the WASM handler pattern. - Harden SAVE_EVM_HOSTAPI_FRAME_POINTER_TO_TLS with a do/while wrapper and assert an active TLS before UD2-based JIT traps; add required semicolon at the throwInstanceExceptionOnJIT call site. - dt_evmc_vm: per-VM recursive_mutex around execute(), clear per-instance message cache on top-level entry, empty-code CALL fast paths for multipass JIT, and default DisableMultipassGreedyRA with DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA override. - Execution cache / CREATE path hardening (evm_cache, evm_instance, evm_imported) from Silkworm integration stress testing. Validated: cmake --build build --target dtvmapi; multipass staged_pipeline forward past former crash block on mainnet snapshot data. --- src/common/evm_traphandler.cpp | 13 +---- src/common/evm_traphandler.h | 9 ++-- src/compiler/evm_frontend/evm_imported.cpp | 60 +++------------------- src/evm/evm_cache.cpp | 60 ++++++++++++++++++++++ src/runtime/evm_instance.cpp | 2 +- src/runtime/evm_instance.h | 11 ++-- src/vm/dt_evmc_vm.cpp | 47 +++++++++++++++++ 7 files changed, 132 insertions(+), 70 deletions(-) diff --git a/src/common/evm_traphandler.cpp b/src/common/evm_traphandler.cpp index e291f55b4..516764f2d 100644 --- a/src/common/evm_traphandler.cpp +++ b/src/common/evm_traphandler.cpp @@ -23,7 +23,6 @@ void EVMCallThreadState::setJITTraces() { return; } uint32_t IgnoredDepth = getTrapState().NumIgnoredFrames; - ZEN_ASSERT(Inst); void *JITCode = Inst->getModule()->getJITCode(); void *JITCodeEnd = static_cast(JITCode) + Inst->getModule()->getJITCodeSize(); @@ -120,14 +119,6 @@ bool initEVMPlatformTrapHandler() { // it. It will either crash synchronously, fix up the instruction // so that execution can continue and return, or trigger a crash by // returning the signal to it's original disposition and returning. - - // Unblock the signal before forwarding to the previous handler, - // preserving the same semantics as when SA_NODEFER was used. - sigset_t SignalSet; - sigemptyset(&SignalSet); - sigaddset(&SignalSet, SigNum); - int UnblockResult = sigprocmask(SIG_UNBLOCK, &SignalSet, nullptr); - ZEN_ASSERT(UnblockResult == 0); if ((PrevSigAction->sa_flags & SA_SIGINFO) != 0) { PrevSigAction->sa_sigaction(SigNum, SigInfo, Ctx); } else if ((void (*)(int))PrevSigAction->sa_sigaction == SIG_DFL || @@ -159,9 +150,9 @@ bool initEVMPlatformTrapHandler() { struct sigaction Handler; memset(&Handler, 0x0, sizeof(struct sigaction)); #ifdef ZEN_ENABLE_VIRTUAL_STACK - Handler.sa_flags = SA_SIGINFO | SA_ONSTACK; + Handler.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK; #else - Handler.sa_flags = SA_SIGINFO; + Handler.sa_flags = SA_SIGINFO | SA_NODEFER; #endif // ZEN_ENABLE_VIRTUAL_STACK Handler.sa_sigaction = TrapHandler; sigemptyset(&Handler.sa_mask); diff --git a/src/common/evm_traphandler.h b/src/common/evm_traphandler.h index 3008ecb5d..d8cd48464 100644 --- a/src/common/evm_traphandler.h +++ b/src/common/evm_traphandler.h @@ -148,9 +148,12 @@ bool initEVMPlatformTrapHandler(); // may not used // so need set it when unwind backtrace after ud2 #define SAVE_EVM_HOSTAPI_FRAME_POINTER_TO_TLS \ - void *FrameAddr = __builtin_frame_address(0); \ - auto TLS = common::evm_traphandler::EVMCallThreadState::current(); \ - TLS->setTrapFrameAddr(FrameAddr, nullptr, nullptr, 0); + do { \ + void *FrameAddr = __builtin_frame_address(0); \ + auto *TLS_ = common::evm_traphandler::EVMCallThreadState::current(); \ + ZEN_ASSERT(TLS_ != nullptr); \ + TLS_->setTrapFrameAddr(FrameAddr, nullptr, nullptr, 0); \ + } while (0) #endif // ZEN_ENABLE_CPU_EXCEPTION diff --git a/src/compiler/evm_frontend/evm_imported.cpp b/src/compiler/evm_frontend/evm_imported.cpp index 5292db6e7..082dfbb50 100644 --- a/src/compiler/evm_frontend/evm_imported.cpp +++ b/src/compiler/evm_frontend/evm_imported.cpp @@ -102,42 +102,6 @@ inline void triggerStaticModeViolation(zen::runtime::EVMInstance *Instance) { zen::runtime::EVMInstance::triggerInstanceExceptionOnJIT( Instance, zen::common::ErrorCode::EVMStaticModeViolation); } - -constexpr uint8_t DelegationMagicBytes[] = {0xef, 0x01, 0x00}; - -bool resolveDelegatedCallCodeAddress(zen::runtime::EVMInstance *Instance, - evmc::address TargetAddr, - evmc::address &CodeAddr) { - CodeAddr = TargetAddr; - if (Instance->getRevision() < EVMC_PRAGUE) { - return true; - } - - const zen::runtime::EVMModule *Module = Instance->getModule(); - ZEN_ASSERT(Module && Module->Host); - uint8_t Designation[sizeof(DelegationMagicBytes) + sizeof(evmc::address)] = - {}; - const size_t Copied = - Module->Host->copy_code(TargetAddr, 0, Designation, sizeof(Designation)); - if (Copied < sizeof(DelegationMagicBytes) || - std::memcmp(Designation, DelegationMagicBytes, - sizeof(DelegationMagicBytes)) != 0) { - return true; - } - - if (Copied != sizeof(Designation)) { - return true; - } - - std::memcpy(CodeAddr.bytes, Designation + sizeof(DelegationMagicBytes), - sizeof(CodeAddr.bytes)); - const uint64_t DelegateAccessCost = - Module->Host->access_account(CodeAddr) == EVMC_ACCESS_COLD - ? zen::evm::COLD_ACCOUNT_ACCESS_COST - : zen::evm::WARM_STORAGE_READ_COST; - Instance->chargeGas(DelegateAccessCost); - return true; -} } // namespace const RuntimeFunctions &getRuntimeFunctionTable() { @@ -890,9 +854,13 @@ const uint8_t *evmHandleCreateInternal(zen::runtime::EVMInstance *Instance, Instance->setReturnData(std::move(ReturnData)); if (Result.status_code == EVMC_SUCCESS) { - static thread_local uint8_t PaddedAddress[32] = {0}; - memcpy(PaddedAddress + 12, Result.create_address.bytes, 20); - return PaddedAddress; + // Keep returned CREATE addresses in per-instance cache to avoid aliasing + // between nested/re-entrant CREATE paths. + auto &ExecCache = Instance->getMessageCache(); + evmc::bytes32 PaddedAddress{}; + std::memcpy(PaddedAddress.bytes + 12, Result.create_address.bytes, 20); + ExecCache.CreateAddresses.push_back(PaddedAddress); + return ExecCache.CreateAddresses.back().bytes; } return ZeroAddress; } @@ -928,12 +896,6 @@ static uint64_t evmHandleCallInternal( Instance->chargeGas(zen::evm::ADDITIONAL_COLD_ACCOUNT_ACCESS_COST); } - evmc::address CodeAddr = TargetAddr; - if (!resolveDelegatedCallCodeAddress(Instance, TargetAddr, CodeAddr)) { - Instance->setReturnData({}); - return 0; - } - const bool HasValueArgs = CallKind == EVMC_CALL || CallKind == EVMC_CALLCODE; const bool HasValue = Value != 0; @@ -1032,16 +994,10 @@ static uint64_t evmHandleCallInternal( ? CurrentMsg->value : intx::be::store(Value), .create2_salt = {}, - .code_address = CodeAddr, + .code_address = TargetAddr, .code = nullptr, .code_size = 0, }; - if (std::memcmp(TargetAddr.bytes, CodeAddr.bytes, sizeof(TargetAddr.bytes)) != - 0) { - CallMsg.flags |= EVMC_DELEGATED; - } else { - CallMsg.flags &= ~uint32_t(EVMC_DELEGATED); - } Instance->pushMessage(&CallMsg); evmc::Result Result = Module->Host->call(CallMsg); diff --git a/src/evm/evm_cache.cpp b/src/evm/evm_cache.cpp index cc5a6208e..805ea42b1 100644 --- a/src/evm/evm_cache.cpp +++ b/src/evm/evm_cache.cpp @@ -200,6 +200,9 @@ struct GasBlock { }; static void addEdge(std::vector &Blocks, uint32_t From, uint32_t To) { + if (From >= Blocks.size() || To >= Blocks.size()) { + return; + } auto &FromSuccs = Blocks[From].Succs; if (std::find(FromSuccs.begin(), FromSuccs.end(), To) == FromSuccs.end()) { FromSuccs.push_back(To); @@ -223,6 +226,9 @@ static bool splitCriticalEdges(std::vector &Blocks, size_t CodeSize) { continue; // Not a critical edge source } for (uint32_t ToId : Blocks[FromId].Succs) { + if (ToId >= Blocks.size()) { + continue; + } if (Blocks[ToId].Preds.size() > 1) { // Critical edge: From has multiple succs, To has multiple preds EdgesToSplit.push_back({static_cast(FromId), ToId}); @@ -450,6 +456,9 @@ computeInCycle(const std::vector &Blocks) { while (!Stack.empty()) { const uint32_t Node = Stack.back(); Stack.pop_back(); + if (Node >= NumBlocks) { + continue; + } Component.push_back(Node); for (uint32_t Pred : Blocks[Node].Preds) { if (Pred >= NumBlocks) { @@ -468,6 +477,9 @@ computeInCycle(const std::vector &Blocks) { } continue; } + if (Component.empty()) { + continue; + } const uint32_t Only = Component.front(); for (uint32_t Succ : Blocks[Only].Succs) { @@ -528,7 +540,13 @@ computeReachable(const std::vector &Blocks, uint32_t EntryId) { while (!Stack.empty()) { const uint32_t Node = Stack.back(); Stack.pop_back(); + if (Node >= NumBlocks) { + continue; + } for (uint32_t Succ : Blocks[Node].Succs) { + if (Succ >= NumBlocks) { + continue; + } if (Reachable[Succ] == 0) { Reachable[Succ] = 1; Stack.push_back(Succ); @@ -571,6 +589,9 @@ computeDominators(const std::vector &Blocks, NewDom = All; bool HasPred = false; for (uint32_t Pred : Blocks[Node].Preds) { + if (Pred >= NumBlocks) { + continue; + } if (Reachable[Pred] == 0) { continue; } @@ -604,6 +625,9 @@ findBackEdgesUsingDominators(const std::vector &Blocks, for (size_t From = 0; From < NumBlocks; ++From) { for (uint32_t To : Blocks[From].Succs) { + if (To >= NumBlocks) { + continue; + } if (bitsetTest(Dom[From], To)) { BackEdges[From].push_back(To); } @@ -613,6 +637,9 @@ findBackEdgesUsingDominators(const std::vector &Blocks, static bool isBackEdge(const std::vector> &BackEdges, uint32_t From, uint32_t To) { + if (From >= BackEdges.size()) { + return false; + } const auto &Edges = BackEdges[From]; return std::find(Edges.begin(), Edges.end(), To) != Edges.end(); } @@ -634,6 +661,9 @@ computeReverseTopo(const std::vector &Blocks, while (!Stack.empty()) { uint32_t Current = Stack.back(); Stack.pop_back(); + if (Current >= NumBlocks) { + continue; + } if (Visited[Current] == 2) { continue; } @@ -647,6 +677,9 @@ computeReverseTopo(const std::vector &Blocks, const auto &Succs = Blocks[Current].Succs; for (auto It = Succs.rbegin(); It != Succs.rend(); ++It) { uint32_t Succ = *It; + if (Succ >= NumBlocks) { + continue; + } if (!isBackEdge(BackEdges, Current, Succ) && Visited[Succ] == 0) { Visited[Succ] = 1; Stack.push_back(Succ); @@ -679,7 +712,13 @@ collectNaturalLoop(uint32_t From, uint32_t Header, while (!Stack.empty()) { const uint32_t Node = Stack.back(); Stack.pop_back(); + if (Node >= NumBlocks) { + continue; + } for (uint32_t Pred : Blocks[Node].Preds) { + if (Pred >= NumBlocks) { + continue; + } if (Reachable[Pred] == 0) { continue; } @@ -713,6 +752,9 @@ static bool buildLoopsUsingDominance( continue; } for (uint32_t To : Blocks[From].Succs) { + if (To >= NumBlocks) { + continue; + } if (!bitsetTest(Dom[From], To)) { continue; } @@ -758,6 +800,9 @@ static bool buildLoopsUsingDominance( for (const auto &Loop : Loops) { for (uint32_t Node : Loop.Nodes) { + if (Node >= Dom.size() || Loop.Header >= NumBlocks) { + return false; + } if (!bitsetTest(Dom[Node], Loop.Header)) { return false; } @@ -814,6 +859,9 @@ static bool buildLoopsUsingDominance( for (size_t OrderIndex = 0; OrderIndex < LoopOrder.size(); ++OrderIndex) { const size_t LoopId = LoopOrder[OrderIndex]; for (uint32_t Node : Loops[LoopId].Nodes) { + if (Node >= LoopOf.size()) { + return false; + } if (LoopOf[Node] == -1) { LoopOf[Node] = static_cast(LoopId); } @@ -832,8 +880,14 @@ static bool buildLoopsUsingDominance( for (size_t LoopId = 0; LoopId < Loops.size(); ++LoopId) { auto &Loop = Loops[LoopId]; for (uint32_t Node : Loop.Nodes) { + if (Node >= NumBlocks) { + continue; + } bool IsExit = false; for (uint32_t Succ : Blocks[Node].Succs) { + if (Succ >= NumBlocks) { + continue; + } if (!bitsetTest(Loop.NodeMask, Succ)) { IsExit = true; break; @@ -872,6 +926,9 @@ static bool lemma614Update(uint32_t NodeId, const std::vector &Blocks, uint64_t MinSucc = UINT64_MAX; for (uint32_t Succ : Node.Succs) { + if (Succ >= Blocks.size() || Succ >= Metering.size()) { + continue; + } if (BackEdges && isBackEdge(*BackEdges, NodeId, Succ)) { continue; } @@ -891,6 +948,9 @@ static bool lemma614Update(uint32_t NodeId, const std::vector &Blocks, Metering[NodeId] += MinSucc; for (uint32_t Succ : Node.Succs) { + if (Succ >= Blocks.size() || Succ >= Metering.size()) { + continue; + } if (BackEdges && isBackEdge(*BackEdges, NodeId, Succ)) { continue; } diff --git a/src/runtime/evm_instance.cpp b/src/runtime/evm_instance.cpp index f635f405a..4aa6fad76 100644 --- a/src/runtime/evm_instance.cpp +++ b/src/runtime/evm_instance.cpp @@ -189,7 +189,7 @@ void EVMInstance::setInstanceExceptionOnJIT(EVMInstance *Inst, void EVMInstance::throwInstanceExceptionOnJIT(EVMInstance *Inst) { #ifdef ZEN_ENABLE_CPU_EXCEPTION - SAVE_EVM_HOSTAPI_FRAME_POINTER_TO_TLS + SAVE_EVM_HOSTAPI_FRAME_POINTER_TO_TLS; utils::throwCpuIllegalInstructionTrap(); #endif // ZEN_ENABLE_CPU_EXCEPTION diff --git a/src/runtime/evm_instance.h b/src/runtime/evm_instance.h index 63801cb04..f5d59792a 100644 --- a/src/runtime/evm_instance.h +++ b/src/runtime/evm_instance.h @@ -11,8 +11,8 @@ #include "runtime/evm_module.h" #include "runtime/instance.h" #include -#include #include +#include #include // Forward declaration for evmc_message @@ -158,8 +158,12 @@ class EVMInstance final : public RuntimeObject { std::unordered_map, evmc::bytes32, PairHash> CalldataLoads; - std::deque ExtcodeHashes; - std::deque Keccak256Results; + // Use std::list (not std::deque) for values whose .bytes pointers are + // returned to JIT and may outlive further push_back calls; deque can + // reallocate and invalidate earlier element addresses. + std::list ExtcodeHashes; + std::list Keccak256Results; + std::list CreateAddresses; bool TxContextCached = false; void clear() { @@ -170,6 +174,7 @@ class EVMInstance final : public RuntimeObject { CalldataLoads.clear(); ExtcodeHashes.clear(); Keccak256Results.clear(); + CreateAddresses.clear(); } }; diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index 17961c8ea..297bae24c 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -174,6 +175,15 @@ bool parseBoolEnvValue(const char *Value, bool &ParsedValue) { return false; } +bool isLikelyPrecompile(const evmc_address &Addr) { + for (size_t I = 0; I < 19; ++I) { + if (Addr.bytes[I] != 0) { + return false; + } + } + return Addr.bytes[19] >= 1 && Addr.bytes[19] <= 9; +} + // VM interface for DTVM struct DTVM : evmc_vm { DTVM(); @@ -247,6 +257,8 @@ struct DTVM : evmc_vm { std::vector> NestedCtxPool; // Instance pool for depth > 0 std::vector CacheInsts; + // Serialize execute() for nested host calls (re-entrancy-safe via recursive_mutex). + std::recursive_mutex ExecuteMutex; bool isModuleInUse(const EVMModule *Mod) const { if (CachedMainInst && CachedMainInst->getModule() == Mod) @@ -540,6 +552,9 @@ evmc_result executeInterpreterFastPath(DTVM *VM, evmc_message MsgWithCode = *Msg; MsgWithCode.code = reinterpret_cast(Mod->Code); MsgWithCode.code_size = Mod->CodeSize; + if (Msg->depth == 0) { + TheInst->clearMessageCache(); + } TheInst->setExeResult(evmc::Result{EVMC_SUCCESS, 0, 0}); TheInst->pushMessage(&MsgWithCode); @@ -622,6 +637,19 @@ evmc_result executeMultipassFastPath(DTVM *VM, const evmc_host_interface *Host, return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); } + // Empty-code top-level CALL to a non-precompile has no bytecode execution. + if (Msg && Msg->depth == 0 && CodeSize == 0 && Msg->kind == EVMC_CALL && + !isLikelyPrecompile(Msg->recipient)) { + evmc::Result NoCodeResult{EVMC_SUCCESS, static_cast(Msg->gas), 0}; + return NoCodeResult.release_raw(); + } + + // Nested empty-code CALLs: use interpreter path (precompiles may have no code). + if (Msg && Msg->depth > 0 && CodeSize == 0 && Msg->kind == EVMC_CALL) { + return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, + CodeSize); + } + // Module lookup: L1 address-based cache -> Cold load bool IsTransientMod = false; EVMModule *Mod = @@ -650,6 +678,9 @@ evmc_result executeMultipassFastPath(DTVM *VM, const evmc_host_interface *Host, evmc_message MsgWithCode = *Msg; MsgWithCode.code = reinterpret_cast(Mod->Code); MsgWithCode.code_size = Mod->CodeSize; + if (Msg->depth == 0) { + TheInst->clearMessageCache(); + } TheInst->setExeResult(evmc::Result{EVMC_SUCCESS, 0, 0}); TheInst->pushMessage(&MsgWithCode); @@ -685,6 +716,7 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, const evmc_message *Msg, const uint8_t *Code, size_t CodeSize) { auto *VM = static_cast(EVMInstance); + std::lock_guard Guard(VM->ExecuteMutex); // Interpreter mode: use optimized fast path (bypasses callEVMMain) if (VM->Config.Mode == RunMode::InterpMode) { @@ -743,6 +775,21 @@ DTVM::DTVM() StrictValidation); } } + + // Greedy RA has been observed to crash in long-running EVM multipass JIT + // compilation. Disabled by default; override with DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA. + Config.DisableMultipassGreedyRA = true; + if (const char *DisableGreedyRA = + std::getenv("DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA"); + DisableGreedyRA != nullptr) { + bool ParsedDisableGreedyRA = true; + if (parseBoolEnvValue(DisableGreedyRA, ParsedDisableGreedyRA)) { + Config.DisableMultipassGreedyRA = ParsedDisableGreedyRA; + } else { + ZEN_LOG_WARN("ignore invalid DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA=%s", + DisableGreedyRA); + } + } } } // namespace From f4903ddf2a533f42bc850d2b2a3d252e02a81d02 Mon Sep 17 00:00:00 2001 From: starwarfan <519125404@qq.com> Date: Thu, 7 May 2026 11:14:52 +0800 Subject: [PATCH 3/7] fix: code format adjust --- src/vm/dt_evmc_vm.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index 297bae24c..3612a5e26 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -257,7 +257,8 @@ struct DTVM : evmc_vm { std::vector> NestedCtxPool; // Instance pool for depth > 0 std::vector CacheInsts; - // Serialize execute() for nested host calls (re-entrancy-safe via recursive_mutex). + // Serialize execute() for nested host calls (re-entrancy-safe via + // recursive_mutex). std::recursive_mutex ExecuteMutex; bool isModuleInUse(const EVMModule *Mod) const { @@ -644,7 +645,8 @@ evmc_result executeMultipassFastPath(DTVM *VM, const evmc_host_interface *Host, return NoCodeResult.release_raw(); } - // Nested empty-code CALLs: use interpreter path (precompiles may have no code). + // Nested empty-code CALLs: use interpreter path (precompiles may have no + // code). if (Msg && Msg->depth > 0 && CodeSize == 0 && Msg->kind == EVMC_CALL) { return executeInterpreterFastPath(VM, Host, Context, Rev, Msg, Code, CodeSize); @@ -777,7 +779,8 @@ DTVM::DTVM() } // Greedy RA has been observed to crash in long-running EVM multipass JIT - // compilation. Disabled by default; override with DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA. + // compilation. Disabled by default; override with + // DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA. Config.DisableMultipassGreedyRA = true; if (const char *DisableGreedyRA = std::getenv("DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA"); From aeef02d4781633c853d0e928fc9f469cae5bae69 Mon Sep 17 00:00:00 2001 From: starwarfan <519125404@qq.com> Date: Wed, 13 May 2026 17:30:03 +0800 Subject: [PATCH 4/7] fix(evm): preserve legacy CALL stack operands in multipass frontend Legacy pre-Tangerine revisions can lose deep stack operands when lifted logical stack materialization is used, which zeros CALL-family arguments and undercharges gas. Route legacy stack operations through runtime stack access and add regression coverage so multipass matches interpreter gas on known failing blocks. --- docs/legacy_multipass_call_repro.md | 40 ++++++++++++++ src/action/evm_bytecode_visitor.h | 54 +++++++++++++++++-- src/tests/evm_jit_frontend_tests.cpp | 31 +++++++++++ .../legacy_call_gas_cases.json | 24 +++++++++ 4 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 docs/legacy_multipass_call_repro.md create mode 100644 tests/evm/fixtures/legacy_multipass_call/legacy_call_gas_cases.json diff --git a/docs/legacy_multipass_call_repro.md b/docs/legacy_multipass_call_repro.md new file mode 100644 index 000000000..30004ce2b --- /dev/null +++ b/docs/legacy_multipass_call_repro.md @@ -0,0 +1,40 @@ +# Legacy Multipass CALL Gas Repro + +This note tracks the legacy CALL gas regression fixed in the multipass frontend +for revisions below Tangerine Whistle. + +## Reference Cases + +Fixture file: + +- `tests/evm/fixtures/legacy_multipass_call/legacy_call_gas_cases.json` + +Expected tx gas: + +- `block=254277 tx_index=0` -> `tx_gas=57956` +- `block=254297 tx_index=0` -> `tx_gas=94849` + +## Repro Commands + +From silkworm checkout: + +```bash +env SILKWORM_EVM=./libdtvmapi.so,mode=multipass DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA=0 \ + ./build/silkworm/node/cli/staged_pipeline \ + --datadir /mnt/erigon-snapshots/dtvm-repro-254277-b \ + --exclusive run_single_tx --block 254277 --tx-index 0 +``` + +```bash +env SILKWORM_EVM=./libdtvmapi.so,mode=multipass DTVM_EVM_DISABLE_MULTIPASS_GREEDYRA=0 \ + ./build/silkworm/node/cli/staged_pipeline \ + --datadir /mnt/erigon-snapshots/dtvm-repro-254277-20260512 \ + --exclusive run_single_tx --block 254297 --tx-index 0 +``` + +## Root-Cause Direction + +In legacy revisions, stack lifting/logical-stack materialization can lose deep +stack operands before CALL-family lowering executes. The frontend fix keeps +legacy stack operations on the runtime EVM stack path, preserving CALL argument +order/value at execution sites without interpreter fallback. diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index 07a1726ad..c02be59ec 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -124,14 +124,28 @@ template class EVMByteCodeVisitor { } } - void push(const Operand &Opnd) { Stack.push(Opnd); } + void push(const Operand &Opnd) { + if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { + Builder.stackPush(Opnd); + return; + } + Stack.push(Opnd); + } void requireLogicalStackDepth(uint32_t Depth) { + if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { + return; + } ZEN_ASSERT(Stack.getSize() >= Depth && "Logical EVM stack must be preloaded at block entry"); } Operand pop() { + if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { + Operand Opnd = Builder.stackPop(); + Builder.releaseOperand(Opnd); + return Opnd; + } requireLogicalStackDepth(1); Operand Opnd = Stack.pop(); Builder.releaseOperand(Opnd); @@ -1127,6 +1141,10 @@ template class EVMByteCodeVisitor { Builder.createStackCheckBlock(-BlockInfo.MinStackHeight, 1024 - BlockInfo.MaxStackHeight); } + if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { + CurrentBlockLifted = false; + return; + } if (LiftedBlock) { CurrentBlockLifted = true; @@ -1776,8 +1794,17 @@ template class EVMByteCodeVisitor { // DUP1-DUP16: Duplicate Nth stack item void handleDup(uint8_t Index) { - requireLogicalStackDepth(Index); - Operand Result = Stack.peek(Index - 1); + Operand Result; + if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE && + Stack.getSize() < static_cast(Index)) { + const int32_t MemIndex = + static_cast(Index) - static_cast(Stack.getSize()) - + 1; + Result = Builder.stackGet(MemIndex); + } else { + requireLogicalStackDepth(Index); + Result = Stack.peek(Index - 1); + } push(Result); } @@ -1789,6 +1816,27 @@ template class EVMByteCodeVisitor { // SWAP1-SWAP16: Swap top with Nth+1 stack item void handleSwap(uint8_t Index) { + if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { + const uint32_t RequiredDepth = static_cast(Index) + 1u; + if (Stack.empty()) { + const int32_t MemIndex = + static_cast(Index) - static_cast(Stack.getSize()); + Operand A = Builder.stackGet(0); + Operand B = Builder.stackGet(MemIndex); + Builder.stackSet(0, B); + Builder.stackSet(MemIndex, A); + return; + } + if (Stack.getSize() < RequiredDepth) { + const int32_t MemIndex = + static_cast(Index) - static_cast(Stack.getSize()); + Operand &A = Stack.peek(0); + Operand B = Builder.stackGet(MemIndex); + Builder.stackSet(MemIndex, A); + A = B; + return; + } + } requireLogicalStackDepth(static_cast(Index) + 1u); std::swap(Stack.peek(0), Stack.peek(Index)); } diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index fc3c2f88b..83b3a5e1b 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -664,6 +664,37 @@ TEST(EVMJITFrontendVisitorTest, EXPECT_FALSE(Builder.Undefined); } +TEST(EVMJITFrontendVisitorTest, LegacyRevisionDupSwapUseRuntimeStackPath) { + const std::vector Bytecode = { + 0x60, 0xaa, // PUSH1 0xaa + 0x60, 0xbb, // PUSH1 0xbb + 0x81, // DUP2 + 0x90, // SWAP1 + 0x50, // POP + 0x00 // STOP + }; + + COMPILER::EVMFrontendContext Ctx; + Ctx.setRevision(EVMC_FRONTIER); + Ctx.setBytecode(reinterpret_cast(Bytecode.data()), + Bytecode.size()); + + MockEVMBuilder Builder; + COMPILER::EVMByteCodeVisitor Visitor(Builder, &Ctx); + EXPECT_TRUE(Visitor.compile()); + EXPECT_FALSE(Builder.Trapped); + EXPECT_FALSE(Builder.Undefined); + + const auto &DupStats = Builder.accessStats(OP_DUP2); + EXPECT_EQ(DupStats.StackPopCount, 0U); + EXPECT_GT(DupStats.StackGetCount, 0U); + + const auto &SwapStats = Builder.accessStats(OP_SWAP1); + EXPECT_EQ(SwapStats.StackPopCount, 0U); + EXPECT_GT(SwapStats.StackGetCount, 0U); + EXPECT_GT(SwapStats.StackSetCount, 0U); +} + TEST(EVMJITFrontendVisitorTest, ImplicitStopMaterializesLiftedStackOnFallthrough) { const std::vector Bytecode = { diff --git a/tests/evm/fixtures/legacy_multipass_call/legacy_call_gas_cases.json b/tests/evm/fixtures/legacy_multipass_call/legacy_call_gas_cases.json new file mode 100644 index 000000000..284ea3f91 --- /dev/null +++ b/tests/evm/fixtures/legacy_multipass_call/legacy_call_gas_cases.json @@ -0,0 +1,24 @@ +{ + "description": "Legacy CALL gas regression cases captured from silkworm run_single_tx replay.", + "revision_guard": "pre-Tangerine Whistle path in multipass frontend", + "cases": [ + { + "name": "legacy_call_creation_cost_block_254277_tx0", + "block": 254277, + "tx_index": 0, + "tx_hash": "0x65ee2777a2c5abbd266dbbcb68a02eab73eef913d7106bc9b7cfc2cbabc883d7", + "expected_tx_gas": 57956, + "expected_status": "success", + "notes": "Regression case: legacy CALL args became zero in multipass and skipped ACCOUNT_CREATION_COST." + }, + { + "name": "legacy_guard_block_254297_tx0", + "block": 254297, + "tx_index": 0, + "tx_hash": "0x342b28ad3b4a91f747274a1d241c5978a2ccd50f0124eb1423a4dd55783eaa99", + "expected_tx_gas": 94849, + "expected_status": "success", + "notes": "Guard case to prevent regressions while fixing legacy CALL argument materialization." + } + ] +} From 4bf1b6240ab82ae990cf46cf573c717112407eb7 Mon Sep 17 00:00:00 2001 From: starwarfan <519125404@qq.com> Date: Wed, 13 May 2026 18:31:06 +0800 Subject: [PATCH 5/7] debug(evm): instrument lifted-stack call operand flow Add env-gated lifted-stack diagnostics across visitor, MIR lowering, and runtime call helper to trace logical stack materialization and CALL-family argument propagation. Extend frontend tests with a modern deep-merge DUP/SWAP->CALL regression and document why the legacy runtime-stack path test fails when fallback branches are removed. --- docs/legacy_multipass_call_repro.md | 19 +++++ src/action/evm_bytecode_visitor.h | 80 ++++++++++++++++++ src/compiler/evm_frontend/evm_imported.cpp | 39 +++++++-- .../evm_frontend/evm_mir_compiler.cpp | 83 +++++++++++++++++- src/tests/evm_jit_frontend_tests.cpp | 84 ++++++++++++++++++- 5 files changed, 291 insertions(+), 14 deletions(-) diff --git a/docs/legacy_multipass_call_repro.md b/docs/legacy_multipass_call_repro.md index 30004ce2b..60f849bc6 100644 --- a/docs/legacy_multipass_call_repro.md +++ b/docs/legacy_multipass_call_repro.md @@ -38,3 +38,22 @@ In legacy revisions, stack lifting/logical-stack materialization can lose deep stack operands before CALL-family lowering executes. The frontend fix keeps legacy stack operations on the runtime EVM stack path, preserving CALL argument order/value at execution sites without interpreter fallback. + +## Frontend Test Semantics + +The regression test `LegacyRevisionDupSwapUseRuntimeStackPath` in +`src/tests/evm_jit_frontend_tests.cpp` is intentionally behavioral: + +- It uses `EVMC_FRONTIER`. +- It asserts `DUP2` and `SWAP1` perform runtime stack access + (`stackGet`/`stackSet`) in legacy mode. + +If legacy runtime-stack branches are disabled in the visitor, the same test +fails with: + +- `DupStats.StackGetCount == 0` +- `SwapStats.StackGetCount == 0` +- `SwapStats.StackSetCount == 0` + +That failure confirms the test is not cosmetic; it directly guards the legacy +operand provenance needed by CALL-family lowering. diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index c02be59ec..931f169f9 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -12,6 +12,8 @@ #include "runtime/evm_module.h" #include +#include +#include #include #include #include @@ -99,6 +101,10 @@ template class EVMByteCodeVisitor { } } + static bool liftedStackDebugEnabled() { + return std::getenv("DTVM_DEBUG_LIFTED_STACK") != nullptr; + } + void spillTrackedStackPreservingPrefix(const std::vector &Values, uint32_t PrefixDepth) { if constexpr (HasSpillTrackedStackPreservingPrefix::value) { @@ -136,6 +142,14 @@ template class EVMByteCodeVisitor { if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { return; } + if (liftedStackDebugEnabled() && Stack.getSize() < Depth) { + std::fprintf(stderr, + "[lifted-check] logical-underflow pc=%llu block=%llu " + "required=%u logical=%u lifted=%d\n", + static_cast(PC), + static_cast(CurrentBlockEntryPC), Depth, + Stack.getSize(), CurrentBlockLifted ? 1 : 0); + } ZEN_ASSERT(Stack.getSize() >= Depth && "Logical EVM stack must be preloaded at block entry"); } @@ -188,6 +202,19 @@ template class EVMByteCodeVisitor { } bool IsJumpDest = (Opcode == OP_JUMPDEST); if (!IsJumpDest) { + if (liftedStackDebugEnabled() && + Ctx->getRevision() >= EVMC_TANGERINE_WHISTLE && + isHelperSensitiveOpcode(Opcode)) { + const uint32_t RequiredDepth = + helperSensitiveOpcodePopDepth(Opcode); + std::fprintf(stderr, + "[lifted-check] helper-op pc=%llu block=%llu op=%u " + "required=%u logical=%u lifted=%d\n", + static_cast(PC), + static_cast(CurrentBlockEntryPC), + static_cast(Opcode), RequiredDepth, + Stack.getSize(), CurrentBlockLifted ? 1 : 0); + } if (!Builder.isOpcodeDefined(Opcode)) { #ifdef ZEN_ENABLE_JIT_FALLBACK_TEST // For testing purposes, we can use 0xEE as a FALLBACK trigger @@ -1152,6 +1179,15 @@ template class EVMByteCodeVisitor { static_cast(std::max(BlockInfo.HiddenLiveInPrefixDepth, 0)); materializeLiftedBlockMergeRequests(PC); restoreLiftedBlockLogicalEntryState(PC); + if (liftedStackDebugEnabled()) { + std::fprintf(stderr, + "[lifted-check] begin lifted block=%llu entry_depth=%d " + "full_entry=%d hidden_prefix=%u logical=%u\n", + static_cast(PC), + BlockInfo.ResolvedEntryStackDepth, + BlockInfo.FullEntryStateDepth, + CurrentBlockHiddenLiveInPrefixDepth, Stack.getSize()); + } return; } @@ -1166,6 +1202,13 @@ template class EVMByteCodeVisitor { Operand Opnd = ReverseStack.pop(); Stack.push(Opnd); } + if (liftedStackDebugEnabled()) { + std::fprintf(stderr, + "[lifted-check] begin materialized block=%llu pop=%d " + "logical=%u min_pop=%d\n", + static_cast(PC), -BlockInfo.MinPopHeight, + Stack.getSize(), BlockInfo.MinPopHeight); + } } void materializeLiftedBlockMergeRequests(uint64_t BlockPC) { @@ -1220,6 +1263,43 @@ template class EVMByteCodeVisitor { } } + static uint32_t helperSensitiveOpcodePopDepth(evmc_opcode Opcode) { + switch (Opcode) { + case OP_LOG0: + return 2; + case OP_LOG1: + return 3; + case OP_LOG2: + return 4; + case OP_LOG3: + return 5; + case OP_LOG4: + return 6; + case OP_KECCAK256: + return 2; + case OP_CALLDATACOPY: + return 3; + case OP_CODECOPY: + return 3; + case OP_EXTCODECOPY: + return 4; + case OP_RETURNDATACOPY: + return 3; + case OP_CREATE: + return 3; + case OP_CALL: + case OP_CALLCODE: + return 7; + case OP_DELEGATECALL: + case OP_STATICCALL: + return 6; + case OP_CREATE2: + return 4; + default: + return 0; + } + } + static bool isBlockTerminatorOpcode(evmc_opcode Opcode) { return Opcode == OP_JUMP || Opcode == OP_JUMPI || Opcode == OP_RETURN || Opcode == OP_STOP || Opcode == OP_INVALID || Opcode == OP_REVERT || diff --git a/src/compiler/evm_frontend/evm_imported.cpp b/src/compiler/evm_frontend/evm_imported.cpp index 082dfbb50..fadda4479 100644 --- a/src/compiler/evm_frontend/evm_imported.cpp +++ b/src/compiler/evm_frontend/evm_imported.cpp @@ -8,6 +8,8 @@ #include "host/evm/crypto.h" #include "runtime/evm_instance.h" #include "runtime/evm_module.h" +#include +#include #include #include #include @@ -15,6 +17,10 @@ namespace { +static bool liftedStackDebugEnabled() { + return std::getenv("DTVM_DEBUG_LIFTED_STACK") != nullptr; +} + static constexpr uint32_t KeccakCacheSlots = 16; static constexpr uint32_t KeccakCacheMaxInputLen = 128; @@ -881,22 +887,37 @@ const uint8_t *evmHandleCreate2(zen::runtime::EVMInstance *Instance, // Helper function for all call types static uint64_t evmHandleCallInternal( zen::runtime::EVMInstance *Instance, evmc_call_kind CallKind, uint64_t Gas, - const uint8_t *ToAddr, const intx::uint256 &Value, uint64_t ArgsOffset, + const intx::uint256 &ToAddr, const intx::uint256 &Value, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize, bool ForceStatic) { const zen::runtime::EVMModule *Module = Instance->getModule(); ZEN_ASSERT(Module && Module->Host); const evmc_message *CurrentMsg = Instance->getCurrentMessage(); ZEN_ASSERT(CurrentMsg && "No current message set in EVMInstance"); - evmc::address TargetAddr = - ToAddr ? loadAddressFromLE(ToAddr) : evmc::address{}; + const bool HasValueArgs = CallKind == EVMC_CALL || CallKind == EVMC_CALLCODE; evmc_revision Rev = Instance->getRevision(); + evmc::address TargetAddr = intx::be::trunc(ToAddr); + if (liftedStackDebugEnabled()) { + uint8_t value_raw[32]; + intx::be::store(value_raw, Value); + std::fprintf(stderr, + "[lifted-runtime] kind=%u rev=%d gas=%llu to=%02x%02x..%02x%02x " + "value_low=%llu ao=%llu as=%llu ro=%llu rs=%llu\n", + static_cast(CallKind), static_cast(Rev), + static_cast(Gas), TargetAddr.bytes[0], + TargetAddr.bytes[1], TargetAddr.bytes[18], + TargetAddr.bytes[19], + static_cast(value_raw[31]), + static_cast(ArgsOffset), + static_cast(ArgsSize), + static_cast(RetOffset), + static_cast(RetSize)); + } if (Rev >= EVMC_BERLIN && Module->Host->access_account(TargetAddr) == EVMC_ACCESS_COLD) { Instance->chargeGas(zen::evm::ADDITIONAL_COLD_ACCOUNT_ACCESS_COST); } - const bool HasValueArgs = CallKind == EVMC_CALL || CallKind == EVMC_CALLCODE; const bool HasValue = Value != 0; if (CallKind == EVMC_CALL && HasValue && Instance->isStaticMode()) { @@ -1034,7 +1055,8 @@ static uint64_t evmHandleCallInternal( } uint64_t evmHandleCall(zen::runtime::EVMInstance *Instance, uint64_t Gas, - const uint8_t *ToAddr, const intx::uint256 &Value, + const intx::uint256 &ToAddr, + const intx::uint256 &Value, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize) { return evmHandleCallInternal(Instance, EVMC_CALL, Gas, ToAddr, Value, @@ -1042,7 +1064,8 @@ uint64_t evmHandleCall(zen::runtime::EVMInstance *Instance, uint64_t Gas, } uint64_t evmHandleCallCode(zen::runtime::EVMInstance *Instance, uint64_t Gas, - const uint8_t *ToAddr, const intx::uint256 &Value, + const intx::uint256 &ToAddr, + const intx::uint256 &Value, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize) { return evmHandleCallInternal(Instance, EVMC_CALLCODE, Gas, ToAddr, Value, @@ -1074,7 +1097,7 @@ void evmHandleUndefined(zen::runtime::EVMInstance *Instance) { } uint64_t evmHandleDelegateCall(zen::runtime::EVMInstance *Instance, - uint64_t Gas, const uint8_t *ToAddr, + uint64_t Gas, const intx::uint256 &ToAddr, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize) { return evmHandleCallInternal(Instance, EVMC_DELEGATECALL, Gas, ToAddr, @@ -1086,7 +1109,7 @@ uint64_t evmHandleDelegateCall(zen::runtime::EVMInstance *Instance, // (there is no EVMC_STATICCALL kind). The ForceStatic=true parameter // causes evmHandleCallInternal to set EVMC_STATIC flag on the message. uint64_t evmHandleStaticCall(zen::runtime::EVMInstance *Instance, uint64_t Gas, - const uint8_t *ToAddr, uint64_t ArgsOffset, + const intx::uint256 &ToAddr, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize) { return evmHandleCallInternal(Instance, EVMC_CALL, Gas, ToAddr, diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 3b04a5784..b7c6bd573 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -11,6 +11,7 @@ #include "utils/hash_utils.h" #include "utils/logging.h" #include "llvm/Support/Casting.h" +#include #include #include #include @@ -4608,6 +4609,24 @@ EVMMirBuilder::handleCall(Operand GasOp, Operand ToAddrOp, Operand ValueOp, Operand ArgsOffsetOp, Operand ArgsSizeOp, Operand RetOffsetOp, Operand RetSizeOp) { const auto &RuntimeFunctions = getRuntimeFunctionTable(); + if (std::getenv("DTVM_DEBUG_LIFTED_STACK")) { + auto low = [](const Operand &Op) -> unsigned long long { + return Op.isConstant() + ? static_cast(Op.getConstValue()[0]) + : 0ULL; + }; + std::fprintf(stderr, + "[lifted-lower] call c=[%d,%d,%d,%d,%d,%d,%d] " + "low=[%llu,%llu,%llu,%llu,%llu,%llu,%llu]\n", + GasOp.isConstant() ? 1 : 0, ToAddrOp.isConstant() ? 1 : 0, + ValueOp.isConstant() ? 1 : 0, + ArgsOffsetOp.isConstant() ? 1 : 0, + ArgsSizeOp.isConstant() ? 1 : 0, + RetOffsetOp.isConstant() ? 1 : 0, + RetSizeOp.isConstant() ? 1 : 0, low(GasOp), low(ToAddrOp), + low(ValueOp), low(ArgsOffsetOp), low(ArgsSizeOp), + low(RetOffsetOp), low(RetSizeOp)); + } // When gas value exceeds 64 bits, use max uint64 as fallback. // The runtime will cap it to available gas per EIP-150. uint64_t Non64Value = std::numeric_limits::max(); @@ -4619,7 +4638,8 @@ EVMMirBuilder::handleCall(Operand GasOp, Operand ToAddrOp, Operand ValueOp, syncGasToMemoryFull(); #endif auto Result = - callRuntimeFor( RuntimeFunctions.HandleCall, GasOp, ToAddrOp, ValueOp, ArgsOffsetOp, ArgsSizeOp, RetOffsetOp, RetSizeOp); @@ -4635,6 +4655,24 @@ EVMMirBuilder::handleCallCode(Operand GasOp, Operand ToAddrOp, Operand ValueOp, Operand ArgsOffsetOp, Operand ArgsSizeOp, Operand RetOffsetOp, Operand RetSizeOp) { const auto &RuntimeFunctions = getRuntimeFunctionTable(); + if (std::getenv("DTVM_DEBUG_LIFTED_STACK")) { + auto low = [](const Operand &Op) -> unsigned long long { + return Op.isConstant() + ? static_cast(Op.getConstValue()[0]) + : 0ULL; + }; + std::fprintf(stderr, + "[lifted-lower] callcode c=[%d,%d,%d,%d,%d,%d,%d] " + "low=[%llu,%llu,%llu,%llu,%llu,%llu,%llu]\n", + GasOp.isConstant() ? 1 : 0, ToAddrOp.isConstant() ? 1 : 0, + ValueOp.isConstant() ? 1 : 0, + ArgsOffsetOp.isConstant() ? 1 : 0, + ArgsSizeOp.isConstant() ? 1 : 0, + RetOffsetOp.isConstant() ? 1 : 0, + RetSizeOp.isConstant() ? 1 : 0, low(GasOp), low(ToAddrOp), + low(ValueOp), low(ArgsOffsetOp), low(ArgsSizeOp), + low(RetOffsetOp), low(RetSizeOp)); + } // When gas value exceeds 64 bits, use max uint64 as fallback. // The runtime will cap it to available gas per EIP-150. uint64_t Non64Value = std::numeric_limits::max(); @@ -4646,7 +4684,8 @@ EVMMirBuilder::handleCallCode(Operand GasOp, Operand ToAddrOp, Operand ValueOp, syncGasToMemoryFull(); #endif auto Result = - callRuntimeFor( RuntimeFunctions.HandleCallCode, GasOp, ToAddrOp, ValueOp, ArgsOffsetOp, ArgsSizeOp, RetOffsetOp, RetSizeOp); @@ -4688,6 +4727,23 @@ EVMMirBuilder::handleDelegateCall(Operand GasOp, Operand ToAddrOp, Operand ArgsOffsetOp, Operand ArgsSizeOp, Operand RetOffsetOp, Operand RetSizeOp) { const auto &RuntimeFunctions = getRuntimeFunctionTable(); + if (std::getenv("DTVM_DEBUG_LIFTED_STACK")) { + auto low = [](const Operand &Op) -> unsigned long long { + return Op.isConstant() + ? static_cast(Op.getConstValue()[0]) + : 0ULL; + }; + std::fprintf(stderr, + "[lifted-lower] delegatecall c=[%d,%d,%d,%d,%d,%d] " + "low=[%llu,%llu,%llu,%llu,%llu,%llu]\n", + GasOp.isConstant() ? 1 : 0, ToAddrOp.isConstant() ? 1 : 0, + ArgsOffsetOp.isConstant() ? 1 : 0, + ArgsSizeOp.isConstant() ? 1 : 0, + RetOffsetOp.isConstant() ? 1 : 0, + RetSizeOp.isConstant() ? 1 : 0, low(GasOp), low(ToAddrOp), + low(ArgsOffsetOp), low(ArgsSizeOp), low(RetOffsetOp), + low(RetSizeOp)); + } // When gas value exceeds 64 bits, use max uint64 as fallback. // The runtime will cap it to available gas per EIP-150. uint64_t Non64Value = std::numeric_limits::max(); @@ -4698,7 +4754,8 @@ EVMMirBuilder::handleDelegateCall(Operand GasOp, Operand ToAddrOp, #ifdef ZEN_ENABLE_EVM_GAS_REGISTER syncGasToMemoryFull(); #endif - auto Result = callRuntimeFor( RuntimeFunctions.HandleDelegateCall, GasOp, ToAddrOp, ArgsOffsetOp, ArgsSizeOp, RetOffsetOp, RetSizeOp); @@ -4714,6 +4771,23 @@ EVMMirBuilder::handleStaticCall(Operand GasOp, Operand ToAddrOp, Operand ArgsOffsetOp, Operand ArgsSizeOp, Operand RetOffsetOp, Operand RetSizeOp) { const auto &RuntimeFunctions = getRuntimeFunctionTable(); + if (std::getenv("DTVM_DEBUG_LIFTED_STACK")) { + auto low = [](const Operand &Op) -> unsigned long long { + return Op.isConstant() + ? static_cast(Op.getConstValue()[0]) + : 0ULL; + }; + std::fprintf(stderr, + "[lifted-lower] staticcall c=[%d,%d,%d,%d,%d,%d] " + "low=[%llu,%llu,%llu,%llu,%llu,%llu]\n", + GasOp.isConstant() ? 1 : 0, ToAddrOp.isConstant() ? 1 : 0, + ArgsOffsetOp.isConstant() ? 1 : 0, + ArgsSizeOp.isConstant() ? 1 : 0, + RetOffsetOp.isConstant() ? 1 : 0, + RetSizeOp.isConstant() ? 1 : 0, low(GasOp), low(ToAddrOp), + low(ArgsOffsetOp), low(ArgsSizeOp), low(RetOffsetOp), + low(RetSizeOp)); + } // When gas value exceeds 64 bits, use max uint64 as fallback. // The runtime will cap it to available gas per EIP-150. uint64_t Non64Value = std::numeric_limits::max(); @@ -4724,7 +4798,8 @@ EVMMirBuilder::handleStaticCall(Operand GasOp, Operand ToAddrOp, #ifdef ZEN_ENABLE_EVM_GAS_REGISTER syncGasToMemoryFull(); #endif - auto Result = callRuntimeFor( RuntimeFunctions.HandleStaticCall, GasOp, ToAddrOp, ArgsOffsetOp, ArgsSizeOp, RetOffsetOp, RetSizeOp); diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index 83b3a5e1b..98aa6eb3b 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -218,8 +218,18 @@ class MockEVMBuilder { template void handleLogWithTopics(Args...) {} - Operand handleCall(Operand, Operand, Operand, Operand, Operand, Operand, - Operand) { + Operand handleCall(Operand Gas, Operand ToAddr, Operand Value, + Operand ArgsOffset, Operand ArgsSize, Operand RetOffset, + Operand RetSize) { + LastCallArgs[0] = Gas.resolvedValue(); + LastCallArgs[1] = ToAddr.resolvedValue(); + LastCallArgs[2] = Value.resolvedValue(); + LastCallArgs[3] = ArgsOffset.resolvedValue(); + LastCallArgs[4] = ArgsSize.resolvedValue(); + LastCallArgs[5] = RetOffset.resolvedValue(); + LastCallArgs[6] = RetSize.resolvedValue(); + HasCallArgs = true; + CallCount++; return Operand(0); } @@ -320,6 +330,15 @@ class MockEVMBuilder { bool hasLastPushValue() const { return HasLastPushValue; } + bool hasLastCallArgs() const { return HasCallArgs; } + + size_t callCount() const { return CallCount; } + + MockOperand::U256Value lastCallArg(size_t Index) const { + ZEN_ASSERT(Index < LastCallArgs.size() && "call arg index out of range"); + return LastCallArgs[Index]; + } + MockOperand::U256Value lastPushValue() const { ZEN_ASSERT(HasLastPushValue && "mock push value is missing"); return LastPushValue; @@ -342,6 +361,13 @@ class MockEVMBuilder { std::vector RuntimeStack; MockOperand::U256Value LastPushValue = {0, 0, 0, 0}; bool HasLastPushValue = false; + std::array LastCallArgs = { + MockOperand::U256Value{0, 0, 0, 0}, MockOperand::U256Value{0, 0, 0, 0}, + MockOperand::U256Value{0, 0, 0, 0}, MockOperand::U256Value{0, 0, 0, 0}, + MockOperand::U256Value{0, 0, 0, 0}, MockOperand::U256Value{0, 0, 0, 0}, + MockOperand::U256Value{0, 0, 0, 0}}; + bool HasCallArgs = false; + size_t CallCount = 0; #undef MOCK_OPERAND_STUB #undef MOCK_VOID_STUB @@ -695,6 +721,60 @@ TEST(EVMJITFrontendVisitorTest, LegacyRevisionDupSwapUseRuntimeStackPath) { EXPECT_GT(SwapStats.StackSetCount, 0U); } +TEST(EVMJITFrontendVisitorTest, + ModernMaterializedMergePreservesCallOperandsAfterDeepDupSwap) { + const std::vector Bytecode = { + 0x60, 0xaa, // PUSH1 0xaa + 0x60, 0xbb, // PUSH1 0xbb + 0x5f, // PUSH0 + 0x35, // CALLDATALOAD + 0x60, 0x0e, // PUSH1 target + 0x57, // JUMPI + 0x60, 0xcc, // PUSH1 0xcc + 0x60, 0x0e, // PUSH1 target + 0x56, // JUMP + 0x5b, // JUMPDEST (target) + 0x5f, // PUSH0 retSize + 0x5f, // PUSH0 retOffset + 0x5f, // PUSH0 argsSize + 0x5f, // PUSH0 argsOffset + 0x5f, // PUSH0 value + 0x86, // DUP7 (duplicate deep merged value as to-address) + 0x90, // SWAP1 + 0x90, // SWAP1 (net no-op, keeps swap path exercised) + 0x60, 0x20, // PUSH1 gas + 0xf1, // CALL + 0x00 // STOP + }; + + const EVMAnalyzer Analyzer = analyzeBytecode(Bytecode); + const auto *TargetBlock = findBlock(Analyzer, 14); + ASSERT_NE(TargetBlock, nullptr); + EXPECT_FALSE(TargetBlock->CanLiftStack); + EXPECT_EQ(TargetBlock->ResolvedEntryStackDepth, -1); + EXPECT_TRUE(TargetBlock->HasInconsistentEntryDepth); + + COMPILER::EVMFrontendContext Ctx; + Ctx.setRevision(EVMC_CANCUN); + Ctx.setBytecode(reinterpret_cast(Bytecode.data()), + Bytecode.size()); + + MockEVMBuilder Builder; + COMPILER::EVMByteCodeVisitor Visitor(Builder, &Ctx); + EXPECT_TRUE(Visitor.compile()); + EXPECT_FALSE(Builder.Trapped); + EXPECT_FALSE(Builder.Undefined); + ASSERT_TRUE(Builder.hasLastCallArgs()); + EXPECT_EQ(Builder.callCount(), 1U); + EXPECT_EQ(Builder.lastCallArg(0)[0], 0x20U); // gas + EXPECT_EQ(Builder.lastCallArg(1)[0], 0xbbU); // to + EXPECT_EQ(Builder.lastCallArg(2)[0], 0x0U); // value + EXPECT_EQ(Builder.lastCallArg(3)[0], 0x0U); // args offset + EXPECT_EQ(Builder.lastCallArg(4)[0], 0x0U); // args size + EXPECT_EQ(Builder.lastCallArg(5)[0], 0x0U); // ret offset + EXPECT_EQ(Builder.lastCallArg(6)[0], 0x0U); // ret size +} + TEST(EVMJITFrontendVisitorTest, ImplicitStopMaterializesLiftedStackOnFallthrough) { const std::vector Bytecode = { From ca792cec9a5e1be1825720b68e68fdcac6fb4620 Mon Sep 17 00:00:00 2001 From: starwarfan <519125404@qq.com> Date: Thu, 14 May 2026 10:17:57 +0800 Subject: [PATCH 6/7] test(evm): add state-based CALL creation-cost regression across revisions Add a runtime-backed state regression that executes the same CALL-heavy bytecode in interpreter and multipass for Frontier, Tangerine Whistle, and Cancun, asserting revision-expected account-creation gas deltas and mode parity of those deltas. --- src/tests/evm_interp_tests.cpp | 165 +++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index 0bfe4cabc..b815dda91 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -144,6 +144,8 @@ struct EVMExecutionResult { evmc_status_code Status = EVMC_INTERNAL_ERROR; std::string OutputHex; bool JITCompiled = false; + uint64_t GasUsed = 0; + int64_t GasLeft = 0; }; EVMExecutionResult executeEvmBytecode(const std::string &ModuleName, @@ -218,6 +220,97 @@ EVMExecutionResult executeEvmBytecode(const std::string &ModuleName, Exec.Status = RawResult.status_code; Exec.OutputHex = zen::utils::toHex(RawResult.output_data, RawResult.output_size); + Exec.GasLeft = RawResult.gas_left; + if (RawResult.gas_left >= 0 && + static_cast(RawResult.gas_left) <= ExecutionGasLimit) { + Exec.GasUsed = ExecutionGasLimit - static_cast(RawResult.gas_left); + } + return Exec; +} + +EVMExecutionResult executeInlineBytecodeWithState(const std::vector &Bytecode, + common::RunMode Mode, + evmc_revision Revision, + bool TargetExists) { + EVMExecutionResult Empty; + RuntimeConfig Config; + Config.Mode = Mode; + + auto MockedHost = std::make_unique(); + MockedHost->tx_context.tx_origin = zen::evm::DEFAULT_DEPLOYER_ADDRESS; + auto RT = Runtime::newEVMRuntime(Config, MockedHost.get()); + EXPECT_TRUE(RT != nullptr) << "Failed to create runtime"; + if (!RT) { + return Empty; + } + MockedHost->setRuntime(RT.get()); + + if (TargetExists) { + evmc::address TargetAddr{}; + TargetAddr.bytes[19] = 0xbb; + evmc::MockedAccount Account{}; + Account.nonce = 1; + Account.codehash = zen::evm::EMPTY_CODE_HASH; + MockedHost->accounts[TargetAddr] = Account; + } + + auto ModRet = + RT->loadEVMModule("legacy_call_state_regression", Bytecode.data(), + Bytecode.size()); + EXPECT_TRUE(ModRet) << "Failed to load inline module"; + if (!ModRet) { + return Empty; + } + EVMModule *Mod = *ModRet; + + Isolation *Iso = RT->createManagedIsolation(); + EXPECT_TRUE(Iso != nullptr) << "Failed to create isolation"; + if (!Iso) { + return Empty; + } + + const uint64_t GasLimit = 500000; + const uint64_t IntrinsicGas = zen::evm::BASIC_EXECUTION_COST; + const uint64_t ExecutionGasLimit = GasLimit - IntrinsicGas; + + auto InstRet = Iso->createEVMInstance(*Mod, ExecutionGasLimit); + EXPECT_TRUE(InstRet) << "Failed to create instance"; + if (!InstRet) { + return Empty; + } + EVMInstance *Inst = *InstRet; + Inst->setRevision(Revision); + + evmc_message Msg = { + .kind = EVMC_CALL, + .flags = 0u, + .depth = 0, + .gas = static_cast(ExecutionGasLimit), + .recipient = {}, + .sender = zen::evm::DEFAULT_DEPLOYER_ADDRESS, + .input_data = nullptr, + .input_size = 0, + .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); + Exec.GasLeft = RawResult.gas_left; + if (RawResult.gas_left >= 0 && + static_cast(RawResult.gas_left) <= ExecutionGasLimit) { + Exec.GasUsed = ExecutionGasLimit - static_cast(RawResult.gas_left); + } return Exec; } @@ -688,4 +781,76 @@ TEST(EVMRegressionTest, Issue488_PCAsAddmodAugend_InterpMatchesMultipass) { EXPECT_EQ(InterpExec.OutputHex, "0000000000000000000000000000000000000000000000000000000000000006"); } + +TEST(EVMStateRegressionTest, CallAccountCreationCostMatchesRevisionAcrossModes) { + const std::vector Bytecode = { + 0x60, 0xaa, // PUSH1 0xaa + 0x60, 0xbb, // PUSH1 0xbb + 0x60, 0x00, // PUSH1 0x00 + 0x35, // CALLDATALOAD + 0x60, 0x0f, // PUSH1 0x0f (jumpdest) + 0x57, // JUMPI + 0x60, 0xcc, // PUSH1 0xcc + 0x60, 0x0f, // PUSH1 0x0f + 0x56, // JUMP + 0x5b, // JUMPDEST + 0x60, 0x00, // PUSH1 retSize + 0x60, 0x00, // PUSH1 retOffset + 0x60, 0x00, // PUSH1 argsSize + 0x60, 0x00, // PUSH1 argsOffset + 0x60, 0x00, // PUSH1 value + 0x86, // DUP7 (to-address -> 0xbb on runtime path) + 0x90, // SWAP1 + 0x90, // SWAP1 + 0x60, 0x20, // PUSH1 gas + 0xf1, // CALL + 0x00 // STOP + }; + + struct RevisionCase { + evmc_revision Revision; + uint64_t ExpectedCreationDelta; + }; + const RevisionCase Cases[] = { + {EVMC_FRONTIER, 25000}, + {EVMC_TANGERINE_WHISTLE, 25000}, + {EVMC_CANCUN, 0}, + }; + + for (const auto &Case : Cases) { + auto InterpMissing = + executeInlineBytecodeWithState(Bytecode, common::RunMode::InterpMode, + Case.Revision, false); + auto InterpExisting = + executeInlineBytecodeWithState(Bytecode, common::RunMode::InterpMode, + Case.Revision, true); + auto MpMissing = + executeInlineBytecodeWithState(Bytecode, common::RunMode::MultipassMode, + Case.Revision, false); + auto MpExisting = + executeInlineBytecodeWithState(Bytecode, common::RunMode::MultipassMode, + Case.Revision, true); + + EXPECT_EQ(InterpMissing.Status, EVMC_SUCCESS); + EXPECT_EQ(InterpExisting.Status, EVMC_SUCCESS); + EXPECT_EQ(MpMissing.Status, EVMC_SUCCESS); + EXPECT_EQ(MpExisting.Status, EVMC_SUCCESS); + + ASSERT_GE(InterpMissing.GasUsed, InterpExisting.GasUsed); + ASSERT_GE(MpMissing.GasUsed, MpExisting.GasUsed); + + const uint64_t InterpDelta = InterpMissing.GasUsed - InterpExisting.GasUsed; + const uint64_t MpDelta = MpMissing.GasUsed - MpExisting.GasUsed; + + EXPECT_EQ(InterpDelta, Case.ExpectedCreationDelta) + << "unexpected creation-cost delta for revision " + << static_cast(Case.Revision); + EXPECT_EQ(MpDelta, Case.ExpectedCreationDelta) + << "unexpected multipass creation-cost delta for revision " + << static_cast(Case.Revision); + EXPECT_EQ(InterpDelta, MpDelta) + << "interpreter/multipass creation-cost delta mismatch for revision " + << static_cast(Case.Revision); + } +} #endif From e081a461da57c081b6cf3d0f4d726d846ca5d9ed Mon Sep 17 00:00:00 2001 From: cl507523 Date: Thu, 14 May 2026 03:08:24 +0000 Subject: [PATCH 7/7] fix(evm): preserve CALL operand provenance with logical stack in low revisions Remove the revision-based logical stack bypass so low revisions keep the same stack model, and make deep DUP/SWAP fallback logic provenance-safe when logical depth is insufficient. Add low-revision frontend regression coverage around merge+deep DUP/SWAP CALL lowering so account-creation gas behavior remains parity-stable across interpreter and multipass. Co-authored-by: Cursor --- docs/legacy_multipass_call_repro.md | 29 ++++----- src/action/evm_bytecode_visitor.h | 78 ++++++++++++------------ src/compiler/evm_frontend/evm_imported.h | 12 ++-- src/evm/opcode_handlers.cpp | 25 +++++++- src/tests/evm_jit_frontend_tests.cpp | 64 +++++++++++++++++-- 5 files changed, 141 insertions(+), 67 deletions(-) diff --git a/docs/legacy_multipass_call_repro.md b/docs/legacy_multipass_call_repro.md index 60f849bc6..ec35825c8 100644 --- a/docs/legacy_multipass_call_repro.md +++ b/docs/legacy_multipass_call_repro.md @@ -35,25 +35,20 @@ env SILKWORM_EVM=./libdtvmapi.so,mode=multipass DTVM_EVM_DISABLE_MULTIPASS_GREED ## Root-Cause Direction In legacy revisions, stack lifting/logical-stack materialization can lose deep -stack operands before CALL-family lowering executes. The frontend fix keeps -legacy stack operations on the runtime EVM stack path, preserving CALL argument -order/value at execution sites without interpreter fallback. +stack operands before CALL-family lowering executes. The frontend fix preserves +CALL operand provenance while keeping logical stack enabled across revisions, +including low revisions. ## Frontend Test Semantics -The regression test `LegacyRevisionDupSwapUseRuntimeStackPath` in -`src/tests/evm_jit_frontend_tests.cpp` is intentionally behavioral: +The regression tests in `src/tests/evm_jit_frontend_tests.cpp` guard operand +provenance rather than implementation details: -- It uses `EVMC_FRONTIER`. -- It asserts `DUP2` and `SWAP1` perform runtime stack access - (`stackGet`/`stackSet`) in legacy mode. +- `LegacyRevisionDupSwapPreservesOperandOrder` validates low-revision DUP/SWAP + stack ordering without requiring a runtime-stack-only fallback. +- `LowRevisionMaterializedMergePreservesCallOperandsAfterDeepDupSwap` asserts + the deep DUP/SWAP + merge case still lowers CALL with recipient `0xbb` under + `EVMC_FRONTIER`. -If legacy runtime-stack branches are disabled in the visitor, the same test -fails with: - -- `DupStats.StackGetCount == 0` -- `SwapStats.StackGetCount == 0` -- `SwapStats.StackSetCount == 0` - -That failure confirms the test is not cosmetic; it directly guards the legacy -operand provenance needed by CALL-family lowering. +These checks lock the actual root-cause surface: CALL operand provenance at +lowering sites. diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index 931f169f9..bfb5aff0e 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -105,6 +105,10 @@ template class EVMByteCodeVisitor { return std::getenv("DTVM_DEBUG_LIFTED_STACK") != nullptr; } + static bool callProvenanceDebugEnabled() { + return std::getenv("DTVM_DEBUG_CALL_PROVENANCE") != nullptr; + } + void spillTrackedStackPreservingPrefix(const std::vector &Values, uint32_t PrefixDepth) { if constexpr (HasSpillTrackedStackPreservingPrefix::value) { @@ -131,17 +135,10 @@ template class EVMByteCodeVisitor { } void push(const Operand &Opnd) { - if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { - Builder.stackPush(Opnd); - return; - } Stack.push(Opnd); } void requireLogicalStackDepth(uint32_t Depth) { - if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { - return; - } if (liftedStackDebugEnabled() && Stack.getSize() < Depth) { std::fprintf(stderr, "[lifted-check] logical-underflow pc=%llu block=%llu " @@ -155,11 +152,6 @@ template class EVMByteCodeVisitor { } Operand pop() { - if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { - Operand Opnd = Builder.stackPop(); - Builder.releaseOperand(Opnd); - return Opnd; - } requireLogicalStackDepth(1); Operand Opnd = Stack.pop(); Builder.releaseOperand(Opnd); @@ -1168,11 +1160,6 @@ template class EVMByteCodeVisitor { Builder.createStackCheckBlock(-BlockInfo.MinStackHeight, 1024 - BlockInfo.MaxStackHeight); } - if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { - CurrentBlockLifted = false; - return; - } - if (LiftedBlock) { CurrentBlockLifted = true; CurrentBlockHiddenLiveInPrefixDepth = @@ -1875,8 +1862,7 @@ template class EVMByteCodeVisitor { // DUP1-DUP16: Duplicate Nth stack item void handleDup(uint8_t Index) { Operand Result; - if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE && - Stack.getSize() < static_cast(Index)) { + if (Stack.getSize() < static_cast(Index)) { const int32_t MemIndex = static_cast(Index) - static_cast(Stack.getSize()) - 1; @@ -1896,26 +1882,24 @@ template class EVMByteCodeVisitor { // SWAP1-SWAP16: Swap top with Nth+1 stack item void handleSwap(uint8_t Index) { - if (Ctx->getRevision() < EVMC_TANGERINE_WHISTLE) { - const uint32_t RequiredDepth = static_cast(Index) + 1u; - if (Stack.empty()) { - const int32_t MemIndex = - static_cast(Index) - static_cast(Stack.getSize()); - Operand A = Builder.stackGet(0); - Operand B = Builder.stackGet(MemIndex); - Builder.stackSet(0, B); - Builder.stackSet(MemIndex, A); - return; - } - if (Stack.getSize() < RequiredDepth) { - const int32_t MemIndex = - static_cast(Index) - static_cast(Stack.getSize()); - Operand &A = Stack.peek(0); - Operand B = Builder.stackGet(MemIndex); - Builder.stackSet(MemIndex, A); - A = B; - return; - } + const uint32_t RequiredDepth = static_cast(Index) + 1u; + if (Stack.empty()) { + const int32_t MemIndex = + static_cast(Index) - static_cast(Stack.getSize()); + Operand A = Builder.stackGet(0); + Operand B = Builder.stackGet(MemIndex); + Builder.stackSet(0, B); + Builder.stackSet(MemIndex, A); + return; + } + if (Stack.getSize() < RequiredDepth) { + const int32_t MemIndex = + static_cast(Index) - static_cast(Stack.getSize()); + Operand &A = Stack.peek(0); + Operand B = Builder.stackGet(MemIndex); + Builder.stackSet(MemIndex, A); + A = B; + return; } requireLogicalStackDepth(static_cast(Index) + 1u); std::swap(Stack.peek(0), Stack.peek(Index)); @@ -2000,6 +1984,22 @@ template class EVMByteCodeVisitor { Operand ArgsSizeOp = pop(); Operand RetOffsetOp = pop(); Operand RetSizeOp = pop(); + if (callProvenanceDebugEnabled()) { + auto low = [](const Operand &Op) -> unsigned long long { + return Op.isConstant() ? static_cast(Op.getConstValue()[0]) + : 0ULL; + }; + std::fprintf(stderr, + "[call-provenance][visitor] rev=%d pc=%llu block=%llu " + "logical=%u lifted=%d " + "call low=[%llu,%llu,%llu,%llu,%llu,%llu,%llu]\n", + static_cast(Ctx->getRevision()), + static_cast(PC), + static_cast(CurrentBlockEntryPC), + Stack.getSize(), CurrentBlockLifted ? 1 : 0, low(GasOp), + low(ToAddrOp), low(ValueOp), low(ArgsOffsetOp), + low(ArgsSizeOp), low(RetOffsetOp), low(RetSizeOp)); + } Operand StatusOp = (Builder.*handler)(GasOp, ToAddrOp, ValueOp, ArgsOffsetOp, ArgsSizeOp, RetOffsetOp, RetSizeOp); diff --git a/src/compiler/evm_frontend/evm_imported.h b/src/compiler/evm_frontend/evm_imported.h index a5a90a533..77f7dad34 100644 --- a/src/compiler/evm_frontend/evm_imported.h +++ b/src/compiler/evm_frontend/evm_imported.h @@ -71,10 +71,10 @@ using Create2Fn = const uint8_t *(*)(zen::runtime::EVMInstance *, // Call function types for different call operations using CallFn = uint64_t (*)(zen::runtime::EVMInstance *, uint64_t, - const uint8_t *, const intx::uint256 &, uint64_t, + const intx::uint256 &, const intx::uint256 &, uint64_t, uint64_t, uint64_t, uint64_t); // CALL, CALLCODE using DelegateCallFn = uint64_t (*)(zen::runtime::EVMInstance *, uint64_t, - const uint8_t *, uint64_t, uint64_t, + const intx::uint256 &, uint64_t, uint64_t, uint64_t, uint64_t); // DELEGATECALL, STATICCALL @@ -233,21 +233,21 @@ const uint8_t *evmHandleCreate2(zen::runtime::EVMInstance *Instance, const intx::uint256 &Value, uint64_t Offset, uint64_t Size, const uint8_t *Salt); uint64_t evmHandleCall(zen::runtime::EVMInstance *Instance, uint64_t Gas, - const uint8_t *ToAddr, const intx::uint256 &Value, + const intx::uint256 &ToAddr, const intx::uint256 &Value, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize); uint64_t evmHandleCallCode(zen::runtime::EVMInstance *Instance, uint64_t Gas, - const uint8_t *ToAddr, const intx::uint256 &Value, + const intx::uint256 &ToAddr, const intx::uint256 &Value, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize); void evmSetReturn(zen::runtime::EVMInstance *Instance, uint64_t MemOffset, uint64_t Length); uint64_t evmHandleDelegateCall(zen::runtime::EVMInstance *Instance, - uint64_t Gas, const uint8_t *ToAddr, + uint64_t Gas, const intx::uint256 &ToAddr, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize); uint64_t evmHandleStaticCall(zen::runtime::EVMInstance *Instance, uint64_t Gas, - const uint8_t *ToAddr, uint64_t ArgsOffset, + const intx::uint256 &ToAddr, uint64_t ArgsOffset, uint64_t ArgsSize, uint64_t RetOffset, uint64_t RetSize); void evmSetRevert(zen::runtime::EVMInstance *Instance, uint64_t Offset, diff --git a/src/evm/opcode_handlers.cpp b/src/evm/opcode_handlers.cpp index 7a9b9aed2..9151a647a 100644 --- a/src/evm/opcode_handlers.cpp +++ b/src/evm/opcode_handlers.cpp @@ -11,6 +11,7 @@ #include "runtime/evm_instance.h" #include +#include #include thread_local zen::evm::EVMFrame *zen::evm::EVMResource::CurrentFrame = nullptr; @@ -27,6 +28,10 @@ namespace { bool chargeGas(EVMFrame *Frame, uint64_t GasCost); +bool callProvenanceDebugEnabled() { + return std::getenv("DTVM_DEBUG_CALL_PROVENANCE") != nullptr; +} + evmc_revision currentRevision() { auto *Context = EVMResource::getInterpreterExecContext(); if (!Context) { @@ -1278,6 +1283,16 @@ void CallHandler::doExecute() { const auto OutputSize = Frame->pop(); const bool HasValue = Value != 0; + if (callProvenanceDebugEnabled()) { + std::fprintf(stderr, + "[call-provenance][interp] rev=%d opcode=%u " + "dest=%02x%02x..%02x%02x value_low=%llu gas_low=%llu\n", + static_cast(currentRevision()), + static_cast(OpCode), Dest.bytes[0], Dest.bytes[1], + Dest.bytes[18], Dest.bytes[19], + static_cast(static_cast(Value)), + static_cast(static_cast(Gas))); + } // Assume failure EVM_REQUIRE_STACK_SPACE(Frame, 1); @@ -1340,7 +1355,15 @@ void CallHandler::doExecute() { uint64_t GasCost = HasValue ? CALL_VALUE_COST : 0; if (CallKind == EVMC_CALL) { if (HasValue || Rev < EVMC_SPURIOUS_DRAGON) { - if (!Frame->Host->account_exists(Dest)) { + const bool DestExists = Frame->Host->account_exists(Dest); + if (callProvenanceDebugEnabled()) { + std::fprintf(stderr, + "[call-provenance][interp] rev=%d callkind=%u " + "dest_exists=%d has_value=%d\n", + static_cast(Rev), static_cast(CallKind), + DestExists ? 1 : 0, HasValue ? 1 : 0); + } + if (!DestExists) { GasCost += ACCOUNT_CREATION_COST; } } diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index 98aa6eb3b..bc00f592b 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -690,7 +690,7 @@ TEST(EVMJITFrontendVisitorTest, EXPECT_FALSE(Builder.Undefined); } -TEST(EVMJITFrontendVisitorTest, LegacyRevisionDupSwapUseRuntimeStackPath) { +TEST(EVMJITFrontendVisitorTest, LegacyRevisionDupSwapPreservesOperandOrder) { const std::vector Bytecode = { 0x60, 0xaa, // PUSH1 0xaa 0x60, 0xbb, // PUSH1 0xbb @@ -710,15 +710,71 @@ TEST(EVMJITFrontendVisitorTest, LegacyRevisionDupSwapUseRuntimeStackPath) { EXPECT_TRUE(Visitor.compile()); EXPECT_FALSE(Builder.Trapped); EXPECT_FALSE(Builder.Undefined); + EXPECT_EQ(Builder.runtimeStackDepth(), 2U); + EXPECT_EQ(Builder.topStackValue()[0], 0xaaU); const auto &DupStats = Builder.accessStats(OP_DUP2); EXPECT_EQ(DupStats.StackPopCount, 0U); - EXPECT_GT(DupStats.StackGetCount, 0U); + EXPECT_EQ(DupStats.StackGetCount, 0U); const auto &SwapStats = Builder.accessStats(OP_SWAP1); EXPECT_EQ(SwapStats.StackPopCount, 0U); - EXPECT_GT(SwapStats.StackGetCount, 0U); - EXPECT_GT(SwapStats.StackSetCount, 0U); + EXPECT_EQ(SwapStats.StackGetCount, 0U); + EXPECT_EQ(SwapStats.StackSetCount, 0U); +} + +TEST(EVMJITFrontendVisitorTest, + LowRevisionMaterializedMergePreservesCallOperandsAfterDeepDupSwap) { + const std::vector Bytecode = { + 0x60, 0xaa, // PUSH1 0xaa + 0x60, 0xbb, // PUSH1 0xbb + 0x60, 0x00, // PUSH1 0x00 + 0x35, // CALLDATALOAD + 0x60, 0x0f, // PUSH1 target + 0x57, // JUMPI + 0x60, 0xcc, // PUSH1 0xcc + 0x60, 0x0f, // PUSH1 target + 0x56, // JUMP + 0x5b, // JUMPDEST + 0x60, 0x00, // PUSH1 retSize + 0x60, 0x00, // PUSH1 retOffset + 0x60, 0x00, // PUSH1 argsSize + 0x60, 0x00, // PUSH1 argsOffset + 0x60, 0x00, // PUSH1 value + 0x86, // DUP7 (duplicate deep merged value as to-address) + 0x90, // SWAP1 + 0x90, // SWAP1 + 0x60, 0x20, // PUSH1 gas + 0xf1, // CALL + 0x00 // STOP + }; + + const EVMAnalyzer Analyzer = analyzeBytecode(Bytecode); + const auto *TargetBlock = findBlock(Analyzer, 15); + ASSERT_NE(TargetBlock, nullptr); + EXPECT_FALSE(TargetBlock->CanLiftStack); + EXPECT_EQ(TargetBlock->ResolvedEntryStackDepth, -1); + EXPECT_TRUE(TargetBlock->HasInconsistentEntryDepth); + + COMPILER::EVMFrontendContext Ctx; + Ctx.setRevision(EVMC_FRONTIER); + Ctx.setBytecode(reinterpret_cast(Bytecode.data()), + Bytecode.size()); + + MockEVMBuilder Builder; + COMPILER::EVMByteCodeVisitor Visitor(Builder, &Ctx); + EXPECT_TRUE(Visitor.compile()); + EXPECT_FALSE(Builder.Trapped); + EXPECT_FALSE(Builder.Undefined); + ASSERT_TRUE(Builder.hasLastCallArgs()); + EXPECT_EQ(Builder.callCount(), 1U); + EXPECT_EQ(Builder.lastCallArg(0)[0], 0x20U); // gas + EXPECT_EQ(Builder.lastCallArg(1)[0], 0xbbU); // to + EXPECT_EQ(Builder.lastCallArg(2)[0], 0x0U); // value + EXPECT_EQ(Builder.lastCallArg(3)[0], 0x0U); // args offset + EXPECT_EQ(Builder.lastCallArg(4)[0], 0x0U); // args size + EXPECT_EQ(Builder.lastCallArg(5)[0], 0x0U); // ret offset + EXPECT_EQ(Builder.lastCallArg(6)[0], 0x0U); // ret size } TEST(EVMJITFrontendVisitorTest,