CIRC: Coordinated Inter-Rollup Communication

Abstract

Layer 2s (L2s) have emerged from Ethereum over the past few years. Their aim is to reduce latency, increase throughput, lower fees, and allow more customization relative to Ethereum while still maintaining the Layer 1’s strong security. L2s have been largely successful at accomplishing these goals, and continue to grow today. However, L2s naturally suffer from fragmentation: Chains are siloed from each other and do not share their state with other L2s, limiting interoperability and resulting in a degraded user experience.

In this work we introduce CIRC (Coordinated Inter-Rollup Communication), a simple yet powerful cross-chain messaging protocol that enables synchronous composability across L2s. CIRC offers several key advantages:

  • No VM modifications: L2s can implement the protocol using smart contracts, without altering their existing virtual machines.
  • Parallel proving: L2s can independently prove state transitions while maintaining interoperability with other chains.
  • Cryptographic enforcement: The protocol is cryptographically enforceable. It does not rely on economic security or slashing mechanisms for correctness.
  • Integration with a Confirmation Layer: CIRC can be complemented with a layer that sequences transactions across rollups and provides fast, reliable pre-confirmations.

CIRC is built on three core components:

  1. Mailboxes: Messages between applications on different chains are handled by mailboxes, which are implemented as contracts on each rollup (rather than on Layer 1). This allows decentralized apps (dapps) to engage in several rounds of communication within a single L1 time slot.
  2. Simple and flexible message handling: Messages are stored in a key-value map, avoiding the overhead of enforcing messages order (as would be necessary with a Merkle tree for example). Session identifiers differentiate between multiple concurrent cross-chain interactions, and message payloads is defined by the application itself. As a consequence, while we describe our solution assuming all chains are EVM compatible, our design naturally extends to non-EVM chains.
  3. Settlement Layer Contract: The correctness of state updates and consistency across rollups are enforced by an aggregated proof verified by a shared Settlement Layer Contract deployed on Layer 1. This contract also serves as a unified bridge.

We demonstrate how CIRC can support fundamental cross-chain use cases such as flash loans. We believe CIRC unique set of features and simplicity of design is the missing piece for achieving an optimal user experience in Ethereum’s multi-rollup world.

Authors (alphabetical)

Benedikt Bünz, Philippe Camacho, Binyi Chen, Ellie Davidson, Ben Fisch, Gus Gutovski, Anders Konring, Chengyu Lin, Dahlia Malkhi, and Alex Xiong.

Introduction

Layer 2s (L2s) have emerged from Ethereum over the past few years. Their aim is to reduce latency, increase throughput, lower fees, and allow more customization relative to Ethereum while still maintaining the Layer 1’s strong security. L2s have been largely successful at accomplishing these goals, and continue to grow today. However, L2s naturally suffer from fragmentation: Chains are siloed from each other and do not share their state with other L2s. It is onerous for users to access another L2’s liquidity, ensure atomicity between transactions submitted to multiple L2s, and enable composability between L2s as if they were one chain. Fragmentation is inefficient and limits L2 expressivity. Given this state of affairs, a natural question arises: Can we recover the synchronous composability that applications on Ethereum today enjoy while retaining the benefits of L2s? More specifically, can we design a rollup interoperability protocol that satisfies the following properties (ACID)

  • Atomicity: every step of a (cross-chain) transaction is executed in a all or nothing fashion.
  • Consistency: after each (cross-chain) transaction is executed, the global state composed by the union of all the rollups states remains consistent.
  • Isolation: the execution of a (cross-chain) transaction does not interfere with the execution of another (cross-chain) transaction. Said differently, the final state can be computed by applying the (cross-chain) transactions in some sequence.
  • Durability: once a (cross-chain) transaction is committed, it remains permanently part the global log of all rollups transactions.

Contributions

