Axiom V2 Docs Old
  • Introduction
    • What is Axiom?
    • Quickstart
  • Examples
    • Autonomous Airdrop
      • AxiomREPL Code
      • Contract
      • Web App
      • DataQuery-only Version
  • Developers
    • Axiom for Developers
    • Specifying a Query into Axiom
    • AxiomREPL
      • AxiomREPL Examples
    • Exporting a Client Side Prover
    • Handling Axiom Callbacks
    • Common Issues
      • Callback Debugging
  • SDK and REPL Reference
    • Axiom SDK Reference
      • QueryBuilderV2
      • Data Subqueries
        • Header Subquery
        • Account Subquery
        • Storage Subquery
        • Transaction Subquery
        • Receipt Subquery
        • Solidity Nested Mapping Subquery
    • AxiomREPL Reference
      • Circuit Types
      • Circuit Functions
      • Data Functions
      • Compute Functions
  • Protocol Design
    • Architecture Overview
    • Caching Block Hashes
    • Axiom Query Protocol
      • Axiom Query Format
    • ZK Circuits for Axiom Queries
    • Ethereum On-chain Data
    • Guardrails
  • Transparency and Security
    • KZG Trusted Setup
    • Contract Addresses
    • On-chain ZK Verifiers
    • Security
  • Zero Knowledge Proofs
    • Introduction to ZK
    • ZK Examples
    • Getting Started with halo2
    • halo2-repl
  • Additional Resources
    • Axiom V2 Explorer
    • Github
    • Website
    • Telegram
    • Discord
    • Axiom V1 Docs
Powered by GitBook
On this page
  • Caching Merkle roots of block hashes
  • Updating the padded Merkle mountain range
  • Reading from the cache
  1. Protocol Design

Caching Block Hashes

How AxiomV2Core caches block hashes and how to interact with them.

PreviousArchitecture OverviewNextAxiom Query Protocol

Last updated 1 year ago

The AxiomV2Core smart contract caches block hashes from Ethereum's history and allows smart contracts to verify them against this cache. These historic block hashes are stored in two ways:

  • As a Merkle root corresponding to a batch of block numbers [startBlockNumber, startBlockNumber + numFinal) where startBlockNumber is a multiple of 1024, and numFinal is in [1,1024]. This is stored in historicalRoots.

  • As a padded of the Merkle roots of batches of 1024 block hashes starting from genesis to a recent block.

Caching Merkle roots of block hashes

AxiomV2Core caches the Merkle roots of consecutive sequences of blocks, up to 1024 in total, in the mapping

mapping(uint32 => bytes32) public historicalRoots;

Here historicalRoots[startBlockNumber] is 0x0 unless the block hashes for block numbers [startBlockNumber, startBlockNumber + numFinal) have already been verified. In the latter case,

historicalRoots[startBlockNumber] = keccak(prevHash . root . numFinal)

where . denotes concatenation,

  • prevHash is the block hash of block startBlockNumber - 1,

  • root is the Merkle root of the block hashes with block numbers in [startBlockNumber, startBlockNumber + numFinal), padded with bytes32(0x0) to form the 1024 leaves of a Merkle tree, and

  • numFinal in [1, 1024] is the number of block hashes verified in this range of blocks.

The cache is updated via the interface IAxiomV2Update by calling 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[128] calldata roots,
    bytes32[11][127] calldata endHashProofs,
    bytes calldata proofData
) external;

These functions verify a ZK proof of the block header commitment chain and update historicalRoots accordingly:

  • updateOld: Verifies a zero-knowledge proof that proves the block header commitment chain from [startBlockNumber, startBlockNumber + 1024) is correct, where block startBlockNumber + 1024 must already be cached by the smart contract. This stores a single new Merkle root in the cache.

  • updateHistorical: Same as updateOld except that it uses a different zero-knowledge proof to prove the block header commitment chain from [startBlockNumber, startBlockNumber + 2 ** 17). Requires block startBlockNumber + 2 ** 17 to already be cached by the smart contract. This stores 2 ** 7 = 128 new Merkle roots in the cache.

These functions emit the event

event UpdateEvent(
    uint32 startBlockNumber,
    bytes32 prevHash,
    bytes32 root,
    uint32 numFinal
);

for each update of historicalRoots.

Updating the padded Merkle mountain range

  • A Merkle mountain range over Merkle roots of 1024 consecutive block hashes

  • A padded Merkle root of part of the most recent 1024 block hashes.

The latest padded Merkle mountain range is stored in blockhashPmmr. The mapping

mapping(uint32 => bytes32) public pmmrSnapshots;

caches commitments to recent values of blockhashPmmr to faciliate asynchronous proving against a Merkle mountain range which may be updated on-chain during proving.

function appendHistoricalMMR(
    uint32 startBlockNumber, 
    bytes32[] calldata roots, 
    bytes32[] calldata prevHashes
) external;

This function batch appends new Merkle roots in historicalRoots which are not already committed to in blockhashPmmr (usually because they were added by updateOld).

Reading from the cache

There are two ways to read from the cache, encapsulated by the IAxiomV2Verifier interface:

  • Verifying the block hash of a block within the last 256 most recent blocks can be done through isRecentBlockHashValid.

  • To verify a historical block hash, one should use the isBlockHashValid method which takes in a struct IAxiomV2Verifier.BlockHashWitness. This provides a Merkle proof of a block hash into one of the Merkle roots stored in historicalRoots. The isBlockHashValid method verifies that the Merkle proof is a valid Merkle path for the relevant block hash and checks that the Merkle root lies in the AxiomV2Core cache.

struct BlockHashWitness {
    uint32 blockNumber;
    bytes32 claimedBlockHash;
    bytes32 prevHash;
    uint32 numFinal;
    bytes32[] merkleProof;
}

updateRecent: Verifies a zero-knowledge proof that proves the block header commitment chain from [startBlockNumber, startBlockNumber + numFinal) is correct, where startBlockNumber is a multiple of 1024, and numFinal is in [1,1024]. This reverts unless startBlockNumber + numFinal - 1 is in 256 most recent block hashes, i.e., if blockhash(startBlockNumber + numFinal - 1) is from within the smart contract at the block this function is called. The zero-knowledge proof checks that each parent hash is in the block header of the next block, and that the block header hashes to the block hash. This is accepted only if the block hash of startBlockNumber + numFinal - 1, according to the zero-knowledge proof, matches the block hash according to the EVM.

In order to allow access to block hashes across large block ranges, AxiomV2Core stores historic block hashes in a second redundant form by maintaining a padded which commits to a continguous chain of block hashes starting from genesis using:

Updates to blockhashPmmr are made using newly verified Merkle roots added to . Updates are made either alongside historicalRoots updates in or by calling appendHistoricalMMR, which has the following function signature:

Merkle mountain range
Keccak
accessible
Merkle mountain range
historicalRoots
updateRecent