diff --git a/docs/src/guides/build-docker.md b/docs/src/guides/build-docker.md index 5dd9cff022b9..6b0608275d8b 100644 --- a/docs/src/guides/build-docker.md +++ b/docs/src/guides/build-docker.md @@ -10,7 +10,8 @@ Install prerequisites: see ## Build docker files -You may build all images with [Makefile](../../docker/Makefile) located in [docker](../../docker) directory in this repository +You may build all images with [Makefile](../../docker/Makefile) located in [docker](../../docker) directory in this +repository > All commands should be run from the root directory of the repository diff --git a/docs/src/specs/interop/README.md b/docs/src/specs/interop/README.md index d8cdaa84af92..88e3d514b562 100644 --- a/docs/src/specs/interop/README.md +++ b/docs/src/specs/interop/README.md @@ -3,4 +3,4 @@ - [Overview](./overview.md) - [Interop Messages](./interopmessages.md) - [Bundles and Calls](./bundlesandcalls.md) -- [Interop Transactions](./interoptransactions.md) \ No newline at end of file +- [Interop Transactions](./interoptransactions.md) diff --git a/docs/src/specs/interop/bundlesandcalls.md b/docs/src/specs/interop/bundlesandcalls.md index 00926f4b833f..b04924770c70 100644 --- a/docs/src/specs/interop/bundlesandcalls.md +++ b/docs/src/specs/interop/bundlesandcalls.md @@ -2,13 +2,16 @@ ## Basics Calls -Interop Calls are the next level of interfaces, built on top of Interop Messages, enabling you to call contracts on other chains. +Interop Calls are the next level of interfaces, built on top of Interop Messages, enabling you to call contracts on +other chains. ![interopcall.png](../img/interopcall.png) -At this level, the system handles replay protection—once a call is successfully executed, it cannot be executed again (eliminating the need for your own nullifiers or similar mechanisms). +At this level, the system handles replay protection—once a call is successfully executed, it cannot be executed again +(eliminating the need for your own nullifiers or similar mechanisms). -Additionally, these calls originate from aliased accounts, simplifying permission management (more details on this below). +Additionally, these calls originate from aliased accounts, simplifying permission management (more details on this +below). Cancellations and retries are managed at the next level (Bundles), which are covered in the following section. @@ -22,7 +25,7 @@ struct InteropCall { address destinationAddress, uint256 destinationChainId, calldata data, - uint256 value + uint256 value } contract InteropCenter { // On source chain. @@ -32,8 +35,8 @@ contract InteropCenter { } ``` - -In return, you receive a `bundleId` (we’ll explain bundles later, but for now, think of it as a unique identifier for your call). +In return, you receive a `bundleId` (we’ll explain bundles later, but for now, think of it as a unique identifier for +your call). On the destination chain, you can execute the call using the execute method: @@ -42,14 +45,17 @@ contract InteropCenter { // Executes a given bundle. // interopMessage is the message that contains your bundle as payload. // If it fails, it can be called again. - function executeInteropBundle(interopMessage, proof); - // If the bundle didn't execute succesfully yet, it can be marked as cancelled. - // See details below. + function executeInteropBundle(interopMessage, proof); + + // If the bundle didn't execute succesfully yet, it can be marked as cancelled. + // See details below. function cancelInteropBundle(interopMessage, proof); } + ``` -You can retrieve the `interopMessage` (which contains your entire payload) from the Gateway, or you can construct it yourself using L1 data. +You can retrieve the `interopMessage` (which contains your entire payload) from the Gateway, or you can construct it +yourself using L1 data. Under the hood, this process calls the `destinationAddress` with the specified calldata. @@ -57,17 +63,23 @@ This leads to an important question: **Who is the msg.sender for this call?** ## `msg.sender` of the Destination Call -The `msg.sender` on the destination chain will be the **AliasedAccount** — an address created as a hash of the original sender and the original source chain. +The `msg.sender` on the destination chain will be the **AliasedAccount** — an address created as a hash of the original +sender and the original source chain. -(Normally, we’d like to use `sourceAccount@sourceChain`, but since Ethereum limits the size of addresses to 20 bytes, we compute the Keccak hash of the string above and use this as the address.) +(Normally, we’d like to use `sourceAccount@sourceChain`, but since Ethereum limits the size of addresses to 20 bytes, we +compute the Keccak hash of the string above and use this as the address.) -One way to think about it is this: You (as account `0x5bFF1...` on chain A) can send a call to a contract on a destination chain, and for that contract, it will appear as if the call came locally from the address `keccak(0x5bFF1 || A)`. This means you are effectively "controlling" such an account address on **every ZK Chain** by sending interop messages from the `0x5bFF1...` account on chain A. +One way to think about it is this: You (as account `0x5bFF1...` on chain A) can send a call to a contract on a +destination chain, and for that contract, it will appear as if the call came locally from the address +`keccak(0x5bFF1 || A)`. This means you are effectively "controlling" such an account address on **every ZK Chain** by +sending interop messages from the `0x5bFF1...` account on chain A. ![msgdotsender.png](../img/msgdotsender.png) ## Simple Example -Imagine you have contracts on chains B, C, and D, and you’d like them to send "reports" to the Headquarters (HQ) contract on chain A every time a customer makes a purchase. +Imagine you have contracts on chains B, C, and D, and you’d like them to send "reports" to the Headquarters (HQ) +contract on chain A every time a customer makes a purchase. ```solidity // Deployed on chains B, C, D. @@ -95,23 +107,24 @@ contract HQ { // Adding aliased accounts. shops[address(keccak(addressOnChain || chainId))] = true; } - + function reportSales(uint256 itemPrice) { // only allow calls from our shops (their aliased accounts). require(shops[msg.sender]); sales[msg.sender] += itemPrice; - } + } } ``` #### Who is paying for gas? How does this Call get to the destination chain? -At this level, the **InteropCall** acts like a hitchhiker — it relies on someone (anyone) to pick it up, execute it, and pay for the gas! +At this level, the **InteropCall** acts like a hitchhiker — it relies on someone (anyone) to pick it up, execute it, and +pay for the gas! ![callride.png](../img/callride.png) -While any transaction on the destination chain can simply call `InteropCenter.executeInteropBundle`, if you don’t want to rely on hitchhiking, you can create one yourself. We’ll discuss this in the section about **Interop Transactions**. - +While any transaction on the destination chain can simply call `InteropCenter.executeInteropBundle`, if you don’t want +to rely on hitchhiking, you can create one yourself. We’ll discuss this in the section about **Interop Transactions**. ## Bundles @@ -121,18 +134,20 @@ Before we proceed to discuss **InteropTransactions**, there is one more layer in **Bundles Offer:** -- **Shared Fate**: All calls in the bundle either succeed or fail together. -- **Retries**: If a bundle fails, it can be retried (e.g., with more gas). -- **Cancellations**: If a bundle has not been successfully executed yet, it can be cancelled. - +- **Shared Fate**: All calls in the bundle either succeed or fail together. +- **Retries**: If a bundle fails, it can be retried (e.g., with more gas). +- **Cancellations**: If a bundle has not been successfully executed yet, it can be cancelled. -If you look closely at the interface we used earlier, you’ll notice that we were already discussing the execution of **Bundles** rather than single calls. So, let’s dive into what bundles are and the role they fulfill. +If you look closely at the interface we used earlier, you’ll notice that we were already discussing the execution of +**Bundles** rather than single calls. So, let’s dive into what bundles are and the role they fulfill. -The primary purpose of a bundle is to ensure that a given list of calls is executed in a specific order and has a shared fate (i.e., either all succeed or all fail). +The primary purpose of a bundle is to ensure that a given list of calls is executed in a specific order and has a shared +fate (i.e., either all succeed or all fail). In this sense, you can think of a bundle as a **"multicall"**, but with two key differences: -1. You cannot "unbundle" items—an individual `InteropCall` cannot be run independently; it is tightly tied to the bundle. +1. You cannot "unbundle" items—an individual `InteropCall` cannot be run independently; it is tightly tied to the + bundle. 2. Each `InteropCall` within a bundle can use a different aliased account, enabling separate permissions for each call. @@ -142,13 +157,13 @@ contract InteropCenter { // Calls have to be done in this order. InteropCall calls[]; uint256 destinationChain; - + // If not set - anyone can execute it. - address executionAddresses[]; + address executionAddresses[]; // Who can 'cancel' this bundle. address cancellationAddress; } - + // Starts a new bundle. // All the calls that will be added to this bundle (potentially by different contracts) // will have a 'shared fate'. @@ -168,14 +183,16 @@ Imagine you want to perform a swap on chain B, exchanging USDC for PEPE, but all This process would typically involve four steps: -1. Transfer USDC from chain A to chain B. -2. Set allowance for the swap. -3. Execute the swap. -4. Transfer PEPE back to chain A. +1. Transfer USDC from chain A to chain B. +2. Set allowance for the swap. +3. Execute the swap. +4. Transfer PEPE back to chain A. -Each of these steps is a separate "call," but you need them to execute in exactly this order and, ideally, atomically. If the swap fails, you wouldn’t want the allowance to remain set on the destination chain. +Each of these steps is a separate "call," but you need them to execute in exactly this order and, ideally, atomically. +If the swap fails, you wouldn’t want the allowance to remain set on the destination chain. -Below is an example of how this process could look (note that the code is pseudocode; we’ll explain the helper methods required to make it work in a later section). +Below is an example of how this process could look (note that the code is pseudocode; we’ll explain the helper methods +required to make it work in a later section). ```solidity bundleId = InteropCenter(INTEROP_CENTER).startBundle(chainD); @@ -183,8 +200,8 @@ bundleId = InteropCenter(INTEROP_CENTER).startBundle(chainD); // when this call is executed on chainD, it will mint 1k USDC there. // BUT - this interopCall is tied to this bundle id. USDCBridge.transferWithBundle( - bundleId, - chainD, + bundleId, + chainD, aliasedAccount(this(account), block.chain_id), 1000); @@ -194,9 +211,9 @@ InteropCenter.addToBundle(bundleId, USDCOnDestinationChain, createCalldata("approve", 1000, poolOnDestinationChain), 0); -// This will create interopCall to do the swap. -InteropCenter.addToBundle(bundleId, - poolOnDestinationChain, +// This will create interopCall to do the swap. +InteropCenter.addToBundle(bundleId, + poolOnDestinationChain, createCalldata("swap", "USDC_PEPE", 1000, ...), 0) // And this will be the interopcall to transfer all the assets back. @@ -204,30 +221,37 @@ InteropCenter.addToBundle(bundleId, pepeBridgeOnDestinationChain, createCalldata("transferAll", block.chain_id, this(account)), 0) - + bundleHash = interopCenter.finishAndSendBundle(bundleId); ``` -In the code above, we created a bundle that anyone can execute on the destination chain. This bundle will handle the entire process: minting, approving, swapping, and transferring back. +In the code above, we created a bundle that anyone can execute on the destination chain. This bundle will handle the +entire process: minting, approving, swapping, and transferring back. ### Bundle Restrictions -When starting a bundle, if you specify the `executionAddress`, only that account will be able to execute the bundle on the destination chain. If no `executionAddress` is specified, anyone can trigger the execution. +When starting a bundle, if you specify the `executionAddress`, only that account will be able to execute the bundle on +the destination chain. If no `executionAddress` is specified, anyone can trigger the execution. ## Retries and Cancellations -If bundle execution fails — whether due to a contract error or running out of gas—none of its calls will be applied. The bundle can be re-run on the **destination chain** without requiring any updates or notifications to the source chain. More details about retries and gas will be covered in the next level, **Interop Transactions**. +If bundle execution fails — whether due to a contract error or running out of gas—none of its calls will be applied. The +bundle can be re-run on the **destination chain** without requiring any updates or notifications to the source chain. +More details about retries and gas will be covered in the next level, **Interop Transactions**. -This process can be likened to a "hitchhiker" (or in the case of a bundle, a group of hitchhikers) — if the car they’re traveling in doesn’t reach the destination, they simply find another ride rather than returning home. +This process can be likened to a "hitchhiker" (or in the case of a bundle, a group of hitchhikers) — if the car they’re +traveling in doesn’t reach the destination, they simply find another ride rather than returning home. -However, there are cases where the bundle should be cancelled. Cancellation can be performed by the `cancellationAddress` specified in the bundle itself. +However, there are cases where the bundle should be cancelled. Cancellation can be performed by the +`cancellationAddress` specified in the bundle itself. #### For our cross chain swap example: 1. Call `cancelInteropBundle(interopMessage, proof)` on the destination chain. - A helper method for this will be introduced in the later section. -2. When cancellation occurs, the destination chain will generate an `InteropMessage` containing cancellation information. +2. When cancellation occurs, the destination chain will generate an `InteropMessage` containing cancellation + information. 3. Using the proof from this method, the user can call the USDC bridge to recover their assets: ```solidity @@ -243,62 +267,72 @@ Superchain offers the `L2ToL2CrossDomainMessenger` contract. Similar to **InteropCall**, it allows you to send a message to the target contract: ```solidity -function sendMessage(uint256 _destination, address _target, bytes calldata _message) external returns (bytes32); +function sendMessage( + uint256 _destination, + address _target, + bytes calldata _message +) external returns (bytes32); + ``` This contract appends a unique nonce and the sender’s address, resulting in a message structure like this: ```solidity event SentMessage( - uint256 indexed destination, - address indexed target, - uint256 indexed messageNonce, - address sender, + uint256 indexed destination, + address indexed target, + uint256 indexed messageNonce, + address sender, bytes message ); ``` -The user is then responsible for calling the `L2ToL2CrossDomainMessenger` on the destination chain. This verifies the message and calls the target contract. +The user is then responsible for calling the `L2ToL2CrossDomainMessenger` on the destination chain. This verifies the +message and calls the target contract. -The target contract must "trust" messages originating from the CrossDomainMessenger contract, as it can directly inspect the source sender, which is not aliased. +The target contract must "trust" messages originating from the CrossDomainMessenger contract, as it can directly inspect +the source sender, which is not aliased. #### Message Expiration -Superchain allows messages to expire (a.k.a. be canceled), but only after a predefined EXPIRY_WINDOW. Anyone can call `sendExpire` on the destination chain, and then they must call `relayExpire` to propagate this information back to the source chain. +Superchain allows messages to expire (a.k.a. be canceled), but only after a predefined EXPIRY_WINDOW. Anyone can call +`sendExpire` on the destination chain, and then they must call `relayExpire` to propagate this information back to the +source chain. ### Differences in Approaches + TL;DR: Superchain provides a "lower-level" interface with fewer features compared to ElasticChain. #### Destination Contract -- **Superchain:** -The destination contract must be aware that it is being called by Superchain messages and must trust calls originating from the pre-deployed `L2ToL2CrossDomainMessenger`. -- **ElasticChain:** -The destination contract does not need to know it is being called via an interop call. Requests arrive from `aliased accounts'. +- **Superchain:** The destination contract must be aware that it is being called by Superchain messages and must trust + calls originating from the pre-deployed `L2ToL2CrossDomainMessenger`. + +- **ElasticChain:** The destination contract does not need to know it is being called via an interop call. Requests + arrive from `aliased accounts'. #### Batching -- **Superchain:** -Supports single calls with no guarantee of ordering. -- **ElasticChain:** -Supports bundling of messages, ensuring shared fate and strict order. +- **Superchain:** Supports single calls with no guarantee of ordering. + +- **ElasticChain:** Supports bundling of messages, ensuring shared fate and strict order. #### Execution Permissions -- **Superchain:** -No restrictions — anyone can forward or execute a message on the destination chain. -- **ElasticChain:** -Allows restricting who can execute the call or bundle on the destination chain. +- **Superchain:** No restrictions — anyone can forward or execute a message on the destination chain. + +- **ElasticChain:** Allows restricting who can execute the call or bundle on the destination chain. #### Cancellations -- **Superchain:** -Messages can be canceled by anyone, but only after a fixed EXPIRY_TIME has passed. -- **ElasticChain:** -Supports restricting who can cancel. Cancellation can happen at any time. +- **Superchain:** Messages can be canceled by anyone, but only after a fixed EXPIRY_TIME has passed. + +- **ElasticChain:** Supports restricting who can cancel. Cancellation can happen at any time. ### How to Bring Them Closer -ElasticChain could introduce an option to be "SuperChain" compatible by adding a special mode to the `call`. This mode would specify that the destination contract should be called from a pre-deployed address, allowing anyone to execute or cancel the message. +ElasticChain could introduce an option to be "SuperChain" compatible by adding a special mode to the `call`. This mode +would specify that the destination contract should be called from a pre-deployed address, allowing anyone to execute or +cancel the message. -Additionally, ElasticChain would need to implement a **cancellation cutoff timestamp**. +Additionally, ElasticChain would need to implement a **cancellation cutoff timestamp**. diff --git a/docs/src/specs/interop/interopmessages.md b/docs/src/specs/interop/interopmessages.md index 4bac919cafc9..90c05b86709e 100644 --- a/docs/src/specs/interop/interopmessages.md +++ b/docs/src/specs/interop/interopmessages.md @@ -1,10 +1,14 @@ # Interop Messages -In this section, we’re going to cover the lowest level of the interop stack: **Interop Messages** — the interface that forms the foundation for everything else. +In this section, we’re going to cover the lowest level of the interop stack: **Interop Messages** — the interface that +forms the foundation for everything else. -We’ll explore the details of the interface, its use cases, and how it compares to similar interfaces from Superchain/Optimism. +We’ll explore the details of the interface, its use cases, and how it compares to similar interfaces from +Superchain/Optimism. + +This is an advanced document. While most users and app developers typically interact with higher levels of interop, it’s +still valuable to understand how the internals work. -This is an advanced document. While most users and app developers typically interact with higher levels of interop, it’s still valuable to understand how the internals work. ## Basics ![interopmsg.png](../img/interopmsg.png) @@ -16,7 +20,8 @@ An **InteropMessage** contains data and offers two methods: - Send a message - Verify that a given message was sent on some chain -Notice that the message itself doesn’t have any ‘destination chain’ or address—it is simply a payload that a user (or contract) is creating. Think of it as a broadcast. +Notice that the message itself doesn’t have any ‘destination chain’ or address—it is simply a payload that a user (or +contract) is creating. Think of it as a broadcast. The `InteropCenter` is a contract that is pre-deployed on all chains at a fixed address `0x00..1234`. @@ -33,32 +38,42 @@ contract InteropCenter { uint256 sourceChainId; // filled by InteropCenter uint256 messageNum; // a 'nonce' to guarantee different hashes. } - + // Verifies if such interop message was ever producted. function verifyInteropMessage(bytes32 interopHash, Proof merkleProof) return bool; } ``` -When you call `sendInteropMessage`, the `InteropCenter` adds additional fields, such as your sender address, source chain ID, and messageNum (a nonce ensuring the hash of this structure is globally unique). It then returns the `interopHash`. +When you call `sendInteropMessage`, the `InteropCenter` adds additional fields, such as your sender address, source +chain ID, and messageNum (a nonce ensuring the hash of this structure is globally unique). It then returns the +`interopHash`. -This `interopHash` serves as a globally unique identifier that can be used on any chain in the network to call `verifyInteropMessage`. +This `interopHash` serves as a globally unique identifier that can be used on any chain in the network to call +`verifyInteropMessage`. ![A message created on one chain can be verified on any other chain.](../img/verifyinteropmsg.png) - - #### How do I get the proof? -You’ll notice that **verifyInteropMessage** has a second argument — a proof that you need to provide. This proof is a Merkle tree proof (more details below). You can obtain it by querying the Settlement Layer (Gateway) or generating it off-chain by examining the Gateway state on L1. + +You’ll notice that **verifyInteropMessage** has a second argument — a proof that you need to provide. This proof is a +Merkle tree proof (more details below). You can obtain it by querying the Settlement Layer (Gateway) or generating it +off-chain by examining the Gateway state on L1. #### How does the interop message differ from other layers (InteropTransactions, InteropCalls)? -As the most basic layer, an interop message doesn’t include any advanced features — it lacks support for selecting destination chains, nullifiers/replay, cancellation, and more. -If you need these capabilities, consider integrating with a higher layer of interop, such as Call or Bundle, which provide these additional functionalities. +As the most basic layer, an interop message doesn’t include any advanced features — it lacks support for selecting +destination chains, nullifiers/replay, cancellation, and more. + +If you need these capabilities, consider integrating with a higher layer of interop, such as Call or Bundle, which +provide these additional functionalities. ## Simple Use Case -Before we dive into the details of how the system works, let’s look at a simple use case for a DApp that decides to use InteropMessage. -For this example, imagine a basic cross-chain contract where the `signup()` method can be called on chains B, C, and D only if someone has first called `signup_open()` on chain A. +Before we dive into the details of how the system works, let’s look at a simple use case for a DApp that decides to use +InteropMessage. + +For this example, imagine a basic cross-chain contract where the `signup()` method can be called on chains B, C, and D +only if someone has first called `signup_open()` on chain A. ```solidity // Contract deployed on chain A. @@ -81,15 +96,17 @@ contract SignupContract { require(message.data == "We are open"); signupIsOpen = true; } - + function signup() { require(signupIsOpen); - signedUpUser[msg.sender] = true; + signedUpUser[msg.sender] = true; } } ``` -In the example above, the `signupManager` on chain A calls the `signup_open` method. After that, any user on other chains can retrieve the `signup_open_msg_hash`, obtain the necessary proof from the Gateway (or another source), and call the `openSignup` function on any destination chain. +In the example above, the `signupManager` on chain A calls the `signup_open` method. After that, any user on other +chains can retrieve the `signup_open_msg_hash`, obtain the necessary proof from the Gateway (or another source), and +call the `openSignup` function on any destination chain. ## Deeper Technical Dive @@ -107,40 +124,51 @@ function sendInteropMessage(bytes data) { As you can see, it populates the necessary data and then calls the `sendToL1` method. -The `sendToL1` method is part of a system contract that gathers all messages during a batch, constructs a Merkle tree from them at the end of the batch, and sends this tree to the SettlementLayer (Gateway) when the batch is committed. +The `sendToL1` method is part of a system contract that gathers all messages during a batch, constructs a Merkle tree +from them at the end of the batch, and sends this tree to the SettlementLayer (Gateway) when the batch is committed. ![sendtol1.png](../img/sendtol1.png) -The Gateway will verify the hashes of the messages to ensure it has received the correct preimages. Once the proof for the batch is submitted (or more accurately, during the "execute" step), it will add the root of the Merkle tree to its `globalRoot`. +The Gateway will verify the hashes of the messages to ensure it has received the correct preimages. Once the proof for +the batch is submitted (or more accurately, during the "execute" step), it will add the root of the Merkle tree to its +`globalRoot`. ![globalroot.png](../img/globalroot.png) -The `globalRoot` is the root of the Merkle tree that includes all messages from all chains. Each chain regularly reads the globalRoot value from the Gateway to stay synchronized. +The `globalRoot` is the root of the Merkle tree that includes all messages from all chains. Each chain regularly reads +the globalRoot value from the Gateway to stay synchronized. ![gateway.png](../img/gateway.png) -If a user wants to call `verifyInteropMessage` on a chain, they first need to query the Gateway for the Merkle path from the batch they are interested in up to the `globalRoot`. Once they have this path, they can provide it as an argument when calling a method on the destination chain (such as the `openSignup` method in our example). +If a user wants to call `verifyInteropMessage` on a chain, they first need to query the Gateway for the Merkle path from +the batch they are interested in up to the `globalRoot`. Once they have this path, they can provide it as an argument +when calling a method on the destination chain (such as the `openSignup` method in our example). ![proofmerklepath.png](../img/proofmerklepath.png) #### What if the Gateway doesn’t respond? -If the Gateway doesn’t respond, users can manually re-create the Merkle proof using data available on L1. Every interopMessage is also sent to L1. +If the Gateway doesn’t respond, users can manually re-create the Merkle proof using data available on L1. Every +interopMessage is also sent to L1. #### Global roots change frequently -Yes, global roots update continuously as new chains prove their blocks. However, chains retain historical global roots for a reasonable period (around 24 hours) to ensure that recently generated Merkle paths remain valid. - +Yes, global roots update continuously as new chains prove their blocks. However, chains retain historical global roots +for a reasonable period (around 24 hours) to ensure that recently generated Merkle paths remain valid. #### Is this secure? Could a chain operator, like Chain D, use a different global root? -Yes, it’s secure. If a malicious operator on Chain D attempted to use a different global root, they wouldn’t be able to submit the proof for their new batch to the Gateway. This is because the proof’s public inputs must include the valid global root. +Yes, it’s secure. If a malicious operator on Chain D attempted to use a different global root, they wouldn’t be able to +submit the proof for their new batch to the Gateway. This is because the proof’s public inputs must include the valid +global root. #### What if the Gateway is malicious? -If the Gateway behaves maliciously, it wouldn’t be able to submit its batches to L1, as the proof would fail verification. A separate section will cover interop transaction security in more detail. +If the Gateway behaves maliciously, it wouldn’t be able to submit its batches to L1, as the proof would fail +verification. A separate section will cover interop transaction security in more detail. ## ElasticChain vs SuperChain + Optimism’s SuperChain and ElasticChain offer similar interfaces, but there are notable differences. Let’s compare them. ### Main Contract @@ -149,19 +177,23 @@ Optimism’s SuperChain and ElasticChain offer similar interfaces, but there are - **ElasticChain:** `InteropCenter` – predeployed at a TBD address. -Note: SuperChain also provides a `L2ToL2CrossDomainMessenger`, which will be compared with ElasticChain’s `InteropCall` in the next section. +Note: SuperChain also provides a `L2ToL2CrossDomainMessenger`, which will be compared with ElasticChain’s `InteropCall` +in the next section. ### Sending Messages -- **SuperChain:** Any existing event can implicitly act as a message. There is no explicit "send" mechanism; you simply rely on existing Ethereum events. + +- **SuperChain:** Any existing event can implicitly act as a message. There is no explicit "send" mechanism; you simply + rely on existing Ethereum events. - **ElasticChain:** Requires explicitly calling a method to send the message: `SendInteropMessage`. #### Why the Difference? -ElasticChain performs additional operations, such as creating Merkle trees and transmitting message content to the Gateway and L1. Applying these processes to all Ethereum events would increase costs significantly. - -Additionally, in privacy validium setups, not all events can be shared with the settlement layers, necessitating a more deliberate approach. +ElasticChain performs additional operations, such as creating Merkle trees and transmitting message content to the +Gateway and L1. Applying these processes to all Ethereum events would increase costs significantly. +Additionally, in privacy validium setups, not all events can be shared with the settlement layers, necessitating a more +deliberate approach. ### Receiving Messages @@ -170,14 +202,17 @@ Additionally, in privacy validium setups, not all events can be shared with the - **ElasticChain:** `verifyInteropMessage(bytes32 identifier, Proof proof)` #### Why the Difference? -In SuperChain, a larger identifier is required to specify which chain and block should be queried for event data. However, the msgHash (derived from Ethereum logs) is not guaranteed to be globally unique. -In ElasticChain, the identifier is globally unique because it’s derived from the hash of a struct that includes the source chain ID. ElasticChain also requires the caller to provide a proof, such as the Merkle path to the globalRoot. +In SuperChain, a larger identifier is required to specify which chain and block should be queried for event data. +However, the msgHash (derived from Ethereum logs) is not guaranteed to be globally unique. -The main difference lies in the fact that SuperChain relies on the honesty of its sequencers. In the future, it plans to implement cross-chain FaultProofs, though these mechanisms are not yet fully designed. +In ElasticChain, the identifier is globally unique because it’s derived from the hash of a struct that includes the +source chain ID. ElasticChain also requires the caller to provide a proof, such as the Merkle path to the globalRoot. +The main difference lies in the fact that SuperChain relies on the honesty of its sequencers. In the future, it plans to +implement cross-chain FaultProofs, though these mechanisms are not yet fully designed. -### Message Struct & Identifier +### Message Struct & Identifier - **SuperChain:** @@ -195,17 +230,17 @@ struct Identifier { } ``` -- **ElasticChain:** +- **ElasticChain:** ```solidity // Message struct InteropMessage { bytes data; - address sender; - uint256 sourceChainId; + address sender; + uint256 sourceChainId; uint256 messageNum; // a 'nonce' to guarantee different hashes. } - + // Identifier keccak(InteropMessage) ``` @@ -214,12 +249,18 @@ struct Identifier { #### Dependency Set -- **SuperChain:** SuperChain introduces a concept of a dependency set, which defines a list of chains from which a chain will accept messages. This forms a directed graph, meaning chain A can receive messages from chain B, but chain B may not receive messages from chain A. +- **SuperChain:** SuperChain introduces a concept of a dependency set, which defines a list of chains from which a chain + will accept messages. This forms a directed graph, meaning chain A can receive messages from chain B, but chain B may + not receive messages from chain A. -- **ElasticChain:** In ElasticChain, this is implicitly handled by the Gateway. Any chain that is part of the global root can exchange messages with any other chain, effectively forming an undirected graph. +- **ElasticChain:** In ElasticChain, this is implicitly handled by the Gateway. Any chain that is part of the global + root can exchange messages with any other chain, effectively forming an undirected graph. #### Timestamps and Expiration -- **SuperChain:** SuperChain includes the concept of message expiration, preventing verification of events that are too old (though the exact details are still TBD). -- **ElasticChain:** In ElasticChain, older messages become increasingly difficult to validate as it becomes harder to gather the data required to construct a Merkle proof. Expiration is also being considered for this reason, but the specifics are yet to be determined. +- **SuperChain:** SuperChain includes the concept of message expiration, preventing verification of events that are too + old (though the exact details are still TBD). +- **ElasticChain:** In ElasticChain, older messages become increasingly difficult to validate as it becomes harder to + gather the data required to construct a Merkle proof. Expiration is also being considered for this reason, but the + specifics are yet to be determined. diff --git a/docs/src/specs/interop/interoptransactions.md b/docs/src/specs/interop/interoptransactions.md index da0a872e1254..1919ef0e768c 100644 --- a/docs/src/specs/interop/interoptransactions.md +++ b/docs/src/specs/interop/interoptransactions.md @@ -2,30 +2,37 @@ ## Basics -The **InteropTransaction** sits at the top of our interop stack, acting as the “delivery” mechanism for **Interop Bundles**. +The **InteropTransaction** sits at the top of our interop stack, acting as the “delivery” mechanism for **Interop +Bundles**. Think of it like a car that picks up our "hitchhiker" bundles and carries them to their destination. ![interoptx.png](../img/interoptx.png) -**Note:** Interop Transactions aren’t the only way to execute a bundle. Once an interop bundle is created on the source chain, users can simply send a regular transaction on the destination chain to execute it. +**Note:** Interop Transactions aren’t the only way to execute a bundle. Once an interop bundle is created on the source +chain, users can simply send a regular transaction on the destination chain to execute it. -However, this approach can be inconvenient as it requires users to have funds on the destination chain to cover gas fees and to configure the necessary network settings (like the RPC address). +However, this approach can be inconvenient as it requires users to have funds on the destination chain to cover gas fees +and to configure the necessary network settings (like the RPC address). -**InteropTransactions** simplify this process by handling everything from the source chain. They allow you to select which **interopBundle** to execute, specify gas details (such as gas amount and gas price), and determine who will cover the gas costs. This can be achieved using tokens on the source chain or through a paymaster. +**InteropTransactions** simplify this process by handling everything from the source chain. They allow you to select +which **interopBundle** to execute, specify gas details (such as gas amount and gas price), and determine who will cover +the gas costs. This can be achieved using tokens on the source chain or through a paymaster. -Once configured, the transaction will automatically execute, either by the chain operator, the gateway, or off-chain tools. +Once configured, the transaction will automatically execute, either by the chain operator, the gateway, or off-chain +tools. An **InteropTransaction** contains two pointers to bundles: -- **feesBundle**: Holds interop calls to cover fees. -- **bundleHash**: Contains the main execution. +- **feesBundle**: Holds interop calls to cover fees. +- **bundleHash**: Contains the main execution. ![ipointers.png](../img/ipointers.png) ## Interface -The function `sendInteropTransaction` provides all the options. For simpler use cases, refer to the helper methods defined later in the article. +The function `sendInteropTransaction` provides all the options. For simpler use cases, refer to the helper methods +defined later in the article. ```solidity contract InteropCenter { @@ -34,15 +41,15 @@ contract InteropCenter { /// This function covers all the cases - we expect most users to use the helper /// functions defined later. function sendInteropTransaction( - destinationChain, + destinationChain, bundleHash, // the main bundle that you want to execute on destination chain gasLimit, // gasLimit & price for execution - gasPrice, + gasPrice, feesBundleHash, // this is the bundle that contains the calls to pay for gas destinationPaymaster, // optionally - you can use a paymaster on destination chain destinationPaymasterInput); // with specific params - + struct InteropTransaction { address sourceChainSender uint256 destinationChain @@ -57,59 +64,70 @@ contract InteropCenter { } ``` -After creating the **InteropBundle**, you can simply call `sendInteropTransaction` to create the complete transaction that will execute the bundle. +After creating the **InteropBundle**, you can simply call `sendInteropTransaction` to create the complete transaction +that will execute the bundle. ## Retries -If your transaction fails to execute the bundle (e.g., due to a low gas limit) or isn’t included at all (e.g., due to too low gasPrice), you can send another transaction to **attempt to execute the same bundle again**. +If your transaction fails to execute the bundle (e.g., due to a low gas limit) or isn’t included at all (e.g., due to +too low gasPrice), you can send another transaction to **attempt to execute the same bundle again**. Simply call `sendInteropTransaction` again with updated gas settings. - ### Example of Retrying -Here’s a concrete example: Suppose you created a bundle to perform a swap that includes transferring 100 ETH, executing the swap, and transferring some tokens back. +Here’s a concrete example: Suppose you created a bundle to perform a swap that includes transferring 100 ETH, executing +the swap, and transferring some tokens back. -You attempted to send the interop transaction with a low gas limit (e.g., 100). Since you didn’t have any base tokens on the destination chain, you created a separate bundle to transfer a small fee (e.g., 0.0001) to cover the gas. +You attempted to send the interop transaction with a low gas limit (e.g., 100). Since you didn’t have any base tokens on +the destination chain, you created a separate bundle to transfer a small fee (e.g., 0.0001) to cover the gas. -You sent your first interop transaction to the destination chain, but it failed due to insufficient gas. However, your “fee bundle” was successfully executed, as it covered the gas cost for the failed attempt. +You sent your first interop transaction to the destination chain, but it failed due to insufficient gas. However, your +“fee bundle” was successfully executed, as it covered the gas cost for the failed attempt. Now, you have two options: either cancel the execution bundle (the one with 100 ETH) or retry. -To retry, you decide to set a higher gas limit (e.g., 10,000) and create another fee transfer (e.g., 0.01) but use **the same execution bundle** as before. +To retry, you decide to set a higher gas limit (e.g., 10,000) and create another fee transfer (e.g., 0.01) but use **the +same execution bundle** as before. -This time, the transaction succeeds — the swap completes on the destination chain, and the resulting tokens are successfully transferred back to the source chain. +This time, the transaction succeeds — the swap completes on the destination chain, and the resulting tokens are +successfully transferred back to the source chain. ![retryexample.png](../img/retryexample.png) ## Fees & Restrictions -Using an **InteropBundle** for fee payments offers flexibility, allowing users to transfer a small amount to cover the fees while keeping the main assets in the execution bundle itself. +Using an **InteropBundle** for fee payments offers flexibility, allowing users to transfer a small amount to cover the +fees while keeping the main assets in the execution bundle itself. ### Restrictions -This flexibility comes with trade-offs, similar to the validation phases in **Account Abstraction** or **ERC4337**, primarily designed to prevent DoS attacks. Key restrictions include: +This flexibility comes with trade-offs, similar to the validation phases in **Account Abstraction** or **ERC4337**, +primarily designed to prevent DoS attacks. Key restrictions include: -- **Lower gas limits** +- **Lower gas limits** - **Limited access to specific slots** -Additionally, when the `INTEROP_CENTER` constructs an **InteropTransaction**, it enforces extra restrictions on **feePaymentBundles**: +Additionally, when the `INTEROP_CENTER` constructs an **InteropTransaction**, it enforces extra restrictions on +**feePaymentBundles**: - **Restricted Executors**: - Only your **AliasedAccount** on the receiving side can execute the `feePaymentBundle`. - -This restriction is crucial for security, preventing others from executing your **fee bundle**, which could cause your transaction to fail and prevent the **execution bundle** from processing. - + Only your **AliasedAccount** on the receiving side can execute the `feePaymentBundle`. +This restriction is crucial for security, preventing others from executing your **fee bundle**, which could cause your +transaction to fail and prevent the **execution bundle** from processing. ### **Types of Fees** #### Using the Destination Chain’s Base Token -The simplest scenario is when you (as the sender) already have the destination chain’s base token available on the source chain. +The simplest scenario is when you (as the sender) already have the destination chain’s base token available on the +source chain. For example: -- If you are sending a transaction from **Era** (base token: ETH) to **Sophon** (base token: SOPH) and already have SOPH on ERA, you can use it for the fee. + +- If you are sending a transaction from **Era** (base token: ETH) to **Sophon** (base token: SOPH) and already have SOPH + on ERA, you can use it for the fee. To make this easier, we’ll provide a helper function: @@ -119,7 +137,7 @@ contract InteropCenter { // Before calling, you have to 'approve' InteropCenter to the ERC20/Bridge that holds the destination chain's base tokens. // or if the destination chain's tokens are the same as yours, just attach value to this call. function sendInteropTxMinimal( - destinationChain, + destinationChain, bundleHash, // the main bundle that you want to execute on destination chain gasLimit, // gasLimit & price for execution gasPrice, @@ -129,9 +147,11 @@ contract InteropCenter { #### Using paymaster on the destination chain -If you don’t have the base token from the destination chain (e.g., SOPH in our example) on your source chain, you’ll need to use a paymaster on the destination chain instead. +If you don’t have the base token from the destination chain (e.g., SOPH in our example) on your source chain, you’ll +need to use a paymaster on the destination chain instead. -In this case, you’ll send the token you do have (e.g., USDC) to the destination chain as part of the **feeBundleHash**. Once there, you’ll use it to pay the paymaster on the destination chain to cover your gas fees. +In this case, you’ll send the token you do have (e.g., USDC) to the destination chain as part of the **feeBundleHash**. +Once there, you’ll use it to pay the paymaster on the destination chain to cover your gas fees. Your **InteropTransaction** would look like this: @@ -139,44 +159,42 @@ Your **InteropTransaction** would look like this: ## **Automatic Execution** -One of the main advantages of **InteropTransactions** is that they execute automatically. As the sender on the source chain, you don’t need to worry about technical details like RPC addresses or obtaining proofs — it’s all handled for you. +One of the main advantages of **InteropTransactions** is that they execute automatically. As the sender on the source +chain, you don’t need to worry about technical details like RPC addresses or obtaining proofs — it’s all handled for +you. -After creating an **InteropTransaction**, it can be relayed to the destination chain by anyone. The transaction already includes a signature (also known as an interop message proof), making it fully self-contained and ready to send without requiring additional permissions. +After creating an **InteropTransaction**, it can be relayed to the destination chain by anyone. The transaction already +includes a signature (also known as an interop message proof), making it fully self-contained and ready to send without +requiring additional permissions. -Typically, the destination chain’s operator will handle and include incoming **InteropTransactions**. However, if they don’t, the **Gateway** or other participants can step in to prepare and send them. +Typically, the destination chain’s operator will handle and include incoming **InteropTransactions**. However, if they +don’t, the **Gateway** or other participants can step in to prepare and send them. -You can also use the available tools to create and send the destination transaction yourself. Since the transaction is self-contained, it doesn’t require additional funds or signatures to execute. +You can also use the available tools to create and send the destination transaction yourself. Since the transaction is +self-contained, it doesn’t require additional funds or signatures to execute. ![Usually destination chain operator will keep querying gateway to see if there are any messages for their chain.](../img/autoexecution.png) -Once they see the message, they can request the proof from the **Gateway** and also fetch the **InteropBundles** contained within the message (along with their respective proofs). +Once they see the message, they can request the proof from the **Gateway** and also fetch the **InteropBundles** +contained within the message (along with their respective proofs). ![Operator getting necessary data from Gateway.](../img/chainop.png) -As the final step, the operator can use the received data to create a regular transaction, which can then be sent to their chain. - +As the final step, the operator can use the received data to create a regular transaction, which can then be sent to +their chain. ![Creating the final transaction to send to the destination chain](../img/finaltx.png) The steps above don’t require any special permissions and can be executed by anyone. -While the **Gateway** was used above for tasks like providing proofs, if the Gateway becomes malicious, all this information can still be constructed off-chain using data available on L1. - +While the **Gateway** was used above for tasks like providing proofs, if the Gateway becomes malicious, all this +information can still be constructed off-chain using data available on L1. ### How it Works Under the Hood -We’ll modify the default account to accept interop proofs as signatures, seamlessly integrating with the existing ZKSync native **Account Abstraction** model. - +We’ll modify the default account to accept interop proofs as signatures, seamlessly integrating with the existing ZKSync +native **Account Abstraction** model. ### Comparison with Superchain Superchain doesn’t offer anything that is comparable to **InteropTransactions** on this level. - - - - - - - - - diff --git a/docs/src/specs/interop/overview.md b/docs/src/specs/interop/overview.md index 7f8f388d6b63..4437f5f03795 100644 --- a/docs/src/specs/interop/overview.md +++ b/docs/src/specs/interop/overview.md @@ -1,26 +1,25 @@ # Intro Guide to Interop ## What is Interop? + Interop is a way to communicate and transact between two ZK Stack chains. It allows you to: -**1. Observe messages:** -Track when an interop message (think of it as a special event) is created on the source chain. +**1. Observe messages:** Track when an interop message (think of it as a special event) is created on the source chain. -**2. Send assets:** -Transfer ERC20 tokens and other assets between chains. +**2. Send assets:** Transfer ERC20 tokens and other assets between chains. -**3. Execute calls:** -Call a contract on a remote chain with specific calldata and value. +**3. Execute calls:** Call a contract on a remote chain with specific calldata and value. -With interop, you automatically get an account (a.k.a. aliasedAccount) on each chain, which you can control from the source chain. +With interop, you automatically get an account (a.k.a. aliasedAccount) on each chain, which you can control from the +source chain. -**4. Execute bundles of calls:** -Group multiple remote calls into a single bundle, ensuring all of them execute at once. +**4. Execute bundles of calls:** Group multiple remote calls into a single bundle, ensuring all of them execute at once. -**5. Execute transactions:** -Create transactions on the source chain, which will automatically get executed on the destination chain, with options to choose from various cross-chain Paymaster solutions to handle gas fees. +**5. Execute transactions:** Create transactions on the source chain, which will automatically get executed on the +destination chain, with options to choose from various cross-chain Paymaster solutions to handle gas fees. ## How to Use Interop + Here’s a simple example of calling a contract on a destination chain: ```solidity @@ -34,24 +33,29 @@ cast send source-chain-rpc.com INTEROP_CENTER_ADDRESS sendInteropWithSingleCall( ) ``` -While this looks very similar to a 'regular' call, there are some nuances, especially around handling failures and errors. +While this looks very similar to a 'regular' call, there are some nuances, especially around handling failures and +errors. Let’s explore these key details together. ## Common Questions and Considerations #### 1. Who pays for gas? -When using this method, your account must hold `gasLimit * gasPrice` worth of destination chain tokens on the source chain. -For example, if you’re sending the request from Era and the destination chain is Sophon (with SOPH tokens), you’ll need SOPH tokens available on Era. +When using this method, your account must hold `gasLimit * gasPrice` worth of destination chain tokens on the source +chain. + +For example, if you’re sending the request from Era and the destination chain is Sophon (with SOPH tokens), you’ll need +SOPH tokens available on Era. Additional payment options are available, which will be covered in later sections. #### 2. How does the destination contract know it’s from me? -The destination contract will see `msg.sender` as `keccak(source_account, source_chain)[:20]`. -Ideally, we would use something like `source_account@source_chain` (similar to an email format), but since Ethereum addresses are limited to 20 bytes, we use a Keccak hash to fit this constraint. +The destination contract will see `msg.sender` as `keccak(source_account, source_chain)[:20]`. +Ideally, we would use something like `source_account@source_chain` (similar to an email format), but since Ethereum +addresses are limited to 20 bytes, we use a Keccak hash to fit this constraint. #### 3. Who executes it on the destination chain? @@ -61,17 +65,19 @@ The call is auto-executed on the destination chain. As a user, you don’t need In either scenario, you can retry the transaction using the `retryInteropTransaction` method: - ```solidity - cast send source-chain.com INTEROP_CENTER_ADDRESS retryInteropTransaction( - 0x2654.. // previous interop transaction hash from above - 200_000, // new gasLimit - 300_000_000 // new gasPrice - ) +```solidity + cast send source-chain.com INTEROP_CENTER_ADDRESS retryInteropTransaction( + 0x2654.. // previous interop transaction hash from above + 200_000, // new gasLimit + 300_000_000 // new gasPrice + ) ``` -**Important** : Depending on your use case, it’s crucial to retry the transaction rather than creating a new one with `sendInteropWithSingleCall`. +**Important** : Depending on your use case, it’s crucial to retry the transaction rather than creating a new one with +`sendInteropWithSingleCall`. -For example, if your call involves transferring a large amount of assets, initiating a new `sendInteropWithSingleCall` could result in freezing or burning those assets again. +For example, if your call involves transferring a large amount of assets, initiating a new `sendInteropWithSingleCall` +could result in freezing or burning those assets again. #### 5. What if my assets were burned during the transaction, but it failed on the destination chain? How do I get them back? @@ -89,7 +95,8 @@ cast send source-chain INTEROP_CENTER_ADDRESS cancelInteropTransaction( ) ``` -After cancellation, call the claimFailedDeposit method on the source chain contracts to recover the burned assets. Note that the details for this step may vary depending on the contract specifics. +After cancellation, call the claimFailedDeposit method on the source chain contracts to recover the burned assets. Note +that the details for this step may vary depending on the contract specifics. ## Complex Scenario @@ -104,33 +111,43 @@ To accomplish this, you’ll need to: The step-by-step process and exact details will be covered in the next section. ## Technical Details + ### How is Interop Different from a Bridge? + Bridges generally fall into two categories: Native and Third-Party. #### 1. Native Bridges -Native bridges enable asset transfers “up and down” (from L2 to L1 and vice versa). In contrast, interop allows direct transfers between different L2s. +Native bridges enable asset transfers “up and down” (from L2 to L1 and vice versa). In contrast, interop allows direct +transfers between different L2s. -Instead of doing a "round trip" (L2 → L1 → another L2), interop lets you move assets directly between two L2s, saving both time and cost. +Instead of doing a "round trip" (L2 → L1 → another L2), interop lets you move assets directly between two L2s, saving +both time and cost. #### 2. Third-Party Bridging -Third-party bridges enable transfers between two L2s, but they rely on their own liquidity. While you, as the user, receive assets on the destination chain instantly, these assets come from the bridge’s liquidity pool. +Third-party bridges enable transfers between two L2s, but they rely on their own liquidity. While you, as the user, +receive assets on the destination chain instantly, these assets come from the bridge’s liquidity pool. -Bridge operators then rebalance using native bridging, which requires maintaining token reserves on both sides. This adds costs for the bridge operators, often resulting in higher fees for users. +Bridge operators then rebalance using native bridging, which requires maintaining token reserves on both sides. This +adds costs for the bridge operators, often resulting in higher fees for users. -The good news is that third-party bridges can use interop to improve their token transfers by utilizing the **InteropMessage** layer. +The good news is that third-party bridges can use interop to improve their token transfers by utilizing the +**InteropMessage** layer. More details on this will follow below. ### How Fast is It? -Interop speed is determined by its lowest level: **InteropMessage** propagation speed. This essentially depends on how quickly the destination chain can confirm that the message created by the source chain is valid. -- **Default Mode:** -To prioritize security, the default interop mode waits for a ZK proof to validate the message, which typically takes around 10 minutes. +Interop speed is determined by its lowest level: **InteropMessage** propagation speed. This essentially depends on how +quickly the destination chain can confirm that the message created by the source chain is valid. -- **Fast Mode (Planned):** -We are developing an alternative **INTEROP_CENTER** contract (using a different address but the same interface) that will operate within 1 second. However, this faster mode comes with additional risks, similar to the approach used by optimistic chains. +- **Default Mode:** To prioritize security, the default interop mode waits for a ZK proof to validate the message, which + typically takes around 10 minutes. + +- **Fast Mode (Planned):** We are developing an alternative **INTEROP_CENTER** contract (using a different address but + the same interface) that will operate within 1 second. However, this faster mode comes with additional risks, similar + to the approach used by optimistic chains. ### 4 Levels of Interop @@ -147,4 +164,3 @@ When analyzing interop, it can be broken into four levels, allowing you to choos ![levelsofinterop.png](../img/levelsofinterop.png) We will be covering the details of each layer in the next section. -