-
Notifications
You must be signed in to change notification settings - Fork 21.6k
core/vm/program: create evm programming utility #30725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2a21fa2
core/vm/program: create evm programming utility
holiman b3afc7e
core/vm/program: add warning
holiman 659cd13
core/vm/program: api polishes and more tests
holiman 32b1831
core/vm/runtime: utilize the programs a bit
holiman 726db0d
core/vm/runtime: avoid overflow on 32-bit
holiman 6d7e9b9
core/vm/runtime: minor cleanup
holiman 55edee5
core/vm/program: minor UX tweaks
holiman a103de4
core/vm/program: add another constructor helper
holiman 3a3e7fd
core/vm/program: review concerns from @lightclient
holiman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,396 @@ | ||
| // 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 <http://www.gnu.org/licenses/>. | ||
|
|
||
| // 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 ( | ||
| "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)) | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| p.add(byte(vm.PUSH1) - 1 + byte(bLen)) | ||
| p.Append(valBytes) | ||
|
|
||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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 | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // 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) | ||
| } | ||
|
|
||
| // 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 | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func (p *Program) ExtcodeCopy(address, memOffset, codeOffset, length any) *Program { | ||
| p.Push(length) | ||
| p.Push(codeOffset) | ||
| p.Push(memOffset) | ||
| p.Push(address) | ||
| 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) *Program { | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
| } else { | ||
| p.pushBig(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 { | ||
| 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) | ||
| } else { | ||
| p.pushBig(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 { | ||
| 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) | ||
| } else { | ||
| p.pushBig(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 { | ||
| 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 { | ||
| p.Op(vm.GAS) | ||
| } else { | ||
| p.pushBig(gas) | ||
| } | ||
| return 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() (*Program, uint64) { | ||
| here := p.Label() | ||
| p.Op(vm.JUMPDEST) | ||
| return p, here | ||
| } | ||
|
|
||
| // 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) { | ||
| p.Push(condition) | ||
| p.Push(loc) | ||
| p.Op(vm.JUMPI) | ||
| } | ||
|
|
||
| func (p *Program) Size() int { | ||
holiman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return len(p.code) | ||
| } | ||
|
|
||
| // InputToMemory stores the input (calldata) to memory as address (20 bytes). | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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") | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| p.Push(mask) // turn into address | ||
| return p.Op(vm.AND) | ||
| } | ||
|
|
||
| // MStore stores the provided data (into the memory area starting at memStart) | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
|
|
||
| // 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 | ||
| // 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 { | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 { | ||
holiman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.