FullRootHash
Welcome to Part II of our deep dive into the ZKStack's crosschain architecture. This two-part series explores the technical structure of the upcoming ZKStack protocol upgrade introducing a ZKChain ecosystem that allows easy new rollup ZKChain creation, builtin crosschain functionality and cheaper settlement cost by settling on another ZKChain instead of L1.
In Part I, we delineated the three critical Merkle tree implementations that form the backbone of secure cross-layer communication in the ZKChain ecosystem: the L2ToL1LogsTree
, the ChainTree
, and the SharedTree
. We explored how these trees are structured, their leaves and roots, and how they collectively enable the hierarchical settlement architecture that powers ZKStack's cross-chain capabilities.
Part II puts this theoretical foundation into practice by illustrating the most important use case of this hierarchy of trees: how a ZKChain that settles on a special whitelisted ZKChain, named Gateway
in the ZK Stack, (instead of settling directly on L1) constructs its recursive proof for crosschain token transfers. We'll walk through the complete process from token deposits to withdrawal finalization, demonstrating how the Gateway
acts as an intermediary settlement layer while maintaining security guarantees that ultimately trace back to L1.
Through concrete examples of log inclusion proofs, we'll show how the tree structures described in Part I enable seamless cross-chain communication, whether between L2-to-L1 or L2-to-L2 layers, while preserving backward compatibility and reducing settlement costs.
In general, any ZKChain that allows other ZKChains to settle on it is referred to as a Settlement Layer. By the time of writing, the Gateway
is the only available Settlement Layer other than L1.
This series describes the current state of the ZKChain ecosystem and its internal procedures as of time of writing. As the ecosystem evolves, some described features may be updated in the future.
When an L2 ZKChain is created, a set of smart contracts are deployed on L1, altogether called the DiamondProxy
of the ZKChain. This set of smart contracts perform key operations regarding the ZKChain's state transition (most notably, batch commitment and verification) as well as store any states and configuration regarding this chain. By default, L1 hosts the DiamondProxy
for all newly created ZKChains and is the Settlement Layer of any newly created L2 ZKChain.
At any later point, it is possible that an L2 ZKChain "migrates" its settlement layer to Gateway
. In this case, a new DiamondProxy
for the ZKChain is deployed on Gateway
, it is initialized with the states from L1 and the ZKChain now interacts with Gateway
regarding batches publication. Note that the inverse procedure, essentially migrating a ZKChain's settlement layer from Gateway
to L1, is also possible.
Even if Gateway
is the Settlement Layer for some ZKChains, the L1 Ethereum network is the only chain which guarantees security for all ZKChains. In essence, Gateway
itself settles on L1 and each of its batches also contains a commitment to the batches of all the ZKChains which are settled on top of it. So all ZKChains are eventually settled on L1 at some point. The difference lies in the fact that some chains publish all their data on L1, while other chains publish an aggregated commitment of their data to L1 through the Gateway's batches.
FullRootHash
The transactions of a ZKChain are published in batches to its Settlement Layer. In particular, to settle a batch on the Settlement Layer means to commit, prove and execute the batches on the Settlement Layer's DiamondProxy
, either this is L1 or Gateway
.
To commit a batch, each ZKChain uses the fullRootHash
which is in itself not a merkle root but a hash of two separate merkle roots:
localLogsRootHash
of the ZKChain's L2ToL1LogsTree
: representing the finalized or initiated cross-chain transactions included in the batch;aggregatedRoot
of the ZKChain's SharedTree
: its value depends on whether the ZKChain is a Settlement Layer or not:
chainId
, maintaining the same value in all batches.Gateway
: a root hash which commits to the chainRoots
of all ZKChains that settle on Gateway.
This universal settlement scheme not only preserves the structure of the three Merkle Trees but also maintains the same Rollup-To-SettlementLayer communication style regardless of the involved layers, i.e. whether it is L2-to-L2 or L2-to-L1 commitment, thus maintaining backward compatibility.
Our main purpose in this part is to unravel the L2 log inclusion proofs on L1. Specifically, consider an L2-to-L1 log of a ZKChain settling on Gateway which is contained within a settled batch on L1 as a leaf in the ZKChain's L2ToL1LogsTree
. Since this log is committed a Gateway's batch, a user should be able to construct a proof of inclusion to prove its existence. For example, the log could be that of a token transfer and the user should prove its existence in order to unlock the funds on L1.
Keep in mind that a cross-chain token transfer follows the general principle of:
Even if the transfer log is always committed through a fullRootHash
, the format of the inclusion proof depends on whether the ZKChain, where the log was produced, is settling on L2 or L1. Can you see why?
In the following, we will follow the use case of an L1 token withdrawal to illustrate the inclusion proofs structure in both cases. For completeness, and to give some extra pointers to readers who wish to dive more into the codebase, we first describe two preliminary steps before withdrawing funds: i) bridging funds from L1 to a ZKChain and ii) initiating a withdrawal on the ZKChain.
Imagine a user transferring an ERC20 token from L1 to a ZKChain. This is possible by calling the requestL2TransactionTwoBridges
function of the Bridgehub
contract. This function sends a rollup priority transaction request directly from the ZKChain's DiamondProxy
facet.
Different flow is followed depending on whether the destination is a ZKChain settling on L1 or Gateway:
Gateway
, which is then responsible to relay it to the destination ZKChain.They have different routes because, at the moment, a ZKChain accepts cross-chain interactions only from its Settlement Layer.
Suppose the user would like to withdraw their funds back to L1 after some time. The user initiates the withdrawal by calling the withdraw
function of the L2AssetRouter
contract on the ZKChain, which will first burn the funds on the ZKChain and then simply send a message to L1 through the L1Messenger
contract.
Note that this message will become an L2ToL1Log
leaf that makes up the batch's fullRootHash
committed to L1 or Gateway
.
To finalize the withdrawal on L1, the user needs to call the finalizeWithdrawal
function on the L1AssetRouter
contract with the following information:
chainId
: the ZKChain's chainId, where the withdrawal has been initiated,l2BatchNumber
, l2TxNumberInBatch
: the batch number and transaction number within the batch to verify withdrawal,l2MessageIndex
: the leaf index of the L2ToL1LogsTree
, which will be used in combination with the merkleProof
later,message
: the bytes
message itself which is used to form the corresponding log included in the L2ToL1LogsTree
,merkleProof
: the inclusion proof that verifies that the L2Log
from this message
is at index l2MessageIndex
of the L2ToL1LogsTree
of batch number l2BatchNumber
from the ZKChain with chainId
.The structure of the merkleProof
depends on whether the ZKChain is settling on L1 or on Gateway. Let's analyze these two cases separately.
An L2 chain settles on L1. Hence to verify an L2 chain token withdrawal, one is required to prove an L2 Log inclusion on the chain's DiamondProxy
on L1.
When an L2 ZKChain settles a batch on L1, it stores its logs root, i.e. the fullRootHash
, in the chain's corresponding DiamondProxy
.
Recall that the fullRootHash
combines the roots of L2ToL1LogsTree
and SharedTree
, i.e. fullRootHash = hash(localLogsRootHash, aggregatedRoot)
.
Hence the merkleProof
should include a path that hashes from the L2ToL1Log
at index l2MessageIndex
all the way up to the localLogsRootHash
(root of the L2ToL1LogsTree
) as well as one extra element, the aggregatedRoot
, to arrive at the committed fullRootHash
. This makes the path length of merkleProof
to equal the height of L2ToL1LogsTree
+ 1.
Now let's consider an L2 chain which settles on Gateway
, which in turn settles on L1. To verify token withdrawal of this chain, one needs to prove an L2 Log inclusion on the ZKChain's DiamondProxy
on L1 which, however, will need to go through the Gateway's DiamondProxy
on L1, since the corresponding aggregatedRoot
is committed with the Gateway
's batch.
Recall that the user's withdrawal log, i.e. the L2 Log, resides in this case in the right subtree of the L1 committed fullRootHash
as follows:
L2ToL1Log
leaf of the ZKChain's L2ToL1LogsTree
.L2ToL1LogsTree
, LocalLogsRootHash
, is hashed together with an empty SharedTree
, i.e. a default aggregatedRoot
, to calculate the ZKChain's fullRootHash
for a specific batch. This root is settled on Gateway
.fullRootHash
to the ZKChain's chainTree
, leading to an updated chainRoot
.chainRoot
leaf in the SharedTree
, leading to an updated aggregatedRoot
.LocalLogsRootHash
with the aggregatedRoot
to retrieve the final fullRootHash
which is committed on L1 along with a batch.Thus, in this case, the merkleProof
should include a path starting from the ZKChain's L2ToL1LogsTree
log leaf and goes all the way up to the aggregatedRoot
of the SharedTree
.
The proof verification consists of the following 3 steps:
fullRootHash
. Similar to the L2 log inclusion described previously, use a LocalLogsTree
merkle path to reconstruct the LocalLogsRootHash
starting from the L2ToL1Log
leaf with index l2MessageIndex
and finally hash with the chain's default aggregatedRoot
.chainRoot
of the ZKChain's ChainTree
with a ChainTree
merkle path which starts from the fullRootHash
leaf calculated in Step 1.aggregatedRoot
by using a SharedTree
merkle path starting from the ChainRoot
leaf calcualted in Step 2.By aggregating the merkle paths from the above 3 steps within the merkleProof
, we have a path leading up to the aggregatedRoot
. In order to form the final Gateway fullRootHash
committed on L1, one last trick is in store.
Notice that fullRootHash = hash(LocalLogsRootHash, aggregatedRoot)
, where the aggregatedRoot
is hashed on the right and the Gateway's LocalLogsRootHash
is on the left. In order to append Gateway's LocalLogsRootHash
to the final aggregated mekleProof
on the left in the final hash, one needs to specially craft the index of chainRoot
in Step 3. Specifically, the index
of the chainRoot
becomes index + 2^N
, where N
is the height of the SharedTree
. This ensures that a final hash will take place with ChainRoot
on the right side of the hash.
This completes the recursive proof construction to verify a Gateway-settling ZKChain's Log inclusion on L1.
By leveraging the Gateway
ZKChain as an intermediary settlement layer, and devising this recursive proof scheme, the ZK Stack maintains a consistent Rollup-to-SettlementLayer communication style, ensures backward compatibility, and allows scalable and efficient mechanism for cross-chain transactions and proofs.