specops

package module
v0.0.0-...-8d9cde1 Latest Latest
Warning

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

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

README

SpecOps Go Go Reference

specops is a low-level, domain-specific language and compiler for crafting Ethereum VM bytecode. The project also includes a CLI with code execution and terminal-based debugger.

This is a very early release, a weekend project gone rogue. Feedback and contributions appreciated.

special opcodes

Writing bytecode is hard. Tracking stack items is difficult enough, made worse by refactoring that renders every DUP and SWAP off-by-X. Reverse Polish Notation may be suited to stack-based programming, but it's unintuitive when context-switching from Solidity.

There's always a 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 of them are interpreted by the compiler, converting them into regular equivalents, while others are simply compiler hints that leave the resulting bytecode unchanged.

Getting started

See the getting-started/ directory for creating your first SpecOps code. Also check out the examples and the documentation.

Do I have to learn Go?

No.

There's more about this in the getting-started/ README, including the rationale for a Go-based DSL.

Features

New features will be prioritised based on demand. If there's something you'd like included, please file an Issue.

  • JUMPDEST labels (absolute)
  • JUMPDEST labels (relative to PC)
  • Function-like syntax (i.e. Reverse Polish Notation is 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)
  • Automated optimal (least-gas) stack transformations
    • Permutations (SWAP-only transforms)
    • General-purpose (combined DUP + SWAP + POP)
    • Caching of search for optimal route
  • Standalone compiler
  • In-process EVM execution (geth)
  • Debugger
    • Single call frame (via vm.EVMInterpreter)
    • Multiple call frames; i.e. support *CALL methods
    • 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 specops Go documentation covers all functionality.

Examples

Hello world

To run this example Code block with the SpecOps CLI, see the getting-started/ directory.

import . github.com/solidifylabs/specops

…

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) -----
//
// ***** See below for the debugger's terminal UI *****
//

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
Debugger

Key bindings are described in the getting-started/ README.

image

Acknowledgements

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

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

Examples

Constants

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

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

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

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

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() ([]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 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 stack.SetDepth() for caveats. It is best practice to use `Inverted` in conjunction with stack.{Set/Expect}Depth().

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.

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

Jump to

Keyboard shortcuts

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