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 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) and rlp(acct), where rlp is the RLP serialization and acct 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) and rlp(slotValue) which encodes its local storage, which is a mapping between the uint256 slot and uint256 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 serialization rlp(tx).
  • Receipt trie: Finally, the receipts trie commits to a mapping between the encoded receipt index rlp(receiptIndex) and the serialization rlp(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 and updateOld prove Keccak header chains of length up to 1024.
  • updateHistorical provides a recursive proof of the validity of Keccak header chains of length 128 * 1024. It adds Merkle roots of each group of 1024 blocks by proving the prevHash for each group relative to the Merkle root of all 128 * 1024 block hashes using the Merkle proofs in endHashProofs.

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 the stateRoot.
  • 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 at stateRoot.
  • 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 at storageRoot.

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 contains stateRoot.
  • 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 containing storageRoot.
  • 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 at blockNumber is blockHash, the value of slot for address at blockNumber is slotValue.

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 cache
  • proof 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:

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
  • 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.