To achieve synchronous composability among L2s we present CIRC (*), a simple yet powerful cross-chain messaging protocol. This protocol acts as a foundation to building more complex synchronous composability logic by providing a safe, trustless interface for L2s to quickly exchange messages between themselves. CIRC has several desirable features:

  • L2s do not need to alter their existing VM. All enforcement and message handling can be done via smart contracts deployed on each chain.
  • The protocol allows parallel proving among chains. L2s interoperating between each other can prove state transitions separately.
  • The protocol is cryptographically enforceable. It does not rely on economic security or slashing mechanisms for correctness.
  • The messaging protocol is flexible as it does not impose any format on the messages nor any constraints on how those messages are interpreted. As a consequence, while we describe the cross-chain applications examples assuming all chains are EVM compatible, our design naturally extends to non-EVM chains.
  • Our protocol can be integrated with a Confirmation Layer that allows to sequence transactions from different rollups as well as provide fast and reliable preconfirmations. We highlight that the combination of such Confirmation Layer and CIRC maximizes user satisfaction.

(*) Coordinated Inter Rollup Communication.

Overview

Figure 1: A user (or group of users) wanting to perform some cross-chain task (e.g. flash loan) will call contracts on several chains using the same session identifier.

Our solution relies on 3 simple ideas:

  • Mailbox Contracts. In order to handle messages exchanged between applications deployed on different chains, we rely on mailboxes which are implemented as a contract on each chain in the set of interconnected chains. By having the mailboxes deployed on each rollup, we allow dApps to engage in several rounds of communication within a single L1 block.
  • Message representation, handling and storage. Messages in a mailbox are stored in a key-value map instead of an ordered list (e.g. leaves of a Merkle tree). This approach eliminates overhead of enforcing correct ordering of messages. Session identifiers provide a convenient way to distinguish among multiple concurrent instances of a cross-chain conversation (See Figure 1). Moreover messages payload do not need to adhere to a specific format, nor is the interpretation of a message constrained by the protocol.
  • Settlement Layer Contract. The correct state update of each rollup and additional constraints, especially the consistency of the mailboxes, are guaranteed via an aggregated proof that is checked by a Settlement Layer Contract deployed on Layer 1 and shared among all rollups.

It is helpful to view CIRC as emulating a channel-based concurrency system where each chain is a separate thread and each inbox key is a single-producer-single-consumer channel from one chain to another. While arbitrary interactions and sequences of messages exchanged between chains are captured by this emulation (e.g. cross-chain flashloan), in practice each chain execution can be verified in parallel. This is achieved by providing each chain with a set of precomputed messages ahead of time. These messages can then be consumed by transactions as if they were effectively sent from other chains in “real-time”.

In the following we expand on the core aspects of our solution and then illustrate its workings with a detailed description of a flash loan implemented on top of CIRC.

Design

Workflow

Figure 2: CIRC can be integrated with a Confirmation Layer used by a Shared Sequencer to sequence transactions into a super block. Some of these transactions include inbox messages for each chain. Then rollups can fetch their own part of the super block, compute state update proofs that are finally aggregated into a single proof. This single proof allows all rollup states to be settled at once on L1.

The CIRC workflow can vary based on different integration flavours. We describe the most common setting where a Shared Sequencer uses a Confirmation Layer such as Espresso to coordinate all rollups.

  1. Transactions are generated by users on each rollup.
  2. The Shared Sequencer collects those transactions from public mempools or private order flows and combine them into a super block, that is a single block composed by transactions from different rollups. Then the Shared Sequencer simulates the execution which might generate some cross-chain messages. In practice these messages need to be copied to the inbox of each chain. This is achieved by a special transaction that fills the inbox at the start of each rollup block.
  3. This super block is then handed over to the Confirmation Layer, which in practice can be instantiated as a high throughput, low latency BFT protocol.
  4. Rollups monitor the Confirmation Layer in order to fetch possibly several super blocks and filter out their transactions to form their own block. Such block is then used to update the rollup state and compute the corresponding snark proof.
  5. Rollup state update proofs are aggregated into a single proof that is also used to check cross-chain conditions such as Mailbox consistency. Once this proof is verified, all the rollups states are settled on L1 (e.g. Ethereum).

Mailbox

Figure 3: A Mailbox contract is deployed on each chain. Any application on the chain can read from and write to the mailbox and several rounds of communications can occur between two mailboxes within a single L1 block.

