How Axiom Works
Explaining how Axiom caches historic block hashes and verifies queries against this cache to provide smart contracts with trustless access to on-chain data.
This post describes the demo version of Axiom, which was not intended for production usage and is now deprecated. The Axiom mainnet alpha is now live as of July 5, 2023. See the developer docs for an updated explanation of the Axiom architecture.
Axiom is a ZK coprocessor for Ethereum which provides smart contracts trustless access to historic on-chain data and arbitrary expressive compute. The initial version of Axiom powering our demo at demo.axiom.xyz consists of two technical pieces:
- The
AxiomV0
smart contract which maintains a cache of Merkle roots of groups of 1024 adjacent Ethereum block hashes by accessing recent block hashes available to the EVM and reversing the commitment chain of block headers in ZK. - The
AxiomV0StoragePf
smart contract which verifies ZK validity proofs for data import. These circuits check Merkle-Patricia trie inclusion proofs to prove Ethereum on-chain data against Ethereum block hashes.
On top of this initial release of Axiom, applications can apply verified compute primitives like basic analytics (sum, count, max, min) and cryptographic operations (signature verification, key aggregation) on the imported historic data. In this post, we explain how each of these parts works and why they together enable Axiom to provide smart contracts with trustless access to on-chain data.
What does Axiom prove?
Axiom gives smart contracts trustless access to historic Ethereum on-chain data. This data is committed to in Ethereum block headers, which contain roots of four different Merkle-Patricia tries which encode mappings which comprise all Ethereum data. These are:
- State trie: This is a mapping between
keccak(address)
andrlp(acct)
, whererlp
is the RLP serialization andacct
is the information[nonce, balance, storageRoot, codeHash]
associated to each Ethereum account. - Storage trie: Each account has a storage trie which is a mapping between
keccak(slot)
andrlp(slotValue)
which encodes its local storage, which is a mapping between theuint256
slot anduint256
slot value. - Transaction trie: The transactions trie encodes all transactions in a block in a mapping between the encoded transaction index
rlp(txIndex)
and the serializationrlp(tx)
. - Receipt trie: Finally, the receipts trie commits to a mapping between the encoded receipt index
rlp(receiptIndex)
and the serializationrlp(receipt)
.
The first version of Axiom supports proving all data in state and storage tries. To do so, Axiom trustlessly stores a cache of all historic Ethereum block hashes on-chain and roots trust for all queries into Axiom in this cache.
Caching block hashes in AxiomV0
The AxiomV0
smart contract caches block hashes from the Ethereum history and allows smart contracts to verify them against this cache. To do so, AxiomV0
stores the Keccak Merkle roots of consecutive length 1024 sequences of blocks with block numbers [1024 * x, ..., 1024 * x + 1023]
for an index x
in the mapping
mapping(uint32 => bytes32) public historicalRoots;
Here historicalRoots[startBlockNumber]
holds the hash keccak(prevHash || root || numFinal)
, where prevHash
is the block hash of block startBlockNumber - 1
, root
is the Merkle root of the block hashes with index in [startBlockNumber, startBlockNumber + 1023]
, with block hashes after startBlockNumber + numFinal - 1
replaced by 0
, and numFinal
is the number of block hashes verified in this range of blocks.
To update this block hash cache, we use the fact that each block header in Ethereum contains the hash of the previous block header in the parentHash
field, meaning it commits to all previous block headers. This is implemented in the updateRecent
, updateOld
, or updateHistorical
functions with the following function signatures:
function updateRecent(bytes calldata proofData) external;
function updateOld(
bytes32 nextRoot,
uint32 nextNumFinal,
bytes calldata proofData
) external;
function updateHistorical(
bytes32 nextRoot,
uint32 nextNumFinal,
bytes32[HISTORICAL_NUM_ROOTS] calldata roots,
bytes32[TREE_DEPTH + 1][HISTORICAL_NUM_ROOTS - 1] calldata endHashProofs,
bytes calldata proofData
) external;
These functions verify a ZK proof that there exists a chain of block headers, each of which has Keccak hash included in their child header. They then update historicalRoots
accordingly:
updateRecent
andupdateOld
prove Keccak header chains of length up to 1024.updateHistorical
provides a recursive proof of the validity of Keccak header chains of length128 * 1024
. It adds Merkle roots of each group of 1024 blocks by proving theprevHash
for each group relative to the Merkle root of all128 * 1024
block hashes using the Merkle proofs inendHashProofs
.
These functions emit the event
UpdateEvent(uint32 startBlockNumber, bytes32 prevHash, bytes32 root, uint32 numFinal)
for each update of a Merkle root of 1024 consecutive block hashes. To read from the block hash cache, AxiomV0
provides the isBlockHashValid
method which takes in a witness that a block hash is included in the cache, formatted via
struct BlockHashWitness {
uint32 blockNumber;
bytes32 claimedBlockHash;
bytes32 prevHash;
uint32 numFinal;
bytes32[TREE_DEPTH] merkleProof;
}
This method verifies that merkleProof
is a valid Merkle path for the relevant block hash and checks that the Merkle root lies in the cache.
Verifying storage proofs with Axiom
To prove a piece of Ethereum on-chain data, Axiom first generates a Ethereum light client proof for it. For example, suppose we wish to prove the value at storage slot slot
for address address
at block blockNumber
. This light client proof can be fetched from any Ethereum archive node using the eth_getProof
JSON-RPC call and consists of:
- The block header at block
blockNumber
and in particular thestateRoot
. - A proof of Merkle-Patricia inclusion for the key-value pair
(keccak(address), rlp([nonce, balance, storageRoot, codeHash]))
of the RLP-encoded account data in the state trie rooted atstateRoot
. - A proof of Merkle-Patricia inclusion for the key-value pair
(keccak(slot), rlp(slotValue))
of the storage slot data in the storage trie rooted atstorageRoot
.
Verifying this light client proof requires the trusted block hash blockHash
for block blockNumber
and requires checking:
- The block header is properly formatted, has Keccak hash
blockHash
, and containsstateRoot
. - The state trie proof is properly formatted, has key
keccak(address)
, Keccak hashes of each node along the Merkle-Patricia inclusion proof match the appropriate field in the previous node, and has value containingstorageRoot
. - A similar validity check for the Merkle-Patricia inclusion proof for the storage trie.
Axiom does each of these checks in ZK via the EthBlockStorageProof
circuit, which proves validity of the statement
Assuming the block hash atblockNumber
isblockHash
, the value ofslot
foraddress
atblockNumber
isslotValue
.
To verify this ZK proof, users and applications can use the attestSlots
function in the AxiomV0StoragePf
smart contract, which has the following signature:
function attestSlots(
IAxiomV0.BlockHashWitness calldata blockData,
bytes calldata proof
) external;
This takes in a block hash witness and a ZK proof with public inputs
blockHash
: The claimed block hash in the attestation.blockNumber
: The claimed block number in the attestation.addr
: The claimed address in the attestation.slotArray
: An array of claimed (slot, slotValue) pairs in the account storage of addr.
The function body checks that
blockData
is a valid Merkle inclusion proof into the block hash cacheproof
verifies correctly against our SNARK verifier- the public inputs of
proof
are as claimed.
If all of these checks pass, attestSlot
emits:
SlotAttestationEvent(uint32 blockNumber, address addr, uint256 slot, uint256 slotValue)
and sets the value of keccak(blockNumber || addr || slot || slotValue)
to true
in the mapping
mapping(bytes32 => bool) public slotAttestations;
What's next?
In our description of the inner workings of Axiom so far, the ZK proofs for checking validity of chains of block headers and verifying light client proofs play an important role. This means that understanding how Axiom works requires diving deeper into the ZK circuits powering Axiom. These circuits, implemented in the halo2 proof system, are open-sourced on GitHub. In the coming weeks, we will release more information about these ZK circuits and the framework and libraries we wrote on top of halo2 to implement them.
In the meantime, if you are a developer and would like to build on Axiom, we are looking for early integration partners! To discuss possible applications or learn more:
- Check out our live demo at demo.axiom.xyz and docs at docs.axiom.xyz.
- DM us on Twitter (@axiom_xyz), come chat on Discord, or fill out our interest form. We'd love to support your dapp!
If you'd like to join us in empowering smart contract developers with ZK:
- We are hiring developers to join us in tackling the hard technical problems necessary to develop, scale, and optimize Axiom. Check out our jobs page here or reach out directly at jobs@intrinsictech.xyz.
- If you want to get straight to the code, check out our Github repos. We are open to extensions or contributions!
To stay in touch about Axiom, join our community on Twitter, Telegram, or Discord.