Caching Block Hashes

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

The AxiomV1 smart contract caches block hashes from the Ethereum 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 Merkle mountain range of the Merkle roots of batches of 1024 block hashes starting from genesis to a recent block. The block hashes committed to by this Merkle mountain range will always be a subset of those whose Merkle roots are stored in the previous format.

Caching Merkle roots of block hashes

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

The cache is updated 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[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 of the block header commitment chain and update historicalRoots accordingly:

  • 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 [block.number - 256, block.number), i.e., if blockhash(startBlockNumber + numFinal - 1) is accessible 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.

  • 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 a Merkle root.

Updating the Merkle mountain range

In order to allow access to block hashes across large block ranges, AxiomV1 stores historic block hashes in a second redundant form by maintaining a Merkle mountain range of the Merkle roots cached in historicalRoots. The latest Merkle mountain range is stored in historicalMMR and is kept updated by updateRecent and appendHistoricalMMR, which has the following function signature:

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

To facilitate asynchronous proving against a Merkle mountain range which may be updated on-chain during proving, we cache commitments to recent values of historicalMMR in the ring buffer mmrRingBuffer.

Reading from the cache

There are two ways to read from the cache. First, users may verify block hashes against Merkle roots in the cache via the isBlockHashValid method. This requires users to provide 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. To verify block hashes against a cached Merkle mountain range, we also providea mmrVerifyBlockHash function.

Second, to access commitments to the entire cache simultaneously, users may access recent Merkle mountain ranges in mmrRingBuffer. This is primarily used for fulfillment of queries by AxiomV1Query, but users may also access it for their own purposes.

Last updated