From 8310f185b02f205ab7bc4445cf1e1bab1d412562 Mon Sep 17 00:00:00 2001 From: annielz Date: Mon, 1 Sep 2025 15:00:34 +0800 Subject: [PATCH 1/4] feat: add handle for fail in middle of superinstruction opcode --- core/vm/interpreter.go | 9 +++ core/vm/interpreter_si.go | 124 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 core/vm/interpreter_si.go diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index bb961d12a0..c5160fa8de 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -264,6 +264,10 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } // for tracing: this gas consumption event is emitted below in the debug section. if contract.Gas < cost { + if seq, isSuper := DecomposeSuperInstruction(op); isSuper { + err = in.tryFallbackForSuperInstruction(&pc, seq, contract, stack, mem, callContext) + return nil, err + } return nil, ErrOutOfGas } else { contract.Gas -= cost @@ -298,6 +302,11 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } // for tracing: this gas consumption event is emitted below in the debug section. if contract.Gas < dynamicCost { + contract.Gas += operation.constantGas // restore deducted constant gas first + if seq, isSuper := DecomposeSuperInstruction(op); isSuper { + err = in.tryFallbackForSuperInstruction(&pc, seq, contract, stack, mem, callContext) + return nil, err + } return nil, ErrOutOfGas } else { contract.Gas -= dynamicCost diff --git a/core/vm/interpreter_si.go b/core/vm/interpreter_si.go new file mode 100644 index 0000000000..e6d944775f --- /dev/null +++ b/core/vm/interpreter_si.go @@ -0,0 +1,124 @@ +package vm + +import ( + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/log" +) + +// superInstructionMap maps super-instruction opcodes to the slice of ordinary opcodes +// they were fused from. The mapping comes from the fusion patterns implemented in +// core/opcodeCompiler/compiler/opCodeProcessor.go (applyFusionPatterns). When that file +// is updated with new fusion rules, this map should be kept in sync. +var superInstructionMap = map[OpCode][]OpCode{ + AndSwap1PopSwap2Swap1: {AND, SWAP1, POP, SWAP2, SWAP1}, + Swap2Swap1PopJump: {SWAP2, SWAP1, POP, JUMP}, + Swap1PopSwap2Swap1: {SWAP1, POP, SWAP2, SWAP1}, + PopSwap2Swap1Pop: {POP, SWAP2, SWAP1, POP}, + Push2Jump: {PUSH2, JUMP}, // PUSH2 embeds 2-byte immediate + Push2JumpI: {PUSH2, JUMPI}, + Push1Push1: {PUSH1, PUSH1}, + Push1Add: {PUSH1, ADD}, + Push1Shl: {PUSH1, SHL}, + Push1Dup1: {PUSH1, DUP1}, + Swap1Pop: {SWAP1, POP}, + PopJump: {POP, JUMP}, + Pop2: {POP, POP}, + Swap2Swap1: {SWAP2, SWAP1}, + Swap2Pop: {SWAP2, POP}, + Dup2LT: {DUP2, LT}, + JumpIfZero: {ISZERO, PUSH2, JUMPI}, // PUSH2 embeds 2-byte immediate + IsZeroPush2: {ISZERO, PUSH2}, + Dup2MStorePush1Add: {DUP2, MSTORE, PUSH1, ADD}, + Dup1Push4EqPush2: {DUP1, PUSH4, EQ, PUSH2}, + Push1CalldataloadPush1ShrDup1Push4GtPush2: {PUSH1, CALLDATALOAD, PUSH1, SHR, DUP1, PUSH4, GT, PUSH2}, + Push1Push1Push1SHLSub: {PUSH1, PUSH1, PUSH1, SHL, SUB}, + AndDup2AddSwap1Dup2LT: {AND, DUP2, ADD, SWAP1, DUP2, LT}, + Swap1Push1Dup1NotSwap2AddAndDup2AddSwap1Dup2LT: {SWAP1, PUSH1, DUP1, NOT, SWAP2, ADD, AND, DUP2, ADD, SWAP1, DUP2, LT}, + Dup3And: {DUP3, AND}, + Swap2Swap1Dup3SubSwap2Dup3GtPush2: {SWAP2, SWAP1, DUP3, SUB, SWAP2, DUP3, GT, PUSH2}, + Swap1Dup2: {SWAP1, DUP2}, + SHRSHRDup1MulDup1: {SHR, SHR, DUP1, MUL, DUP1}, + Swap3PopPopPop: {SWAP3, POP, POP, POP}, + SubSLTIsZeroPush2: {SUB, SLT, ISZERO, PUSH2}, + Dup11MulDup3SubMulDup1: {DUP11, MUL, DUP3, SUB, MUL, DUP1}, +} + +// DecomposeSuperInstruction returns the underlying opcode sequence of a fused +// super-instruction. If the provided opcode is not a super-instruction (or is +// unknown), the second return value will be false. +func DecomposeSuperInstruction(op OpCode) ([]OpCode, bool) { + seq, ok := superInstructionMap[op] + return seq, ok +} + +// DecomposeSuperInstructionByName works like DecomposeSuperInstruction but takes the +// textual name (case-insensitive) instead of the opcode constant. +func DecomposeSuperInstructionByName(name string) ([]OpCode, bool) { + op := StringToOp(strings.ToUpper(name)) + return DecomposeSuperInstruction(op) +} + +func (in *EVMInterpreter) executeSingleOpcode(pc *uint64, op OpCode, contract *Contract, stack *Stack, mem *Memory, callCtx *ScopeContext) error { + operation := in.table[op] + if operation == nil { + return fmt.Errorf("unknown opcode %02x", op) + } + + // -------- check static gas -------- + if contract.Gas < operation.constantGas { + return ErrOutOfGas + } + contract.Gas -= operation.constantGas + + // -------- check dynamic gas -------- + var memorySize uint64 + if operation.memorySize != nil { + memSize, overflow := operation.memorySize(stack) + if overflow { + return ErrGasUintOverflow + } + if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow { + return ErrGasUintOverflow + } + } + + if operation.dynamicGas != nil { + dyn, err := operation.dynamicGas(in.evm, contract, stack, mem, memorySize) + if err != nil { + return err + } + if contract.Gas < dyn { + return ErrOutOfGas + } + contract.Gas -= dyn + } + + if memorySize > 0 { + mem.Resize(memorySize) + } + + // -------- execute -------- + _, err := operation.execute(pc, in, callCtx) + return err +} + +// tryFallbackForSuperInstruction break down superinstruction to normal opcode and execute in sequence, until gas deplete or succeed +// return nil show successful execution of si or OOG in the middle (and updated pc/gas), shall continue in main loop +func (in *EVMInterpreter) tryFallbackForSuperInstruction(pc *uint64, seq []OpCode, contract *Contract, stack *Stack, mem *Memory, callCtx *ScopeContext) error { + startPC := *pc + + log.Error("[FALLBACK]", "start", startPC, "seqLen", len(seq)) + + for _, sub := range seq { + log.Error("[FALLBACK-EXEC]", "pc", *pc, "op", sub.String(), "gasBefore", contract.Gas) + if err := in.executeSingleOpcode(pc, sub, contract, stack, mem, callCtx); err != nil { + log.Error("[FALLBACK-EXEC]", "op", sub.String(), "err", err, "gasLeft", contract.Gas) + return err // OutOfGas or other errors, will let upper level handle + } + log.Error("[FALLBACK-EXEC]", "ok", true, "nextPC", *pc, "gasAfter", contract.Gas) + } + return nil +} From 1ddb74f470f0582d70a1383cc131f084fa63b787 Mon Sep 17 00:00:00 2001 From: annielz Date: Mon, 1 Sep 2025 17:01:23 +0800 Subject: [PATCH 2/4] feat: modify block terminator and fallthrough --- .../compiler/opCodeProcessor.go | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/core/opcodeCompiler/compiler/opCodeProcessor.go b/core/opcodeCompiler/compiler/opCodeProcessor.go index 4df39ee47f..3017417364 100644 --- a/core/opcodeCompiler/compiler/opCodeProcessor.go +++ b/core/opcodeCompiler/compiler/opCodeProcessor.go @@ -637,12 +637,21 @@ func getBlockType(block BasicBlock, blocks []BasicBlock, blockIndex int) string return "JumpDest" } - // Check for conditional fallthrough (previous block ends with JUMPI) + // Check for conditional fallthrough (previous block ends with JUMPI or CALL related) if blockIndex > 0 { prevBlock := blocks[blockIndex-1] if len(prevBlock.Opcodes) > 0 { lastOp := ByteCode(prevBlock.Opcodes[len(prevBlock.Opcodes)-1]) - if lastOp == JUMPI { + if lastOp == JUMPI || + lastOp == CALL || + lastOp == CALLCODE || + lastOp == DELEGATECALL || + lastOp == STATICCALL || + lastOp == EXTCALL || + lastOp == EXTDELEGATECALL || + lastOp == EXTSTATICCALL || + lastOp == GAS || + lastOp == SSTORE { return "conditional fallthrough" } } @@ -772,13 +781,20 @@ func GenerateBasicBlocks(code []byte) []BasicBlock { // isBlockTerminator checks if an opcode terminates a basic block func isBlockTerminator(op ByteCode) bool { switch op { + // Unconditional terminators or explicit halts case STOP, RETURN, REVERT, SELFDESTRUCT: return true - case JUMP, JUMPI: + // Unconditional / conditional jumps that alter the control-flow within the same contract + case JUMP, JUMPI, GAS, SSTORE, RJUMP, RJUMPI, RJUMPV, CALLF, RETF, JUMPF: return true - case RJUMP, RJUMPI, RJUMPV: + // External message calls — these transfer control to another context and therefore + // must terminate the current basic block for correct static-gas accounting + case CALL, CALLCODE, DELEGATECALL, STATICCALL, + EXTCALL, EXTDELEGATECALL, EXTSTATICCALL: return true - case CALLF, RETF, JUMPF: + // Contract creation opcodes have similar control-flow behaviour (external call & potential revert) + // so we also treat them as block terminators + case CREATE, CREATE2: return true default: return false From 93e511fb70cd127ff6e281a0e1ba5dc0c007189b Mon Sep 17 00:00:00 2001 From: annielz Date: Mon, 1 Sep 2025 17:31:42 +0800 Subject: [PATCH 3/4] Revert "feat: modify block terminator and fallthrough" This reverts commit 1ddb74f470f0582d70a1383cc131f084fa63b787. --- .../compiler/opCodeProcessor.go | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/core/opcodeCompiler/compiler/opCodeProcessor.go b/core/opcodeCompiler/compiler/opCodeProcessor.go index 3017417364..4df39ee47f 100644 --- a/core/opcodeCompiler/compiler/opCodeProcessor.go +++ b/core/opcodeCompiler/compiler/opCodeProcessor.go @@ -637,21 +637,12 @@ func getBlockType(block BasicBlock, blocks []BasicBlock, blockIndex int) string return "JumpDest" } - // Check for conditional fallthrough (previous block ends with JUMPI or CALL related) + // Check for conditional fallthrough (previous block ends with JUMPI) if blockIndex > 0 { prevBlock := blocks[blockIndex-1] if len(prevBlock.Opcodes) > 0 { lastOp := ByteCode(prevBlock.Opcodes[len(prevBlock.Opcodes)-1]) - if lastOp == JUMPI || - lastOp == CALL || - lastOp == CALLCODE || - lastOp == DELEGATECALL || - lastOp == STATICCALL || - lastOp == EXTCALL || - lastOp == EXTDELEGATECALL || - lastOp == EXTSTATICCALL || - lastOp == GAS || - lastOp == SSTORE { + if lastOp == JUMPI { return "conditional fallthrough" } } @@ -781,20 +772,13 @@ func GenerateBasicBlocks(code []byte) []BasicBlock { // isBlockTerminator checks if an opcode terminates a basic block func isBlockTerminator(op ByteCode) bool { switch op { - // Unconditional terminators or explicit halts case STOP, RETURN, REVERT, SELFDESTRUCT: return true - // Unconditional / conditional jumps that alter the control-flow within the same contract - case JUMP, JUMPI, GAS, SSTORE, RJUMP, RJUMPI, RJUMPV, CALLF, RETF, JUMPF: + case JUMP, JUMPI: return true - // External message calls — these transfer control to another context and therefore - // must terminate the current basic block for correct static-gas accounting - case CALL, CALLCODE, DELEGATECALL, STATICCALL, - EXTCALL, EXTDELEGATECALL, EXTSTATICCALL: + case RJUMP, RJUMPI, RJUMPV: return true - // Contract creation opcodes have similar control-flow behaviour (external call & potential revert) - // so we also treat them as block terminators - case CREATE, CREATE2: + case CALLF, RETF, JUMPF: return true default: return false From 817d6ad6ab681fe1d3a89fea07b33943d09206a9 Mon Sep 17 00:00:00 2001 From: cbh876 <3930922419@qq.com> Date: Mon, 1 Sep 2025 17:34:08 +0800 Subject: [PATCH 4/4] fix: mem last gas cost bug --- core/vm/interpreter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index c5160fa8de..1a434a363d 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -295,6 +295,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( // cost is explicitly set so that the capture state defer method can get the proper cost // cost is explicitly set so that the capture state defer method can get the proper cost var dynamicCost uint64 + memLastGasCost := mem.lastGasCost dynamicCost, err = operation.dynamicGas(in.evm, contract, stack, mem, memorySize) cost += dynamicCost // for tracing if err != nil { @@ -303,6 +304,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( // for tracing: this gas consumption event is emitted below in the debug section. if contract.Gas < dynamicCost { contract.Gas += operation.constantGas // restore deducted constant gas first + mem.lastGasCost = memLastGasCost if seq, isSuper := DecomposeSuperInstruction(op); isSuper { err = in.tryFallbackForSuperInstruction(&pc, seq, contract, stack, mem, callContext) return nil, err