From ec5ac3a536dad496652573824ad86349490c30c0 Mon Sep 17 00:00:00 2001 From: Andrew Ashikhmin <34320705+yperbasis@users.noreply.github.com> Date: Thu, 13 Jul 2023 19:36:06 +0200 Subject: [PATCH] core/vm: implement EIP-5656, mcopy instruction (#7887) Cherry pick https://github.com/ethereum/go-ethereum/pull/26181 Co-authored-by: Charles Cooper --- core/vm/eips.go | 27 +++++++ core/vm/gas_table.go | 4 +- core/vm/instructions_test.go | 140 +++++++++++++++++++++++++++++++++++ core/vm/jump_table.go | 3 +- core/vm/memory.go | 22 ++---- core/vm/memory_table.go | 8 ++ core/vm/memory_test.go | 69 +++++++++++++++++ core/vm/opcodes.go | 3 + 8 files changed, 260 insertions(+), 16 deletions(-) create mode 100644 core/vm/memory_test.go diff --git a/core/vm/eips.go b/core/vm/eips.go index 381306bacd2..d126b1a62c7 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -28,6 +28,7 @@ import ( ) var activators = map[int]func(*JumpTable){ + 5656: enable5656, 4844: enable4844, 3860: enable3860, 3855: enable3855, @@ -264,3 +265,29 @@ func opDataHash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([ } return nil, nil } + +// enable5656 enables EIP-5656 (MCOPY opcode) +// https://eips.ethereum.org/EIPS/eip-5656 +func enable5656(jt *JumpTable) { + jt[MCOPY] = &operation{ + execute: opMcopy, + constantGas: GasFastestStep, + dynamicGas: gasMcopy, + numPop: 3, + numPush: 0, + memorySize: memoryMcopy, + } +} + +// opMcopy implements the MCOPY opcode (https://eips.ethereum.org/EIPS/eip-5656) +func opMcopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + var ( + dst = scope.Stack.Pop() + src = scope.Stack.Pop() + length = scope.Stack.Pop() + ) + // These values are checked for overflow during memory expansion calculation + // (the memorySize function on the opcode). + scope.Memory.Copy(dst.Uint64(), src.Uint64(), length.Uint64()) + return nil, nil +} diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index a48eb6c3c4b..f4495765f4c 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -64,7 +64,8 @@ func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error) { // as argument: // CALLDATACOPY (stack position 2) // CODECOPY (stack position 2) -// EXTCODECOPY (stack poition 3) +// MCOPY (stack position 2) +// EXTCODECOPY (stack position 3) // RETURNDATACOPY (stack position 2) func memoryCopierGas(stackpos int) gasFunc { return func(_ VMInterpreter, contract *Contract, stack *stack.Stack, mem *Memory, memorySize uint64) (uint64, error) { @@ -93,6 +94,7 @@ func memoryCopierGas(stackpos int) gasFunc { var ( gasCallDataCopy = memoryCopierGas(2) gasCodeCopy = memoryCopierGas(2) + gasMcopy = memoryCopierGas(2) gasExtCodeCopy = memoryCopierGas(3) gasReturnDataCopy = memoryCopierGas(2) ) diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index 9be42471be1..c64b0f34697 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -22,11 +22,13 @@ import ( "encoding/json" "fmt" "os" + "strings" "testing" "github.com/holiman/uint256" libcommon "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon-lib/common/math" "github.com/ledgerwatch/erigon/common" "github.com/ledgerwatch/erigon/common/u256" @@ -702,3 +704,141 @@ func TestCreate2Addreses(t *testing.T) { } } } + +func TestOpMCopy(t *testing.T) { + // Test cases from https://eips.ethereum.org/EIPS/eip-5656#test-cases + for i, tc := range []struct { + dst, src, len string + pre string + want string + wantGas uint64 + }{ + { // MCOPY 0 32 32 - copy 32 bytes from offset 32 to offset 0. + dst: "0x0", src: "0x20", len: "0x20", + pre: "0000000000000000000000000000000000000000000000000000000000000000 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + want: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + wantGas: 6, + }, + + { // MCOPY 0 0 32 - copy 32 bytes from offset 0 to offset 0. + dst: "0x0", src: "0x0", len: "0x20", + pre: "0101010101010101010101010101010101010101010101010101010101010101", + want: "0101010101010101010101010101010101010101010101010101010101010101", + wantGas: 6, + }, + { // MCOPY 0 1 8 - copy 8 bytes from offset 1 to offset 0 (overlapping). + dst: "0x0", src: "0x1", len: "0x8", + pre: "000102030405060708 000000000000000000000000000000000000000000000000", + want: "010203040506070808 000000000000000000000000000000000000000000000000", + wantGas: 6, + }, + { // MCOPY 1 0 8 - copy 8 bytes from offset 0 to offset 1 (overlapping). + dst: "0x1", src: "0x0", len: "0x8", + pre: "000102030405060708 000000000000000000000000000000000000000000000000", + want: "000001020304050607 000000000000000000000000000000000000000000000000", + wantGas: 6, + }, + // Tests below are not in the EIP, but maybe should be added + { // MCOPY 0xFFFFFFFFFFFF 0xFFFFFFFFFFFF 0 - copy zero bytes from out-of-bounds index(overlapping). + dst: "0xFFFFFFFFFFFF", src: "0xFFFFFFFFFFFF", len: "0x0", + pre: "11", + want: "11", + wantGas: 3, + }, + { // MCOPY 0xFFFFFFFFFFFF 0 0 - copy zero bytes from start of mem to out-of-bounds. + dst: "0xFFFFFFFFFFFF", src: "0x0", len: "0x0", + pre: "11", + want: "11", + wantGas: 3, + }, + { // MCOPY 0 0xFFFFFFFFFFFF 0 - copy zero bytes from out-of-bounds to start of mem + dst: "0x0", src: "0xFFFFFFFFFFFF", len: "0x0", + pre: "11", + want: "11", + wantGas: 3, + }, + { // MCOPY - copy 1 from space outside of uint64 space + dst: "0x0", src: "0x10000000000000000", len: "0x1", + pre: "0", + }, + { // MCOPY - copy 1 from 0 to space outside of uint64 + dst: "0x10000000000000000", src: "0x0", len: "0x1", + pre: "0", + }, + { // MCOPY - copy nothing from 0 to space outside of uint64 + dst: "0x10000000000000000", src: "0x0", len: "0x0", + pre: "", + want: "", + wantGas: 3, + }, + { // MCOPY - copy 1 from 0x20 to 0x10, with no prior allocated mem + dst: "0x10", src: "0x20", len: "0x1", + pre: "", + // 64 bytes + want: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + wantGas: 12, + }, + { // MCOPY - copy 1 from 0x19 to 0x10, with no prior allocated mem + dst: "0x10", src: "0x19", len: "0x1", + pre: "", + // 32 bytes + want: "0x0000000000000000000000000000000000000000000000000000000000000000", + wantGas: 9, + }, + } { + var ( + env = NewEVM(evmtypes.BlockContext{}, evmtypes.TxContext{}, nil, params.TestChainConfig, Config{}) + stack = stack.New() + pc = uint64(0) + evmInterpreter = NewEVMInterpreter(env, env.Config()) + ) + data := common.FromHex(strings.ReplaceAll(tc.pre, " ", "")) + // Set pre + mem := NewMemory() + mem.Resize(uint64(len(data))) + mem.Set(0, uint64(len(data)), data) + // Push stack args + len, _ := uint256.FromHex(tc.len) + src, _ := uint256.FromHex(tc.src) + dst, _ := uint256.FromHex(tc.dst) + + stack.Push(len) + stack.Push(src) + stack.Push(dst) + wantErr := (tc.wantGas == 0) + // Calc mem expansion + var memorySize uint64 + if memSize, overflow := memoryMcopy(stack); overflow { + if wantErr { + continue + } + t.Errorf("overflow") + } else { + var overflow bool + if memorySize, overflow = math.SafeMul(ToWordSize(memSize), 32); overflow { + t.Error(ErrGasUintOverflow) + } + } + // and the dynamic cost + var haveGas uint64 + if dynamicCost, err := gasMcopy(env, nil, stack, mem, memorySize); err != nil { + t.Error(err) + } else { + haveGas = GasFastestStep + dynamicCost + } + // Expand mem + if memorySize > 0 { + mem.Resize(memorySize) + } + // Do the copy + opMcopy(&pc, evmInterpreter, &ScopeContext{mem, stack, nil}) + want := common.FromHex(strings.ReplaceAll(tc.want, " ", "")) + if have := mem.store; !bytes.Equal(want, have) { + t.Errorf("case %d: \nwant: %#x\nhave: %#x\n", i, want, have) + } + wantGas := tc.wantGas + if haveGas != wantGas { + t.Errorf("case %d: gas wrong, want %d have %d\n", i, wantGas, haveGas) + } + } +} diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 107ba62ac15..5e2350ceb28 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -100,8 +100,9 @@ func newPragueInstructionSet() JumpTable { // and cancun instructions. func newCancunInstructionSet() JumpTable { instructionSet := newShanghaiInstructionSet() - enable4844(&instructionSet) // BLOBHASH opcode enable1153(&instructionSet) // Transient storage opcodes + enable4844(&instructionSet) // BLOBHASH opcode + enable5656(&instructionSet) // MCOPY opcode validateAndFillMaxStack(&instructionSet) return instructionSet } diff --git a/core/vm/memory.go b/core/vm/memory.go index 9a9f72c6667..6b7b3b64429 100644 --- a/core/vm/memory.go +++ b/core/vm/memory.go @@ -17,8 +17,6 @@ package vm import ( - "fmt" - "github.com/holiman/uint256" ) @@ -122,17 +120,13 @@ func (m *Memory) Data() []byte { return m.store } -// Print dumps the content of the memory. -func (m *Memory) Print() { - fmt.Printf("### mem %d bytes ###\n", len(m.store)) - if len(m.store) > 0 { - addr := 0 - for i := 0; i+32 <= len(m.store); i += 32 { - fmt.Printf("%03d: % x\n", addr, m.store[i:i+32]) - addr++ - } - } else { - fmt.Println("-- empty --") +// Copy copies data from the src position slice into the dst position. +// The source and destination may overlap. +// OBS: This operation assumes that any necessary memory expansion has already been performed, +// and this method may panic otherwise. +func (m *Memory) Copy(dst, src, len uint64) { + if len == 0 { + return } - fmt.Println("####################") + copy(m.store[dst:], m.store[src:src+len]) } diff --git a/core/vm/memory_table.go b/core/vm/memory_table.go index 73addad8b38..0207f792bc9 100644 --- a/core/vm/memory_table.go +++ b/core/vm/memory_table.go @@ -50,6 +50,14 @@ func memoryMStore(stack *stack.Stack) (uint64, bool) { return calcMemSize64WithUint(stack.Back(0), 32) } +func memoryMcopy(stack *stack.Stack) (uint64, bool) { + mStart := stack.Back(0) // stack[0]: dest + if stack.Back(1).Gt(mStart) { + mStart = stack.Back(1) // stack[1]: source + } + return calcMemSize64(mStart, stack.Back(2)) // stack[2]: length +} + func memoryCreate(stack *stack.Stack) (uint64, bool) { return calcMemSize64(stack.Back(1), stack.Back(2)) } diff --git a/core/vm/memory_test.go b/core/vm/memory_test.go new file mode 100644 index 00000000000..5418bbbc305 --- /dev/null +++ b/core/vm/memory_test.go @@ -0,0 +1,69 @@ +package vm + +import ( + "bytes" + "strings" + "testing" + + "github.com/ledgerwatch/erigon/common" +) + +func TestMemoryCopy(t *testing.T) { + // Test cases from https://eips.ethereum.org/EIPS/eip-5656#test-cases + for i, tc := range []struct { + dst, src, len uint64 + pre string + want string + }{ + { // MCOPY 0 32 32 - copy 32 bytes from offset 32 to offset 0. + 0, 32, 32, + "0000000000000000000000000000000000000000000000000000000000000000 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + }, + + { // MCOPY 0 0 32 - copy 32 bytes from offset 0 to offset 0. + 0, 0, 32, + "0101010101010101010101010101010101010101010101010101010101010101", + "0101010101010101010101010101010101010101010101010101010101010101", + }, + { // MCOPY 0 1 8 - copy 8 bytes from offset 1 to offset 0 (overlapping). + 0, 1, 8, + "000102030405060708 000000000000000000000000000000000000000000000000", + "010203040506070808 000000000000000000000000000000000000000000000000", + }, + { // MCOPY 1 0 8 - copy 8 bytes from offset 0 to offset 1 (overlapping). + 1, 0, 8, + "000102030405060708 000000000000000000000000000000000000000000000000", + "000001020304050607 000000000000000000000000000000000000000000000000", + }, + // Tests below are not in the EIP, but maybe should be added + { // MCOPY 0xFFFFFFFFFFFF 0xFFFFFFFFFFFF 0 - copy zero bytes from out-of-bounds index(overlapping). + 0xFFFFFFFFFFFF, 0xFFFFFFFFFFFF, 0, + "11", + "11", + }, + { // MCOPY 0xFFFFFFFFFFFF 0 0 - copy zero bytes from start of mem to out-of-bounds. + 0xFFFFFFFFFFFF, 0, 0, + "11", + "11", + }, + { // MCOPY 0 0xFFFFFFFFFFFF 0 - copy zero bytes from out-of-bounds to start of mem + 0, 0xFFFFFFFFFFFF, 0, + "11", + "11", + }, + } { + m := NewMemory() + // Clean spaces + data := common.FromHex(strings.ReplaceAll(tc.pre, " ", "")) + // Set pre + m.Resize(uint64(len(data))) + m.Set(0, uint64(len(data)), data) + // Do the copy + m.Copy(tc.dst, tc.src, tc.len) + want := common.FromHex(strings.ReplaceAll(tc.want, " ", "")) + if have := m.store; !bytes.Equal(want, have) { + t.Errorf("case %d: want: %#x\nhave: %#x\n", i, want, have) + } + } +} diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index c787fd5303c..72e0a84d108 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -123,6 +123,7 @@ const ( JUMPDEST OpCode = 0x5b TLOAD OpCode = 0x5c TSTORE OpCode = 0x5d + MCOPY OpCode = 0x5e PUSH0 OpCode = 0x5f ) @@ -299,6 +300,7 @@ var opCodeToString = map[OpCode]string{ JUMPDEST: "JUMPDEST", TLOAD: "TLOAD", TSTORE: "TSTORE", + MCOPY: "MCOPY", PUSH0: "PUSH0", // 0x60 range - push. @@ -466,6 +468,7 @@ var stringToOp = map[string]OpCode{ "JUMPDEST": JUMPDEST, "TLOAD": TLOAD, "TSTORE": TSTORE, + "MCOPY": MCOPY, "PUSH0": PUSH0, "PUSH1": PUSH1, "PUSH2": PUSH2,