Documentation ¶
Overview ¶
Package specops 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
- func PUSHSize[T ~string, U ~string](a T, b U) 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) (*core.ExecutionResult, error)
- func (c Code) RunTerminalDebugger(callData []byte, opts ...runopts.Option) error
- func (c Code) StartDebugging(callData []byte, opts ...runopts.Option) (*evmdebug.Debugger, func() (*core.ExecutionResult, error), error)
- type Inverted
- type JUMPDEST
- type Label
- type Raw
Examples ¶
Constants ¶
const ( STOP = types.OpCode(vm.STOP) ADD = types.OpCode(vm.ADD) MUL = types.OpCode(vm.MUL) SUB = types.OpCode(vm.SUB) DIV = types.OpCode(vm.DIV) SDIV = types.OpCode(vm.SDIV) MOD = types.OpCode(vm.MOD) SMOD = types.OpCode(vm.SMOD) ADDMOD = types.OpCode(vm.ADDMOD) MULMOD = types.OpCode(vm.MULMOD) EXP = types.OpCode(vm.EXP) SIGNEXTEND = types.OpCode(vm.SIGNEXTEND) LT = types.OpCode(vm.LT) GT = types.OpCode(vm.GT) SLT = types.OpCode(vm.SLT) SGT = types.OpCode(vm.SGT) EQ = types.OpCode(vm.EQ) ISZERO = types.OpCode(vm.ISZERO) AND = types.OpCode(vm.AND) OR = types.OpCode(vm.OR) XOR = types.OpCode(vm.XOR) NOT = types.OpCode(vm.NOT) BYTE = types.OpCode(vm.BYTE) SHL = types.OpCode(vm.SHL) SHR = types.OpCode(vm.SHR) SAR = types.OpCode(vm.SAR) KECCAK256 = types.OpCode(vm.KECCAK256) ADDRESS = types.OpCode(vm.ADDRESS) BALANCE = types.OpCode(vm.BALANCE) ORIGIN = types.OpCode(vm.ORIGIN) CALLER = types.OpCode(vm.CALLER) CALLVALUE = types.OpCode(vm.CALLVALUE) CALLDATALOAD = types.OpCode(vm.CALLDATALOAD) CALLDATASIZE = types.OpCode(vm.CALLDATASIZE) CALLDATACOPY = types.OpCode(vm.CALLDATACOPY) CODESIZE = types.OpCode(vm.CODESIZE) CODECOPY = types.OpCode(vm.CODECOPY) GASPRICE = types.OpCode(vm.GASPRICE) EXTCODESIZE = types.OpCode(vm.EXTCODESIZE) EXTCODECOPY = types.OpCode(vm.EXTCODECOPY) RETURNDATASIZE = types.OpCode(vm.RETURNDATASIZE) RETURNDATACOPY = types.OpCode(vm.RETURNDATACOPY) EXTCODEHASH = types.OpCode(vm.EXTCODEHASH) BLOCKHASH = types.OpCode(vm.BLOCKHASH) COINBASE = types.OpCode(vm.COINBASE) TIMESTAMP = types.OpCode(vm.TIMESTAMP) NUMBER = types.OpCode(vm.NUMBER) DIFFICULTY = types.OpCode(vm.DIFFICULTY) GASLIMIT = types.OpCode(vm.GASLIMIT) CHAINID = types.OpCode(vm.CHAINID) SELFBALANCE = types.OpCode(vm.SELFBALANCE) BASEFEE = types.OpCode(vm.BASEFEE) BLOBHASH = types.OpCode(vm.BLOBHASH) BLOBBASEFEE = types.OpCode(vm.BLOBBASEFEE) POP = types.OpCode(vm.POP) MLOAD = types.OpCode(vm.MLOAD) MSTORE = types.OpCode(vm.MSTORE) MSTORE8 = types.OpCode(vm.MSTORE8) SLOAD = types.OpCode(vm.SLOAD) SSTORE = types.OpCode(vm.SSTORE) JUMP = types.OpCode(vm.JUMP) JUMPI = types.OpCode(vm.JUMPI) PC = types.OpCode(vm.PC) MSIZE = types.OpCode(vm.MSIZE) GAS = types.OpCode(vm.GAS) TLOAD = types.OpCode(vm.TLOAD) TSTORE = types.OpCode(vm.TSTORE) MCOPY = types.OpCode(vm.MCOPY) PUSH0 = types.OpCode(vm.PUSH0) DUP1 = types.OpCode(vm.DUP1) DUP2 = types.OpCode(vm.DUP2) DUP3 = types.OpCode(vm.DUP3) DUP4 = types.OpCode(vm.DUP4) DUP5 = types.OpCode(vm.DUP5) DUP6 = types.OpCode(vm.DUP6) DUP7 = types.OpCode(vm.DUP7) DUP8 = types.OpCode(vm.DUP8) DUP9 = types.OpCode(vm.DUP9) DUP10 = types.OpCode(vm.DUP10) DUP11 = types.OpCode(vm.DUP11) DUP12 = types.OpCode(vm.DUP12) DUP13 = types.OpCode(vm.DUP13) DUP14 = types.OpCode(vm.DUP14) DUP15 = types.OpCode(vm.DUP15) DUP16 = types.OpCode(vm.DUP16) SWAP1 = types.OpCode(vm.SWAP1) SWAP2 = types.OpCode(vm.SWAP2) SWAP3 = types.OpCode(vm.SWAP3) SWAP4 = types.OpCode(vm.SWAP4) SWAP5 = types.OpCode(vm.SWAP5) SWAP6 = types.OpCode(vm.SWAP6) SWAP7 = types.OpCode(vm.SWAP7) SWAP8 = types.OpCode(vm.SWAP8) SWAP9 = types.OpCode(vm.SWAP9) SWAP10 = types.OpCode(vm.SWAP10) SWAP11 = types.OpCode(vm.SWAP11) SWAP12 = types.OpCode(vm.SWAP12) SWAP13 = types.OpCode(vm.SWAP13) SWAP14 = types.OpCode(vm.SWAP14) SWAP15 = types.OpCode(vm.SWAP15) SWAP16 = types.OpCode(vm.SWAP16) LOG0 = types.OpCode(vm.LOG0) LOG1 = types.OpCode(vm.LOG1) LOG2 = types.OpCode(vm.LOG2) LOG3 = types.OpCode(vm.LOG3) LOG4 = types.OpCode(vm.LOG4) CREATE = types.OpCode(vm.CREATE) CALL = types.OpCode(vm.CALL) CALLCODE = types.OpCode(vm.CALLCODE) RETURN = types.OpCode(vm.RETURN) DELEGATECALL = types.OpCode(vm.DELEGATECALL) CREATE2 = types.OpCode(vm.CREATE2) STATICCALL = types.OpCode(vm.STATICCALL) REVERT = types.OpCode(vm.REVERT) INVALID = types.OpCode(vm.INVALID) SELFDESTRUCT = types.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 | common.Hash | uint256.Int | byte | []byte | JUMPDEST | []JUMPDEST | Label | []Label | string | []string }](v P, ) types.Bytecoder
PUSH returns a PUSH<n> Bytecoder appropriate for the type. It panics if v is negative. A string refers to the respective JUMPDEST or Label while a []string refers to a concatenation of the same (e.g. a JUMP table).
Example (JumpTable) ¶
// This is a highly optimised factorial function, implementing one of the // Curta gas-golfing (https://www.curta.wtf/golf/2) solutions by philogy.eth // https://basescan.org/address/0x550d8df432706504b550c7cf93660cd362d7f95c prod := func(start, end uint64) uint64 { x := end for i := start; i < end; i++ { x *= i } return x } rangeMuls := Code{ JUMPDEST("49:54"), stack.SetDepth(2), Fn(MUL, PUSH(prod(49, 54)), ), JUMPDEST("43:48"), stack.SetDepth(2), Fn(MUL, PUSH(prod(43, 48)), ), JUMPDEST("37:42"), stack.SetDepth(2), Fn(MUL, PUSH(prod(37, 42)), ), JUMPDEST("31:36"), stack.SetDepth(2), Fn(MUL, PUSH(prod(31, 36)), ), JUMPDEST("25:30"), stack.SetDepth(2), Fn(MUL, PUSH(prod(25, 30)), ), JUMPDEST("19:24"), stack.SetDepth(2), Fn(MUL, PUSH(prod(19, 24)), ), JUMPDEST("13:18"), stack.SetDepth(2), Fn(MUL, PUSH(prod(13, 18)), ), JUMPDEST("7:12"), stack.SetDepth(2), Fn(MUL, PUSH(prod(7, 12)), ), JUMPDEST("1:6"), stack.SetDepth(2), Fn(MUL, PUSH(prod(1, 6)), ), JUMPDEST("no-range-mul"), stack.SetDepth(2), } ranges := []string{ "no-range-mul", "1:6", "7:12", "13:18", "19:24", "25:30", "31:36", "37:42", "43:48", "49:54", } const Input = Inverted(DUP1) // always bottom of the stack remainderMuls := Code{ JUMPDEST("sub4"), stack.SetDepth(2), Fn(MUL, Fn(SUB, Input, PUSH(4)), ), JUMPDEST("sub3"), stack.SetDepth(2), Fn(MUL, Fn(SUB, Input, PUSH(3)), ), JUMPDEST("sub2"), stack.SetDepth(2), Fn(MUL, Fn(SUB, Input, PUSH(2)), ), JUMPDEST("sub1"), stack.SetDepth(2), Fn(MUL, Fn(SUB, Input, PUSH(1)), ), JUMPDEST("sub0"), stack.SetDepth(2), MUL, /* result * input */ stack.ExpectDepth(1), JUMPDEST("no-remainder-mul"), stack.SetDepth(2), } remainders := []string{ "no-remainder-mul", "sub0", "sub1", "sub2", "sub3", "sub4", } const divisor = 6 code := Code{ PUSH(4), CALLDATALOAD, PUSH(1), // Result Fn(JUMPI, Fn(BYTE, Fn(ADD, Fn(DIV, Input, PUSH(divisor)), PUSH(32-len(ranges)), ), PUSH(ranges), ), Fn(LT, Input, PUSH(58)), ), RETURNDATASIZE, RETURNDATASIZE, REVERT, rangeMuls, Fn(JUMP, Fn(BYTE, Fn(ADD, Fn(MOD, Input, PUSH(divisor)), PUSH(32-len(remainders)), ), PUSH(remainders), ), ), remainderMuls, Fn(MSTORE, RETURNDATASIZE), Fn(RETURN, RETURNDATASIZE, MSIZE), } got, err := code.Compile() if err != nil { log.Fatal(err) } fmt.Printf("%#x", got)
Output: 0x6004356001603a8210695d58524c453e37302820601660068504011a573d3dfd5b64045461b590025b64020ea2db80025b63e11fed20025b6353971500025b63197b6830025b6305c6b740025b62cbf340025b620a26c0025b6102d0025b658886807a746e601a60068406011a565b60048203025b60038203025b60028203025b60018203025b025b3d52593df3
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 (Eip1167) ¶
// Demonstrates verbatim recreation of EIP-1167 Minimal Proxy Contract and a // modern equivalent with PUSH0. 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, ), stack.ExpectDepth(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 ), stack.ExpectDepth(2), // <suc 0> SWAP1, RETURNDATASIZE, SWAP2, // <suc 0 rds> Fn(JUMPI, PUSH("return")), Fn(REVERT, stack.ExpectDepth(2)), // Compiler hint for argc JUMPDEST("return"), stack.SetDepth(2), // Required after a JUMPDEST 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), stack.ExpectDepth(1), // `success` Fn(RETURNDATACOPY, PUSH0, PUSH0, RETURNDATASIZE), stack.ExpectDepth(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, stack.ExpectDepth(2)), JUMPDEST("return"), Fn(RETURN, stack.SetDepth(2)), } for _, eg := range []struct { name string code Code }{ {"EIP-1167", eip1167}, {"Modernised EIP-1167", eip1167Modern}, } { 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
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 stack.ExpectDepth(6), JUMPDEST("loop"), stack.SetDepth(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 ), stack.ExpectDepth(9), SwapLimit, POP, POP, Fn(MSTORE, PUSH0), Fn(JUMP, PUSH("loop")), stack.ExpectDepth(6), JUMPDEST("return"), stack.SetDepth(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 stack.ExpectDepth(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{ stack.ExpectDepth(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 stack.ExpectDepth(6), } // Single round of Newton–Raphson newton := Code{ stack.ExpectDepth(6), // Original: result = (result + x / result) >> 1; Fn(SetResult, Fn(SHR, One, Fn(ADD, Result, Fn(DIV, Input, Result), ), ), ), POP, stack.ExpectDepth(6), } sqrt := Code{ stack.ExpectDepth(1), // Input PUSH(1), // One PUSH(128), // ThresholdBits Fn(SUB, Fn(SHL, ThresholdBits, One), One), // Threshold Input, // xAux := Input One, // Result stack.ExpectDepth(6), approx, approx, approx, approx, approx, approx, approx, stack.ExpectDepth(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 (Succinct0ageMetamorphic) ¶
// Identical to the other metamorphic example, but with explanatory comments // removed to demonstrate succinct but readable production usage. const zero = Inverted(DUP1) // see first opcode metamorphic := Code{ // Keep a zero at the bottom of the stack PC, // Prepare a STATICCALL signature Fn( /*STATICCALL*/ GAS, CALLER, PUSH(28), PC /*4*/, zero, PUSH(32)), Fn(MSTORE, zero, PUSHSelector("getImplementation()")), // stack unchanged Fn(ISZERO, STATICCALL), // consumes all values except the zero stack.ExpectDepth(2), // [0, fail?] <addr> Fn(MLOAD, zero), // [0, fail?, addr] Fn(EXTCODESIZE, DUP1), // [0, fail?, addr, size] } { // Current stack, top to bottom const ( size = iota address callFailed // presumed to be 0 zero depth ) metamorphic = append( metamorphic, stack.Transform(depth)( /*EXTCODECOPY*/ address, zero, zero, size, /*RETURN*/ callFailed /*0*/, size, ).WithOps( // In reality we wouldn't override the ops, but let the // stack.Transformation find an optimal path. DUP1, SWAP4, DUP1, SWAP2, SWAP3, ), EXTCODECOPY, RETURN, ) } bytecode, err := metamorphic.Compile() if err != nil { log.Fatal(err) } fmt.Printf("%#x", bytecode)
Output: 0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3
Example (Verbose0ageMetamorphic) ¶
// Demonstrates verbatim recreation of 0age's metamorphic contract // constructor: https://github.com/0age/metamorphic/blob/55adac1d2487046002fc33a5dff7d669b5419a3a/contracts/MetamorphicContractFactory.sol#L55 // // Using stack.Transform() automation we also see how the size could have // been reduced. Granted, only by a single byte, but it also saves a lot of // development time. metamorphicPrelude := 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, stack.ExpectDepth(7))), // Recall that the return (offset, size) were set to (0,32). stack.ExpectDepth(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 } // For reference, a snippet from 0age's comments to explain the stack // transformation that now occurs. // // * ** get extcodesize on fourth stack item for extcodecopy ** // * 18 3b extcodesize [0, 0, address, size] <> // ... // ... // * 23 92 swap3 [size, 0, size, 0, 0, address] <> // The stack as it currently stands, labelled top to bottom. const ( size = iota address callFailed // presumably zero zero depth ) metamorphic := Code{ metamorphicPrelude, stack.Transform(depth)(address, zero, zero, size, callFailed, size).WithOps( // The exact opcodes from the original, which the compiler will // confirm as having the intended result. DUP1, SWAP4, DUP1, SWAP2, SWAP3, ), stack.ExpectDepth(6), EXTCODECOPY, RETURN, } autoMetamorphic := Code{ metamorphicPrelude, stack.Transform(depth)(address, zero, zero, size, callFailed, size), stack.ExpectDepth(6), EXTCODECOPY, RETURN, } for _, eg := range []struct { name string code Code }{ {" 0age/metamorphic", metamorphic}, {"Auto stack transformation", autoMetamorphic}, } { bytecode, err := eg.code.Compile() if err != nil { log.Fatal(err) } fmt.Printf("%19s: %#x\n", eg.name, bytecode) }
Output: 0age/metamorphic: 0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3 Auto stack transformation: 0x5860208158601c335a63aaf10f428752fa158151803b928084923cf3
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.EVM. See runopts for configuring the EVM and call parameters, and for intercepting bytecode.
Run returns an error if the code reverts. The error will be a revert.Error carrying the same revert error and data as the core.ExecutionResult returned by Run. To only return errors in the core.ExecutionResult, use runopts.NoErrorOnRevert.
func (Code) RunTerminalDebugger ¶
RunTerminalDebugger is equivalent to StartDebugging(), but instead of returning the Debugger and results function, it calls Debugger.RunTerminalUI().
func (Code) StartDebugging ¶
func (c Code) StartDebugging(callData []byte, opts ...runopts.Option) (*evmdebug.Debugger, func() (*core.ExecutionResult, 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 the results 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 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 stack.SetDepth() for caveats. It is best practice to use `Inverted` in conjunction with stack.{Set/Expect}Depth().
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 PUSH[string|JUMPDEST](<lbl>).
type Label ¶
type Label string
A Label marks a specific point in the code without adding any bytes when compiled. The corresponding numerical value is the first byte *after* the Label.
Example ¶
const size = Inverted(DUP1) dataTable := Code{ PUSHSize("data", "end"), // calculated during compilation Fn(CODECOPY, PUSH0, PUSH("data"), size), Fn(RETURN, PUSH0 /* size already on stack */), Label("data"), // not compiled into anything Raw("hello world"), Label("end"), } fmt.Println(string(compileAndRun(dataTable, []byte{})))
Output: hello world
Directories ¶
Path | Synopsis |
---|---|
Package evmdebug provides debugging mechanisms for EVM contracts, intercepting opcode-level execution and allowing for inspection of data such as the VM's stack and memory.
|
Package evmdebug provides debugging mechanisms for EVM contracts, intercepting opcode-level execution and allowing for inspection of data such as the VM's stack and memory. |
internal
|
|
opcopy
The opcopy binary generates a Go file for use in the `specops` package.
|
The opcopy binary generates a Go file for use in the `specops` package. |
sync
Package sync provides synchronisation primitives not available in the standard sync package.
|
Package sync provides synchronisation primitives not available in the standard sync package. |
Package revert provides errors and error handling for EVM smart contracts that revert.
|
Package revert provides errors and error handling for EVM smart contracts that revert. |
Package runopts provides configuration options for specops.Code.Run().
|
Package runopts provides configuration options for specops.Code.Run(). |
Package specopscli provides a CLI for developing specops.Code.
|
Package specopscli provides a CLI for developing specops.Code. |
Package stack provides stack-related "special opcodes" for use in specops code.
|
Package stack provides stack-related "special opcodes" for use in specops code. |
Package types defines types used by the specops package, which is intended to be dot-imported so requires a minimal footprint of exported symbols.
|
Package types defines types used by the specops package, which is intended to be dot-imported so requires a minimal footprint of exported symbols. |