core/vm: add EIP-8024 DUPN/SWAPN/EXCHANGE opcodes and tests

This commit is contained in:
jonny rhea 2025-11-03 17:08:21 -06:00
parent 243407a3aa
commit 342d3f415c
6 changed files with 291 additions and 0 deletions

View file

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

View file

@ -149,6 +149,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon
evm.precompiles = activePrecompiledContracts(evm.chainRules) evm.precompiles = activePrecompiledContracts(evm.chainRules)
switch { switch {
case evm.chainRules.IsAmsterdam:
evm.table = &amsterdamInstructionSet
case evm.chainRules.IsOsaka: case evm.chainRules.IsOsaka:
evm.table = &osakaInstructionSet evm.table = &osakaInstructionSet
case evm.chainRules.IsVerkle: case evm.chainRules.IsVerkle:

View file

@ -920,6 +920,115 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro
return nil, errStopToken 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 + 2, r + 2
}
return r + 2, 30 - 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 // following functions are used by the instruction jump table
// make log instruction function // make log instruction function

View file

@ -1008,3 +1008,151 @@ 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,
},
},
{
//NOTE: Spec test seems wrong. Added additional stack item to make it pass.
name: "EXCHANGE",
codeHex: "6000600160026003e801",
wantVals: []uint64{3, 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,
},
{
// Note: This is the spec test case, but we expect it to fail
name: "UNDERFLOW_EXCHANGE",
codeHex: "600060016002e801", // (n,m=2,3 need at least 4 items have 3)
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)
}
}
})
}
}

View file

@ -63,6 +63,7 @@ var (
verkleInstructionSet = newVerkleInstructionSet() verkleInstructionSet = newVerkleInstructionSet()
pragueInstructionSet = newPragueInstructionSet() pragueInstructionSet = newPragueInstructionSet()
osakaInstructionSet = newOsakaInstructionSet() osakaInstructionSet = newOsakaInstructionSet()
amsterdamInstructionSet = newAmsterdamInstructionSet()
) )
// JumpTable contains the EVM opcodes supported at a given fork. // JumpTable contains the EVM opcodes supported at a given fork.
@ -92,6 +93,12 @@ func newVerkleInstructionSet() JumpTable {
return validate(instructionSet) return validate(instructionSet)
} }
func newAmsterdamInstructionSet() JumpTable {
instructionSet := newOsakaInstructionSet()
enable8024(&instructionSet) // EIP-8024 (DUPN, SWAPN, EXCHANGE)
return validate(instructionSet)
}
func newOsakaInstructionSet() JumpTable { func newOsakaInstructionSet() JumpTable {
instructionSet := newPragueInstructionSet() instructionSet := newPragueInstructionSet()
enable7939(&instructionSet) // EIP-7939 (CLZ opcode) enable7939(&instructionSet) // EIP-7939 (CLZ opcode)

View file

@ -28,6 +28,8 @@ func LookupInstructionSet(rules params.Rules) (JumpTable, error) {
switch { switch {
case rules.IsVerkle: case rules.IsVerkle:
return newCancunInstructionSet(), errors.New("verkle-fork not defined yet") return newCancunInstructionSet(), errors.New("verkle-fork not defined yet")
case rules.IsAmsterdam:
return newAmsterdamInstructionSet(), nil
case rules.IsOsaka: case rules.IsOsaka:
return newOsakaInstructionSet(), nil return newOsakaInstructionSet(), nil
case rules.IsPrague: case rules.IsPrague: