Fault Proof VM: Cannon
Cannon is Optimism's default Fault Proof Virtual Machine (FPVM). Cannon has two main components:
- Onchain
MIPS.sol
: EVM implementation to verify execution of a single MIPS instruction. - Offchain
mipsevm
: Go implementation to produce a proof for any MIPS instruction to verify onchain.
Between these two modes of operation, the witness data is essential, to reproduce the same instruction onchain as offchain.
Onchain MIPS.sol
This is an onchain implementation of big-endian 32-bit MIPS instruction execution.
This covers MIPS III, R3000, as required by the mips
Go compiler/runtime target.
The syscall instruction is implemented to simulate a minimal subset of the Linux kernel, just enough to serve the needs of a basic Go program: allocate memory, read/write to certain file-descriptors, and exit.
Note that this does not include concurrency related system calls: when running Go programs, the GC has to be disabled, since it runs concurrently. This is done by patching out specific runtime functions that start the GC, by simply inserting jumps to the return-address, before the functions spin up any additional threads.
Offchain mipsevm
This is an instrumented emulator, also running big-endian 32-bit MIPS, that executes one step at a time, and maintains the same state as used for onchain execution. The difference is that it has access to the full memory, and pre-image oracle. And as it executes each step, it can optionally produce the witness data for the step, to repeat it onchain.
The Cannon CLI is used to load a program into an initial state, transition it to N steps quickly without witness generation, and 1 step while producing a witness.
mipsevm
is instrumented for proof generation and handles delay-slots by isolating each individual instruction
and tracking nextPC
to emulate the delayed PC
changes after delay-slot execution.
Witness Data
There are 3 types of witness data involved in onchain execution:
Packed State
The Packed State is provided in every executed onchain instruction. See Cannon VM Specs (opens in a new tab) for details on the state structure.
The packed state is small! The State
data can be packed in such a small amount of EVM words,
that it is more efficient to fully provide it, than to create a proof for each individual part involved in execution.
The program stops changing the state when exited
is set to true. The exit-code is remembered,
to determine if the program is successful, or panicked/exited in some unexpected way.
This outcome can be used to determine truthiness of claims that are verified as part of the program execution.
Memory Proofs
Simplicity is key here. Previous versions of Cannon used to overlap memory and register state, and represent it with a Merkle Patricia Trie. This added a lot of complexity, as there would be multiple dynamically-sized MPT proofs, with read and write operations, during a single instruction.
Instead, memory in Cannon is now represented as a binary merkle tree:
The tree has a fixed-depth of 27 levels, with leaf values of 32 bytes each.
This spans the full 32-bit address space: 2**27 * 32 = 2**32
.
Each leaf contains the memory at that part of the tree.
The tree is efficiently allocated, since the root of fully zeroed sub-trees
can be computed without actually creating the full-subtree: zero_hash[d] = hash(zero_hash[d-1], zero_hash[d-1])
,
until the base-case of zero_hash[0] == bytes32(0)
.
Nodes in this memory tree are combined as: out = keccak256(left ++ right)
, where ++
is concatenation,
and left
and right
are the 32-byte outputs of the respective sub-trees or the leaf values themselves.
Individual leaf nodes are not hashed.
The proof format is a concatenated byte array of 28 bytes32
. The first bytes32
is the leaf value,
and the remaining 27 bytes32
are the sibling nodes up till the root of the tree,
along the branch of the leaf value, starting from the bottom.
To verify the proof, start with the leaf value as node
, and combine it with respective sibling values:
node = keccak256(node ++ sibling)
or node = keccak256(sibling ++ node)
,
depending the position of node
at that level of the tree.
During the onchain execution, each instruction only executes 1 or 2 memory reads, followed by 0 or 1 writes, where the write is over the same memory as was last read.
The memory access is specifically:
- instruction (4 byte) read at
PC
- load or syscall mem read, always aligned 4 bytes, read at any
addr
- store or syscall mem write, always aligned 4 bytes, at the same
addr
Writing only once, at the last read leaf, also means that the leaf can be safely updated and the same proof-data
that was used to verify the read, can be used to reconstruct the new memRoot
of the memory tree,
since all sibling data that is combined with the new leaf value was already authenticated during the read part.
Pre-Image Data
Pre-image data is accessed through syscalls exclusively. The OP-stack fault-proof Pre-image Oracle specs (opens in a new tab) define the ABI for communicating pre-images.
This ABI is implemented by the VM by intercepting the read
/write
syscalls to specific file descriptors. See Cannon VM Specs (opens in a new tab) for more details.
The data is loaded into PreimageOracle.sol
using the respective loading function based on the pre-image type.
And then retrieved during execution of the read
syscall.
Note that although the oracle provides up to 32 bytes of the pre-image, Cannon only supports reading at most 4 bytes at a time, to unify the memory operations with regular load/stores.
Usage in Dispute Game
The onchain MIPS execution functions as base-case of an interactive dispute game. The dispute game narrows down a sequence of disputed instructions to a single disputed instruction. This instruction can then be executed onchain to determine objective truth.
The Dispute Game itself is out of scope for Cannon: Cannon is just one example of a fault-proof VM that can be used to resolve disputes. See MIPS reference for an alternative FPVM that can be used.