core/vm: implement EIP-8024 (#33095)

EIP-8024: Backward compatible SWAPN, DUPN, EXCHANGE

Introduces additional instructions for manipulating the stack which
allow accessing the stack at higher depths. This is an initial implementation
of the EIP, which is still in Review stage.
This commit is contained in:
Jonny Rhea 2025-11-26 08:58:59 -06:00 committed by GitHub
parent ed4d00fd83
commit 689ea10f35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 293 additions and 0 deletions

View file

@ -42,6 +42,7 @@ var activators = map[int]func(*JumpTable){
4762: enable4762,
7702: enable7702,
7939: enable7939,
8024: enable8024,
}
// EnableEIP enables the given EIP on the config.
@ -342,6 +343,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),
}
}
func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
var (
stack = scope.Stack

View file

@ -920,6 +920,115 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro
return nil, errStopToken
}
func decodeSingle(x byte) int {
if x <= 90 {
return int(x) + 17
}
return int(x) - 20
}
func decodePair(x byte) (int, int) {
var k int
if x <= 79 {
k = int(x)
} else {
k = int(x) - 48
}
q, r := k/16, k%16
if q < r {
return q + 1, r + 1
}
return r + 1, 29 - q
}
func opDupN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
code := scope.Contract.Code
i := *pc + 1
// Ensure an immediate byte exists after DUPN
if i >= uint64(len(code)) {
return nil, &ErrInvalidOpCode{opcode: INVALID}
}
x := code[i]
// This range is excluded to preserve compatibility with existing opcodes.
if x > 90 && x < 128 {
return nil, &ErrInvalidOpCode{opcode: OpCode(x)}
}
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 nth stack item is duplicated at the top of the stack.
scope.Stack.push(scope.Stack.Back(n - 1))
*pc += 2
return nil, nil
}
func opSwapN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
code := scope.Contract.Code
i := *pc + 1
// Ensure an immediate byte exists after SWAPN
if i >= uint64(len(code)) {
return nil, &ErrInvalidOpCode{opcode: INVALID}
}
x := code[i]
// This range is excluded to preserve compatibility with existing opcodes.
if x > 90 && x < 128 {
return nil, &ErrInvalidOpCode{opcode: OpCode(x)}
}
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 += 2
return nil, nil
}
func opExchange(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
code := scope.Contract.Code
i := *pc + 1
// Ensure an immediate byte exists after EXCHANGE
if i >= uint64(len(code)) {
return nil, &ErrInvalidOpCode{opcode: INVALID}
}
x := code[i]
// This range is excluded both to preserve compatibility with existing opcodes
// and to keep decode_pairs 16-aligned arithmetic mapping valid (079, 128255).
if x > 79 && x < 128 {
return nil, &ErrInvalidOpCode{opcode: OpCode(x)}
}
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 += 2
return nil, nil
}
// following functions are used by the instruction jump table
// make log instruction function

View file

@ -1008,3 +1008,164 @@ func TestOpCLZ(t *testing.T) {
}
}
}
func TestEIP8024_Execution(t *testing.T) {
evm := NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
tests := []struct {
name string
codeHex string
wantErr bool
wantVals []uint64
}{
{
name: "DUPN",
codeHex: "60016000808080808080808080808080808080e600",
wantVals: []uint64{
1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1,
},
},
{
name: "SWAPN",
codeHex: "600160008080808080808080808080808080806002e700",
wantVals: []uint64{
1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2,
},
},
{
name: "EXCHANGE",
codeHex: "600060016002e801",
wantVals: []uint64{2, 0, 1},
},
{
name: "INVALID_SWAPN_LOW",
codeHex: "e75b",
wantErr: true,
},
{
name: "JUMP over INVALID_DUPN",
codeHex: "600456e65b",
wantErr: false,
},
// Additional test cases
{
name: "INVALID_DUPN_LOW",
codeHex: "e65b",
wantErr: true,
},
{
name: "INVALID_EXCHANGE_LOW",
codeHex: "e850",
wantErr: true,
},
{
name: "INVALID_DUPN_HIGH",
codeHex: "e67f",
wantErr: true,
},
{
name: "INVALID_SWAPN_HIGH",
codeHex: "e77f",
wantErr: true,
},
{
name: "INVALID_EXCHANGE_HIGH",
codeHex: "e87f",
wantErr: true,
},
{
name: "UNDERFLOW_DUPN",
codeHex: "5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5fe600", // (n=17, need 17 items, have 16)
wantErr: true,
},
{
name: "UNDERFLOW_SWAPN",
codeHex: "5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5fe700", // (n=17, need 18 items, have 17)
wantErr: true,
},
{
name: "UNDERFLOW_EXCHANGE",
codeHex: "60016002e801", // (n,m)=(1,2), need 3 items, have 2
wantErr: true,
},
{
name: "MISSING_IMMEDIATE_DUPN",
codeHex: "e6", // no operand
wantErr: true,
},
{
name: "MISSING_IMMEDIATE_SWAPN",
codeHex: "e7", // no operand
wantErr: true,
},
{
name: "MISSING_IMMEDIATE_EXCHANGE",
codeHex: "e8", // no operand
wantErr: true,
},
}
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
for pc < uint64(len(code)) && err == nil {
op := code[pc]
switch op {
case 0x00:
return
case 0x60:
_, err = opPush1(&pc, evm, scope)
pc++
case 0x80:
dup1 := makeDup(1)
_, err = dup1(&pc, evm, scope)
pc++
case 0x56:
_, err = opJump(&pc, evm, scope)
pc++
case 0x5b:
_, err = opJumpdest(&pc, evm, scope)
pc++
case 0xe6:
_, err = opDupN(&pc, evm, scope)
case 0xe7:
_, err = opSwapN(&pc, evm, scope)
case 0xe8:
_, err = opExchange(&pc, evm, scope)
default:
err = &ErrInvalidOpCode{opcode: OpCode(op)}
}
}
if tc.wantErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
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)
}
}
})
}
}