The Mailbox is implemented as a contract deployed on each chain (see Figure 3). Its key features are:

  • Messages are stored in a key-value map. The key is composed by the following fields: source chain, destination chain, sender, receiver, session identifier, label. The first 4 fields simply allow to ensure the messages are routed to the right parties and can be possibly be replied to. The session identifier and the label allow for contracts deployed on different chains to communicate in order to achieve some cross-chain interaction requested by a user (e.g. cross-chain arbitrage) or group of users (e.g. cross-chain swap). Intuitively the session identifier represents the execution context (across all chains) for a specific interaction (e.g. Flashloan) and the label allows to differentiate messages within the same session.
  • Reading/writing messages:
    • Any contract can write to the outbox but the source is populated automatically using msg.sender .
    • Messages from the inbox can be read by any contract any number of times.
  • Interpreting messages: It is up to the application to interpret the message read from the inbox and perform some computation based on its content. Messages are not tied to any function call. Note that this differs from other designs like LxLy where messages automatically trigger a call to a function.
  • Mailbox setup: The inbox/outbox for each chain is cleared up at the beginning of each rollup block. Moreover the inboxes are filled with the messages computed by the Coordinator. In practice this is achieved by a transaction calling a specific function ( Mailbox.putInbox(…)) of the Mailbox contract at the “top” of the rollup block.

Settlement Layer Contract

Contract

Figure 4: The Settlement Layer Contract state (SLC) is updated with the combined states of rollup A and rollup B. Each of the rollup contract can then pull its respective state by reading from the SLC.

In order to finalize atomically the state of several rollups, we rely on the Settlement Layer Contract (SLC) (see Figure 4). This contract verifies a statement about the state updates of several rollups and additional conditions with the help of a snark proof. The rollup contracts communicate with the SLC in order to fetch their state after engaging in a cross-chain interaction. Similarly, the SLC will fetch the latest state of each rollup (that might have been updated on its own) before verifying the snark proof.

Statement

