Documentation
¶
Overview ¶
Package specialops implements a DSL for crafting raw EVM bytecode. It provides "special" opcodes as drop-in replacements for regular ones, e.g. JUMPDEST labels, PUSH<N> aliases, and DUP/SWAP from the bottom of the stack. It also provides pseudo opcodes that act as compiler hints.
It is designed to be dot-imported such that all exported identifiers are available in the importing package, allowing a mnemonic-style programming environment akin to writing assembly. As a result, there are few top-level identifiers.
Example (HelloWorld) ¶
hello := []byte("Hello world")
code := Code{
// The compiler determines the shortest-possible PUSH<n> opcode.
// Fn() simply reverses its arguments (a surprisingly powerful construct)!
Fn(MSTORE, PUSH0, PUSH(hello)),
Fn(RETURN, PUSH(32-len(hello)), PUSH(len(hello))),
}
compiled, err := code.Compile()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#x\n", compiled)
fmt.Println(string(mustRunByteCode(compiled, []byte{} /*callData*/)))
Output: 0x6a48656c6c6f20776f726c645f52600b6015f3 Hello world
Index ¶
- Constants
- func Fn(bcs ...types.Bytecoder) types.BytecodeHolder
- func PUSH[P interface{ ... }](v P) types.Bytecoder
- func PUSHBytes(bs ...byte) types.Bytecoder
- func PUSHSelector(sig string) types.Bytecoder
- type Code
- func (c Code) Bytecode() ([]byte, error)
- func (c Code) Bytecoders() []types.Bytecoder
- func (c Code) Compile() ([]byte, error)
- func (c Code) Run(callData []byte, opts ...runopts.Option) ([]byte, error)
- func (c Code) StartDebugging(callData []byte, opts ...runopts.Option) (*runopts.Debugger, func() ([]byte, error), error)
- type ExpectStackDepth
- type Inverted
- type JUMPDEST
- type PUSHJUMPDEST
- type Raw
- type SetStackDepth
Examples ¶
Constants ¶
const ( STOP = opCode(vm.STOP) ADD = opCode(vm.ADD) MUL = opCode(vm.MUL) SUB = opCode(vm.SUB) DIV = opCode(vm.DIV) SDIV = opCode(vm.SDIV) MOD = opCode(vm.MOD) SMOD = opCode(vm.SMOD) ADDMOD = opCode(vm.ADDMOD) MULMOD = opCode(vm.MULMOD) EXP = opCode(vm.EXP) SIGNEXTEND = opCode(vm.SIGNEXTEND) LT = opCode(vm.LT) GT = opCode(vm.GT) SLT = opCode(vm.SLT) SGT = opCode(vm.SGT) EQ = opCode(vm.EQ) ISZERO = opCode(vm.ISZERO) AND = opCode(vm.AND) OR = opCode(vm.OR) XOR = opCode(vm.XOR) NOT = opCode(vm.NOT) BYTE = opCode(vm.BYTE) SHL = opCode(vm.SHL) SHR = opCode(vm.SHR) SAR = opCode(vm.SAR) KECCAK256 = opCode(vm.KECCAK256) ADDRESS = opCode(vm.ADDRESS) BALANCE = opCode(vm.BALANCE) ORIGIN = opCode(vm.ORIGIN) CALLER = opCode(vm.CALLER) CALLVALUE = opCode(vm.CALLVALUE) CALLDATALOAD = opCode(vm.CALLDATALOAD) CALLDATASIZE = opCode(vm.CALLDATASIZE) CALLDATACOPY = opCode(vm.CALLDATACOPY) CODESIZE = opCode(vm.CODESIZE) CODECOPY = opCode(vm.CODECOPY) GASPRICE = opCode(vm.GASPRICE) EXTCODESIZE = opCode(vm.EXTCODESIZE) EXTCODECOPY = opCode(vm.EXTCODECOPY) RETURNDATASIZE = opCode(vm.RETURNDATASIZE) RETURNDATACOPY = opCode(vm.RETURNDATACOPY) EXTCODEHASH = opCode(vm.EXTCODEHASH) BLOCKHASH = opCode(vm.BLOCKHASH) COINBASE = opCode(vm.COINBASE) TIMESTAMP = opCode(vm.TIMESTAMP) NUMBER = opCode(vm.NUMBER) DIFFICULTY = opCode(vm.DIFFICULTY) GASLIMIT = opCode(vm.GASLIMIT) CHAINID = opCode(vm.CHAINID) SELFBALANCE = opCode(vm.SELFBALANCE) BASEFEE = opCode(vm.BASEFEE) BLOBHASH = opCode(vm.BLOBHASH) BLOBBASEFEE = opCode(vm.BLOBBASEFEE) POP = opCode(vm.POP) MLOAD = opCode(vm.MLOAD) MSTORE = opCode(vm.MSTORE) MSTORE8 = opCode(vm.MSTORE8) SLOAD = opCode(vm.SLOAD) SSTORE = opCode(vm.SSTORE) JUMP = opCode(vm.JUMP) JUMPI = opCode(vm.JUMPI) PC = opCode(vm.PC) MSIZE = opCode(vm.MSIZE) GAS = opCode(vm.GAS) TLOAD = opCode(vm.TLOAD) TSTORE = opCode(vm.TSTORE) MCOPY = opCode(vm.MCOPY) PUSH0 = opCode(vm.PUSH0) DUP1 = opCode(vm.DUP1) DUP2 = opCode(vm.DUP2) DUP3 = opCode(vm.DUP3) DUP4 = opCode(vm.DUP4) DUP5 = opCode(vm.DUP5) DUP6 = opCode(vm.DUP6) DUP7 = opCode(vm.DUP7) DUP8 = opCode(vm.DUP8) DUP9 = opCode(vm.DUP9) DUP10 = opCode(vm.DUP10) DUP11 = opCode(vm.DUP11) DUP12 = opCode(vm.DUP12) DUP13 = opCode(vm.DUP13) DUP14 = opCode(vm.DUP14) DUP15 = opCode(vm.DUP15) DUP16 = opCode(vm.DUP16) SWAP1 = opCode(vm.SWAP1) SWAP2 = opCode(vm.SWAP2) SWAP3 = opCode(vm.SWAP3) SWAP4 = opCode(vm.SWAP4) SWAP5 = opCode(vm.SWAP5) SWAP6 = opCode(vm.SWAP6) SWAP7 = opCode(vm.SWAP7) SWAP8 = opCode(vm.SWAP8) SWAP9 = opCode(vm.SWAP9) SWAP10 = opCode(vm.SWAP10) SWAP11 = opCode(vm.SWAP11) SWAP12 = opCode(vm.SWAP12) SWAP13 = opCode(vm.SWAP13) SWAP14 = opCode(vm.SWAP14) SWAP15 = opCode(vm.SWAP15) SWAP16 = opCode(vm.SWAP16) LOG0 = opCode(vm.LOG0) LOG1 = opCode(vm.LOG1) LOG2 = opCode(vm.LOG2) LOG3 = opCode(vm.LOG3) LOG4 = opCode(vm.LOG4) CREATE = opCode(vm.CREATE) CALL = opCode(vm.CALL) CALLCODE = opCode(vm.CALLCODE) RETURN = opCode(vm.RETURN) DELEGATECALL = opCode(vm.DELEGATECALL) CREATE2 = opCode(vm.CREATE2) STATICCALL = opCode(vm.STATICCALL) REVERT = opCode(vm.REVERT) INVALID = opCode(vm.INVALID) SELFDESTRUCT = opCode(vm.SELFDESTRUCT) )
Aliases of all regular vm.OpCode constants that don't have "special" replacements.
Variables ¶
This section is empty.
Functions ¶
func Fn ¶
func Fn(bcs ...types.Bytecoder) types.BytecodeHolder
Fn returns a Bytecoder that returns the concatenation of the *reverse* of bcs. This allows for a more human-readable syntax akin to a function call (hence the name). Fn is similar to Yul except that "return" values are left on the stack to be used by later Fn()s (or raw bytecode).
Although the returned BytecodeHolder can contain JUMPDESTs, they're hard to reason about so should be used with care.
func PUSH ¶
func PUSH[P interface {
int | uint64 | common.Address | uint256.Int | byte | []byte | JUMPDEST | string
}](v P,
) types.Bytecoder
PUSH returns a PUSH<n> Bytecoder appropriate for the type. It panics if v is negative. A string is equivalent to PUSHJUMPDEST(v).
func PUSHBytes ¶
PUSHBytes accepts [1,32] bytes, returning a PUSH<x> Bytecoder where x is the smallest number of bytes (possibly zero) that can represent the concatenated values; i.e. x = len(bs) - leadingZeros(bs).
func PUSHSelector ¶
PUSHSelector returns a PUSH4 Bytecoder that pushes the selector of the signature, i.e. `sha3(sig)[:4]`.
Types ¶
type Code ¶
Code is a slice of Bytecoders; it is itself a Bytecoder, allowing for nesting.
Example (MonteCarloPi) ¶
// A unit circle inside a 2x2 square covers π/4 of the area. We can
// (inefficiently) approximate π using sha3 as a source of entropy!
//
// Bottom of the stack will always be:
// - loop total
// - loops remaining
// - hit counter (values inside the circle)
// - constant: 1 (to use DUP instead of PUSH)
// - constant: 1 << 128 - 1
// - constant: 1 << 64 - 1
// - Entropy (hash)
//
// We can therefore use Inverted(DUP/SWAPn) to access them as required,
// effectively creating variables.
const (
Total = Inverted(DUP1) + iota
Limit
Hits
One
Bits128
Bits64
Hash
)
const (
SwapLimit = Limit + 16 + iota
SwapHits
)
const bitPrecision = 128
code := Code{
PUSH(0x02b000), // loop total (~30M gas); kept as the denominator
DUP1, // loops remaining
PUSH0, // inside-circle count (numerator)
PUSH(1), // constant-value 1
Fn(SUB, Fn(SHL, PUSH(0x80), One), One), // 128-bit mask
Fn(SUB, Fn(SHL, PUSH(0x40), One), One), // 64-bit mask
ExpectStackDepth(6),
JUMPDEST("loop"), SetStackDepth(6),
Fn(KECCAK256, PUSH0, PUSH(32)),
Fn(AND, Bits64, Hash), // x = lowest 64 bits
Fn(AND, Bits64, Fn(SHR, PUSH(64), Hash)), // y = next lowest 64 bits
Fn(GT,
Bits128,
Fn(ADD,
Fn(MUL, DUP1), // y^2
SWAP1, // x^2 <-> y
Fn(MUL, DUP1), // x^2
),
),
Fn(SwapHits, Fn(ADD, Hits)),
Fn(JUMPI,
PUSH("return"),
Fn(ISZERO, DUP1, Fn(SUB, Limit, One)), // DUP1 uses the top of the stack without consuming it
),
ExpectStackDepth(9),
SwapLimit, POP, POP,
Fn(MSTORE, PUSH0),
Fn(JUMP, PUSH("loop")), ExpectStackDepth(6),
JUMPDEST("return"), SetStackDepth(9),
POP, POP,
Fn(MSTORE,
PUSH0,
Fn(DIV,
Fn(SHL, PUSH(bitPrecision+2), Hits), // extra 2 to undo π/4
Total,
),
),
Fn(RETURN, PUSH0, PUSH(32)),
}
pi := new(big.Rat).SetFrac(
new(big.Int).SetBytes(compileAndRun(code, []byte{})),
new(big.Int).Lsh(big.NewInt(1), bitPrecision),
)
fmt.Println(pi.FloatString(2))
Output: 3.14
Example (Sqrt) ¶
// This implements the same sqrt() algorithm as prb-math:
// https://github.com/PaulRBerg/prb-math/blob/5b6279a0cf7c1b1b6a5cc96082811f7ef620cf60/src/Common.sol#L595
// Snippets included under MIT, Copyright (c) 2023 Paul Razvan Berg
//
// See the Monte-Carlo π for explanation of "variables".
const (
Input = Inverted(DUP1) + iota
One
ThresholdBits
Threshold
xAux
Result
Branch
)
const (
SwapInput = Input + 16 + iota
_ // SetOne
SetThresholdBits
SetThreshold
SetXAux
SetResult
SetBranch
)
// Placing ExpectStackDepth(i/o) at the beginning/end of a Code
// effectively turns it into a macro that can either be embedded in another
// Code (as below) or for use in Solidity `verbatim_Xi_Yo`.
approx := Code{
ExpectStackDepth(6),
// Original:
//
// if (xAux >= 2 ** 128) {
// xAux >>= 128;
// result <<= 64;
// }
// if (xAux >= 2 ** 64) {
// ...
//
Fn(GT, xAux, Threshold), // Branch
Fn(SetXAux,
Fn(SHR,
Fn(MUL, ThresholdBits, Branch),
xAux,
),
), POP, // old value; TODO: improve this by using a SWAP instead of a DUP inside the Fn()
Fn(SetThresholdBits,
Fn(SHR, One, ThresholdBits),
), POP,
Fn(SetThreshold,
Fn(SUB, Fn(SHL, ThresholdBits, One), One),
), POP,
Fn(SetResult,
Fn(SHL,
Fn(MUL, ThresholdBits, Branch),
Result,
),
), POP,
POP, // Branch
ExpectStackDepth(6),
}
// Single round of Newton–Raphson
newton := Code{
ExpectStackDepth(6),
// Original: result = (result + x / result) >> 1;
Fn(SetResult,
Fn(SHR,
One,
Fn(ADD,
Result,
Fn(DIV, Input, Result),
),
),
), POP,
ExpectStackDepth(6),
}
sqrt := Code{
ExpectStackDepth(1), // Input
PUSH(1), // One
PUSH(128), // ThresholdBits
Fn(SUB, Fn(SHL, ThresholdBits, One), One), // Threshold
Input, // xAux := Input
One, // Result
ExpectStackDepth(6),
approx, approx, approx, approx, approx, approx, approx,
ExpectStackDepth(6),
newton, newton, newton, newton, newton, newton, newton,
}
code := Code{
Fn(CALLDATALOAD, PUSH0),
sqrt,
Fn(MSTORE, PUSH0),
Fn(RETURN, PUSH0, PUSH(32)),
}
root := new(uint256.Int) // can we get this back? ;)
if err := root.SetFromHex("0xDecafC0ffeeBad15DeadC0deCafe"); err != nil {
log.Fatal(err)
}
callData := new(uint256.Int).Mul(root, root).Bytes32()
result := new(uint256.Int).SetBytes(
compileAndRun(code, callData),
)
fmt.Println(" In:", root.Hex())
fmt.Println("Result:", result.Hex())
fmt.Println(" Equal:", root.Eq(result))
Output: In: 0xdecafc0ffeebad15deadc0decafe Result: 0xdecafc0ffeebad15deadc0decafe Equal: true
Example (WellKnown) ¶
// This example demonstrates some well-known bytecode examples implemented
// with `specialops`:
//
// - EIP-1167 Minimal Proxy Contract
// - 0age/metamorphic Metamorphic contract constructor https://github.com/0age/metamorphic/blob/55adac1d2487046002fc33a5dff7d669b5419a3a/contracts/MetamorphicContractFactory.sol#L55
//
// The compiled bytecode is identical to the originals.
impl := common.HexToAddress("bebebebebebebebebebebebebebebebebebebebe")
eip1167 := Code{
// Think of RETURNDATASIZE before DELEGATECALL as PUSH0 (the EIP predated it)
Fn(CALLDATACOPY, RETURNDATASIZE, RETURNDATASIZE, CALLDATASIZE), // Copy calldata to memory
RETURNDATASIZE,
Fn( // Delegate-call the implementation, forwarding all gas, and propagating calldata
DELEGATECALL,
GAS,
PUSH(impl), // Native Go values!
RETURNDATASIZE, CALLDATASIZE, RETURNDATASIZE, RETURNDATASIZE,
),
ExpectStackDepth(2), // top <suc 0> bot
Fn(
RETURNDATACOPY,
DUP1, // This could equivalently be Inverted(DUP1)==DUP4
Inverted(DUP1), // DUP the 0 at the bottom; the compiler knows to convert this to DUP3
RETURNDATASIZE, // Actually return-data size now
),
ExpectStackDepth(2), // <suc 0>
SWAP1, RETURNDATASIZE, SWAP2, // <suc 0 rds>
Fn(JUMPI, PUSH("return")),
Fn(REVERT, ExpectStackDepth(2)), // Compiler hint for argc
JUMPDEST("return"),
SetStackDepth(2), // Required after a JUMPDEST
RETURN,
}
metamorphic := Code{
// 0age uses PC to place a 0 on the bottom of the stack and then
// duplicates it as necessary. Using `Inverted(DUP1)` makes this
// much easier to reason about. This is especially so when
// refactoring as the specific DUP<N> would otherwise have to
// change.
Fn(
// Although Fn() wasn't intended to be used without a
// function-like opcode at the beginning, it sheds light on
// what 0age was doing here: setting up all the arguments
// for a later STATICCALL. While nested Fn()s act like
// regular functions (see ISZERO later), sequential ones
// have the effect of "piping" arguments to the next, which
// may or may not use them. As the MSTORE Fn() has
// sufficient arguments, the ones set up here are left for
// the STATICCALL.
//
// Note that everything in Fn() is reversed so PCs count
// from the right, but the rest is easier to read as it is
// Yul-like. I'm guessing that this argument setup without
// the call was a trick to cheaply get the PC=4 in the right
// place.
GAS, CALLER, PUSH(28), PC /*4*/, Inverted(DUP1) /*0*/, PUSH(32), PC,
),
Fn(
MSTORE,
Inverted(DUP1), // Compiler knows this is a DUP8 to copy the 0 from the bottom
PUSHSelector("getImplementation()"),
),
// Although the inner Fn() is equivalent to a raw STATICCALL,
// the compiler hint for the stack depth is useful (and also
// signals the reader of the code to remember the earlier
// setup), while placing it in Fn() makes the order more
// readable.
Fn(ISZERO, Fn(STATICCALL, ExpectStackDepth(7))),
// Recall that the return (offset, size) were set to (0,32).
ExpectStackDepth(2), // [0, fail?] memory:<addr>
Fn(MLOAD, Inverted(DUP1) /*0*/), // [0, fail?, addr]
Fn(EXTCODESIZE, DUP1), // DUP1 as a single argument is like a stack peek
DUP1, // [0, fail?, addr, size, size]
Fn(EXTCODECOPY, SWAP3, SWAP2, DUP1, SWAP4), // TODO: can the arguments be simplified with `Inverted` equivalents?
RETURN,
}
// Using PUSH0, here is a modernised version of EIP-1167, reduced by 1 byte
// and easy to read.
eip1167Modern := Code{
Fn(CALLDATACOPY, PUSH0, PUSH0, CALLDATASIZE),
Fn(DELEGATECALL, GAS, PUSH(impl), PUSH0, CALLDATASIZE, PUSH0, PUSH0),
ExpectStackDepth(1), // `success`
Fn(RETURNDATACOPY, PUSH0, PUSH0, RETURNDATASIZE),
ExpectStackDepth(1), // unchanged
PUSH0, RETURNDATASIZE, // prepare for the REVERT/RETURN; these are in "human" order because of the next SWAP
Inverted(SWAP1), // bring `success` from the bottom
Fn(JUMPI, PUSH("return")),
Fn(REVERT, ExpectStackDepth(2)),
JUMPDEST("return"),
Fn(RETURN, SetStackDepth(2)),
}
for _, eg := range []struct {
name string
code Code
}{
{"EIP-1167", eip1167},
{"Modernised EIP-1167", eip1167Modern},
{"0age/metamorphic", metamorphic},
} {
bytecode, err := eg.code.Compile()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%19s: %#x\n", eg.name, bytecode)
}
Output: EIP-1167: 0x363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 Modernised EIP-1167: 0x365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3 0age/metamorphic: 0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3
func (Code) Bytecode ¶
Bytecode always returns an error; use Code.Compile instead(), which flattens nested Code instances.
func (Code) Bytecoders ¶
Bytecoders returns the Code as a slice of Bytecoders.
func (Code) Run ¶
Run calls c.Compile() and runs the compiled bytecode on a freshly instantiated vm.EVMInterpreter. The default EVM parameters MUST NOT be considered stable: they are currently such that code runs on the Cancun fork with no state DB.
func (Code) StartDebugging ¶
func (c Code) StartDebugging(callData []byte, opts ...runopts.Option) (*runopts.Debugger, func() ([]byte, error), error)
StartDebugging appends a runopts.Debugger (`dbg`) to the Options, calls c.Run() in a new goroutine, and returns `dbg` along with a function to retrieve ther esults of Run(). The function will block until Run() returns, i.e. when dbg.Done() returns true. There is no need to call dbg.Wait().
If execution never completes, such that dbg.Done() always returns false, then the goroutine will be leaked.
Any compilation error will be returned by StartDebugging() while execution errors are returned by a call to the returned function. Said execution errors can be errors.Unwrap()d to access the same error available in `dbg.State().Err`.
type ExpectStackDepth ¶
type ExpectStackDepth uint
ExpectStackDepth is a sentinel value that singals to Code.Compile() that it must assert the expected stack depth, returning an error if incorrect. See SetStackDepth() for caveats; note that the expectation is with respect to Compile() and has nothing to do with concrete (runtime) depths.
func (ExpectStackDepth) Bytecode ¶
func (d ExpectStackDepth) Bytecode() ([]byte, error)
Bytecode always returns an error.
type Inverted ¶
Inverted applies DUP<X> and SWAP<X> opcodes relative to the bottom-most value on the stack unless there are more than 16 values, in which case they are applied relative to the 16th.
For a stack with n <= 16 values on it, `Inverted(DUP1)` and `Inverted(SWAP1)` will apply to the nth value instead of the first. Similarly, `Inverted(DUP2)` will apply to the (n-1)the value, etc. For a stack with >16 items, the same logic applies but with n = 16.
Note that the semantics disallow `Inverted(SWAP16)` as it would be a noop. In fact, in all cases, inverted SWAPs are capped at `depth-1`. While they could be offset by one (like regular SWAPs) this is less intuitive than `Inverted(SWAP1)` being the bottom of a (sub-16-depth) stack.
See SetStackDepth() for caveats. It is best practice to use `Inverted` in conjunction with {Set/Expect}StackDepth().
type JUMPDEST ¶
type JUMPDEST string
A JUMPDEST is a Bytecoder that is converted into a vm.JUMPDEST while also storing its location in the bytecode for use via a PUSHJUMPDEST or PUSH[string|JUMPDEST](<lbl>).
type PUSHJUMPDEST ¶
type PUSHJUMPDEST string
PUSHJUMPDEST pushes the bytecode location of the respective JUMPDEST.
func (PUSHJUMPDEST) Bytecode ¶
func (p PUSHJUMPDEST) Bytecode() ([]byte, error)
Bytecode always returns an error as PUSHJUMPDEST values have special handling inside Code.Compile().
type Raw ¶
type Raw []byte
Raw is a Bytecoder that bypasses all compiler checks and simply appends its contents to bytecode. It can be used for raw data, not meant to be executed.
type SetStackDepth ¶
type SetStackDepth uint
SetStackDepth is a sentinel value that signals to Code.Compile() that it must modify its internal counter reflecting the current stack depth.
For each vm.OpCode that it encounters, Code.Compile() adjusts a value that reflects its belief about the stack depth. This is a crude mechanism that only works for non-JUMPing code. The programmer can therefore signal, typically after a JUMPDEST, the actual stack depth.
func (SetStackDepth) Bytecode ¶
func (d SetStackDepth) Bytecode() ([]byte, error)
Bytecode always returns an error.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
opcopy
command
The opcopy binary generates a Go file for use in the `specialops` package.
|
The opcopy binary generates a Go file for use in the `specialops` package. |
|
Package runopts provides configuration options for specialops.Code.Run().
|
Package runopts provides configuration options for specialops.Code.Run(). |
|
Package types defines types used by the specialops package, which is intended to be dot-imported so requires a minimal footprint of exported symbols.
|
Package types defines types used by the specialops package, which is intended to be dot-imported so requires a minimal footprint of exported symbols. |