diff --git a/core/vm/eips.go b/core/vm/eips.go index e63cde393fe4..3e1194b134cb 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -41,6 +41,7 @@ var activators = map[int]func(*JumpTable){ 1153: enable1153, 7702: enable7702, 7939: enable7939, + 8024: enable8024, } // EnableEIP enables the given EIP on the config. @@ -331,6 +332,28 @@ func enable6780(jt *JumpTable) { } } +// enable8024 applies EIP-8024 (DUPN, SWAPN, EXCHANGE) +func enable8024(jt *JumpTable) { + jt[DUPN] = &operation{ + execute: opDupN, + constantGas: GasFastestStep, + minStack: minStack(1, 0), + maxStack: maxStack(0, 1), + } + jt[SWAPN] = &operation{ + execute: opSwapN, + constantGas: GasFastestStep, + minStack: minStack(2, 0), + maxStack: maxStack(0, 0), + } + jt[EXCHANGE] = &operation{ + execute: opExchange, + constantGas: GasFastestStep, + minStack: minStack(2, 0), + maxStack: maxStack(0, 0), + } +} + // opExtCodeCopyEIP7702 implements the EIP-7702 variation of opExtCodeCopy. func opExtCodeCopyEIP7702(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { var ( diff --git a/core/vm/errors.go b/core/vm/errors.go index c71dc3122a84..357fa5cfd4d2 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -68,10 +68,16 @@ func (e *ErrStackOverflow) Error() string { // ErrInvalidOpCode wraps an evm error when an invalid opcode is encountered. type ErrInvalidOpCode struct { - opcode OpCode + opcode OpCode + operand *byte } -func (e *ErrInvalidOpCode) Error() string { return fmt.Sprintf("invalid opcode: %s", e.opcode) } +func (e *ErrInvalidOpCode) Error() string { + if e.operand != nil { + return fmt.Sprintf("invalid opcode: %s (operand: 0x%02x)", e.opcode, *e.operand) + } + return fmt.Sprintf("invalid opcode: %s", e.opcode) +} // rpcError is the same interface as the one defined in rpc/errors.go // but we do not want to depend on rpc package here so we redefine it. diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 4d16f6ca859b..33b2c6717e2a 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -926,6 +926,128 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro return nil, errStopToken } +// decodeSingle decodes the immediate operand of a backward-compatible DUPN or SWAPN instruction (EIP-8024) +// https://eips.ethereum.org/EIPS/eip-8024 +func decodeSingle(x byte) int { + // Depths 1-16 are already covered by the legacy opcodes. The forbidden byte range [91, 127] removes + // 37 values from the 256 possible immediates, leaving 219 usable values, so this encoding covers depths + // 17 through 235. The immediate is encoded as (x + 111) % 256, where 111 is chosen so that these values + // avoid the forbidden range. Decoding is simply the modular inverse (i.e. 111+145=256). + return (int(x) + 145) % 256 +} + +// decodePair decodes the immediate operand of a backward-compatible EXCHANGE +// instruction (EIP-8024) into stack indices (n, m) where 1 <= n < m +// and n + m <= 30. The forbidden byte range [82, 127] removes 46 values from +// the 256 possible immediates, leaving exactly 210 usable bytes. +// https://eips.ethereum.org/EIPS/eip-8024 +func decodePair(x byte) (int, int) { + // XOR with 143 remaps the forbidden bytes [82, 127] to an unused corner + // of the 16x16 grid below. + k := int(x ^ 143) + // Split into row q and column r of a 16x16 grid. The 210 valid pairs + // occupy two triangles within this grid. + q, r := k/16, k%16 + // Upper triangle (q < r): pairs where m <= 16, encoded directly as + // (q+1, r+1). + if q < r { + return q + 1, r + 1 + } + // Lower triangle: pairs where m > 16, recovered as (r+1, 29-q). + return r + 1, 29 - q +} + +func opDupN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + code := scope.Contract.Code + i := *pc + 1 + + // If the immediate byte is missing, treat as 0x00 (same convention as PUSHn). + var x byte + if i < uint64(len(code)) { + x = code[i] + } + + // This range is excluded to preserve compatibility with existing opcodes. + if x > 90 && x < 128 { + operand := x + return nil, &ErrInvalidOpCode{opcode: DUPN, operand: &operand} + } + n := decodeSingle(x) + + // DUPN duplicates the n'th stack item, so the stack must contain at least n elements. + if scope.Stack.len() < n { + return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: n} + } + + //The n‘th stack item is duplicated at the top of the stack. + scope.Stack.push(scope.Stack.Back(n - 1)) + *pc += 1 + return nil, nil +} + +func opSwapN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + code := scope.Contract.Code + i := *pc + 1 + + // If the immediate byte is missing, treat as 0x00 (same convention as PUSHn). + var x byte + if i < uint64(len(code)) { + x = code[i] + } + + // This range is excluded to preserve compatibility with existing opcodes. + if x > 90 && x < 128 { + operand := x + return nil, &ErrInvalidOpCode{opcode: SWAPN, operand: &operand} + } + n := decodeSingle(x) + + // SWAPN operates on the top and n+1 stack items, so the stack must contain at least n+1 elements. + if scope.Stack.len() < n+1 { + return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: n + 1} + } + + // The (n+1)‘th stack item is swapped with the top of the stack. + indexTop := scope.Stack.len() - 1 + indexN := scope.Stack.len() - 1 - n + scope.Stack.data[indexTop], scope.Stack.data[indexN] = scope.Stack.data[indexN], scope.Stack.data[indexTop] + *pc += 1 + return nil, nil +} + +func opExchange(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + code := scope.Contract.Code + i := *pc + 1 + + // If the immediate byte is missing, treat as 0x00 (same convention as PUSHn). + var x byte + if i < uint64(len(code)) { + x = code[i] + } + + // This range is excluded both to preserve compatibility with existing opcodes + // and to keep decode_pair’s 16-aligned arithmetic mapping valid (0–81, 128–255). + if x > 81 && x < 128 { + operand := x + return nil, &ErrInvalidOpCode{opcode: EXCHANGE, operand: &operand} + } + n, m := decodePair(x) + need := max(n, m) + 1 + + // EXCHANGE operates on the (n+1)'th and (m+1)'th stack items, + // so the stack must contain at least max(n, m)+1 elements. + if scope.Stack.len() < need { + return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: need} + } + + // The (n+1)‘th stack item is swapped with the (m+1)‘th stack item. + indexN := scope.Stack.len() - 1 - n + indexM := scope.Stack.len() - 1 - m + scope.Stack.data[indexN], scope.Stack.data[indexM] = scope.Stack.data[indexM], scope.Stack.data[indexN] + *pc += 1 + return nil, nil +} + // following functions are used by the instruction jump table // make log instruction function diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index 4ff2a0d90b1f..681b52ee009c 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -19,6 +19,7 @@ package vm import ( "bytes" "encoding/json" + "errors" "fmt" "log" "math/big" @@ -1019,3 +1020,203 @@ func TestPush(t *testing.T) { } } } + +func TestEIP8024_Execution(t *testing.T) { + evm := NewEVM(BlockContext{}, nil, nil, params.TestChainConfig, Config{}) + + tests := []struct { + name string + codeHex string + wantErr error + wantOpcode OpCode + wantOperand *byte + wantVals []uint64 + }{ + { + name: "DUPN", + codeHex: "60016000808080808080808080808080808080e680", + wantVals: []uint64{ + 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, + }, + }, + { + name: "SWAPN", + codeHex: "600160008080808080808080808080808080806002e780", + wantVals: []uint64{ + 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, + }, + }, + { + name: "EXCHANGE_MISSING_IMMEDIATE", + codeHex: "600260008080808080600160008080808080808080e8", + wantVals: []uint64{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, // 10th from top + 0, 0, 0, 0, 0, 0, + 1, // bottom + }, + }, + { + name: "EXCHANGE", + codeHex: "600060016002e88e", + wantVals: []uint64{2, 0, 1}, + }, + { + name: "EXCHANGE", + codeHex: "600080808080808080808080808080808080808080808080808080808060016002e88f", + wantVals: []uint64{ + 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, + }, + }, + { + name: "INVALID_DUPN_LOW", + codeHex: "e65b", + wantErr: &ErrInvalidOpCode{}, + wantOpcode: DUPN, + wantOperand: ptrToByte(0x5b), + }, + { + name: "INVALID_SWAPN_LOW", + codeHex: "e75b", + wantErr: &ErrInvalidOpCode{}, + wantOpcode: SWAPN, + wantOperand: ptrToByte(0x5b), + }, + { + name: "JUMP_OVER_INVALID_DUPN", + codeHex: "600456e65b", + wantErr: nil, + }, + { + name: "EXCHANGE", + codeHex: "60008080e88e15", + wantVals: []uint64{1, 0, 0}, + }, + { + name: "INVALID_EXCHANGE", + codeHex: "e852", + wantErr: &ErrInvalidOpCode{}, + wantOpcode: EXCHANGE, + wantOperand: ptrToByte(0x52), + }, + { + name: "UNDERFLOW_DUPN", + codeHex: "6000808080808080808080808080808080e680", + wantErr: &ErrStackUnderflow{}, + wantOpcode: DUPN, + }, + // Additional test cases + { + name: "PC_INCREMENT", + codeHex: "600060006000e88e15", + wantVals: []uint64{1, 0, 0}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + code := common.FromHex(tc.codeHex) + stack := newstack() + pc := uint64(0) + scope := &ScopeContext{Stack: stack, Contract: &Contract{Code: code}} + var err error + var errOp OpCode + for pc < uint64(len(code)) && err == nil { + op := code[pc] + switch OpCode(op) { + case STOP: + return + case PUSH1: + _, err = opPush1(&pc, evm, scope) + case DUP1: + dup1 := makeDup(1) + _, err = dup1(&pc, evm, scope) + case JUMP: + _, err = opJump(&pc, evm, scope) + case JUMPDEST: + _, err = opJumpdest(&pc, evm, scope) + case ISZERO: + _, err = opIszero(&pc, evm, scope) + case PUSH0: + _, err = opPush0(&pc, evm, scope) + case DUPN: + _, err = opDupN(&pc, evm, scope) + case SWAPN: + _, err = opSwapN(&pc, evm, scope) + case EXCHANGE: + _, err = opExchange(&pc, evm, scope) + default: + t.Fatalf("unexpected opcode %s at pc=%d", OpCode(op), pc) + } + if err != nil { + errOp = OpCode(op) + } + pc++ + } + if tc.wantErr != nil { + // Fail because we wanted an error, but didn't get one. + if err == nil { + t.Fatalf("expected error, got nil") + } + // Fail if the wrong opcode threw an error. + if errOp != tc.wantOpcode { + t.Fatalf("expected error from opcode %s, got %s", tc.wantOpcode, errOp) + } + // Fail if we don't get the error we expect. + switch tc.wantErr.(type) { + case *ErrInvalidOpCode: + var got *ErrInvalidOpCode + if !errors.As(err, &got) { + t.Fatalf("expected ErrInvalidOpCode, got %v", err) + } + if got.opcode != tc.wantOpcode { + t.Fatalf("ErrInvalidOpCode.opcode=%s; want %s", got.opcode, tc.wantOpcode) + } + if tc.wantOperand != nil { + if got.operand == nil { + t.Fatalf("ErrInvalidOpCode.operand=nil; want 0x%02x", *tc.wantOperand) + } + if *got.operand != *tc.wantOperand { + t.Fatalf("ErrInvalidOpCode.operand=0x%02x; want 0x%02x", *got.operand, *tc.wantOperand) + } + } + case *ErrStackUnderflow: + var want *ErrStackUnderflow + if !errors.As(err, &want) { + t.Fatalf("expected ErrStackUnderflow, got %v", err) + } + default: + t.Fatalf("unsupported wantErr type %T", tc.wantErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := make([]uint64, 0, stack.len()) + for i := stack.len() - 1; i >= 0; i-- { + got = append(got, stack.data[i].Uint64()) + } + if len(got) != len(tc.wantVals) { + t.Fatalf("stack len=%d; want %d", len(got), len(tc.wantVals)) + } + for i := range got { + if got[i] != tc.wantVals[i] { + t.Fatalf("[%s] stack[%d]=%d; want %d\nstack=%v", + tc.name, i, got[i], tc.wantVals[i], got) + } + } + }) + } +} + +func ptrToByte(v byte) *byte { + b := v + return &b +} diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 024f91739f84..d2edd8ea72d7 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -60,6 +60,7 @@ var ( cancunInstructionSet = newCancunInstructionSet() pragueInstructionSet = newPragueInstructionSet() osakaInstructionSet = newOsakaInstructionSet() + amsterdamInstructionSet = newAmsterdamInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -83,6 +84,12 @@ func validate(jt JumpTable) JumpTable { return jt } +func newAmsterdamInstructionSet() JumpTable { + instructionSet := newOsakaInstructionSet() + enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) + return validate(instructionSet) +} + func newOsakaInstructionSet() JumpTable { instructionSet := newPragueInstructionSet() enable7939(&instructionSet) // EIP-7939 (CLZ opcode) diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index d094b4e05c28..eb143e840703 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -210,6 +210,13 @@ const ( LOG4 ) +// 0xe0 range - eof operations. +const ( + DUPN OpCode = 0xe6 + SWAPN OpCode = 0xe7 + EXCHANGE OpCode = 0xe8 +) + // 0xf0 range - closures. const ( CREATE OpCode = 0xf0 @@ -386,6 +393,11 @@ var opCodeToString = [256]string{ LOG3: "LOG3", LOG4: "LOG4", + // 0xe0 range. + DUPN: "DUPN", + SWAPN: "SWAPN", + EXCHANGE: "EXCHANGE", + // 0xf0 range - closures. CREATE: "CREATE", CALL: "CALL", @@ -552,6 +564,9 @@ var stringToOp = map[string]OpCode{ "LOG2": LOG2, "LOG3": LOG3, "LOG4": LOG4, + "DUPN": DUPN, + "SWAPN": SWAPN, + "EXCHANGE": EXCHANGE, "CREATE": CREATE, "CREATE2": CREATE2, "CALL": CALL,