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
14 changes: 12 additions & 2 deletions src/evm/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include "intx/intx.hpp"

#include <array>
#include <deque>
#include <vector>

namespace zen {
Expand Down Expand Up @@ -70,7 +69,7 @@ struct EVMFrame {
class InterpreterExecContext {
private:
runtime::EVMInstance *Inst;
std::deque<EVMFrame> FrameStack;
std::vector<EVMFrame> FrameStack;
evmc_status_code Status = EVMC_SUCCESS;
Comment on lines 69 to 73
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing FrameStack from std::deque to std::vector can invalidate pointers/references to existing frames on growth (reallocation). This is unsafe here because allocTopFrame() stores pointers to Frame.Msg in EVMInstance::MessageStack and interpreter code keeps EVMFrame* (e.g., parent frame) across nested calls; a reallocation would turn those pointers into dangling pointers and can cause memory corruption. Use a container with stable addresses (e.g., keep std::deque), or reserve a hard upper bound up-front and enforce max depth so FrameStack never reallocates, or store frames/messages in separately allocated stable storage.

Copilot uses AI. Check for mistakes.
std::vector<uint8_t> ReturnData;
evmc::Result ExeResult;
Expand All @@ -80,6 +79,17 @@ class InterpreterExecContext {

InterpreterExecContext(runtime::EVMInstance *Inst) : Inst(Inst) {}

/// Reset state for reuse across calls. Keeps allocated capacity to avoid
/// re-allocating the ~32KB EVMFrame on every call.
void resetForNewCall(runtime::EVMInstance *NewInst) {
Inst = NewInst;
FrameStack.clear(); // keeps vector capacity
Status = EVMC_SUCCESS;
ReturnData.clear(); // keeps vector capacity
IsJump = false;
ExeResult = evmc::Result{EVMC_SUCCESS, 0, 0};
}
Comment on lines +82 to +91
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetForNewCall() intends to preserve ReturnData/FrameStack capacity, but many interpreter paths set return data via setReturnData(std::vector<uint8_t>()) / move-assigning a new vector, which typically discards the existing buffer and defeats cross-call capacity caching. To realize the intended optimization, consider switching those "clear return data" sites to a clear() on the existing buffer (or add a dedicated clearReturnData() API) instead of replacing the vector instance.

Copilot uses AI. Check for mistakes.

EVMFrame *allocTopFrame(evmc_message *Msg);
void freeBackFrame();

Expand Down
36 changes: 36 additions & 0 deletions src/runtime/evm_instance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,42 @@ EVMInstanceUniquePtr EVMInstance::newEVMInstance(Isolation &Iso,

EVMInstance::~EVMInstance() {}

void EVMInstance::resetForNewCall(evmc_revision NewRev) {
// Reset gas accounting
Gas = 0;
GasRefund = 0;
Rev = NewRev;
InstanceExitCode = 0;
Err = common::ErrorCode::NoError;

// Reset message stack (clear but keep capacity)
CurrentMessage = nullptr;
MessageStack.clear();
GasRefundStack.clear();

// Reset memory: keep the 16MB allocation, just reset the size
MemoryBase = nullptr;
MemorySize = 0;
// Don't release Memory - reuse the allocation on next pushMessage()
MemoryStack.clear();

// Reset output
ReturnData.clear();
ExeResult = evmc::Result{EVMC_SUCCESS, 0, 0};

// Reset execution cache: clear() keeps allocated bucket arrays,
// avoiding repeated alloc/free of unordered_map internals.
InstanceExecutionCache.BlockHashes.clear();
InstanceExecutionCache.BlobHashes.clear();
InstanceExecutionCache.CalldataLoads.clear();
InstanceExecutionCache.ExtcodeHashes.clear();
InstanceExecutionCache.Keccak256Results.clear();
InstanceExecutionCache.TxContextCached = false;

// Reset JIT stack
EVMStackSize = 0;
}

void EVMInstance::setGas(uint64_t NewGas) { Gas = NewGas; }

void EVMInstance::pushMessage(evmc_message *Msg) {
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/evm_instance.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ class EVMInstance final : public RuntimeObject<EVMInstance> {
void setError(const Error &E) { Err = E; }
void clearError() { Err = ErrorCode::NoError; }

/// Reset instance state for reuse in interpreter mode.
/// Avoids the cost of destroy + recreate on every EVMC execute() call.
Comment on lines +104 to +105
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says resetForNewCall() is for reuse in interpreter mode, but the new dt_evmc_vm path calls it for multipass/JIT executions as well (getOrCreateInstance()). Please update the comment to reflect that the reset is used for general cross-call instance reuse across modes, or restrict usage if it truly must be interpreter-only.

Suggested change
/// Reset instance state for reuse in interpreter mode.
/// Avoids the cost of destroy + recreate on every EVMC execute() call.
/// Reset instance state for reuse across EVMC execute() calls, regardless
/// of execution mode (interpreter, multipass, or JIT). This avoids the cost
/// of destroying and recreating the instance for each call.

Copilot uses AI. Check for mistakes.
void resetForNewCall(evmc_revision NewRev);

// can only called by hostapi directly
// setExceptionByHostapi must be inline to capture the hostapi's frame
// pointer
Expand Down
Loading