Inside ZKStack's Crosschain Architecture — Part II: Gateway Settlement & Recursive Proofs

Table of contents 

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 commitprove 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's L2ToL1LogsTree: representing the finalized or initiated cross-chain transactions included in the batch;
  • the aggregatedRoot of the ZKChain's SharedTree: 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 the chainRoots of all ZKChains that settle on Gateway.

img1-3

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:

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,
  • l2BatchNumberl2TxNumberInBatch: 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.


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.

img2-3

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.

img3-3

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's L2ToL1LogsTree.
  • The root of the ZKChain's L2ToL1LogsTreeLocalLogsRootHash, 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.
  • Gateway appends the settled fullRootHash to the ZKChain's chainTree, leading to an updated chainRoot.
  • Gateway updates the new chainRoot leaf in the SharedTree, leading to an updated aggregatedRoot.
  • Gateway hashes its own 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:

  • Step 1: Reconstruct the ZKChain's 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.
  • Step 2: Reconstruct the chainRoot of the ZKChain's ChainTree with a ChainTree merkle path which starts from the fullRootHash leaf calculated in Step 1.
  • Step 3: Reconstruct the 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 mekleProofon the left in the final hash, one needs to specially craft the index of chainRoot in Step 3. Specifically, the index of the chainRootbecomes 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.

Request Audit