A small EVM assembler and disassembler with a lisp-like front-end.
You describe a script as a tree of operations — nested opcode tuples, integers, and binaries — and the module lowers it to EVM bytecode:
compile/1 tree of operations -> flat list of opcodes
assemble/1 flat list of opcodes -> bytecode binary
build/1 tree of operations -> bytecode binary (compile |> assemble)
disassemble/1 bytecode binary -> list of opcodescompile/1 handles operand ordering (operands are emitted so they land on
the stack in source order), {:if, …} branching with unique jumpi/
jump_dest labels, and {:push, …} synthesis for binaries/integers.
This is not a toy: Cartouche.VM assembles its EVM scaffolding through
assemble/1, and the assembler builds the small scripts cartouche injects
during simulation (e.g. the eth_estimateGas guard that reverts when
tx.origin is zero). It is also genuinely handy for hand-writing and
inspecting short scripts in tests.
Usage
Build EVM bytecode from a tree of operations:
Cartouche.Assembly.build([
{:log1, 0, 0, 55}
])produces 0x603760006000a1, which decompiles
to log(memory[0x00:0x00], [0x37]), i.e. the assembly:
0000 60 PUSH1 0x37
0002 60 PUSH1 0x00
0004 60 PUSH1 0x00
0006 A1 LOG1Scripts can branch — e.g. revert if tx.origin is zero (as during an
eth_estimateGas):
Cartouche.Assembly.build([
{:mstore, 0, 0x01020304},
{:if, :origin, {:revert, 28, 4}, {:return, 0, 0}}
])
Summary
Functions
Assmbles opcodes into raw evm bytecode
Compiles and assembles assembly operations.
Compiles a lisp-like assembly tree into a flat list of low-level opcodes
(which assemble/1 then turns into bytecode).
Returns a simple EVM program that returns the input code as the output of an Ethereum "initCode" constructor.
Disassembles opcodes from raw evm bytecode to opcodes.
Returns a textual representation of the given operation.
Types
@type opcode() :: atom() | {:push, non_neg_integer(), binary()} | {:dup, non_neg_integer()} | {:swap, non_neg_integer()} | {:invalid, binary()} | {:jump_ptr, integer()} | {:jump_dest, integer()}
Functions
Assmbles opcodes into raw evm bytecode
Examples
iex> [{:push, 0, ""}, {:push, 4, <<0x11, 0x22, 0x33, 0x44>>}, :mstore, {:push, 1, <<4>>}, {:push, 1, <<28>>}, :revert]
...> |> Cartouche.Assembly.assemble()
<<95, 99, 17, 34, 51, 68, 82, 96, 4, 96, 28, 253>>
iex> [
...> {:push, 2, <<0x01, 0x02>>},
...> {:push, 1, <<0>>},
...> :mstore,
...> :callvalue,
...> {:push, 1, <<0>>},
...> :sub,
...> {:jump_ptr, 0},
...> :jumpi,
...> {:push, 1, <<2>>},
...> {:push, 1, <<30>>},
...> :revert,
...> {:jump_dest, 0},
...> {:push, 1, <<2>>},
...> {:push, 1, <<31>>},
...> :revert
...> ]
...> |> Cartouche.Assembly.assemble()
...> |> Cartouche.Hex.to_hex()
"0x6101026000523460000362000014576002601efd5b6002601ffd"
iex> [
...> {:dup, 2},
...> {:swap, 3},
...> {:invalid, ~h[0x010203]}
...> ]
...> |> Cartouche.Assembly.assemble()
...> |> Cartouche.Hex.to_hex()
"0x8192fe010203"
Compiles and assembles assembly operations.
Examples
iex> use Cartouche.Hex
...> [
...> {:mstore, 0, ~h[0x11223344]},
...> {:revert, 28, 4}
...> ]
...> |> Cartouche.Assembly.build()
...> |> to_hex()
"0x63112233446000526004601cfd"
Compiles a lisp-like assembly tree into a flat list of low-level opcodes
(which assemble/1 then turns into bytecode).
Accepts any of:
- a list of operations — each is compiled and the results concatenated;
- an operand-bearing opcode tuple
{opcode, arg1, …, argN}— every argument is compiled and emitted in reverse order (so operands land on the stack in source order), followed by the opcode.Nmust equal the opcode's declared operand count in@opcodes; - an
{:if, cond, non_zero, zero}tuple — compiled to ajumpi/jump_destbranch with a unique label; - a binary of ≤ 32 bytes — emitted as
{:push, byte_size, bytes}; - a non-negative integer — encoded big-endian via
:binary.encode_unsigned/1, then pushed; - a no-operand opcode atom (e.g.
:add,:stop) or:self_code_sz— returned unchanged (the one scalar-return case).
Raises InvalidAssembly on an unknown or wrong-arity opcode tuple, a binary
larger than 32 bytes, or any other unrecognized term.
Examples
iex> use Cartouche.Hex
...> [
...> {:mstore, 0, ~h[0x11223344]},
...> {:revert, 4, 28}
...> ]
...> |> Cartouche.Assembly.compile()
[{:push, 4, ~h[0x11223344]}, {:push, 1, <<0>>}, :mstore, {:push, 1, <<28>>}, {:push, 1, <<0x04>>}, :revert]
Returns a simple EVM program that returns the input code as the output of an Ethereum "initCode" constructor.
Examples
iex> use Cartouche.Hex
...> Cartouche.Assembly.constructor(~h[0xaabbcc])
...> |> to_hex()
"0x60036200000e60003960036000f3aabbcc"
Disassembles opcodes from raw evm bytecode to opcodes.
Examples
iex> Cartouche.Assembly.disassemble(~h[0x6101026000523460000362000014576002601efd5b6002601ffd])
[
{:push, 2, <<0x01, 0x02>>},
{:push, 1, <<0>>},
:mstore,
:callvalue,
{:push, 1, <<0>>},
:sub,
{:push, 3, <<0, 0, 20>>},
:jumpi,
{:push, 1, <<2>>},
{:push, 1, <<30>>},
:revert,
:jumpdest,
{:push, 1, <<2>>},
{:push, 1, <<31>>},
:revert
]
iex> Cartouche.Assembly.disassemble(~h[0x8192fe010203])
[
{:dup, 2},
{:swap, 3},
{:invalid, ~h[0x010203]}
]
Returns a textual representation of the given operation.
Examples
iex> Cartouche.Assembly.show_opcode(:add)
"ADD"
iex> Cartouche.Assembly.show_opcode({:push, 5, <<1,2,3,4,5>>})
"PUSH5 0x0102030405"