specialops

package module
v0.0.0-...-806e181 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 2, 2024 License: Apache-2.0 Imports: 12 Imported by: 0

README

SpecialOps Go Go Reference

specialops is a low-level, domain-specific language (and compiler) for crafting Ethereum VM bytecode in Go.

This is a very early release. In fact, it's just a weekend project gone rogue so is less than a week old.

special opcodes

Writing bytecode is hard. There's always that temptation to give up and use a higher-level language with all of its conveniences, but that defeats the point. What if we could maintain full control of the opcode placement, but with syntactic sugar to help the medicine go down?

Special opcodes provide just that. Some are interpreted by the compiler, converting them to regular equivalents, while others are simply compiler hints that leave the resulting bytecode unchanged.

Features

  • JUMPDEST labels (absolute)
  • JUMPDEST labels (relative to PC)
  • Function-like syntax (optional)
  • Inverted DUP/SWAP special opcodes from "bottom" of stack (a.k.a. pseudo-variables)
  • PUSH<T> for native Go types
  • PUSH(v) length detection
  • Macros
  • Compiler-state assertions (e.g. expected stack depth)
  • Automatic stack permutation
  • Standalone compiler
  • In-process EVM execution (geth)
  • Debugger
    • Stepping
    • Breakpoints
    • Programmatic inspection (e.g. native Go tests at opcode resolution)
      • Memory
      • Stack
    • User interface
  • Source mapping
  • Coverage analysis
  • Fork testing with RPC URL
Documentation

The specialops Go documentation covers all functionality.

Examples

Hello world

TODO: link to Go playground; for now, here's the real implementation.

The specialops Go package has a minimal footprint to allow for dot-importing, making all exported symbols available. TODO: expand on the implications, rationale, and recommendations as this goes against the style guide.

import . github.com/solidifylabs/specialops

…

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))),
}

// ----- COMPILE -----
bytecode, err := code.Compile()
// ...

// ----- EXECUTE -----

result, err := code.Run(nil /*callData*/ /*, [runopts.Options]...*/)
// ...

// ----- DEBUG (Programmatic) -----

dbg, results := code.StartDebugging(nil /*callData*/ /*, Options...*/)
defer dbg.FastForward() // best practice to avoid resource leaks

state := dbg.State() // is updated on calls to Step() / FastForward()

for !dbg.Done() {
  dbg.Step()
  fmt.Println("Peek-a-boo", state.ScopeContext.Stack().Back(0))
}

result, err := results()
//...
Other examples

Acknowledgements

Some of SpecialOps was, of course, inspired by Huff. I hope to provide something different, of value, and to inspire them too.

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

Examples

Constants

View Source
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

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

func PUSHBytes(bs ...byte) types.Bytecoder

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

func PUSHSelector(sig string) types.Bytecoder

PUSHSelector returns a PUSH4 Bytecoder that pushes the selector of the signature, i.e. `sha3(sig)[:4]`.

Types

type Code

type Code []types.Bytecoder

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

func (c Code) Bytecode() ([]byte, error)

Bytecode always returns an error; use Code.Compile instead(), which flattens nested Code instances.

func (Code) Bytecoders

func (c Code) Bytecoders() []types.Bytecoder

Bytecoders returns the Code as a slice of Bytecoders.

func (Code) Compile

func (c Code) Compile() ([]byte, error)

Compile returns a compiled EVM contract with all special opcodes interpreted.

func (Code) Run

func (c Code) Run(callData []byte, opts ...runopts.Option) ([]byte, error)

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

type Inverted vm.OpCode

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().

func (Inverted) Bytecode

func (i Inverted) Bytecode() ([]byte, error)

Bytecode always returns an error.

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>).

func (JUMPDEST) Bytecode

func (j JUMPDEST) Bytecode() ([]byte, error)

Bytecode always returns an error as PUSHJUMPDEST values have special handling inside Code.Compile().

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.

func (Raw) Bytecode

func (r Raw) Bytecode() ([]byte, error)

Bytecode returns `r` unchanged, and a nil error.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL