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
12 changes: 6 additions & 6 deletions src/action/evm_bytecode_visitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -575,17 +575,17 @@ template <typename IRBuilder> class EVMByteCodeVisitor {
}

case OP_JUMPDEST: {
// Skip to the last consecutive JUMPDEST if there are multiple
uint64_t PrevPC = PC;
// Consecutive JUMPDEST opcodes share one body BB in multipass.
// Charge all skipped metering points before jumping to the shared
// destination at the end of the run.
uint64_t RunStartPC = PC;
while (Ip < IpEnd && static_cast<evmc_opcode>(*Ip) == OP_JUMPDEST) {
Ip++;
PC++;
}
if (PC > PrevPC) {
// Meter gas for the skipped JUMPDESTs
Builder.meterGas(PC - PrevPC);
if (PC > RunStartPC) {
Builder.meterOpcodeRange(RunStartPC, PC);
}
// Now process the last JUMPDEST (current position after skipping)
handleEndBlock();
Builder.handleJumpDest(PC);
Builder.meterOpcode(Opcode, PC);
Expand Down
225 changes: 143 additions & 82 deletions src/compiler/evm_frontend/evm_mir_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#include "runtime/evm_instance.h"
#include "utils/hash_utils.h"
#include <cstring>
#include <unordered_set>

#ifdef ZEN_ENABLE_EVM_GAS_REGISTER
#include "compiler/llvm-prebuild/Target/X86/X86Subtarget.h"
Expand All @@ -21,6 +20,7 @@ namespace COMPILER {
constexpr uint64_t HashMultiplier = 0x9E3779B97F4A7C15ULL;
constexpr uint64_t MinHashSize = 5;
constexpr uint64_t MaxHashSize = 1024;
constexpr uint32_t InvalidJumpDestRunPC = 0xFFFFFFFFu;

zen::common::EVMU256Type *EVMFrontendContext::getEVMU256Type() {
static zen::common::EVMU256Type U256Type;
Expand Down Expand Up @@ -257,7 +257,6 @@ void EVMMirBuilder::initEVM(CompilerContext *Context) {
evmc_get_instruction_names_table(zen::evm::DEFAULT_REVISION);
}

createJumpTable();
ReturnBB = createBasicBlock();
loadEVMInstanceAttr();

Expand All @@ -269,6 +268,8 @@ void EVMMirBuilder::initEVM(CompilerContext *Context) {
initGasRegister();
#endif

createJumpTable();

#ifdef ZEN_ENABLE_LINUX_PERF
CurBB->setSourceOffset(1);
CurBB->setSourceName("MAIN_ENTRY");
Expand Down Expand Up @@ -404,6 +405,56 @@ void EVMMirBuilder::meterOpcode(evmc_opcode Opcode, uint64_t PC) {
meterGas(static_cast<uint64_t>(Metrics.gas_cost));
}

void EVMMirBuilder::meterOpcodeRange(uint64_t StartPC,
Copy link
Contributor

@starwarfan starwarfan Feb 9, 2026

Choose a reason for hiding this comment

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

Is this meterOpcodeRange function change enough to fix the gas calculation for jumpdest? Do you have a bytecode to confirm this?

I heard that consecutive JUMPDEST opcodes are generally NOT common in well-optimized EVM bytecode.

Are all other changes in this PRs necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previously, pr merged consecutive jumpdest values and assumed that all jumpdest values were equivalent to deducting 1. However, in reality, SPP charges for jumpdest based on a cost of 0, 1, or greater. Therefore, the corresponding GasChunkCost should be deducted.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand, my question is that are other changes like the JumpDestBodyTable related to this fix?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I understand

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand, my question is that are other changes like the JumpDestBodyTable related to this fix?

Yes, it separates "the shared main block BodyBB that the linear decode should enter when encountering JUMPDEST" from "the compensation entry thunk (JumpDestTable) that might need to be deducted before the jump/jumpi jump" to avoid linear execution entering the thunk by mistake, which could lead to disorder in instruction insertion or repeated deduction.

uint64_t EndPCExclusive) {
if (!Ctx.isGasMeteringEnabled() || StartPC >= EndPCExclusive) {
return;
}

// Fast path for merged consecutive JUMPDEST runs: the skipped cost for
// [StartPC, EndPCExclusive) is precomputed once when building the jump table.
if (StartPC < JumpDestRunLastPC.size()) {
const uint32_t RunLastPC = JumpDestRunLastPC[static_cast<size_t>(StartPC)];
if (RunLastPC != InvalidJumpDestRunPC &&
static_cast<uint64_t>(RunLastPC) == EndPCExclusive) {
meterGas(JumpDestRunSkipCost[static_cast<size_t>(StartPC)]);
return;
}
}

const auto *EvmCtx = static_cast<const EVMFrontendContext *>(&Ctx);
const Byte *Bytecode = EvmCtx->getBytecode();
if (!Bytecode) {
return;
}

const uint64_t CodeSize = static_cast<uint64_t>(EvmCtx->getBytecodeSize());
if (StartPC >= CodeSize) {
return;
}
EndPCExclusive = std::min(EndPCExclusive, CodeSize);

uint64_t TotalCost = 0;
for (uint64_t PC = StartPC; PC < EndPCExclusive; ++PC) {
uint64_t Cost = 0;
if (GasChunkEnd && GasChunkCost && PC < GasChunkSize &&
GasChunkEnd[PC] > PC) {
Cost = GasChunkCost[PC];
} else {
const uint8_t Opcode = static_cast<uint8_t>(Bytecode[PC]);
Cost = static_cast<uint64_t>(InstructionMetrics[Opcode].gas_cost);
}
Comment on lines 408 to 446
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

meterOpcodeRange iterates byte-by-byte and, when gas chunks are not present, treats every byte as an opcode. That will mis-meter ranges that include PUSH immediate data (and is also more expensive than necessary). Consider advancing PC according to opcode length (skip PUSHn immediates), and when gas chunks are present, skipping directly to GasChunkEnd[PC] after charging GasChunkCost[PC] so the range walk stays opcode-aware and efficient.

Copilot uses AI. Check for mistakes.

if (UINT64_MAX - TotalCost < Cost) {
TotalCost = UINT64_MAX;
break;
}
TotalCost += Cost;
}

meterGas(TotalCost);
}

bool EVMMirBuilder::isOpcodeDefined(evmc_opcode Opcode) const {
const uint8_t Index = static_cast<uint8_t>(Opcode);
if (InstructionNames && InstructionNames[Index] != nullptr) {
Expand Down Expand Up @@ -880,30 +931,97 @@ void EVMMirBuilder::createJumpTable() {
const Byte *Bytecode = EvmCtx->getBytecode();
size_t BytecodeSize = EvmCtx->getBytecodeSize();

uint64_t ConsecutiveStart = 0;
bool InConsecutive = false;
JumpDestTable.clear();
JumpDestBodyTable.clear();
JumpHashTable.clear();
JumpHashReverse.clear();
HashMask = 0;
if (Ctx.isGasMeteringEnabled()) {
JumpDestRunLastPC.assign(BytecodeSize, InvalidJumpDestRunPC);
JumpDestRunSkipCost.assign(BytecodeSize, 0);
} else {
JumpDestRunLastPC.clear();
JumpDestRunSkipCost.clear();
}

MBasicBlock *SavedInsertBB = CurBB;

for (size_t PC = 0; PC < BytecodeSize; ++PC) {
if (Bytecode[PC] == static_cast<Byte>(evmc_opcode::OP_JUMPDEST)) {
if (JumpDestTable.count(PC - 1) > 0) {
// Reuse BB for consecutive JUMPDESTs
JumpDestTable[PC] = JumpDestTable[PC - 1];
// We are in a consecutive sequence, continue tracking
if (!InConsecutive) {
ConsecutiveStart = PC - 1;
InConsecutive = true;
const size_t RangeStart = PC;
while (PC + 1 < BytecodeSize &&
Bytecode[PC + 1] == static_cast<Byte>(evmc_opcode::OP_JUMPDEST)) {
++PC;
}
const size_t RangeEnd = PC;

// Share one canonical execution block for the whole run.
MBasicBlock *BodyBB = createBasicBlock();
BodyBB->setJumpDestBB(true);

for (size_t DestPC = RangeStart; DestPC <= RangeEnd; ++DestPC) {
JumpDestBodyTable[DestPC] = BodyBB;
}

if (!Ctx.isGasMeteringEnabled() || RangeStart == RangeEnd) {
for (size_t DestPC = RangeStart; DestPC <= RangeEnd; ++DestPC) {
JumpDestTable[DestPC] = BodyBB;
}
continue;
} else {
// For merged runs, materialize per-target entry thunks that charge the
// exact skipped metering before entering the shared body.
//
// NOTE: We may create O(n) thunks for a run of length n. Avoid an
// O(n^2) compile-time cost by precomputing the suffix sums of skipped
// metering once for the run.
const size_t SkipCount = RangeEnd - RangeStart; // exclude RangeEnd
const uint64_t JumpDestBaseCost = static_cast<uint64_t>(
InstructionMetrics[static_cast<uint8_t>(evmc_opcode::OP_JUMPDEST)]
.gas_cost);
std::vector<uint64_t> SkipCostByOffset(SkipCount, 0);
if (SkipCount > 0) {
uint64_t Running = 0;
for (size_t Offset = SkipCount; Offset > 0; --Offset) {
const size_t Pc = RangeStart + (Offset - 1);
uint64_t Cost = 0;
if (GasChunkEnd && GasChunkCost && Pc < GasChunkSize &&
GasChunkEnd[Pc] > Pc) {
Cost = GasChunkCost[Pc];
} else {
// All bytes in the run are JUMPDEST opcode bytes (PUSH payload is
// skipped in the scan above), so the fallback is a constant.
Cost = JumpDestBaseCost;
}

if (UINT64_MAX - Running < Cost) {
Running = UINT64_MAX;
} else {
Running += Cost;
}
SkipCostByOffset[Pc - RangeStart] = Running;
}
}

// Cache the total skipped cost at run start so the linear decode path
// can reuse it without re-scanning the same consecutive JUMPDEST range.
if (RangeStart < JumpDestRunLastPC.size()) {
JumpDestRunLastPC[RangeStart] = static_cast<uint32_t>(RangeEnd);
JumpDestRunSkipCost[RangeStart] = SkipCostByOffset[0];
}

for (size_t DestPC = RangeStart; DestPC < RangeEnd; ++DestPC) {
MBasicBlock *EntryBB = createBasicBlock();
EntryBB->setJumpDestBB(true);
JumpDestTable[DestPC] = EntryBB;

setInsertBlock(EntryBB);
meterGas(SkipCostByOffset[DestPC - RangeStart]);
createInstruction<BrInstruction>(true, Ctx, BodyBB);
addSuccessor(BodyBB);
}
JumpDestTable[RangeEnd] = BodyBB;
}
MBasicBlock *DestBB = createBasicBlock();
DestBB->setJumpDestBB(true);
JumpDestTable[PC] = DestBB;
} else {
// End of consecutive sequence if we were in one
if (InConsecutive) {
ConsecutiveJumpDests.push_back({ConsecutiveStart, PC - 1});
InConsecutive = false;
}
if (static_cast<Byte>(evmc_opcode::OP_PUSH0) <= Bytecode[PC] &&
Bytecode[PC] <= static_cast<Byte>(evmc_opcode::OP_PUSH32)) {
uint8_t PushSize = static_cast<uint8_t>(Bytecode[PC]) + 1 -
Expand All @@ -912,18 +1030,15 @@ void EVMMirBuilder::createJumpTable() {
}
}
}
// Handle case where consecutive sequence extends to end of bytecode
if (InConsecutive) {
ConsecutiveJumpDests.push_back({ConsecutiveStart, BytecodeSize - 1});
}

setInsertBlock(SavedInsertBB);

// If the size of JumpDests is greater than MinHashSize, create a hash table
// which calculates the hash of DestPC and use it as the index to jump
if (JumpDestTable.size() > MinHashSize) {
uint64_t HashSize =
std::min(nextPowerOfTwo(JumpDestTable.size()), MaxHashSize);
HashMask = HashSize - 1;
std::vector<std::vector<MBasicBlock *>> HashDests(HashSize);
for (const auto &[DestPC, DestBB] : JumpDestTable) {
// HashIndex(a) = (a * HashMultiplier) & (size - 1)
uint64_t Index = (DestPC * HashMultiplier) & HashMask;
Expand All @@ -936,13 +1051,6 @@ void EVMMirBuilder::createJumpTable() {
void EVMMirBuilder::implementConstantJump(uint64_t ConstDest,
MBasicBlock *FailureBB) {
if (JumpDestTable.count(ConstDest)) {
// Check if ConstDest falls within a consecutive JUMPDEST range
for (const auto &[StartPC, EndPC] : ConsecutiveJumpDests) {
if (ConstDest >= StartPC && ConstDest < EndPC) {
meterGas(EndPC - ConstDest);
break;
}
}
createInstruction<BrInstruction>(true, Ctx, JumpDestTable[ConstDest]);
addSuccessor(JumpDestTable[ConstDest]);
} else {
Expand All @@ -960,55 +1068,6 @@ void EVMMirBuilder::implementIndirectJump(MInstruction *JumpTarget,
}
HasIndirectJump = true;

// Check if JumpTarget falls within any consecutive JUMPDEST range
// and charge gas for skipped JUMPDESTs
if (!ConsecutiveJumpDests.empty() && Ctx.isGasMeteringEnabled()) {
MType *I64Type = &Ctx.I64Type;
MBasicBlock *FinalBB = createBasicBlock();

for (const auto &[StartPC, EndPC] : ConsecutiveJumpDests) {
MBasicBlock *NextCheckBB = createBasicBlock();
MBasicBlock *ChargeGasBB = createBasicBlock();

// Check if JumpTarget >= StartPC
MInstruction *StartPCConst = createIntConstInstruction(I64Type, StartPC);
MInstruction *IsGEStart = createInstruction<CmpInstruction>(
false, CmpInstruction::Predicate::ICMP_UGE, I64Type, JumpTarget,
StartPCConst);

// Check if JumpTarget < EndPC
MInstruction *EndPCConst = createIntConstInstruction(I64Type, EndPC);
MInstruction *IsLTEnd = createInstruction<CmpInstruction>(
false, CmpInstruction::Predicate::ICMP_ULT, I64Type, JumpTarget,
EndPCConst);

// Combine: JumpTarget >= StartPC && JumpTarget < EndPC
MInstruction *InRange = createInstruction<BinaryInstruction>(
false, OP_and, I64Type, IsGEStart, IsLTEnd);

createInstruction<BrIfInstruction>(true, Ctx, InRange, ChargeGasBB,
NextCheckBB);
addSuccessor(ChargeGasBB);
addSuccessor(NextCheckBB);

// ChargeGasBB: charge gas for (EndPC - JumpTarget)
setInsertBlock(ChargeGasBB);
MInstruction *GasCost = createInstruction<BinaryInstruction>(
false, OP_sub, I64Type, EndPCConst, JumpTarget);
chargeDynamicGasIR(GasCost);
createInstruction<BrInstruction>(true, Ctx, FinalBB);
addSuccessor(FinalBB);

// Continue to next check
setInsertBlock(NextCheckBB);
}

// After all checks, branch to FinalBB
createInstruction<BrInstruction>(true, Ctx, FinalBB);
addSuccessor(FinalBB);
setInsertBlock(FinalBB);
}

MBasicBlock *TargetBB = getOrCreateIndirectJumpBB();
createInstruction<DassignInstruction>(true, &(Ctx.VoidType), JumpTarget,
JumpTargetVar->getVarIdx());
Expand Down Expand Up @@ -1166,7 +1225,9 @@ void EVMMirBuilder::handleJumpI(Operand Dest, Operand Cond) {
}

void EVMMirBuilder::handleJumpDest(const uint64_t &PC) {
MBasicBlock *DestBB = JumpDestTable.at(PC);
auto BodyIt = JumpDestBodyTable.find(PC);
ZEN_ASSERT(BodyIt != JumpDestBodyTable.end() && "JUMPDEST body not found");
MBasicBlock *DestBB = BodyIt->second;
// Only add successor if the current BB is not ExceptionSetBB,
bool IsExceptionSetBB = false;
for (auto &[EC, BB] : CurFunc->getExceptionSetBBs()) {
Expand Down
13 changes: 10 additions & 3 deletions src/compiler/evm_frontend/evm_mir_compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ class EVMMirBuilder final {
void finalizeEVMBase();

void meterOpcode(evmc_opcode Opcode, uint64_t PC);
void meterOpcodeRange(uint64_t StartPC, uint64_t EndPCExclusive);
bool isOpcodeDefined(evmc_opcode Opcode) const;
void meterGas(uint64_t GasCost);

Expand Down Expand Up @@ -627,10 +628,16 @@ class EVMMirBuilder final {

// Jump table for dynamic jumps
bool HasIndirectJump = false;
// Entry blocks for jump targets (may be tiny thunks for shared JUMPDEST
// bodies).
std::map<uint64_t, MBasicBlock *> JumpDestTable;
// Consecutive JUMPDEST ranges: {start_pc, end_pc}
std::vector<std::pair<uint64_t, uint64_t>> ConsecutiveJumpDests;
MBasicBlock *DefaultJumpBB = nullptr; // For invalid jump destinations
// Canonical execution blocks for JUMPDEST opcodes in linear decode.
std::map<uint64_t, MBasicBlock *> JumpDestBodyTable;
// Cached skipped-metering for merged consecutive JUMPDEST runs.
// Cache it so meterOpcodeRange(S, E) doesn't have to re-scan the same run.
std::vector<uint32_t> JumpDestRunLastPC; // [S] = E, else invalid sentinel
std::vector<uint64_t> JumpDestRunSkipCost; // [S] = sum cost for [S, E)
MBasicBlock *DefaultJumpBB = nullptr; // For invalid jump destinations

std::map<uint64_t, std::vector<MBasicBlock *>> JumpHashTable;
std::map<uint64_t, std::vector<uint64_t>> JumpHashReverse;
Expand Down