Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/legacy_multipass_call_repro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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 preserves
CALL operand provenance while keeping logical stack enabled across revisions,
including low revisions.

## Frontend Test Semantics

The regression tests in `src/tests/evm_jit_frontend_tests.cpp` guard operand
provenance rather than implementation details:

- `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`.

These checks lock the actual root-cause surface: CALL operand provenance at
lowering sites.
136 changes: 132 additions & 4 deletions src/action/evm_bytecode_visitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "runtime/evm_module.h"

#include <array>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <type_traits>
#include <utility>
Expand Down Expand Up @@ -99,6 +101,14 @@ template <typename IRBuilder> class EVMByteCodeVisitor {
}
}

static bool liftedStackDebugEnabled() {
return std::getenv("DTVM_DEBUG_LIFTED_STACK") != nullptr;
}

static bool callProvenanceDebugEnabled() {
return std::getenv("DTVM_DEBUG_CALL_PROVENANCE") != nullptr;
}

void spillTrackedStackPreservingPrefix(const std::vector<Operand> &Values,
uint32_t PrefixDepth) {
if constexpr (HasSpillTrackedStackPreservingPrefix<IRBuilder>::value) {
Expand All @@ -124,9 +134,19 @@ template <typename IRBuilder> class EVMByteCodeVisitor {
}
}

void push(const Operand &Opnd) { Stack.push(Opnd); }
void push(const Operand &Opnd) {
Stack.push(Opnd);
}

void requireLogicalStackDepth(uint32_t Depth) {
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<unsigned long long>(PC),
static_cast<unsigned long long>(CurrentBlockEntryPC), Depth,
Stack.getSize(), CurrentBlockLifted ? 1 : 0);
}
ZEN_ASSERT(Stack.getSize() >= Depth &&
"Logical EVM stack must be preloaded at block entry");
}
Expand Down Expand Up @@ -174,6 +194,19 @@ template <typename IRBuilder> 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<unsigned long long>(PC),
static_cast<unsigned long long>(CurrentBlockEntryPC),
static_cast<unsigned>(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
Expand Down Expand Up @@ -1127,13 +1160,21 @@ template <typename IRBuilder> class EVMByteCodeVisitor {
Builder.createStackCheckBlock(-BlockInfo.MinStackHeight,
1024 - BlockInfo.MaxStackHeight);
}

if (LiftedBlock) {
CurrentBlockLifted = true;
CurrentBlockHiddenLiveInPrefixDepth =
static_cast<uint32_t>(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<unsigned long long>(PC),
BlockInfo.ResolvedEntryStackDepth,
BlockInfo.FullEntryStateDepth,
CurrentBlockHiddenLiveInPrefixDepth, Stack.getSize());
}
return;
}

Expand All @@ -1148,6 +1189,13 @@ template <typename IRBuilder> 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<unsigned long long>(PC), -BlockInfo.MinPopHeight,
Stack.getSize(), BlockInfo.MinPopHeight);
}
}

void materializeLiftedBlockMergeRequests(uint64_t BlockPC) {
Expand Down Expand Up @@ -1202,6 +1250,43 @@ template <typename IRBuilder> 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 ||
Expand Down Expand Up @@ -1776,8 +1861,16 @@ template <typename IRBuilder> class EVMByteCodeVisitor {

// DUP1-DUP16: Duplicate Nth stack item
void handleDup(uint8_t Index) {
requireLogicalStackDepth(Index);
Operand Result = Stack.peek(Index - 1);
Operand Result;
if (Stack.getSize() < static_cast<uint32_t>(Index)) {
const int32_t MemIndex =
static_cast<int32_t>(Index) - static_cast<int32_t>(Stack.getSize()) -
1;
Result = Builder.stackGet(MemIndex);
} else {
requireLogicalStackDepth(Index);
Result = Stack.peek(Index - 1);
}
push(Result);
}

Expand All @@ -1789,6 +1882,25 @@ template <typename IRBuilder> class EVMByteCodeVisitor {

// SWAP1-SWAP16: Swap top with Nth+1 stack item
void handleSwap(uint8_t Index) {
const uint32_t RequiredDepth = static_cast<uint32_t>(Index) + 1u;
if (Stack.empty()) {
const int32_t MemIndex =
static_cast<int32_t>(Index) - static_cast<int32_t>(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<int32_t>(Index) - static_cast<int32_t>(Stack.getSize());
Operand &A = Stack.peek(0);
Operand B = Builder.stackGet(MemIndex);
Builder.stackSet(MemIndex, A);
A = B;
return;
}
requireLogicalStackDepth(static_cast<uint32_t>(Index) + 1u);
std::swap(Stack.peek(0), Stack.peek(Index));
}
Expand Down Expand Up @@ -1872,6 +1984,22 @@ template <typename IRBuilder> 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<unsigned long long>(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<int>(Ctx->getRevision()),
static_cast<unsigned long long>(PC),
static_cast<unsigned long long>(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);
Expand Down
13 changes: 2 additions & 11 deletions src/common/evm_traphandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t *>(JITCode) + Inst->getModule()->getJITCodeSize();
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 6 additions & 3 deletions src/common/evm_traphandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading