Axiom Query Format

A technical specification for on-chain queries into Axiom.

A batch query into Axiom allows a user to access up to 64 pieces of historic on-chain information simultaneously in a smart contract. These queries allow on-chain applications to act based on the on-chain history of Ethereum in a fully trustless way.

Summary

Batch queries consist of up to 64 individual queries, which of which queries information from one of:

  • blockNumber (uint32, required) -- any block number up to the current block

  • address (address, optional) -- any address on Ethereum

  • slot (uint256, optional) -- any storage slot in contract storage

Together, these specify historic data from historic blocks, accounts, and contract storage. Batch queries are identified by:

  • query -- a serialized form of the query

  • keccakQueryResponse -- a Merkle-ized form of the query

Note that keccakQueryResponse can be uniquely determined from query, but it contains additional information from the on-chain history. We describe both of these formats below.

Query format

The batch query is serialized as a list with entries:

  • versionIdx (uint8, required) -- a version byte allowing specification of the query type

  • length (uint32, required) -- the number of queries in the batch

  • encodedQueries (list, required) -- a list of serialized individual queries

Here, encodedQueries is a list which specifies the individual pieces of information from block headers, accounts, and storage slots being queried. The entries of encodedQueries are length 4 lists containing:

  • length (uint8, required) -- the number of entries below which are not null, either 1, 2, or 3.

  • blockNumber (uint32, required) -- the block number

  • address (address, optional) -- the address

  • slot (uint256, optional) -- the storage slot in local account storage for address

If slot is non-empty, then address must be non-empty.

Query response format

Axiom responds to queries by committing to them on-chain in a Merkle-ized format. This allows Axiom to fulfill queries into large amounts of data without paying to store each piece in contract storage. Axiom provides a keccakQueryResponse and poseidonQueryResponse for each query.

Keccak response format

The primary way to access results from Axiom is via the keccakQueryResponse, which is determined by

keccakQueryReponse = keccak(blockResponseRoot . accountResponseRoot . storageResponseRoot)

where blockResponseRoot, accountResponseRoot, and storageResponseRoot are Merkle roots of queries into block headers, accounts, and account storage. These are determined as

  • blockResponseRoot -- the Keccak Merkle root of a tree with 64 leaves given by keccak(blockHash . blockNumber) . Empty leaves are padded with bytes32(0x0).

  • accountResponseRoot -- the Keccak Merkle root of a tree with 64 leaves given by keccak(blockNumber . address . keccak(nonce . balance . storageRoot . codeHash)). Empty leaves are padded with bytes32(0x0).

  • storageResponseRoot -- the Keccak Merkle root of a tree with 64 leaves given by keccak(blockNumber . address . slot . value). Empty leaves are padded with bytes32(0x0).

In each of these data structures, the individual components and types are:

  • blockHash (bytes32) -- the block hash

  • blockNumber (uint32) -- the block number

  • address (address) -- the Ethereum address

  • nonce (uint64) -- the account nonce

  • balance (uint96) -- the account balance

  • storageRoot (bytes32) -- the storage root of the relevant account

  • codeHash (byte32) -- the code hash of the relevant account

  • slot (uint256) -- the storage key in the account storage

  • value (uint256) -- the value in the account storage

Poseidon response format

Axiom also provides an encoding of the response using the Poseidon hash function instead of Keccak. This encoding may be useful for applications that wish to use ZK proofs to read from the results. In particular, we define the poseidonBlockResponse, poseidonAccountResponse, and poseidonStorageResponse by:

poseidonBlockResponse -- the Poseidon Merkle root of a tree with 64 leaves given by

poseidon(blockHash, blockNumber, poseidon_tree_root(blockHeaderFields))

where poseidon_tree_root denotes the Poseidon Merkle root of the parsed fields of the block header. Empty leaves are padded with 0.

poseidonAccountResponse -- the Poseidon Merkle root of a tree with 64 leaves given by

poseidon(
    poseidon(blockHash, blockNumber, poseidon_tree_root(blockHeaderFields)), 
    poseidon(stateRoot, address, poseidon_tree_root(accountState))
)

where poseidon_tree_root denotes the Poseidon Merkle root of the parsed fields nonce, balance, storageRoot, and codeHash of the account. Empty leaves are padded with 0.

poseidonStorageResponse -- the Poseidon Merkle root of a tree with 64 leaves given by

poseidon(
    poseidon(blockHash, blockNumber, poseidon_tree_root(blockHeaderFields)), 
    poseidon(stateRoot, address, poseidon_tree_root(accountState)),
    poseidon(storageRoot, slot, value)
)

Empty leaves are padded with 0.

Last updated