From 2a21fa2c3b275f50ae461fc95ed5a5f77837e7d2 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 11:25:47 +0100 Subject: [PATCH 1/9] core/vm/program: create evm programming utility --- core/vm/program/program.go | 364 ++++++++++++++++++++++++++++++++ core/vm/program/program_test.go | 46 ++++ 2 files changed, 410 insertions(+) create mode 100644 core/vm/program/program.go create mode 100644 core/vm/program/program_test.go diff --git a/core/vm/program/program.go b/core/vm/program/program.go new file mode 100644 index 000000000000..e5b743dc5ab3 --- /dev/null +++ b/core/vm/program/program.go @@ -0,0 +1,364 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the goevmlab library. If not, see . + +package program + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/core/vm" + "github.com/holiman/uint256" +) + +// Program is a simple bytecode container. It can be used to construct +// simple EVM programs. Errors during construction of a Program typically +// cause panics: so avoid using these programs in production settings or on +// untrusted input. +// This package is mainly meant to aid in testing. This is not a production +// -level "compiler". +type Program struct { + code []byte +} + +func New() *Program { + return &Program{ + code: make([]byte, 0), + } +} + +// add adds the op to the code. +func (p *Program) add(op byte) *Program { + p.code = append(p.code, op) + return p +} + +// pushBig creates a PUSHX instruction and pushes the given val. +// - If the val is nil, it pushes zero +// - If the val is bigger than 32 bytes, it panics +func (p *Program) pushBig(val *big.Int) { + if val == nil { + val = big.NewInt(0) + } + valBytes := val.Bytes() + if len(valBytes) == 0 { + valBytes = append(valBytes, 0) + } + bLen := len(valBytes) + if bLen > 32 { + panic(fmt.Sprintf("Push value too large, %d bytes", bLen)) + } + p.add(byte(vm.PUSH1) - 1 + byte(bLen)) + p.Append(valBytes) + +} + +// Append appends the given data to the code. +func (p *Program) Append(data []byte) *Program { + p.code = append(p.code, data...) + return p +} + +// Op appends the given opcode +func (p *Program) Op(op vm.OpCode) *Program { + return p.add(byte(op)) +} + +// Ops appends the given opcodes +func (p *Program) Ops(ops ...vm.OpCode) *Program { + for _, op := range ops { + p.add(byte(op)) + } + return p +} + +// Push creates a PUSHX instruction with the data provided +func (p *Program) Push(val any) *Program { + switch v := val.(type) { + case int: + p.pushBig(new(big.Int).SetUint64(uint64(v))) + case uint64: + p.pushBig(new(big.Int).SetUint64(v)) + case uint32: + p.pushBig(new(big.Int).SetUint64(uint64(v))) + case *big.Int: + p.pushBig(v) + case *uint256.Int: + p.pushBig(v.ToBig()) + case uint256.Int: + p.pushBig(v.ToBig()) + case []byte: + p.pushBig(new(big.Int).SetBytes(v)) + case byte: + p.pushBig(new(big.Int).SetUint64(uint64(v))) + case interface{ Bytes() []byte }: + // Here, we jump through some hovm in order to avoid depending on + // go-ethereum types.Address and common.Hash, and instead use the + // interface. This works on both values and pointers! + p.pushBig(new(big.Int).SetBytes(v.Bytes())) + case nil: + p.pushBig(nil) + default: + panic(fmt.Sprintf("unsupported type %v", v)) + } + return p +} + +// Push0 implements PUSH0 (0x5f) +func (p *Program) Push0() *Program { + return p.Op(vm.PUSH0) +} + +// Bytecode returns the Program bytecode +func (p *Program) Bytecode() []byte { + return p.code +} + +// Hex returns the Program bytecode as a hex string +func (p *Program) Hex() string { + return fmt.Sprintf("%02x", p.Bytecode()) +} + +// ExtcodeCopy performsa an extcodecopy invocation +func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) { + p.Push(length) + p.Push(codeOffset) + p.Push(memOffset) + p.Push(address) + p.Op(vm.EXTCODECOPY) +} + +// Call is a convenience function to make a call. If 'gas' is nil, the opcode GAS will +// be used to provide all gas. +func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) { + p.Push(outSize) + p.Push(outOffset) + p.Push(inSize) + p.Push(inOffset) + p.Push(value) + p.Push(address) + if gas == nil { + p.Op(vm.GAS) + } else { + p.pushBig(gas) + } + p.Op(vm.CALL) +} + +// DelegateCall is a convenience function to make a delegatecall. If 'gas' is nil, the opcode GAS will +// be used to provide all gas. +func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) { + p.Push(outSize) + p.Push(outOffset) + p.Push(inSize) + p.Push(inOffset) + p.Push(address) + if gas == nil { + p.Op(vm.GAS) + } else { + p.pushBig(gas) + } + p.Op(vm.DELEGATECALL) +} + +// StaticCall is a convenience function to make a staticcall. If 'gas' is nil, the opcode GAS will +// be used to provide all gas. +func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) { + p.Push(outSize) + p.Push(outOffset) + p.Push(inSize) + p.Push(inOffset) + p.Push(address) + if gas == nil { + p.Op(vm.GAS) + } else { + p.pushBig(gas) + } + p.Op(vm.STATICCALL) +} + +// StaticCall is a convenience function to make a callcode. If 'gas' is nil, the opcode GAS will +// be used to provide all gas. +func (p *Program) CallCode(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) { + p.Push(outSize) + p.Push(outOffset) + p.Push(inSize) + p.Push(inOffset) + p.Push(value) + p.Push(address) + if gas == nil { + p.Op(vm.GAS) + } else { + p.pushBig(gas) + } + p.Op(vm.CALLCODE) +} + +// Label returns the PC (of the next instruction) +func (p *Program) Label() uint64 { + return uint64(len(p.code)) +} + +// Jumpdest adds a JUMPDEST op, and returns the PC of that instruction +func (p *Program) Jumpdest() uint64 { + here := p.Label() + p.Op(vm.JUMPDEST) + return here +} + +// Jump pushes the destination and adds a JUMP +func (p *Program) Jump(loc any) { + p.Push(loc) + p.Op(vm.JUMP) +} + +// Jump pushes the destination and adds a JUMP +func (p *Program) JumpIf(loc any, condition any) { + p.Push(condition) + p.Push(loc) + p.Op(vm.JUMPI) +} + +func (p *Program) Size() int { + return len(p.code) +} + +// InputToMemory stores the input (calldata) to memory as address (20 bytes). +func (p *Program) InputAddressToStack(inputOffset uint32) *Program { + p.Push(inputOffset) + p.Op(vm.CALLDATALOAD) // Loads [n -> n + 32] of input data to stack top + mask, ok := big.NewInt(0).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16) + if !ok { + panic("whoa") + } + p.Push(mask) // turn into address + return p.Op(vm.AND) +} + +// MStore stores the provided data (into the memory area starting at memStart) +func (p *Program) Mstore(data []byte, memStart uint32) *Program { + var idx = 0 + // We need to store it in chunks of 32 bytes + for ; idx+32 <= len(data); idx += 32 { + chunk := data[idx : idx+32] + // push the value + p.Push(chunk) + // push the memory index + p.Push(uint32(idx) + memStart) + p.Op(vm.MSTORE) + } + // Remainders become stored using MSTORE8 + for ; idx < len(data); idx++ { + b := data[idx] + // push the byte + p.Push(b) + p.Push(uint32(idx) + memStart) + p.Op(vm.MSTORE8) + } + return p +} + +// MemToStorage copies the given memory area into SSTORE slots, +// It expects data to be aligned to 32 byte, and does not zero out +// remainders if some data is not +// I.e, if given a 1-byte area, it will still copy the full 32 bytes to storage +func (p *Program) MemToStorage(memStart, memSize, startSlot int) *Program { + // We need to store it in chunks of 32 bytes + for idx := memStart; idx < (memStart + memSize); idx += 32 { + dataStart := idx + // Mload the chunk + p.Push(dataStart) + p.Op(vm.MLOAD) + // Value is now on stack, + p.Push(startSlot) + p.Op(vm.SSTORE) + startSlot++ + } + return p +} + +// Sstore stores the given byte array to the given slot. +// OBS! Does not verify that the value indeed fits into 32 bytes +// If it does not, it will panic later on via pushBig +func (p *Program) Sstore(slot any, value any) *Program { + p.Push(value) + p.Push(slot) + return p.Op(vm.SSTORE) +} + +// Tstore stores the given byte array to the given t-slot. +// OBS! Does not verify that the value indeed fits into 32 bytes +// If it does not, it will panic later on via pushBig +func (p *Program) Tstore(slot any, value any) *Program { + p.Push(value) + p.Push(slot) + return p.Op(vm.TSTORE) +} + +func (p *Program) Return(offset, len uint32) *Program { + p.Push(len) + p.Push(offset) + return p.Op(vm.RETURN) +} + +// ReturnData loads the given data into memory, and does a return with it +func (p *Program) ReturnData(data []byte) *Program { + p.Mstore(data, 0) + return p.Return(0, uint32(len(data))) +} + +// Create2 uses create2 to construct a contract with the given bytecode. +// This operation leaves either '0' or address on the stack. +func (p *Program) Create2(code []byte, salt any) *Program { + var ( + value = 0 + offset = 0 + size = len(code) + ) + // Load the code into mem + p.Mstore(code, 0) + // Create it + return p.Push(salt). + Push(size). + Push(offset). + Push(value). + Op(vm.CREATE2) + // On the stack now, is either + // zero: in case of failure + // address: in case of success +} + +// Create2AndCall calls create2 with the given initcode and salt, and then calls +// into the created contract (or calls into zero, if the creation failed). +func (p *Program) Create2AndCall(code []byte, salt any) *Program { + p.Create2(code, salt) + // If there happen to be a zero on the stack, it doesn't matter, we're + // not sending any value anyway + p.Push(0).Push(0) // mem out + p.Push(0).Push(0) // mem in + p.Push(0) // value + p.Op(vm.DUP6) // address + p.Op(vm.GAS) + p.Op(vm.CALL) + p.Op(vm.POP) // pop the retval + return p.Op(vm.POP) // pop the address +} + +// Selfdestruct pushes beneficiary and invokes selfdestruct. +func (p *Program) Selfdestruct(beneficiary any) *Program { + p.Push(beneficiary) + return p.Op(vm.SELFDESTRUCT) +} diff --git a/core/vm/program/program_test.go b/core/vm/program/program_test.go new file mode 100644 index 000000000000..3050c83f7772 --- /dev/null +++ b/core/vm/program/program_test.go @@ -0,0 +1,46 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the goevmlab library. If not, see . + +package program + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "bytes" + "testing" +) + +func TestCreate2Call(t *testing.T) { + // Some runtime code + runtime := New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytecode() + want := common.FromHex("0x30ff") + if !bytes.Equal(want, runtime) { + t.Fatalf("runtime code error\nwant: %x\nhave: %x\n", want, runtime) + } + // A constructor returning the runtime code + initcode := New().ReturnData(runtime).Bytecode() + want = common.FromHex("603060005360ff60015360026000f3") + if !bytes.Equal(want, initcode) { + t.Fatalf("initcode error\nwant: %x\nhave: %x\n", want, initcode) + } + // A factory invoking the constructor + outer := New().Create2AndCall(initcode, nil).Bytecode() + want = common.FromHex("60606000536030600153606060025360006003536053600453606060055360ff6006536060600753600160085360536009536060600a536002600b536060600c536000600d5360f3600e536000600f60006000f560006000600060006000855af15050") + if !bytes.Equal(want, outer) { + t.Fatalf("factory error\nwant: %x\nhave: %x\n", want, outer) + } +} From b3afc7e97ea78e514609640f31737eef4e3ed357 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 11:57:10 +0100 Subject: [PATCH 2/9] core/vm/program: add warning --- core/vm/program/program.go | 5 +++++ core/vm/program/readme.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 core/vm/program/readme.md diff --git a/core/vm/program/program.go b/core/vm/program/program.go index e5b743dc5ab3..79c0c296d4b6 100644 --- a/core/vm/program/program.go +++ b/core/vm/program/program.go @@ -14,6 +14,11 @@ // You should have received a copy of the GNU Lesser General Public License // along with the goevmlab library. If not, see . +// package program is a utility to create EVM bytecode for testing, but _not_ for production. As such: +// +// - There are not package guarantees. We might iterate heavily on this package, and do backwards-incompatible changes without warning +// - There are no quality-guarantees. These utilities may produce evm-code that is non-functional. YMMV. +// - There are no stability-guarantees. The utility will `panic` if the inputs do not align / make sense. package program import ( diff --git a/core/vm/program/readme.md b/core/vm/program/readme.md new file mode 100644 index 000000000000..0e4a54d8f181 --- /dev/null +++ b/core/vm/program/readme.md @@ -0,0 +1,30 @@ +### What is this + +In many cases, we have a need to create somewhat nontrivial bytecode, for testing various +quirks related to state transition or evm execution. + +For example, we want to have a `CREATE2`- op create a contract, which is then invoked, and when invoked does a selfdestruct-to-self. + +It is overkill to go full solidity, but it is also a bit tricky do assemble this by concatenating bytes. + +This utility takes an approach from [goevmlab](https://github.com/holiman/goevmlab/) where it has been used for several years, +a go-lang utility to assemble evm bytecode. + +Using this utility, the case above can be expressed as: +```golang + // Some runtime code + runtime := program.New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytecode() + // A constructor returning the runtime code + initcode := program.New().ReturnData(runtime).Bytecode() + // A factory invoking the constructor + outer := program.New().Create2AndCall(initcode, nil).Bytecode() +``` + +### Warning + +This package is a utility for testing, _not_ for production. As such: + +- There are not package guarantees. We might iterate heavily on this package, and do backwards-incompatible changes without warning +- There are no quality-guarantees. These utilities may produce evm-code that is non-functional. YMMV. +- There are no stability-guarantees. The utility will `panic` if the inputs do not align / make sense. + From 659cd13c1f08c2a835ab58e6014b24604405ff54 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 12:14:14 +0100 Subject: [PATCH 3/9] core/vm/program: api polishes and more tests --- core/vm/program/program.go | 20 ++--- core/vm/program/program_test.go | 135 +++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 13 deletions(-) diff --git a/core/vm/program/program.go b/core/vm/program/program.go index 79c0c296d4b6..5fa2a7f87be2 100644 --- a/core/vm/program/program.go +++ b/core/vm/program/program.go @@ -138,17 +138,17 @@ func (p *Program) Hex() string { } // ExtcodeCopy performsa an extcodecopy invocation -func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) { +func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) *Program { p.Push(length) p.Push(codeOffset) p.Push(memOffset) p.Push(address) - p.Op(vm.EXTCODECOPY) + return p.Op(vm.EXTCODECOPY) } // Call is a convenience function to make a call. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) { +func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { p.Push(outSize) p.Push(outOffset) p.Push(inSize) @@ -160,12 +160,12 @@ func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset } else { p.pushBig(gas) } - p.Op(vm.CALL) + return p.Op(vm.CALL) } // DelegateCall is a convenience function to make a delegatecall. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) { +func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) *Program { p.Push(outSize) p.Push(outOffset) p.Push(inSize) @@ -176,12 +176,12 @@ func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffse } else { p.pushBig(gas) } - p.Op(vm.DELEGATECALL) + return p.Op(vm.DELEGATECALL) } // StaticCall is a convenience function to make a staticcall. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) { +func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) *Program { p.Push(outSize) p.Push(outOffset) p.Push(inSize) @@ -192,12 +192,12 @@ func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, } else { p.pushBig(gas) } - p.Op(vm.STATICCALL) + return p.Op(vm.STATICCALL) } // StaticCall is a convenience function to make a callcode. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) CallCode(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) { +func (p *Program) CallCode(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { p.Push(outSize) p.Push(outOffset) p.Push(inSize) @@ -209,7 +209,7 @@ func (p *Program) CallCode(gas *big.Int, address, value, inOffset, inSize, outOf } else { p.pushBig(gas) } - p.Op(vm.CALLCODE) + return p.Op(vm.CALLCODE) } // Label returns the PC (of the next instruction) diff --git a/core/vm/program/program_test.go b/core/vm/program/program_test.go index 3050c83f7772..5291e034effd 100644 --- a/core/vm/program/program_test.go +++ b/core/vm/program/program_test.go @@ -17,13 +17,142 @@ package program import ( - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" - "bytes" + "math/big" "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" ) +func TestPush(t *testing.T) { + tests := []struct { + input interface{} + expected string + }{ + // native ints + {0, "6000"}, + {nil, "6000"}, + {uint64(1), "6001"}, + {0xfff, "610fff"}, + // bigints + {big.NewInt(0), "6000"}, + {big.NewInt(1), "6001"}, + {big.NewInt(0xfff), "610fff"}, + // Addresses + {common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), "73deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, + {&common.Address{}, "6000"}, + } + for i, tc := range tests { + have := New().Push(tc.input).Hex() + if have != tc.expected { + t.Errorf("test %d: got %v expected %v", i, have, tc.expected) + } + } +} + +func TestCall(t *testing.T) { + { // Nil gas + have := New().Call(nil, common.HexToAddress("0x1337"), big.NewInt(1), 1, 2, 3, 4).Hex() + want := "600460036002600160016113375af1" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } + { // Non nil gas + have := New().Call(big.NewInt(0xffff), common.HexToAddress("0x1337"), big.NewInt(1), 1, 2, 3, 4).Hex() + want := "6004600360026001600161133761fffff1" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } +} + +func TestMstore(t *testing.T) { + { + have := New().Mstore(common.FromHex("0xaabb"), 0).Hex() + want := "60aa60005360bb600153" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } + { // store at offset + have := New().Mstore(common.FromHex("0xaabb"), 3).Hex() + want := "60aa60035360bb600453" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } + { // 34 bytes + data := common.FromHex("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFF") + + have := New().Mstore(data, 0).Hex() + want := "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260ff60205360ff602153" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } +} + +func TestMemToStorage(t *testing.T) { + have := New().MemToStorage(0, 33, 1).Hex() + want := "600051600155602051600255" + if have != want { + t.Errorf("have %v want %v", have, want) + } +} + +func TestSstore(t *testing.T) { + have := New().Sstore(0x1337, []byte("1234")).Hex() + want := "633132333461133755" + if have != want { + t.Errorf("have %v want %v", have, want) + } +} + +func TestReturnData(t *testing.T) { + { + have := New().ReturnData([]byte{0xFF}).Hex() + want := "60ff60005360016000f3" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } + { + // 32 bytes + data := common.FromHex("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + have := New().ReturnData(data).Hex() + want := "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260206000f3" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } +} + +func TestCreateAndCall(t *testing.T) { + // A constructor that stores a slot + ctor := New().Sstore(0, big.NewInt(5)) + + // A runtime bytecode which reads the slot and returns + deployed := New() + deployed.Push(0).Op(vm.SLOAD) // [value] in stack + deployed.Push(0) // [value, 0] + deployed.Op(vm.MSTORE) + deployed.Return(0, 32) + + // Pack them + ctor.ReturnData(deployed.Bytecode()) + // Verify constructor + runtime code + { + want := "6005600055606060005360006001536054600253606060035360006004536052600553606060065360206007536060600853600060095360f3600a53600b6000f3" + if got := ctor.Hex(); got != want { + t.Fatalf("1: got %v expected %v", got, want) + } + } +} + func TestCreate2Call(t *testing.T) { // Some runtime code runtime := New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytecode() From 32b183192043e8aeeefe78a340fa396e5fc557f3 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 14:54:48 +0100 Subject: [PATCH 4/9] core/vm/runtime: utilize the programs a bit --- core/vm/program/program.go | 44 ++++----- core/vm/runtime/runtime_test.go | 154 +++++++++++--------------------- 2 files changed, 75 insertions(+), 123 deletions(-) diff --git a/core/vm/program/program.go b/core/vm/program/program.go index 5fa2a7f87be2..19fb861332fa 100644 --- a/core/vm/program/program.go +++ b/core/vm/program/program.go @@ -149,11 +149,11 @@ func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) *Progr // Call is a convenience function to make a call. If 'gas' is nil, the opcode GAS will // be used to provide all gas. func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { - p.Push(outSize) - p.Push(outOffset) - p.Push(inSize) - p.Push(inOffset) - p.Push(value) + if outOffset == outSize && inSize == outSize && inOffset == outSize && value == outSize { + p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1, vm.DUP1) + } else { + p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset).Push(value) + } p.Push(address) if gas == nil { p.Op(vm.GAS) @@ -166,10 +166,11 @@ func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset // DelegateCall is a convenience function to make a delegatecall. If 'gas' is nil, the opcode GAS will // be used to provide all gas. func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) *Program { - p.Push(outSize) - p.Push(outOffset) - p.Push(inSize) - p.Push(inOffset) + if outOffset == outSize && inSize == outSize && inOffset == outSize { + p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1) + } else { + p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset) + } p.Push(address) if gas == nil { p.Op(vm.GAS) @@ -182,10 +183,11 @@ func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffse // StaticCall is a convenience function to make a staticcall. If 'gas' is nil, the opcode GAS will // be used to provide all gas. func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) *Program { - p.Push(outSize) - p.Push(outOffset) - p.Push(inSize) - p.Push(inOffset) + if outOffset == outSize && inSize == outSize && inOffset == outSize { + p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1) + } else { + p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset) + } p.Push(address) if gas == nil { p.Op(vm.GAS) @@ -198,10 +200,11 @@ func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, // StaticCall is a convenience function to make a callcode. If 'gas' is nil, the opcode GAS will // be used to provide all gas. func (p *Program) CallCode(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { - p.Push(outSize) - p.Push(outOffset) - p.Push(inSize) - p.Push(inOffset) + if outOffset == outSize && inSize == outSize && inOffset == outSize { + p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1) + } else { + p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset) + } p.Push(value) p.Push(address) if gas == nil { @@ -218,16 +221,17 @@ func (p *Program) Label() uint64 { } // Jumpdest adds a JUMPDEST op, and returns the PC of that instruction -func (p *Program) Jumpdest() uint64 { +func (p *Program) Jumpdest() (*Program, uint64) { here := p.Label() p.Op(vm.JUMPDEST) - return here + return p, here } // Jump pushes the destination and adds a JUMP -func (p *Program) Jump(loc any) { +func (p *Program) Jump(loc any) *Program { p.Push(loc) p.Op(vm.JUMP) + return p } // Jump pushes the destination and adds a JUMP diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index 97234368ee0c..cb259b66de40 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/core/vm/program" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/params" @@ -436,110 +437,57 @@ func benchmarkNonModifyingCode(gas uint64, code []byte, name string, tracerCode // BenchmarkSimpleLoop test a pretty simple loop which loops until OOG // 55 ms func BenchmarkSimpleLoop(b *testing.B) { - staticCallIdentity := []byte{ - byte(vm.JUMPDEST), // [ count ] - // push args for the call - byte(vm.PUSH1), 0, // out size - byte(vm.DUP1), // out offset - byte(vm.DUP1), // out insize - byte(vm.DUP1), // in offset - byte(vm.PUSH1), 0x4, // address of identity - byte(vm.GAS), // gas - byte(vm.STATICCALL), - byte(vm.POP), // pop return value - byte(vm.PUSH1), 0, // jumpdestination - byte(vm.JUMP), - } - - callIdentity := []byte{ - byte(vm.JUMPDEST), // [ count ] - // push args for the call - byte(vm.PUSH1), 0, // out size - byte(vm.DUP1), // out offset - byte(vm.DUP1), // out insize - byte(vm.DUP1), // in offset - byte(vm.DUP1), // value - byte(vm.PUSH1), 0x4, // address of identity - byte(vm.GAS), // gas - byte(vm.CALL), - byte(vm.POP), // pop return value - byte(vm.PUSH1), 0, // jumpdestination - byte(vm.JUMP), - } - callInexistant := []byte{ - byte(vm.JUMPDEST), // [ count ] - // push args for the call - byte(vm.PUSH1), 0, // out size - byte(vm.DUP1), // out offset - byte(vm.DUP1), // out insize - byte(vm.DUP1), // in offset - byte(vm.DUP1), // value - byte(vm.PUSH1), 0xff, // address of existing contract - byte(vm.GAS), // gas - byte(vm.CALL), - byte(vm.POP), // pop return value - byte(vm.PUSH1), 0, // jumpdestination - byte(vm.JUMP), - } - - callEOA := []byte{ - byte(vm.JUMPDEST), // [ count ] - // push args for the call - byte(vm.PUSH1), 0, // out size - byte(vm.DUP1), // out offset - byte(vm.DUP1), // out insize - byte(vm.DUP1), // in offset - byte(vm.DUP1), // value - byte(vm.PUSH1), 0xE0, // address of EOA - byte(vm.GAS), // gas - byte(vm.CALL), - byte(vm.POP), // pop return value - byte(vm.PUSH1), 0, // jumpdestination - byte(vm.JUMP), - } - - loopingCode := []byte{ - byte(vm.JUMPDEST), // [ count ] - // push args for the call - byte(vm.PUSH1), 0, // out size - byte(vm.DUP1), // out offset - byte(vm.DUP1), // out insize - byte(vm.DUP1), // in offset - byte(vm.PUSH1), 0x4, // address of identity - byte(vm.GAS), // gas - - byte(vm.POP), byte(vm.POP), byte(vm.POP), byte(vm.POP), byte(vm.POP), byte(vm.POP), - byte(vm.PUSH1), 0, // jumpdestination - byte(vm.JUMP), - } - - loopingCode2 := []byte{ - byte(vm.JUMPDEST), // [ count ] - // push args for the call - byte(vm.PUSH4), 1, 2, 3, 4, - byte(vm.PUSH5), 1, 2, 3, 4, 5, - - byte(vm.POP), byte(vm.POP), - byte(vm.PUSH6), 0, 0, 0, 0, 0, 0, // jumpdestination - byte(vm.JUMP), - } - - callRevertingContractWithInput := []byte{ - byte(vm.JUMPDEST), // - // push args for the call - byte(vm.PUSH1), 0, // out size - byte(vm.DUP1), // out offset - byte(vm.PUSH1), 0x20, // in size - byte(vm.PUSH1), 0x00, // in offset - byte(vm.PUSH1), 0x00, // value - byte(vm.PUSH1), 0xEE, // address of reverting contract - byte(vm.GAS), // gas - byte(vm.CALL), - byte(vm.POP), // pop return value - byte(vm.PUSH1), 0, // jumpdestination - byte(vm.JUMP), - } + p, lbl := program.New().Jumpdest() + // Call identity, and pop return value + staticCallIdentity := p. + StaticCall(nil, 0x4, 0, 0, 0, 0). + Op(vm.POP). // pop return value + Jump(lbl). // jump to label + Bytecode() + + p, lbl = program.New().Jumpdest() + callIdentity := p. + Call(nil, 0x4, 0, 0, 0, 0, 0). + Op(vm.POP). // pop return value + Jump(lbl). // jump to label + Bytecode() + + p, lbl = program.New().Jumpdest() + callInexistant := p. + Call(nil, 0xff, 0, 0, 0, 0, 0). + Op(vm.POP). // pop return value + Jump(lbl). // jump to label + Bytecode() + + p, lbl = program.New().Jumpdest() + callEOA := p. + Call(nil, 0xE0, 0, 0, 0, 0, 0). // call addr of EOA + Op(vm.POP). // pop return value + Jump(lbl). // jump to label + Bytecode() + + p, lbl = program.New().Jumpdest() + // Push as if we were making call, then pop it off again, and loop + loopingCode := p.Push(0). + Ops(vm.DUP1, vm.DUP1, vm.DUP1). + Push(0x4). + Ops(vm.GAS, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP). + Jump(lbl).Bytecode() + + p, lbl = program.New().Jumpdest() + loopingCode2 := p. + Push(0x01020304).Push(0x0102030405). + Ops(vm.POP, vm.POP). + Op(vm.PUSH6).Append(make([]byte, 6)).Op(vm.JUMP). // Jumpdest zero expressed in 6 bytes + Bytecode() + + p, lbl = program.New().Jumpdest() + callRevertingContractWithInput := p. + Call(nil, 0xee, 0, 0, 0x20, 0x0, 0x0). + Op(vm.POP). + Jump(lbl). + Bytecode() //tracer := logger.NewJSONLogger(nil, os.Stdout) //Execute(loopingCode, nil, &Config{ From 726db0d3d0ffa07c4c34a910652cf9c5f5b596d6 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 15:03:39 +0100 Subject: [PATCH 5/9] core/vm/runtime: avoid overflow on 32-bit --- core/vm/runtime/runtime_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index cb259b66de40..f68cc3ea40a4 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -477,7 +477,7 @@ func BenchmarkSimpleLoop(b *testing.B) { p, lbl = program.New().Jumpdest() loopingCode2 := p. - Push(0x01020304).Push(0x0102030405). + Push(0x01020304).Push(uint64(0x0102030405)). Ops(vm.POP, vm.POP). Op(vm.PUSH6).Append(make([]byte, 6)).Op(vm.JUMP). // Jumpdest zero expressed in 6 bytes Bytecode() From 6d7e9b9b82a0cc4bf0dbb03dad03dc002ae1eb84 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 15:05:59 +0100 Subject: [PATCH 6/9] core/vm/runtime: minor cleanup --- core/vm/runtime/runtime_test.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index f68cc3ea40a4..dcdcb674d762 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -442,30 +442,22 @@ func BenchmarkSimpleLoop(b *testing.B) { // Call identity, and pop return value staticCallIdentity := p. StaticCall(nil, 0x4, 0, 0, 0, 0). - Op(vm.POP). // pop return value - Jump(lbl). // jump to label - Bytecode() + Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label p, lbl = program.New().Jumpdest() callIdentity := p. Call(nil, 0x4, 0, 0, 0, 0, 0). - Op(vm.POP). // pop return value - Jump(lbl). // jump to label - Bytecode() + Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label p, lbl = program.New().Jumpdest() callInexistant := p. Call(nil, 0xff, 0, 0, 0, 0, 0). - Op(vm.POP). // pop return value - Jump(lbl). // jump to label - Bytecode() + Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label p, lbl = program.New().Jumpdest() callEOA := p. Call(nil, 0xE0, 0, 0, 0, 0, 0). // call addr of EOA - Op(vm.POP). // pop return value - Jump(lbl). // jump to label - Bytecode() + Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label p, lbl = program.New().Jumpdest() // Push as if we were making call, then pop it off again, and loop @@ -485,9 +477,7 @@ func BenchmarkSimpleLoop(b *testing.B) { p, lbl = program.New().Jumpdest() callRevertingContractWithInput := p. Call(nil, 0xee, 0, 0, 0x20, 0x0, 0x0). - Op(vm.POP). - Jump(lbl). - Bytecode() + Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label //tracer := logger.NewJSONLogger(nil, os.Stdout) //Execute(loopingCode, nil, &Config{ From 55edee556590a1f4df96f1dad4c909f6d6c01ced Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 17:00:57 +0100 Subject: [PATCH 7/9] core/vm/program: minor UX tweaks --- core/vm/program/program.go | 29 ++++++- core/vm/program/program_test.go | 121 ++++++++++++++++++++++++++++- core/vm/runtime/runtime_test.go | 133 +++++++++----------------------- 3 files changed, 178 insertions(+), 105 deletions(-) diff --git a/core/vm/program/program.go b/core/vm/program/program.go index 19fb861332fa..e8d667fa9d98 100644 --- a/core/vm/program/program.go +++ b/core/vm/program/program.go @@ -127,14 +127,14 @@ func (p *Program) Push0() *Program { return p.Op(vm.PUSH0) } -// Bytecode returns the Program bytecode -func (p *Program) Bytecode() []byte { +// Bytes returns the Program bytecode +func (p *Program) Bytes() []byte { return p.code } // Hex returns the Program bytecode as a hex string func (p *Program) Hex() string { - return fmt.Sprintf("%02x", p.Bytecode()) + return fmt.Sprintf("%02x", p.Bytes()) } // ExtcodeCopy performsa an extcodecopy invocation @@ -280,6 +280,29 @@ func (p *Program) Mstore(data []byte, memStart uint32) *Program { return p } +// MstorePadded stores the provided data (into the memory area starting at memStart). +// If data does not align on 32 bytes, it will be LHS-padded. +// For example, providing data 0x1122, it will do a PUSH2: +// PUSH2 0x1122, resulting in +// stack: 0x0000000000000000000000000000000000000000000000000000000000001122 +// followed by MSTORE(0,0) +// And thus, the resulting memory will be +// [ 0000000000000000000000000000000000000000000000000000000000001122 ] +func (p *Program) MstorePadded(data []byte, memStart uint32) *Program { + var idx = 0 + // We need to store it in chunks of 32 bytes + for ; idx < len(data); idx += 32 { + end := min(len(data), idx+32) + chunk := data[idx:end] + // push the value + p.Push(chunk) + // push the memory index + p.Push(uint32(idx) + memStart) + p.Op(vm.MSTORE) + } + return p +} + // MemToStorage copies the given memory area into SSTORE slots, // It expects data to be aligned to 32 byte, and does not zero out // remainders if some data is not diff --git a/core/vm/program/program_test.go b/core/vm/program/program_test.go index 5291e034effd..352b59ce960f 100644 --- a/core/vm/program/program_test.go +++ b/core/vm/program/program_test.go @@ -143,7 +143,7 @@ func TestCreateAndCall(t *testing.T) { deployed.Return(0, 32) // Pack them - ctor.ReturnData(deployed.Bytecode()) + ctor.ReturnData(deployed.Bytes()) // Verify constructor + runtime code { want := "6005600055606060005360006001536054600253606060035360006004536052600553606060065360206007536060600853600060095360f3600a53600b6000f3" @@ -155,21 +155,134 @@ func TestCreateAndCall(t *testing.T) { func TestCreate2Call(t *testing.T) { // Some runtime code - runtime := New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytecode() + runtime := New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytes() want := common.FromHex("0x30ff") if !bytes.Equal(want, runtime) { t.Fatalf("runtime code error\nwant: %x\nhave: %x\n", want, runtime) } // A constructor returning the runtime code - initcode := New().ReturnData(runtime).Bytecode() + initcode := New().ReturnData(runtime).Bytes() want = common.FromHex("603060005360ff60015360026000f3") if !bytes.Equal(want, initcode) { t.Fatalf("initcode error\nwant: %x\nhave: %x\n", want, initcode) } // A factory invoking the constructor - outer := New().Create2AndCall(initcode, nil).Bytecode() + outer := New().Create2AndCall(initcode, nil).Bytes() want = common.FromHex("60606000536030600153606060025360006003536053600453606060055360ff6006536060600753600160085360536009536060600a536002600b536060600c536000600d5360f3600e536000600f60006000f560006000600060006000855af15050") if !bytes.Equal(want, outer) { t.Fatalf("factory error\nwant: %x\nhave: %x\n", want, outer) } } + +func TestGenerator(t *testing.T) { + for i, tc := range []struct { + want []byte + haveFn func() []byte + }{ + { // CREATE + want: []byte{ + // Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes) + byte(vm.PUSH5), + // Init code: PUSH1 0, PUSH1 0, RETURN (3 steps) + byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN), + byte(vm.PUSH1), 0, + byte(vm.MSTORE), + // length, offset, value + byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0, + byte(vm.CREATE), + byte(vm.POP), + }, + haveFn: func() []byte { + initcode := New().Return(0, 0).Bytes() + return New().MstorePadded(initcode, 0). + Push(len(initcode)). // length + Push(32 - len(initcode)). // offset + Push(0). // value + Op(vm.CREATE). + Op(vm.POP).Bytes() + }, + }, + { // CREATE2 + want: []byte{ + // Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes) + byte(vm.PUSH5), + // Init code: PUSH1 0, PUSH1 0, RETURN (3 steps) + byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN), + byte(vm.PUSH1), 0, + byte(vm.MSTORE), + // salt, length, offset, value + byte(vm.PUSH1), 1, byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0, + byte(vm.CREATE2), + byte(vm.POP), + }, + haveFn: func() []byte { + initcode := New().Return(0, 0).Bytes() + return New().MstorePadded(initcode, 0). + Push(1). // salt + Push(len(initcode)). // length + Push(32 - len(initcode)). // offset + Push(0). // value + Op(vm.CREATE2). + Op(vm.POP).Bytes() + }, + }, + { // CALL + want: []byte{ + // outsize, outoffset, insize, inoffset + byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), + byte(vm.DUP1), // value + byte(vm.PUSH1), 0xbb, //address + byte(vm.GAS), // gas + byte(vm.CALL), + byte(vm.POP), + }, + haveFn: func() []byte { + return New().Call(nil, 0xbb, 0, 0, 0, 0, 0).Op(vm.POP).Bytes() + }, + }, + { // CALLCODE + want: []byte{ + // outsize, outoffset, insize, inoffset + byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), + byte(vm.PUSH1), 0, // value + byte(vm.PUSH1), 0xcc, //address + byte(vm.GAS), // gas + byte(vm.CALLCODE), + byte(vm.POP), + }, + haveFn: func() []byte { + return New().CallCode(nil, 0xcc, 0, 0, 0, 0, 0).Op(vm.POP).Bytes() + }, + }, + { // STATICCALL + want: []byte{ + // outsize, outoffset, insize, inoffset + byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), + byte(vm.PUSH1), 0xdd, //address + byte(vm.GAS), // gas + byte(vm.STATICCALL), + byte(vm.POP), + }, + haveFn: func() []byte { + return New().StaticCall(nil, 0xdd, 0, 0, 0, 0).Op(vm.POP).Bytes() + }, + }, + { // DELEGATECALL + want: []byte{ + // outsize, outoffset, insize, inoffset + byte(vm.PUSH1), 0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), + byte(vm.PUSH1), 0xee, //address + byte(vm.GAS), // gas + byte(vm.DELEGATECALL), + byte(vm.POP), + }, + haveFn: func() []byte { + return New().DelegateCall(nil, 0xee, 0, 0, 0, 0).Op(vm.POP).Bytes() + }, + }, + } { + if have := tc.haveFn(); !bytes.Equal(have, tc.want) { + t.Fatalf("test %d error\nhave: %x\nwant: %x\n", i, have, tc.want) + } + } +} diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index dcdcb674d762..c8064e85abbc 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -442,22 +442,22 @@ func BenchmarkSimpleLoop(b *testing.B) { // Call identity, and pop return value staticCallIdentity := p. StaticCall(nil, 0x4, 0, 0, 0, 0). - Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label + Op(vm.POP).Jump(lbl).Bytes() // pop return value and jump to label p, lbl = program.New().Jumpdest() callIdentity := p. Call(nil, 0x4, 0, 0, 0, 0, 0). - Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label + Op(vm.POP).Jump(lbl).Bytes() // pop return value and jump to label p, lbl = program.New().Jumpdest() callInexistant := p. Call(nil, 0xff, 0, 0, 0, 0, 0). - Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label + Op(vm.POP).Jump(lbl).Bytes() // pop return value and jump to label p, lbl = program.New().Jumpdest() callEOA := p. Call(nil, 0xE0, 0, 0, 0, 0, 0). // call addr of EOA - Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label + Op(vm.POP).Jump(lbl).Bytes() // pop return value and jump to label p, lbl = program.New().Jumpdest() // Push as if we were making call, then pop it off again, and loop @@ -465,19 +465,19 @@ func BenchmarkSimpleLoop(b *testing.B) { Ops(vm.DUP1, vm.DUP1, vm.DUP1). Push(0x4). Ops(vm.GAS, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP). - Jump(lbl).Bytecode() + Jump(lbl).Bytes() p, lbl = program.New().Jumpdest() loopingCode2 := p. Push(0x01020304).Push(uint64(0x0102030405)). Ops(vm.POP, vm.POP). Op(vm.PUSH6).Append(make([]byte, 6)).Op(vm.JUMP). // Jumpdest zero expressed in 6 bytes - Bytecode() + Bytes() p, lbl = program.New().Jumpdest() callRevertingContractWithInput := p. Call(nil, 0xee, 0, 0, 0x20, 0x0, 0x0). - Op(vm.POP).Jump(lbl).Bytecode() // pop return value and jump to label + Op(vm.POP).Jump(lbl).Bytes() // pop return value and jump to label //tracer := logger.NewJSONLogger(nil, os.Stdout) //Execute(loopingCode, nil, &Config{ @@ -716,104 +716,49 @@ func TestRuntimeJSTracer(t *testing.T) { this.exits++; this.gasUsed = res.getGasUsed(); }}`} + initcode := program.New().Return(0, 0).Bytes() tests := []struct { code []byte // One result per tracer results []string }{ - { - // CREATE - code: []byte{ - // Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes) - byte(vm.PUSH5), - // Init code: PUSH1 0, PUSH1 0, RETURN (3 steps) - byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN), - byte(vm.PUSH1), 0, - byte(vm.MSTORE), - // length, offset, value - byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0, - byte(vm.CREATE), - byte(vm.POP), - }, + { // CREATE + code: program.New().MstorePadded(initcode, 0). + Push(len(initcode)). // length + Push(32 - len(initcode)). // offset + Push(0). // value + Op(vm.CREATE). + Op(vm.POP).Bytes(), results: []string{`"1,1,952853,6,12"`, `"1,1,952853,6,0"`}, }, - { - // CREATE2 - code: []byte{ - // Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes) - byte(vm.PUSH5), - // Init code: PUSH1 0, PUSH1 0, RETURN (3 steps) - byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN), - byte(vm.PUSH1), 0, - byte(vm.MSTORE), - // salt, length, offset, value - byte(vm.PUSH1), 1, byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0, - byte(vm.CREATE2), - byte(vm.POP), - }, + { // CREATE2 + code: program.New().MstorePadded(initcode, 0). + Push(1). // salt + Push(len(initcode)). // length + Push(32 - len(initcode)). // offset + Push(0). // value + Op(vm.CREATE2). + Op(vm.POP).Bytes(), results: []string{`"1,1,952844,6,13"`, `"1,1,952844,6,0"`}, }, - { - // CALL - code: []byte{ - // outsize, outoffset, insize, inoffset - byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, - byte(vm.PUSH1), 0, // value - byte(vm.PUSH1), 0xbb, //address - byte(vm.GAS), // gas - byte(vm.CALL), - byte(vm.POP), - }, + { // CALL + code: program.New().Call(nil, 0xbb, 0, 0, 0, 0, 0).Op(vm.POP).Bytes(), results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0"`}, }, - { - // CALLCODE - code: []byte{ - // outsize, outoffset, insize, inoffset - byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, - byte(vm.PUSH1), 0, // value - byte(vm.PUSH1), 0xcc, //address - byte(vm.GAS), // gas - byte(vm.CALLCODE), - byte(vm.POP), - }, + { // CALLCODE + code: program.New().CallCode(nil, 0xcc, 0, 0, 0, 0, 0).Op(vm.POP).Bytes(), results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0"`}, }, - { - // STATICCALL - code: []byte{ - // outsize, outoffset, insize, inoffset - byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, - byte(vm.PUSH1), 0xdd, //address - byte(vm.GAS), // gas - byte(vm.STATICCALL), - byte(vm.POP), - }, + { // STATICCALL + code: program.New().StaticCall(nil, 0xdd, 0, 0, 0, 0).Op(vm.POP).Bytes(), results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0"`}, }, - { - // DELEGATECALL - code: []byte{ - // outsize, outoffset, insize, inoffset - byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, - byte(vm.PUSH1), 0xee, //address - byte(vm.GAS), // gas - byte(vm.DELEGATECALL), - byte(vm.POP), - }, + { // DELEGATECALL + code: program.New().DelegateCall(nil, 0xee, 0, 0, 0, 0).Op(vm.POP).Bytes(), results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0"`}, }, - { - // CALL self-destructing contract - code: []byte{ - // outsize, outoffset, insize, inoffset - byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, - byte(vm.PUSH1), 0, // value - byte(vm.PUSH1), 0xff, //address - byte(vm.GAS), // gas - byte(vm.CALL), - byte(vm.POP), - }, + { // CALL self-destructing contract + code: program.New().Call(nil, 0xff, 0, 0, 0, 0, 0).Op(vm.POP).Bytes(), results: []string{`"2,2,0,5003,12"`, `"2,2,0,5003,0"`}, }, } @@ -896,16 +841,8 @@ func TestJSTracerCreateTx(t *testing.T) { func BenchmarkTracerStepVsCallFrame(b *testing.B) { // Simply pushes and pops some values in a loop - code := []byte{ - byte(vm.JUMPDEST), - byte(vm.PUSH1), 0, - byte(vm.PUSH1), 0, - byte(vm.POP), - byte(vm.POP), - byte(vm.PUSH1), 0, // jumpdestination - byte(vm.JUMP), - } - + p, lbl := program.New().Jumpdest() + code := p.Push(0).Push(0).Ops(vm.POP, vm.POP).Jump(lbl).Bytes() stepTracer := ` { step: function() {}, From a103de4eff5e86aa7572c6cfb7335ebad9019265 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Tue, 5 Nov 2024 16:59:42 +0100 Subject: [PATCH 8/9] core/vm/program: add another constructor helper --- core/vm/program/program.go | 57 ++++++++++++++++++++++++--------- core/vm/program/program_test.go | 30 +++++++++++++---- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/core/vm/program/program.go b/core/vm/program/program.go index e8d667fa9d98..dd6b81dde774 100644 --- a/core/vm/program/program.go +++ b/core/vm/program/program.go @@ -280,26 +280,29 @@ func (p *Program) Mstore(data []byte, memStart uint32) *Program { return p } -// MstorePadded stores the provided data (into the memory area starting at memStart). -// If data does not align on 32 bytes, it will be LHS-padded. +// MstoreSmall stores the provided data, which must be smaller than 32 bytes, +// into the memory area starting at memStart. +// The data will be LHS zero-added to align on 32 bytes. // For example, providing data 0x1122, it will do a PUSH2: // PUSH2 0x1122, resulting in // stack: 0x0000000000000000000000000000000000000000000000000000000000001122 // followed by MSTORE(0,0) // And thus, the resulting memory will be // [ 0000000000000000000000000000000000000000000000000000000000001122 ] -func (p *Program) MstorePadded(data []byte, memStart uint32) *Program { - var idx = 0 - // We need to store it in chunks of 32 bytes - for ; idx < len(data); idx += 32 { - end := min(len(data), idx+32) - chunk := data[idx:end] - // push the value - p.Push(chunk) - // push the memory index - p.Push(uint32(idx) + memStart) - p.Op(vm.MSTORE) +func (p *Program) MstoreSmall(data []byte, memStart uint32) *Program { + if len(data) > 32 { + // For larger sizes, use Mstore instead. + panic(fmt.Sprintf("only <=32 byte data size supported")) + } + if len(data) == 0 { + // Storing 0-length data smells of an error somewhere. + panic(fmt.Sprintf("data is zero length")) } + // push the value + p.Push(data) + // push the memory index + p.Push(memStart) + p.Op(vm.MSTORE) return p } @@ -322,6 +325,30 @@ func (p *Program) MemToStorage(memStart, memSize, startSlot int) *Program { return p } +// ReturnViaCodeCopy utilises CODECOPY to place the given data in the bytecode of +// p, loads into memory (offset 0) and returns the code. +// This is a typical "constructor". +// Note: since all indexing is calculated immediately, the preceding bytecode +// must not be expanded or shortened. +func (p *Program) ReturnViaCodeCopy(data []byte) *Program { + p.Push(len(data)) + // For convenience, we'll use PUSH2 for the offset. Then we know we can always + // fit, since code is limited to 0xc000 + p.Op(vm.PUSH2) + offsetPos := p.Size() // Need to update this position later on + p.Append([]byte{0, 0}) // Offset of the code to be copied + p.Push(0) // Offset in memory (destination) + p.Op(vm.CODECOPY) // Copy from code[offset:offset+len] to memory[0:] + p.Return(0, len(data)) // Return memory[0:len] + offset := p.Size() + p.Append(data) // And add the data + + // Now, go back and fix the offset + p.code[offsetPos] = byte(offset >> 8) + p.code[offsetPos+1] = byte(offset) + return p +} + // Sstore stores the given byte array to the given slot. // OBS! Does not verify that the value indeed fits into 32 bytes // If it does not, it will panic later on via pushBig @@ -340,7 +367,7 @@ func (p *Program) Tstore(slot any, value any) *Program { return p.Op(vm.TSTORE) } -func (p *Program) Return(offset, len uint32) *Program { +func (p *Program) Return(offset, len int) *Program { p.Push(len) p.Push(offset) return p.Op(vm.RETURN) @@ -349,7 +376,7 @@ func (p *Program) Return(offset, len uint32) *Program { // ReturnData loads the given data into memory, and does a return with it func (p *Program) ReturnData(data []byte) *Program { p.Mstore(data, 0) - return p.Return(0, uint32(len(data))) + return p.Return(0, len(data)) } // Create2 uses create2 to construct a contract with the given bytecode. diff --git a/core/vm/program/program_test.go b/core/vm/program/program_test.go index 352b59ce960f..44350a66bf87 100644 --- a/core/vm/program/program_test.go +++ b/core/vm/program/program_test.go @@ -129,6 +129,22 @@ func TestReturnData(t *testing.T) { t.Errorf("have %v want %v", have, want) } } + { // ReturnViaCodeCopy + data := common.FromHex("0x6001") + have := New().Append([]byte{0x5b, 0x5b, 0x5b}).ReturnViaCodeCopy(data).Hex() + want := "5b5b5b600261001060003960026000f36001" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } + { // ReturnViaCodeCopy larger code + data := common.FromHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260206000f3") + have := New().Append([]byte{0x5b, 0x5b, 0x5b}).ReturnViaCodeCopy(data).Hex() + want := "5b5b5b602961001060003960296000f37fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260206000f3" + if have != want { + t.Errorf("have %v want %v", have, want) + } + } } func TestCreateAndCall(t *testing.T) { @@ -194,10 +210,10 @@ func TestGenerator(t *testing.T) { }, haveFn: func() []byte { initcode := New().Return(0, 0).Bytes() - return New().MstorePadded(initcode, 0). - Push(len(initcode)). // length + return New().MstoreSmall(initcode, 0). + Push(len(initcode)). // length Push(32 - len(initcode)). // offset - Push(0). // value + Push(0). // value Op(vm.CREATE). Op(vm.POP).Bytes() }, @@ -217,11 +233,11 @@ func TestGenerator(t *testing.T) { }, haveFn: func() []byte { initcode := New().Return(0, 0).Bytes() - return New().MstorePadded(initcode, 0). - Push(1). // salt - Push(len(initcode)). // length + return New().MstoreSmall(initcode, 0). + Push(1). // salt + Push(len(initcode)). // length Push(32 - len(initcode)). // offset - Push(0). // value + Push(0). // value Op(vm.CREATE2). Op(vm.POP).Bytes() }, From 3a3e7fd7aaac4dd3e98d457f996dec1b3402f998 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Tue, 5 Nov 2024 18:12:02 +0100 Subject: [PATCH 9/9] core/vm/program: review concerns from @lightclient --- core/vm/program/program.go | 147 +++++++++++++++++--------------- core/vm/program/program_test.go | 25 ++++-- core/vm/runtime/runtime_test.go | 27 +++--- 3 files changed, 106 insertions(+), 93 deletions(-) diff --git a/core/vm/program/program.go b/core/vm/program/program.go index dd6b81dde774..acc7fd25fc94 100644 --- a/core/vm/program/program.go +++ b/core/vm/program/program.go @@ -39,6 +39,7 @@ type Program struct { code []byte } +// New creates a new Program func New() *Program { return &Program{ code: make([]byte, 0), @@ -54,21 +55,17 @@ func (p *Program) add(op byte) *Program { // pushBig creates a PUSHX instruction and pushes the given val. // - If the val is nil, it pushes zero // - If the val is bigger than 32 bytes, it panics -func (p *Program) pushBig(val *big.Int) { +func (p *Program) doPush(val *uint256.Int) { if val == nil { - val = big.NewInt(0) + val = new(uint256.Int) } valBytes := val.Bytes() if len(valBytes) == 0 { valBytes = append(valBytes, 0) } bLen := len(valBytes) - if bLen > 32 { - panic(fmt.Sprintf("Push value too large, %d bytes", bLen)) - } p.add(byte(vm.PUSH1) - 1 + byte(bLen)) p.Append(valBytes) - } // Append appends the given data to the code. @@ -77,67 +74,77 @@ func (p *Program) Append(data []byte) *Program { return p } -// Op appends the given opcode -func (p *Program) Op(op vm.OpCode) *Program { - return p.add(byte(op)) +// Bytes returns the Program bytecode. OBS: This is not a copy. +func (p *Program) Bytes() []byte { + return p.code +} + +// SetBytes sets the Program bytecode. The combination of Bytes and SetBytes means +// that external callers can implement missing functionality: +// +// ... +// prog.Push(1) +// code := prog.Bytes() +// manipulate(code) +// prog.SetBytes(code) +func (p *Program) SetBytes(code []byte) { + p.code = code +} + +// Hex returns the Program bytecode as a hex string. +func (p *Program) Hex() string { + return fmt.Sprintf("%02x", p.Bytes()) } -// Ops appends the given opcodes -func (p *Program) Ops(ops ...vm.OpCode) *Program { +// Op appends the given opcode(s). +func (p *Program) Op(ops ...vm.OpCode) *Program { for _, op := range ops { p.add(byte(op)) } return p } -// Push creates a PUSHX instruction with the data provided +// Push creates a PUSHX instruction with the data provided. If zero is being pushed, +// PUSH0 will be avoided in favour of [PUSH1 0], to ensure backwards compatibility. func (p *Program) Push(val any) *Program { switch v := val.(type) { case int: - p.pushBig(new(big.Int).SetUint64(uint64(v))) + p.doPush(new(uint256.Int).SetUint64(uint64(v))) case uint64: - p.pushBig(new(big.Int).SetUint64(v)) + p.doPush(new(uint256.Int).SetUint64(v)) case uint32: - p.pushBig(new(big.Int).SetUint64(uint64(v))) + p.doPush(new(uint256.Int).SetUint64(uint64(v))) + case uint16: + p.doPush(new(uint256.Int).SetUint64(uint64(v))) case *big.Int: - p.pushBig(v) + p.doPush(uint256.MustFromBig(v)) case *uint256.Int: - p.pushBig(v.ToBig()) + p.doPush(v) case uint256.Int: - p.pushBig(v.ToBig()) + p.doPush(&v) case []byte: - p.pushBig(new(big.Int).SetBytes(v)) + p.doPush(new(uint256.Int).SetBytes(v)) case byte: - p.pushBig(new(big.Int).SetUint64(uint64(v))) + p.doPush(new(uint256.Int).SetUint64(uint64(v))) case interface{ Bytes() []byte }: - // Here, we jump through some hovm in order to avoid depending on + // Here, we jump through some hoops in order to avoid depending on // go-ethereum types.Address and common.Hash, and instead use the // interface. This works on both values and pointers! - p.pushBig(new(big.Int).SetBytes(v.Bytes())) + p.doPush(new(uint256.Int).SetBytes(v.Bytes())) case nil: - p.pushBig(nil) + p.doPush(nil) default: - panic(fmt.Sprintf("unsupported type %v", v)) + panic(fmt.Sprintf("unsupported type %T", v)) } return p } -// Push0 implements PUSH0 (0x5f) +// Push0 implements PUSH0 (0x5f). func (p *Program) Push0() *Program { return p.Op(vm.PUSH0) } -// Bytes returns the Program bytecode -func (p *Program) Bytes() []byte { - return p.code -} - -// Hex returns the Program bytecode as a hex string -func (p *Program) Hex() string { - return fmt.Sprintf("%02x", p.Bytes()) -} - -// ExtcodeCopy performsa an extcodecopy invocation +// ExtcodeCopy performs an extcodecopy invocation. func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) *Program { p.Push(length) p.Push(codeOffset) @@ -148,9 +155,9 @@ func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) *Progr // Call is a convenience function to make a call. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { +func (p *Program) Call(gas *uint256.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { if outOffset == outSize && inSize == outSize && inOffset == outSize && value == outSize { - p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1, vm.DUP1) + p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1, vm.DUP1) } else { p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset).Push(value) } @@ -158,16 +165,16 @@ func (p *Program) Call(gas *big.Int, address, value, inOffset, inSize, outOffset if gas == nil { p.Op(vm.GAS) } else { - p.pushBig(gas) + p.doPush(gas) } return p.Op(vm.CALL) } // DelegateCall is a convenience function to make a delegatecall. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) *Program { +func (p *Program) DelegateCall(gas *uint256.Int, address, inOffset, inSize, outOffset, outSize any) *Program { if outOffset == outSize && inSize == outSize && inOffset == outSize { - p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1) + p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1) } else { p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset) } @@ -175,16 +182,16 @@ func (p *Program) DelegateCall(gas *big.Int, address, inOffset, inSize, outOffse if gas == nil { p.Op(vm.GAS) } else { - p.pushBig(gas) + p.doPush(gas) } return p.Op(vm.DELEGATECALL) } // StaticCall is a convenience function to make a staticcall. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, outSize any) *Program { +func (p *Program) StaticCall(gas *uint256.Int, address, inOffset, inSize, outOffset, outSize any) *Program { if outOffset == outSize && inSize == outSize && inOffset == outSize { - p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1) + p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1) } else { p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset) } @@ -192,16 +199,16 @@ func (p *Program) StaticCall(gas *big.Int, address, inOffset, inSize, outOffset, if gas == nil { p.Op(vm.GAS) } else { - p.pushBig(gas) + p.doPush(gas) } return p.Op(vm.STATICCALL) } // StaticCall is a convenience function to make a callcode. If 'gas' is nil, the opcode GAS will // be used to provide all gas. -func (p *Program) CallCode(gas *big.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { +func (p *Program) CallCode(gas *uint256.Int, address, value, inOffset, inSize, outOffset, outSize any) *Program { if outOffset == outSize && inSize == outSize && inOffset == outSize { - p.Push(outSize).Ops(vm.DUP1, vm.DUP1, vm.DUP1) + p.Push(outSize).Op(vm.DUP1, vm.DUP1, vm.DUP1) } else { p.Push(outSize).Push(outOffset).Push(inSize).Push(inOffset) } @@ -210,54 +217,53 @@ func (p *Program) CallCode(gas *big.Int, address, value, inOffset, inSize, outOf if gas == nil { p.Op(vm.GAS) } else { - p.pushBig(gas) + p.doPush(gas) } return p.Op(vm.CALLCODE) } -// Label returns the PC (of the next instruction) +// Label returns the PC (of the next instruction). func (p *Program) Label() uint64 { return uint64(len(p.code)) } -// Jumpdest adds a JUMPDEST op, and returns the PC of that instruction +// Jumpdest adds a JUMPDEST op, and returns the PC of that instruction. func (p *Program) Jumpdest() (*Program, uint64) { here := p.Label() p.Op(vm.JUMPDEST) return p, here } -// Jump pushes the destination and adds a JUMP +// Jump pushes the destination and adds a JUMP. func (p *Program) Jump(loc any) *Program { p.Push(loc) p.Op(vm.JUMP) return p } -// Jump pushes the destination and adds a JUMP -func (p *Program) JumpIf(loc any, condition any) { +// JumpIf implements JUMPI. +func (p *Program) JumpIf(loc any, condition any) *Program { p.Push(condition) p.Push(loc) p.Op(vm.JUMPI) + return p } +// Size returns the current size of the bytecode. func (p *Program) Size() int { return len(p.code) } -// InputToMemory stores the input (calldata) to memory as address (20 bytes). +// InputAddressToStack stores the input (calldata) to memory as address (20 bytes). func (p *Program) InputAddressToStack(inputOffset uint32) *Program { p.Push(inputOffset) p.Op(vm.CALLDATALOAD) // Loads [n -> n + 32] of input data to stack top - mask, ok := big.NewInt(0).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16) - if !ok { - panic("whoa") - } + mask, _ := big.NewInt(0).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16) p.Push(mask) // turn into address return p.Op(vm.AND) } -// MStore stores the provided data (into the memory area starting at memStart) +// MStore stores the provided data (into the memory area starting at memStart). func (p *Program) Mstore(data []byte, memStart uint32) *Program { var idx = 0 // We need to store it in chunks of 32 bytes @@ -292,11 +298,11 @@ func (p *Program) Mstore(data []byte, memStart uint32) *Program { func (p *Program) MstoreSmall(data []byte, memStart uint32) *Program { if len(data) > 32 { // For larger sizes, use Mstore instead. - panic(fmt.Sprintf("only <=32 byte data size supported")) + panic("only <=32 byte data size supported") } if len(data) == 0 { // Storing 0-length data smells of an error somewhere. - panic(fmt.Sprintf("data is zero length")) + panic("data is zero length") } // push the value p.Push(data) @@ -309,7 +315,7 @@ func (p *Program) MstoreSmall(data []byte, memStart uint32) *Program { // MemToStorage copies the given memory area into SSTORE slots, // It expects data to be aligned to 32 byte, and does not zero out // remainders if some data is not -// I.e, if given a 1-byte area, it will still copy the full 32 bytes to storage +// I.e, if given a 1-byte area, it will still copy the full 32 bytes to storage. func (p *Program) MemToStorage(memStart, memSize, startSlot int) *Program { // We need to store it in chunks of 32 bytes for idx := memStart; idx < (memStart + memSize); idx += 32 { @@ -350,8 +356,8 @@ func (p *Program) ReturnViaCodeCopy(data []byte) *Program { } // Sstore stores the given byte array to the given slot. -// OBS! Does not verify that the value indeed fits into 32 bytes -// If it does not, it will panic later on via pushBig +// OBS! Does not verify that the value indeed fits into 32 bytes. +// If it does not, it will panic later on via doPush. func (p *Program) Sstore(slot any, value any) *Program { p.Push(value) p.Push(slot) @@ -359,14 +365,15 @@ func (p *Program) Sstore(slot any, value any) *Program { } // Tstore stores the given byte array to the given t-slot. -// OBS! Does not verify that the value indeed fits into 32 bytes -// If it does not, it will panic later on via pushBig +// OBS! Does not verify that the value indeed fits into 32 bytes. +// If it does not, it will panic later on via doPush. func (p *Program) Tstore(slot any, value any) *Program { p.Push(value) p.Push(slot) return p.Op(vm.TSTORE) } +// Return implements RETURN func (p *Program) Return(offset, len int) *Program { p.Push(len) p.Push(offset) @@ -396,13 +403,13 @@ func (p *Program) Create2(code []byte, salt any) *Program { Push(value). Op(vm.CREATE2) // On the stack now, is either - // zero: in case of failure - // address: in case of success + // - zero: in case of failure, OR + // - address: in case of success } -// Create2AndCall calls create2 with the given initcode and salt, and then calls +// Create2ThenCall calls create2 with the given initcode and salt, and then calls // into the created contract (or calls into zero, if the creation failed). -func (p *Program) Create2AndCall(code []byte, salt any) *Program { +func (p *Program) Create2ThenCall(code []byte, salt any) *Program { p.Create2(code, salt) // If there happen to be a zero on the stack, it doesn't matter, we're // not sending any value anyway diff --git a/core/vm/program/program_test.go b/core/vm/program/program_test.go index 44350a66bf87..0b34210067ac 100644 --- a/core/vm/program/program_test.go +++ b/core/vm/program/program_test.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + "github.com/holiman/uint256" ) func TestPush(t *testing.T) { @@ -32,13 +33,19 @@ func TestPush(t *testing.T) { }{ // native ints {0, "6000"}, + {0xfff, "610fff"}, {nil, "6000"}, + {uint8(1), "6001"}, + {uint16(1), "6001"}, + {uint32(1), "6001"}, {uint64(1), "6001"}, - {0xfff, "610fff"}, // bigints {big.NewInt(0), "6000"}, {big.NewInt(1), "6001"}, {big.NewInt(0xfff), "610fff"}, + // uint256 + {uint256.NewInt(1), "6001"}, + {uint256.Int{1, 0, 0, 0}, "6001"}, // Addresses {common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), "73deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, {&common.Address{}, "6000"}, @@ -60,7 +67,7 @@ func TestCall(t *testing.T) { } } { // Non nil gas - have := New().Call(big.NewInt(0xffff), common.HexToAddress("0x1337"), big.NewInt(1), 1, 2, 3, 4).Hex() + have := New().Call(uint256.NewInt(0xffff), common.HexToAddress("0x1337"), big.NewInt(1), 1, 2, 3, 4).Hex() want := "6004600360026001600161133761fffff1" if have != want { t.Errorf("have %v want %v", have, want) @@ -171,7 +178,7 @@ func TestCreateAndCall(t *testing.T) { func TestCreate2Call(t *testing.T) { // Some runtime code - runtime := New().Ops(vm.ADDRESS, vm.SELFDESTRUCT).Bytes() + runtime := New().Op(vm.ADDRESS, vm.SELFDESTRUCT).Bytes() want := common.FromHex("0x30ff") if !bytes.Equal(want, runtime) { t.Fatalf("runtime code error\nwant: %x\nhave: %x\n", want, runtime) @@ -183,7 +190,7 @@ func TestCreate2Call(t *testing.T) { t.Fatalf("initcode error\nwant: %x\nhave: %x\n", want, initcode) } // A factory invoking the constructor - outer := New().Create2AndCall(initcode, nil).Bytes() + outer := New().Create2ThenCall(initcode, nil).Bytes() want = common.FromHex("60606000536030600153606060025360006003536053600453606060055360ff6006536060600753600160085360536009536060600a536002600b536060600c536000600d5360f3600e536000600f60006000f560006000600060006000855af15050") if !bytes.Equal(want, outer) { t.Fatalf("factory error\nwant: %x\nhave: %x\n", want, outer) @@ -211,9 +218,9 @@ func TestGenerator(t *testing.T) { haveFn: func() []byte { initcode := New().Return(0, 0).Bytes() return New().MstoreSmall(initcode, 0). - Push(len(initcode)). // length + Push(len(initcode)). // length Push(32 - len(initcode)). // offset - Push(0). // value + Push(0). // value Op(vm.CREATE). Op(vm.POP).Bytes() }, @@ -234,10 +241,10 @@ func TestGenerator(t *testing.T) { haveFn: func() []byte { initcode := New().Return(0, 0).Bytes() return New().MstoreSmall(initcode, 0). - Push(1). // salt - Push(len(initcode)). // length + Push(1). // salt + Push(len(initcode)). // length Push(32 - len(initcode)). // offset - Push(0). // value + Push(0). // value Op(vm.CREATE2). Op(vm.POP).Bytes() }, diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index c8064e85abbc..7d1345a57b4e 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -437,7 +437,6 @@ func benchmarkNonModifyingCode(gas uint64, code []byte, name string, tracerCode // BenchmarkSimpleLoop test a pretty simple loop which loops until OOG // 55 ms func BenchmarkSimpleLoop(b *testing.B) { - p, lbl := program.New().Jumpdest() // Call identity, and pop return value staticCallIdentity := p. @@ -457,22 +456,22 @@ func BenchmarkSimpleLoop(b *testing.B) { p, lbl = program.New().Jumpdest() callEOA := p. Call(nil, 0xE0, 0, 0, 0, 0, 0). // call addr of EOA - Op(vm.POP).Jump(lbl).Bytes() // pop return value and jump to label + Op(vm.POP).Jump(lbl).Bytes() // pop return value and jump to label p, lbl = program.New().Jumpdest() // Push as if we were making call, then pop it off again, and loop loopingCode := p.Push(0). - Ops(vm.DUP1, vm.DUP1, vm.DUP1). + Op(vm.DUP1, vm.DUP1, vm.DUP1). Push(0x4). - Ops(vm.GAS, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP). + Op(vm.GAS, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP, vm.POP). Jump(lbl).Bytes() p, lbl = program.New().Jumpdest() loopingCode2 := p. Push(0x01020304).Push(uint64(0x0102030405)). - Ops(vm.POP, vm.POP). + Op(vm.POP, vm.POP). Op(vm.PUSH6).Append(make([]byte, 6)).Op(vm.JUMP). // Jumpdest zero expressed in 6 bytes - Bytes() + Jump(lbl).Bytes() p, lbl = program.New().Jumpdest() callRevertingContractWithInput := p. @@ -723,20 +722,20 @@ func TestRuntimeJSTracer(t *testing.T) { results []string }{ { // CREATE - code: program.New().MstorePadded(initcode, 0). - Push(len(initcode)). // length + code: program.New().MstoreSmall(initcode, 0). + Push(len(initcode)). // length Push(32 - len(initcode)). // offset - Push(0). // value + Push(0). // value Op(vm.CREATE). Op(vm.POP).Bytes(), results: []string{`"1,1,952853,6,12"`, `"1,1,952853,6,0"`}, }, { // CREATE2 - code: program.New().MstorePadded(initcode, 0). - Push(1). // salt - Push(len(initcode)). // length + code: program.New().MstoreSmall(initcode, 0). + Push(1). // salt + Push(len(initcode)). // length Push(32 - len(initcode)). // offset - Push(0). // value + Push(0). // value Op(vm.CREATE2). Op(vm.POP).Bytes(), results: []string{`"1,1,952844,6,13"`, `"1,1,952844,6,0"`}, @@ -842,7 +841,7 @@ func TestJSTracerCreateTx(t *testing.T) { func BenchmarkTracerStepVsCallFrame(b *testing.B) { // Simply pushes and pops some values in a loop p, lbl := program.New().Jumpdest() - code := p.Push(0).Push(0).Ops(vm.POP, vm.POP).Jump(lbl).Bytes() + code := p.Push(0).Push(0).Op(vm.POP, vm.POP).Jump(lbl).Bytes() stepTracer := ` { step: function() {},