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.
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.
See the getting-started/
directory for creating your first SpecOps code. Also check out the examples and the documentation.
No.
There's more about this in the getting-started/
README, including the rationale for a Go-based DSL.
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 toPC
) -
PUSH(JUMPDEST)
by label with minimal bytes (1 or 2) -
Label
tags; likeJUMPDEST
but don't add to code - Push multiple, concatenated
JUMPDEST
/Label
tags as one word -
PUSHSize(T,T)
pushesLabel
and/orJUMPDEST
distance - 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
- Permutations (
- Standalone compiler
- In-process EVM execution (geth)
- Full control of configuration (e.g.
params.ChainConfig
andvm.Config
) - State preloading (e.g. other contracts to call) and inspection (e.g.
SSTORE
testing) - Message overrides (caller and value)
- Full control of configuration (e.g.
- 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
The specops
Go
documentation covers all
functionality.
To run this example Code
block with the SpecOps CLI, see the getting-started/
directory.
import . github.com/arr4n/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()
//...
- Verbatim reimplementation of well-known contracts
- EIP-1167 Minimal Proxy (original)
- 0age/metamorphic (original)
- Verbose version with explanation of SpecOps functionality + an alternative with automated stack transformation (saves a whole 3 gas!)
- Succinct version as if writing production code
- Monte Carlo approximation of pi
sqrt()
as seenon TVinprb-math
(original)
Key bindings are described in the getting-started/
README.
Some of SpecOps was, of course, inspired by Huff. I hope to provide something different, of value, and to inspire them too.