From 2a21fa2c3b275f50ae461fc95ed5a5f77837e7d2 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 4 Nov 2024 11:25:47 +0100 Subject: [PATCH] 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 0000000000..e5b743dc5a --- /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 0000000000..3050c83f77 --- /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) + } +}