At a high level the statement verified by the SLC on L1 consists of the following:

  • Public inputs
    • The public inputs include
      • Digest(inbox_1), Digest(inbox_2),…,Digest}(inbox_n).
        (Digest is an incremental hash function computed in Mailbox.putInbox and Mailbox.write.)
      • Digest(outbox_1),{Digest(outbox_2),…,Digest(outbox_n).
      • Commitment to each rollup block.
      • List of new states after update for each rollup s_1,s_2,…,s_i.
  • Filling the inboxes.
    • For each rollup, the block starts with a transaction of the form CALL Mailbox.putInbox(…) in order to insert the inbox messages.
  • Checking the content of the outboxes.
    • After the rollup R_i state is updated, verify that the content of each outbox is consistent with Digest(outbox_i). In practice this can be checked against a storage proof in the Merkle Patricia Trie at the contract storage slot for the corresponding storage variable.
  • Inboxes/Outboxes consistency check.
    The set of inboxes and outboxes are consistent across rollups. More precisely
    inbox_1 union inbox_2 union ...union inbox_n = outbox_1 union outbox_2 union … union outbox_n
    where inbox_i and outbox_i are the set of inbox and outbox messages for rollup R_i respectively. Note that the key for each message identifies the source and destination chains so this set equality check is enough.
  • Correct rollup state update. The state of each rollup is updated correctly based on the each rollup’s block transactions.

Bridging


Figure 5: Intuitive sequence diagram for bridging assets across chains. This is what would happen if contracts on chain A and chain B were interacting via some interprocess communication. However, this is not what is happening in CIRC. In actuality, this is the execution carried out by the Shared Sequencer, not the contracts that are deployed to chain A and chain B. The Shared Sequencer will then provide the contracts with the inbox messages they should have received.

Bridging is a fundamental primitive for cross-chain interoperability. It is achieved through 3 contracts on each chain.

  • The Token contract (e.g. USDC) that handles the rules for minting / burning / transferring (locally) the assets. Note that we assume the token contracts on each chain have the same address. If this is not the case, then some kind of mapping between the token contracts across chains must be available.
  • The Mailbox contract that takes care of “sending” messages between chains. Note: this contract doesn’t actually forward messages, it simply records messages. The Shared Sequencer provides the message to the destination chain and if it provides an incorrect message this will be caught at settlement time because the outbox of one chain will be inconsistent with the inbox of another.
  • The Bridge contract that acts as an interface between the Token contract and the Mailbox contract to ensure bridging assets is secure.

Note that the Bridge and the Mailbox contracts are deployed on each chain with the same code and are a part of the CIRC infrastructure.

The sequence diagram described in Figure 5 provides intuition for the sequence of actions that would occur if the chains were interacting by sending messages to each other and waiting for a response. This is not how the contracts actually work! Instead, the Shared Sequencer (or more generally some Coordinator) provides all inbox messages to the two chains ahead of time so that the transactions on each chain can be verified in parallel (see Figure 6) by the settlement layer checker.

Figure 6: At the beginning of each block each chain is initialized with some EVM transactions that fills the inbox. Note that these transactions contain the messages in no specific order given the data structure (mapping) we use to store these messages. In the particular case of our Bridging primitive, Chain A is initialized with the ACK message produced by Chain B and similarly, Chain B is initialized with the SEND message produced by Chain A. So in practice there is no interaction between Chain A and Chain B, allowing for parallel execution of the transactions in their respective block.
Given these messages, the bridge operation is achieved by including two transactions. One transaction calling the BridgeA.send function on chain A and another calling BridgeB.receive on chain B. If one of these transaction is censored the other one will revert, otherwise the assets will be burn on chain A and minted on chain B.

In the sequence diagram of Figure 5, Alice (operating on chain A) wants to send some USDC tokens to Bob (operating on chain B). Alice calls the send function of the BridgeA contract (step 1) specifying the details of the transfer (token address, recipient address, amount, …). The BridgeA contract burns the assets calling the USDCTokenA contract (step 2) and if this burn operation is successful (i.e. Alice has enough funds) the BridgeA contract will write a message in the outbox (step 3). Bob can call the BridgeB contract in order to receive the funds (step 4). For the funds to be received successfully, the BridgeB contract must read a message from the inbox confirming the availability of the funds (step 5). Then the BridgeB contract calls the USDCTokenB contract in order to mint the assets for Bob (step 6). And the BridgeB contract will write to the outbox to confirm the receipt of the funds (step 7). Finally (step 8) the BridgeA contract reads the message confirming receipt of funds in order to confirm the bridge operation has been successful.

The critical aspect of CIRC that differentiates it from standard asynchronous cross-chain messaging protocols and which enables synchronicity, i.e. the ability of a single transaction on the BridgeA contract to create a message for BridgeB and either revert or succeed based on the response, is that all cross-chain messages are pre-populated by the Shared Sequencer (a.k.a Coordinator) in the chain inboxes. Therefore, even a sequence of messages causally succeeding one another can be verified and settled atomically, in a single L1 block. If the Coordinator provides incorrect messages this will be caught by the L1 at settlement time.

To see why this design for bridging assets across chains is secure, observe:

  • Transactions on the two chains settle if and only if the “settlement layer statement” is true, which holds only if every message the builder wrote to the inbox of chain B (resp. chain A) was also written by some transaction to the outbox of chain A (resp. chain B).
  • If Alice does not have enough USDC for the transfer then no message will be written in the outbox of A. Thus, assuming the settlement statement is true (mailbox consistency), Bob’s transaction to the BridgeB contract on chain B would fail to read a bridge message and revert (without minting any USDC).
  • If Bob’s transaction calling BridgeB.receive is not included on chain B, then no message acknowledging the correct receipt of USDC funds will be written to chain B’s outbox. Thus, assuming the settlement statement is true, Alice’s transaction calling BridgeA.send would fail to read a receipt message from the inbox and revert.

Below we provide a detailed implementation of how these conditions are enforced in these contracts.

Token contract API

Token contracts that are expected to send and receive funds from other chains must implement the API BridgeToken. This API simply gives the right to the Bridge contract (unique on each chain) to mint and burn assets of the canonical representation instead of a wrapped token type.

contract BridgeToken {

	// Mints some tokens for a specific address
	// @param dest recipient address of the tokens
	// @param amount of tokens to be minted
	function bridgeMint(address dest, uint256 amount) {
		// Only the Bridge Contract can call this function
		if (msg.sender != BRIDGE_CONTRACT_ADDRESS){
			revert();
		}
		this.balances[src] += amount;
	}
	
	// Only the Bridge Contract can call this function
	function bridgeBurn(address src, uint256 amount) {
	
		// Only the Bridge Contract can call this function
		if (msg.sender != BRIDGE_CONTRACT_ADDRESS){
			revert();
		}
	
		if this.balances[src] < amount {
			revert();
		}
		
		this.balances[src] -= amount;
	}
	
	/// Other standard functions of (ERC20) tokens
	...
}

// This contract is deployed on Chain A and Chain B with the same address 
contract USDC is BridgeToken {
	...	
}

Bridge contract code

The code for the Bridge contract below must be deployed on each chain and contains the logic for coordinating the Mailbox and the Token contract in order for the bridge operation to happen.

contract Bridge {
	// Send some funds to some address on another chain
	// @param chainDest identifier of the destination chain
	// @param token address of the token to be transfered
	// @param sender address of the sender of the tokens (on the source chain)
	// @param receiver address of the recipient of the tokens (on the destination chain)
	// @param amount amount of tokens to be bridged
	// @param sessionId identifier of the user session
	// @param label label to be able to differentiate between different bridge operations within a same session.
	function send(
		uint256 chainDest,
		address token, 
		address sender, 
		address receiver, 
		uint256 amount,
		uint256 sessionId
		bytes label
		){
			
		// Burn the assets
		token.bridgeBurn(sender, amount);
		
		// Write to the outbox			
		Mailbox.write(
			chainDest, 
			receiver, 
			sessionId, 
			string.concat(label, "SEND"), 
			abi.encode(sender, receiver, token,	amount),
		);
		
		// Check the funds have been received on the other chain
		bytes m = Mailbox.read(
			chainDest,
			receiver,
			sessionId, 
			string.concat(label,"ACK"));
		if (m == "") {
			revert();
		}
	
	}
	
	// Process funds reception on the destination chain
	// @param chainSrc source chain identifier the funds are sent from
	// @param sender address of the sender of the funds
	// @param sessionId identifier of the user session
	// @param label label to be able to differentiate between different bridge operations within a same session.
	// @return token address of the token that was transfered
	// @return amount amount of tokens transfered
	function receive(
			uint256 chainSrc, 
			address sender, 
			uint256 sessionId, 
			bytes label) 
		returns (
			uint256 sessionId,
			bytes label){
		
		// Fetch the message
		bytes m = Mailbox.read(
			chainSrc, 
			sender, 
			this.address,
			sessionId,
			string.concat(label,"SEND"));
		
		
		// Check the message is valid
		if (m == ""){
			revert();
		}
		
		// Mint the assets
		address sender;
		address receiver;
		address token;
		address amount;
		
		(sender, receiver, token, amount) = abi.decode(m,
									(address, address, address, address));
			
		token.bridgeMint(receiver, amount);
		
		// Acknowledge the reception of funds
		bytes m = abi.encode("OK");
		Mailbox.write(
			chainSrc,
			sender,
			sessionId,
			string.concat(label,"ACK"),
			 m);	
		
		return (token,amount);
	}
}

Finally note that this bridging primitive could be implemented in different ways and thus is not strictly part of the core CIRC infrastructure. However this approach has the benefit of making the bridge primitive easily reusable in more complex cross-chain applications like shown below.

Flash loan

In this section we illustrate how a non-trivial cross-chain use case (flash loan) can be instantiated easily using CIRC. Our flexible messaging protocol coupled with a message ordering agnostic design allow for developing cross-chain applications that are modular and abstracts away the specifics of each VM.

Figure 7: Intuitive sequence diagram for a cross-chain Flash loan. Our approach yields a very modular design where the lending contract only sends the funds and expects them back (with a fee).

The setting is as follows:

  • The flash loan contract CCLendingContractA (Cross-chain lending contract, see Figure 7) is deployed on chain A.
  • Funds are sent to the TraderContractB on Chain B.
  • The trader strategy is executed as a function of the TraderContractB.
  • The money (plus fee) is returned by the TraderContractB to the CCLendingContractA contract.

A notable property of this approach is that the CCLendingContractA contract is totally unaware of the TraderContractB contract that executes the strategy. Indeed the CCLendingContractA.borrow function only bridges funds and accepts those funds back (with some additional fee). In particular, the CCLendingContractA contract does not make a cross-chain call to TraderContractB .

This is possible thanks to the sessionId value picked by the user (and used on both chains) that allows both contracts on chain A and B to read/write from/to the correct location in the respective mailboxes.

Finally, we highlight the need for using labels as part of the keys when storing messages as it allows each application to differentiate between different messages belonging to the same session. In this particular case, each bridge operation involves a read (when sending or receiving) and we have two bridge operations for each contract (one send and one receive on each side).

So in practice the user will do the following:

  • Sample a value for sessionId, e.g. sessionId = 123.
  • Generate two transactions
    • Chain A: CLendingContractA.borrow(USDC,1000,chainB,TraderContract.address,123);
    • Chain B: TraderContractB.run(chainA,123);
  • Submit those two transactions to the Shared Sequencer.

If one of the transactions is censored then, the other transaction will fail. If both are sequenced and do not revert then the flash loan will complete successfully.

Combining Sync and Async modes

While synchronous composability is the most advanced form of inter-rollup communication, in some scenarios asynchronous composability might be best we can achieve due to the absence of a shared sequencer willing to build blocks for a specific combination of rollups.

We describe how elements of the CIRC design can be adapted to the asynchronous case.

A naive approach to asynchronous composability would be to emulate existing solutions where communication from a rollup A to a rollup B is achieved by passing a commitment to the state of A to rollup B. This creates logical dependencies between rollups that need to be able to parse state commitments and verify witnesses of other rollups.

Figure 8: High level workflow for CIRC in Async mode: After collecting user transactions the shared sequencer will create a super block containing those transactions but also new transactions for the SharedMailbox and inbox messages transactions. Each rollup will then update its state based on the transactions filtered from the super block. The statement verified at the SLC level takes into account the new rollup SharedMailbox that stores permanently all the messages exchanges between rollups.

Figure 9: Flow of messages between mailboxes. Inboxes of each rollup must be filled with messages stored in the SharedMailbox. When rollup transactions are executed, messages are written to their corresponding Mailbox. These messages are then copied and stored permanently into the SharedMailbox for future asynchronous use.

For the asynchronous mode, we reuse the concept of Mailbox in the following way (See Figure 8).

  • We add a special SharedMailbox rollup to the infrastructure that will store all the messages exchanged between rollups enrolled.
  • Like in synchronous CIRC, each rollup has a Mailbox contract deployed and executing transactions will cause the writing of messages to this Mailbox contract. These messages will be copied over to the SharedMailbox rollup that will store them permanently for future use (the messages are never deleted in the case of the SharedMailbox). See Figure 9.
  • When the builder executes transactions for one or more rollups, it monitors if some messages stored in the SharedMailbox need to be fetched. If this is the case, it will insert such messages into the inbox of the rollup at the top of the block. See Figure 9.
  • The settlement on L1 looks similar to synchronous CIRC where the state of one or more rollups will be committed at once. The statement will be slightly different to account for the presence of the special SharedMailbox rollup. In particular one needs to check that:
    • All the messages generated during the execution of transactions in each rollup blocks are copied over to the SharedMailbox rollup state.
    • The state of the SharedMailbox is updated correctly.
    • All messages placed in the inbox of each rollup at the beginning of the execution belong to the SharedMailbox rollup state.
    • All reading failures on each rollup are due to the absence of a specific message on the SharedMailbox. That is all reading failures must be backed by a proof of non-membership of a message expected to be read (through its key).

Finally note that both synchronous and asynchronous modes described in this document can be combined. In this case messages meant for synchronous communication should be marked as such so that they are not stored in the SharedMailbox but simply copied over the destination rollup.

2 Likes