Skip to content
Merged
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
9 changes: 4 additions & 5 deletions core/opcodeCompiler/compiler/MIRBasicBlock.go
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,8 @@ func (b *MIRBasicBlock) CreateStorageOpMIR(op MirOperation, stack *ValueStack, a
accessor.recordStateLoad(key)
}
mir.operands = []*Value{&key}
// TLOAD produces a value
stack.push(mir.Result())
case MirTSTORE:
// EVM pops key (top) then value, same as SSTORE
key := stack.pop()
Expand All @@ -672,10 +674,7 @@ func (b *MIRBasicBlock) CreateStorageOpMIR(op MirOperation, stack *ValueStack, a
// no-op
}

// Push generic result for all except handled separately above
if op != MirSLOAD {
stack.push(mir.Result())
}
// Do not push for SSTORE/TSTORE (no stack result). SLOAD/TLOAD already handled above.
mir = b.appendMIR(mir)
mir.genStackDepth = stack.size()
// noisy generation logging removed
Expand Down Expand Up @@ -884,7 +883,7 @@ func (b *MIRBasicBlock) CreateLogMIR(op MirOperation, stack *ValueStack) *MIR {
}
mir.operands = operands

stack.push(mir.Result())
// LOGx consume operands and do not push a result
mir = b.appendMIR(mir)
mir.genStackDepth = stack.size()
// noisy generation logging removed
Expand Down
146 changes: 144 additions & 2 deletions core/opcodeCompiler/compiler/MIRInterpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ type MIRExecutionEnv struct {

// MIRInterpreter executes MIR instructions and produces values.
type MIRInterpreter struct {
env *MIRExecutionEnv
env *MIRExecutionEnv
// cfg points to the current CFG during RunCFGWithResolver; used for runtime backfill
cfg *CFG
memory []byte
returndata []byte
results []*uint256.Int
Expand Down Expand Up @@ -338,6 +340,37 @@ func (it *MIRInterpreter) RunMIR(block *MIRBasicBlock) ([]byte, error) {
}
}
if onlyNopsAndStop && hasStop {
// Ensure gas/accounting hooks see the landing JUMPDEST and terminal STOP
if it.beforeOp != nil {
instrs := block.instructions
// Block-entry pre-op for the first instruction (likely MirJUMPDEST)
if len(instrs) > 0 && instrs[0] != nil {
ctx := &MIRPreOpContext{
M: instrs[0],
EvmOp: instrs[0].evmOp,
IsBlockEntry: true,
Block: block,
}
if err := it.beforeOp(ctx); err != nil {
return nil, err
}
}
// Also emit a pre-op for the terminal STOP to keep tracer/gas parity
for i := len(instrs) - 1; i >= 0; i-- {
if instrs[i] != nil && instrs[i].op == MirSTOP {
ctx := &MIRPreOpContext{
M: instrs[i],
EvmOp: instrs[i].evmOp,
IsBlockEntry: false,
Block: block,
}
if err := it.beforeOp(ctx); err != nil {
return nil, err
}
break
}
}
}
return it.returndata, nil
}
}
Expand Down Expand Up @@ -440,6 +473,8 @@ func (it *MIRInterpreter) publishLiveOut(block *MIRBasicBlock) {

// RunCFGWithResolver sets up a resolver backed by the given CFG and runs from entry block
func (it *MIRInterpreter) RunCFGWithResolver(cfg *CFG, entry *MIRBasicBlock) ([]byte, error) {
// Record the active CFG for possible runtime backfill of dynamic targets
it.cfg = cfg
// Reset global caches at the start of each execution to avoid stale values
// This ensures values from previous executions or different paths don't pollute the current run
if it.globalResultsBySig != nil {
Expand Down Expand Up @@ -486,8 +521,21 @@ func (it *MIRInterpreter) RunCFGWithResolver(cfg *CFG, entry *MIRBasicBlock) ([]
// This handles cases like entry blocks with only PUSH operations
if bb.Size() == 0 && it.beforeOp != nil {
ctx := &it.preOpCtx
ctx.M = nil // No MIR instruction for blocks with Size=0
// Synthesize JUMPDEST charge for trivial landing blocks with no MIR
ctx.M = nil
ctx.EvmOp = 0
if it.cfg != nil && int(bb.firstPC) >= 0 && int(bb.firstPC) < len(it.cfg.rawCode) {
if ByteCode(it.cfg.rawCode[int(bb.firstPC)]) == JUMPDEST {
// Create a minimal MIR to allow adapter to charge JUMPDEST gas
mirDebugWarn("MIR synthesize JUMPDEST pre-op at block entry", "block", bb.blockNum, "firstPC", bb.firstPC, "size", bb.Size())
synth := new(MIR)
synth.op = MirJUMPDEST
synth.evmPC = uint(bb.firstPC)
synth.evmOp = byte(JUMPDEST)
ctx.M = synth
ctx.EvmOp = synth.evmOp
}
}
ctx.Operands = nil
ctx.MemorySize = 0
ctx.Length = 0
Expand Down Expand Up @@ -592,8 +640,24 @@ func (it *MIRInterpreter) exec(m *MIR) error {
// For block entry, we need to charge gas even if the first instruction is a NOP
if isBlockEntry && it.beforeOp != nil {
ctx := &it.preOpCtx
// Default to the actual first MIR instruction
ctx.M = m
ctx.EvmOp = m.evmOp
// If the underlying bytecode begins with JUMPDEST but the first MIR op is not MirJUMPDEST
// (e.g., it was optimized away), synthesize a MirJUMPDEST so the adapter can charge gas once.
if it.cfg != nil && it.currentBB != nil {
fp := int(it.currentBB.firstPC)
if fp >= 0 && it.cfg.rawCode != nil && fp < len(it.cfg.rawCode) && ByteCode(it.cfg.rawCode[fp]) == JUMPDEST {
if m == nil || m.op != MirJUMPDEST {
synth := new(MIR)
synth.op = MirJUMPDEST
synth.evmPC = uint(it.currentBB.firstPC)
synth.evmOp = byte(JUMPDEST)
ctx.M = synth
ctx.EvmOp = synth.evmOp
}
}
}
ctx.Operands = nil
ctx.MemorySize = 0
ctx.Length = 0
Expand Down Expand Up @@ -3032,6 +3096,84 @@ func (it *MIRInterpreter) scheduleJump(udest uint64, m *MIR, isFallthrough bool)
// Then resolve to a basic block in the CFG
it.nextBB = it.env.ResolveBB(udest)
if it.nextBB == nil {
// Attempt runtime backfill: if destination is a valid JUMPDEST and not yet built,
// synthesize a landing block, wire the current block as parent, seed entry stack,
// and rebuild a bounded set of successors so PHIs stabilize.
if !isFallthrough && it.cfg != nil {
code := it.cfg.rawCode
if int(udest) >= 0 && int(udest) < len(code) && ByteCode(code[udest]) == JUMPDEST {
// Create or get the target BB
targetBB := it.cfg.createBB(uint(udest), it.currentBB)
// Wire parent and seed incoming/entry stacks from current exit
if it.currentBB != nil {
if exit := it.currentBB.ExitStack(); exit != nil {
targetBB.AddIncomingStack(it.currentBB, exit)
targetBB.SetParents([]*MIRBasicBlock{it.currentBB})
vs := ValueStack{}
vs.resetTo(exit)
vs.markAllLiveIn()
targetBB.ResetForRebuild(true)
unproc := MIRBasicBlockStack{}
if err := it.cfg.buildBasicBlock(targetBB, &vs, it.cfg.getMemoryAccessor(), it.cfg.getStateAccessor(), &unproc); err == nil {
targetBB.built = true
targetBB.queued = false
// Rebuild successors with a conservative BFS budget to propagate PHIs
type q struct{ items []*MIRBasicBlock }
push := func(Q *q, b *MIRBasicBlock) {
if b != nil {
Q.items = append(Q.items, b)
}
}
pop := func(Q *q) *MIRBasicBlock {
if len(Q.items) == 0 {
return nil
}
b := Q.items[len(Q.items)-1]
Q.items = Q.items[:len(Q.items)-1]
return b
}
var Q q
for _, ch := range targetBB.Children() {
push(&Q, ch)
}
// Drain any enqueued children produced during the initial build
for unproc.Size() != 0 {
push(&Q, unproc.Pop())
}
visited := make(map[*MIRBasicBlock]bool)
budget := 256
for len(Q.items) > 0 && budget > 0 {
nb := pop(&Q)
if nb == nil || visited[nb] {
budget--
continue
}
visited[nb] = true
vs.resetTo(nil)
// If single parent, seed from that parent's exit; otherwise let PHIs form
if len(nb.Parents()) == 1 {
if ps := nb.Parents()[0].ExitStack(); ps != nil {
vs.resetTo(ps)
vs.markAllLiveIn()
}
}
nb.ResetForRebuild(true)
if err := it.cfg.buildBasicBlock(nb, &vs, it.cfg.getMemoryAccessor(), it.cfg.getStateAccessor(), &unproc); err == nil {
nb.built = true
nb.queued = false
for _, ch := range nb.Children() {
push(&Q, ch)
}
}
budget--
}
// Successfully built target; use it
it.nextBB = targetBB
}
}
}
}
}
// For fallthrough, try to get the block from children if ResolveBB fails
if isFallthrough {
children := it.currentBB.Children()
Expand Down
100 changes: 89 additions & 11 deletions core/vm/mir_interpreter_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ type MIRInterpreterAdapter struct {
blockEntryGasCharges map[*compiler.MIRBasicBlock]uint64
// Current block being executed (for GAS opcode to know which block entry charges to add back)
currentBlock *compiler.MIRBasicBlock
// Track whether the last executed opcode was a control transfer (JUMP/JUMPI)
lastWasJump bool
// Dedup JUMPDEST charge at first instruction of landing block
lastJdPC uint32
lastJdBlockFirstPC uint32
}

// countOpcodesInRange counts EVM opcodes in a given PC range
Expand Down Expand Up @@ -628,6 +633,9 @@ func (adapter *MIRInterpreterAdapter) Run(contract *Contract, input []byte, read
// Fallback to regular EVM interpreter
return adapter.evm.Interpreter().Run(contract, input, readOnly)
}
// Reset JUMPDEST de-dup guard per top-level run
adapter.lastJdPC = ^uint32(0)
adapter.lastJdBlockFirstPC = ^uint32(0)

// Pre-flight fork gating: if the bytecode contains opcodes not enabled at the current fork,
// mirror EVM behavior by returning invalid opcode errors instead of running MIR.
Expand Down Expand Up @@ -716,6 +724,12 @@ func (adapter *MIRInterpreterAdapter) Run(contract *Contract, input []byte, read
if ctx == nil {
return nil
}
// Track if previous op was a JUMP/JUMPI to decide landing-time JUMPDEST charge
if ctx.M != nil {
if OpCode(ctx.EvmOp) == JUMP || OpCode(ctx.EvmOp) == JUMPI || ctx.M.Op() == compiler.MirJUMP || ctx.M.Op() == compiler.MirJUMPI {
adapter.lastWasJump = true
}
}
// On block entry, charge constant gas for all EVM opcodes in the block
// (including PUSH/DUP/SWAP that don't have MIR instructions)
// Exception: EXP and KECCAK256 constant gas is charged per instruction (along with dynamic gas)
Expand All @@ -726,6 +740,23 @@ func (adapter *MIRInterpreterAdapter) Run(contract *Contract, input []byte, read
adapter.currentBlock = ctx.Block
// Track total gas charged at block entry (for GAS opcode to add back)
var blockEntryTotalGas uint64
// If the first opcode of the underlying bytecode at this block is JUMPDEST, EVM charges 1 gas upon entering.
// Charge it up-front here (once), and record for GAS to add back.
if adapter.currentContract != nil {
code := adapter.currentContract.Code
firstPC := int(ctx.Block.FirstPC())
if code != nil && firstPC >= 0 && firstPC < len(code) && OpCode(code[firstPC]) == JUMPDEST {
jg := params.JumpdestGas
if adapter.currentContract.Gas < jg {
return ErrOutOfGas
}
adapter.currentContract.Gas -= jg
blockEntryTotalGas += jg
// remember we charged this (block-first JUMPDEST)
adapter.lastJdPC = uint32(ctx.Block.FirstPC())
adapter.lastJdBlockFirstPC = uint32(ctx.Block.FirstPC())
}
}
// Validate that we're only charging for the current block
// (ctx.Block should match the block we're entering)
// Charge block entry gas for all opcodes in the block
Expand Down Expand Up @@ -879,18 +910,65 @@ func (adapter *MIRInterpreterAdapter) Run(contract *Contract, input []byte, read
scope := &ScopeContext{Memory: adapter.memShadow, Stack: nil, Contract: contract}
adapter.evm.Config.Tracer.OnOpcode(uint64(ctx.M.EvmPC()), byte(evmOp), contract.Gas, 0, scope, nil, adapter.evm.depth, nil)
}
// Constant gas is charged at block entry, so we don't charge it per instruction
// Exception: JUMPDEST must be charged when executed (it's a jump target, charged when PC lands on it)
// JUMPDEST is always skipped at block entry (see block entry charging above)
// We charge JUMPDEST when executed (when MirJUMPDEST is executed)
if ctx.M != nil && ctx.M.Op() == compiler.MirJUMPDEST && !ctx.IsBlockEntry {
// JUMPDEST charges 1 gas when executed (when PC lands on it)
// Only charge if not at block entry (block entry charging was skipped above)
jumpdestGas := params.JumpdestGas
if contract.Gas < jumpdestGas {
return ErrOutOfGas
// Constant gas is charged at block entry; JUMPDEST is charged at block entry of landing blocks.
// Charge JUMPDEST exactly when executed (matches base EVM semantics).
// For zero-size landing blocks, MIR synthesizes a JUMPDEST pre-op at block-entry:
// in that case, charge at block-entry; for normal blocks, charge on instruction (IsBlockEntry=false).
// Charge JUMPDEST on landing exactly once.
// Prefer charging at the instruction itself; if first MIR instruction doesn't carry EvmOp=JUMPDEST,
// charge at block entry when coming from a jump, based on bytecode and/or first MIR instr.
if ctx.M != nil && (ctx.EvmOp == byte(JUMPDEST) || ctx.M.Op() == compiler.MirJUMPDEST) {
lp := uint32(ctx.M.EvmPC())
var bf uint32
if ctx.Block != nil {
bf = uint32(ctx.Block.FirstPC())
}
// Skip if we've already charged for this exact landing in this block
if adapter.lastJdPC == lp && adapter.lastJdBlockFirstPC == bf {
// no-op
} else {
jumpdestGas := params.JumpdestGas
if contract.Gas < jumpdestGas {
return ErrOutOfGas
}
contract.Gas -= jumpdestGas
adapter.lastJdPC = lp
adapter.lastJdBlockFirstPC = bf
}
} else if ctx.IsBlockEntry && ctx.Block != nil && adapter.currentContract != nil {
firstPC := int(ctx.Block.FirstPC())
isJD := false
// Check bytecode at firstPC
code := adapter.currentContract.Code
if code != nil && firstPC >= 0 && firstPC < len(code) {
if OpCode(code[firstPC]) == JUMPDEST {
isJD = true
}
}
// Also check first MIR instruction if available
if !isJD {
if instrs := ctx.Block.Instructions(); len(instrs) > 0 && instrs[0] != nil {
if instrs[0].Op() == compiler.MirJUMPDEST || instrs[0].EvmOp() == byte(JUMPDEST) {
isJD = true
}
}
}
if isJD {
lp := uint32(ctx.Block.FirstPC())
bf := lp
// Dedup
if !(adapter.lastJdPC == lp && adapter.lastJdBlockFirstPC == bf) {
jumpdestGas := params.JumpdestGas
if contract.Gas < jumpdestGas {
return ErrOutOfGas
}
contract.Gas -= jumpdestGas
adapter.lastJdPC = lp
adapter.lastJdBlockFirstPC = bf
}
}
contract.Gas -= jumpdestGas
// Clear jump flag at block entry regardless
adapter.lastWasJump = false
}
// Exception: GAS opcode must read gas BEFORE its own constant gas is charged
// GAS opcode constant gas is skipped at block entry, so we charge it when executed
Expand Down
Loading