diff --git a/core/vm/program/program.go b/core/vm/program/program.go index dd6b81dde7..acc7fd25fc 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 44350a66bf..0b34210067 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 c8064e85ab..7d1345a57b 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() {},