Skip to content

fix(compiler): keep merged jumpdest while charging exact gas in multipass#345

Open
ZR74 wants to merge 3 commits intoDTVMStack:mainfrom
ZR74:pr-jump
Open

fix(compiler): keep merged jumpdest while charging exact gas in multipass#345
ZR74 wants to merge 3 commits intoDTVMStack:mainfrom
ZR74:pr-jump

Conversation

@ZR74
Copy link
Contributor

@ZR74 ZR74 commented Feb 8, 2026

1. Does this PR affect any open issues?(Y/N) and add issue references (e.g. "fix #123", "re #123".):

  • N
  • Y

2. What is the scope of this PR (e.g. component or file name):

3. Provide a description of the PR(e.g. more details, effects, motivations or doc link):

  • Affects user behaviors
  • Contains CI/CD configuration changes
  • Contains documentation changes
  • Contains experimental features
  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Other

4. Are there any breaking changes?(Y/N) and describe the breaking changes(e.g. more details, motivations or doc link):

  • N
  • Y

5. Are there test cases for these changes?(Y/N) select and add more details, references or doc links:

  • Unit test
  • Integration test
  • Benchmark (add benchmark stats below)
  • Manual test (add detailed scripts or steps below)
  • Other

6. Release note

None

Copilot AI review requested due to automatic review settings February 8, 2026 14:49
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adjusts multipass EVM compilation so consecutive JUMPDEST opcodes can share a single canonical “body” basic block while still charging the correct gas for any skipped metering points.

Changes:

  • Add EVMMirBuilder::meterOpcodeRange() and use it to account for gas across a PC range.
  • Rework jump table construction to distinguish per-target entry blocks (JumpDestTable) from canonical linear-decode execution blocks (JumpDestBodyTable), adding per-target thunks for merged JUMPDEST runs when gas metering is enabled.
  • Update multipass bytecode visitor OP_JUMPDEST handling to skip consecutive JUMPDESTs and meter the skipped range before entering the shared destination.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/compiler/evm_frontend/evm_mir_compiler.h Adds range-metering API and replaces consecutive-JUMPDEST tracking with a body-block map.
src/compiler/evm_frontend/evm_mir_compiler.cpp Implements range metering, rebuilds jump table logic with entry thunks + body blocks, and updates handleJumpDest to use canonical bodies.
src/action/evm_bytecode_visitor.h Updates multipass JUMPDEST decoding to merge runs and meter skipped points via the new range API.
Comments suppressed due to low confidence (1)

src/action/evm_bytecode_visitor.h:592

  • This changes control-flow + gas accounting for consecutive JUMPDESTs specifically in multipass mode. There’s no regression test exercising a dynamic jump into the middle of a consecutive-JUMPDEST run with gas metering enabled (and comparing gas left / status vs interpreter). Adding a focused test case would help prevent future gas-metering regressions in this area.
        case OP_JUMPDEST: {
          // 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 > RunStartPC) {
            Builder.meterOpcodeRange(RunStartPC, PC);
          }
          handleEndBlock();
          Builder.handleJumpDest(PC);
          Builder.meterOpcode(Opcode, PC);
          handleBeginBlock(Analyzer);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 408 to 435
void EVMMirBuilder::meterOpcodeRange(uint64_t StartPC,
uint64_t EndPCExclusive) {
if (!Ctx.isGasMeteringEnabled() || StartPC >= EndPCExclusive) {
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);
}
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.
Comment on lines 955 to 961
for (size_t DestPC = RangeStart; DestPC < RangeEnd; ++DestPC) {
MBasicBlock *EntryBB = createBasicBlock();
EntryBB->setJumpDestBB(true);
JumpDestTable[DestPC] = EntryBB;

setInsertBlock(EntryBB);
meterOpcodeRange(DestPC, RangeEnd);
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.

createJumpTable() builds one entry thunk per merged JUMPDEST and each thunk calls meterOpcodeRange(DestPC, RangeEnd). For a run of length N this does O(N^2) work at compile time (summing overlapping suffix ranges repeatedly) and creates O(N) extra blocks, which can become a compile-time DoS vector with crafted bytecode (e.g., long 0x5b runs) when gas metering is enabled. Consider computing a single prefix/suffix cost table for the run once (or using the fact that the run is all JUMPDEST) and then emitting a constant meterGas(precomputed_cost[DestPC]) per thunk.

Suggested change
for (size_t DestPC = RangeStart; DestPC < RangeEnd; ++DestPC) {
MBasicBlock *EntryBB = createBasicBlock();
EntryBB->setJumpDestBB(true);
JumpDestTable[DestPC] = EntryBB;
setInsertBlock(EntryBB);
meterOpcodeRange(DestPC, RangeEnd);
// The run [RangeStart, RangeEnd] consists only of JUMPDEST opcodes, each
// with a fixed per-opcode gas cost. Instead of repeatedly metering the
// overlapping suffix [DestPC, RangeEnd] for every DestPC (O(N^2) work),
// we directly charge a constant amount based on the number of skipped
// JUMPDEST instructions.
constexpr uint64_t JumpdestGasCost = 1;
for (size_t DestPC = RangeStart; DestPC < RangeEnd; ++DestPC) {
MBasicBlock *EntryBB = createBasicBlock();
EntryBB->setJumpDestBB(true);
JumpDestTable[DestPC] = EntryBB;
setInsertBlock(EntryBB);
const uint64_t skippedJumpdests =
static_cast<uint64_t>(RangeEnd - DestPC);
const uint64_t skippedGas = skippedJumpdests * JumpdestGasCost;
meterGas(skippedGas);

Copilot uses AI. Check for mistakes.
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments