# 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)`

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:

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