Caching Block Hashes
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)
wherestartBlockNumber
is a multiple of1024
, andnumFinal
is in[1,1024]
. This is stored inhistoricalRoots
. - As a padded Merkle mountain range 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 Keccak 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 blockstartBlockNumber - 1
,root
is the Merkle root of the block hashes with block numbers in[startBlockNumber, startBlockNumber + numFinal)
, padded withbytes32(0x0)
to form the1024
leaves of a Merkle tree, andnumFinal
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:
updateRecent
: Verifies a zero-knowledge proof that proves the block header commitment chain from[startBlockNumber, startBlockNumber + numFinal)
is correct, wherestartBlockNumber
is a multiple of1024
, andnumFinal
is in[1,1024]
. This reverts unlessstartBlockNumber + numFinal - 1
is in 256 most recent block hashes, i.e., ifblockhash(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 ofstartBlockNumber + 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 blockstartBlockNumber + 1024
must already be cached by the smart contract. This stores a single new Merkle root in the cache.updateHistorical
: Same asupdateOld
except that it uses a different zero-knowledge proof to prove the block header commitment chain from[startBlockNumber, startBlockNumber + 2 ** 17)
. Requires blockstartBlockNumber + 2 ** 17
to already be cached by the smart contract. This stores2 ** 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
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 Merkle mountain range which commits to a contiguous chain of block hashes starting from genesis using:
- 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.
Updates to blockhashPmmr
are made using newly verified Merkle roots added to historicalRoots
. Updates are made either alongside historicalRoots
updates in updateRecent
or by calling appendHistoricalMMR
, which has the following function signature:
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 throughisRecentBlockHashValid
. - To verify a historical block hash, one should use the
isBlockHashValid
method which takes in a structIAxiomV2Verifier.BlockHashWitness
. This provides a Merkle proof of a block hash into one of the Merkle roots stored inhistoricalRoots
. TheisBlockHashValid
method verifies that the Merkle proof is a valid Merkle path for the relevant block hash and checks that the Merkle root lies in theAxiomV2Core
cache.
struct BlockHashWitness {
uint32 blockNumber;
bytes32 claimedBlockHash;
bytes32 prevHash;
uint32 numFinal;
bytes32[] merkleProof;
}