Table of contents
- Introduction
- What does it mean to settle on Gateway?
- Universal ZKChain Settlement - The
FullRootHash
- Log Inclusion Proofs
- Preliminaries
- Proofs Needed: Finalizing Withdrawal on L1
- L2 Log Inclusion Proof for an L1-settling ZKChain
- L3 Log Inclusion Proof for a Gateway-settling ZKChain
- Conclusion
Introduction
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.
What does it mean to settle on Gateway?
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.
Universal ZKChain Settlement - The 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:
- the
localLogsRootHash
of the ZKChain'sL2ToL1LogsTree
: representing the finalized or initiated cross-chain transactions included in the batch; - the
aggregatedRoot
of the ZKChain'sSharedTree
: its value depends on whether the ZKChain is a Settlement Layer or not:- regular ZKChain: a default empty tree initialized with an empty leaf for its own
chainId
, maintaining the same value in all batches. Gateway
: a root hash which commits to thechainRoots
of all ZKChains that settle onGateway.
- regular ZKChain: a default empty tree initialized with an empty leaf for its own
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.
Log Inclusion Proofs
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:
- lock-and-mint: bridging away from a token's native chain, also called deposit
- burn-and-unlock: bridging back to a token's native chain, also called withdraw
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.
Preliminaries
Deposit Funds to a 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:
- For an L1-settling ZKChain, the priority operation is relayed directly to the ZKChain.
- For a Gateway-settling ZKChain, the priority operation is relayed to
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.
Withdraw Funds From a ZKChain
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
.
Proofs Needed: Finalizing Withdrawal on L1
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 theL2ToL1LogsTree
, which will be used in combination with themerkleProof
later,message
: thebytes
message itself which is used to form the corresponding log included in theL2ToL1LogsTree
,merkleProof
: the inclusion proof that verifies that theL2Log
from thismessage
is at indexl2MessageIndex
of theL2ToL1LogsTree
of batch numberl2BatchNumber
from the ZKChain withchainId
.
The structure of the merkleProof
depends on whether the ZKChain is settling on L1 or on Gateway. Let's analyze these two cases separately.
L2 Log Inclusion Proof for an L1-settling ZKChain
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.
L3 Log Inclusion Proof for a Gateway-settling ZKChain
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:
- The L2 log is a
L2ToL1Log
leaf of the ZKChain'sL2ToL1LogsTree
. - The root of the ZKChain's
L2ToL1LogsTree
,LocalLogsRootHash
, is hashed together with an emptySharedTree
, i.e. a defaultaggregatedRoot
, to calculate the ZKChain'sfullRootHash
for a specific batch. This root is settled onGateway
. - Gateway appends the settled
fullRootHash
to the ZKChain'schainTree
, leading to an updatedchainRoot
. - Gateway updates the new
chainRoot
leaf in theSharedTree
, leading to an updatedaggregatedRoot
. - Gateway hashes its own
LocalLogsRootHash
with theaggregatedRoot
to retrieve the finalfullRootHash
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:
- Step 1: Reconstruct the ZKChain's
fullRootHash
. Similar to the L2 log inclusion described previously, use aLocalLogsTree
merkle path to reconstruct theLocalLogsRootHash
starting from theL2ToL1Log
leaf with indexl2MessageIndex
and finally hash with the chain's defaultaggregatedRoot
. - Step 2: Reconstruct the
chainRoot
of the ZKChain'sChainTree
with aChainTree
merkle path which starts from thefullRootHash
leaf calculated in Step 1. - Step 3: Reconstruct the
aggregatedRoot
by using aSharedTree
merkle path starting from theChainRoot
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.
Conclusion
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.