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.
-
JUMPDESTlabels (absolute) -
JUMPDESTlabels (relative toPC) -
PUSH(JUMPDEST)by label with minimal bytes (1 or 2) -
Labeltags; likeJUMPDESTbut don't add to code - Push multiple, concatenated
JUMPDEST/Labeltags as one word -
PUSHSize(T,T)pushesLabeland/orJUMPDESTdistance - Function-like syntax (i.e. Reverse Polish Notation is optional)
- Inverted
DUP/SWAPspecial 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.ChainConfigandvm.Config) - State preloading (e.g. other contracts to call) and inspection (e.g.
SSTOREtesting) - 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.