diff --git a/specs/interop/messaging.md b/specs/interop/messaging.md index add17c6dd..eccaba191 100644 --- a/specs/interop/messaging.md +++ b/specs/interop/messaging.md @@ -101,8 +101,11 @@ An initiating message may be executed many times: no replay-protection is enshri ### Executing Messages An executing message is represented by the [ExecutingMessage event][event] that is emitted by -the `CrossL2Inbox` predeploy. This event is coupled to a `CALL` with the payload that is emitted -within the event, allowing introspection of the data without needing to do call tracing. +the `CrossL2Inbox` predeploy. If the cross chain message is directly executed via [executeMessage](./predeploys.md#executemessage), +the event is coupled to a `CALL` with the payload that is emitted within the event to the target +address, allowing introspection of the data. Contracts can also introduce their own public +entrypoints and solely trigger validation of the cross chain message with [validateMessage](./predeploys.md#validatemessage). + All of the information required to satisfy the invariants MUST be included in this event. [event]: ./predeploys.md#executingmessage-event diff --git a/specs/interop/predeploys.md b/specs/interop/predeploys.md index a2a17c7ba..8d6fa0b49 100644 --- a/specs/interop/predeploys.md +++ b/specs/interop/predeploys.md @@ -5,10 +5,9 @@ **Table of Contents** - [CrossL2Inbox](#crossl2inbox) - - [Message execution arguments](#message-execution-arguments) - - [`_msg`](#_msg) - - [`_id`](#_id) - - [`_target`](#_target) + - [Functions](#functions) + - [executeMessage](#executemessage) + - [validateMessage](#validatemessage) - [Interop Start Timestamp](#interop-start-timestamp) - [`ExecutingMessage` Event](#executingmessage-event) - [Reference implementation](#reference-implementation) @@ -31,7 +30,7 @@ - [Proxy](#proxy) - [Beacon Pattern](#beacon-pattern) - [Deployment history](#deployment-history) - - [Functions](#functions) + - [Functions](#functions-1) - [`deploy`](#deploy) - [Events](#events) - [`OptimismSuperchainERC20Created`](#optimismsuperchainerc20created) @@ -47,7 +46,7 @@ - [OptimismMintableERC20Factory](#optimismmintableerc20factory) - [OptimismMintableERC20](#optimismmintableerc20) - [Updates](#updates) - - [Functions](#functions-1) + - [Functions](#functions-2) - [`createOptimismMintableERC20WithDecimals`](#createoptimismmintableerc20withdecimals) - [`createOptimismMintableERC20`](#createoptimismmintableerc20) - [`createStandardL2Token`](#createstandardl2token) @@ -62,7 +61,7 @@ - [Conversion Flow](#conversion-flow) - [SuperchainERC20Bridge](#superchainerc20bridge) - [Overview](#overview-2) - - [Functions](#functions-2) + - [Functions](#functions-3) - [`sendERC20`](#senderc20) - [`relayERC20`](#relayerc20) - [Events](#events-2) @@ -83,47 +82,30 @@ an update to the `L1Block`, `OptimismMintableERC20Factory` and `L2StandardBridge | -------- | -------------------------------------------- | | Address | `0x4200000000000000000000000000000000000022` | -The `CrossL2Inbox` is responsible for executing a cross chain message on the destination chain. -It is permissionless to execute a cross chain message on behalf of any user. +The `CrossL2Inbox` is the system predeploy for cross chain messaging. Anyone can trigger the execution or validation +of cross chain messages, on behalf of any user. To ensure safety of the protocol, the [Message Invariants](./messaging.md#messaging-invariants) must be enforced. -### Message execution arguments - -The following fields are required for executing a cross chain message: - [message payload]: ./messaging.md#message-payload [`Identifier`]: ./messaging.md#message-identifier +### Functions + +#### executeMessage + +Executes a cross chain message and performs a `CALL` with the payload to the provided target address, allowing +introspection of the data. +Signals the transaction has a cross chain message to validate by emitting the `ExecuteMessage` event. + +The following fields are required for executing a cross chain message: + | Name | Type | Description | | --------- | ------------ | ------------------------------------------------------- | | `_msg` | `bytes` | The [message payload], matching the initiating message. | | `_id` | `Identifier` | A [`Identifier`] pointing to the initiating message. | | `_target` | `address` | Account that is called with `_msg`. | -#### `_msg` - -The [message payload] of the executing message. - -This must match the emitted payload of the initiating message identified by `_id`. - -#### `_id` - -A pointer to the `_msg` in a remote (or local) chain. - -The message [`Identifier`] of the executing message. -This is required to enforce the message executes an existing and valid initiating message. - -By including the [`Identifier`] in the calldata, it makes static analysis much easier for block builders. -It is impossible to check that the [`Identifier`] matches the cross chain message on chain. If the block -builder includes a message that does not correspond to the [`Identifier`], their block will be reorganized -by the derivation pipeline. - -A possible upgrade path to this contract would involve adding a new function. If any fields in the [`Identifier`] -change, then a new 4byte selector will be generated by solc. - -#### `_target` - Messages are broadcast, not directed. Upon execution the caller can specify which `address` to target: there is no protocol enforcement on what this value is. @@ -132,6 +114,26 @@ In practice, the `_target` will be a contract that needs to know the schema of t It MAY call back to the `CrossL2Inbox` to authenticate properties about the `_msg` using the information in the `Identifier`. +```solidity +executeMessage(Identifier calldata _id, address _target, bytes memory _message) +``` + +#### validateMessage + +A helper to enable contracts to provide their own public entrypoints for cross chain interactions. +Emits the `ExecutingMessage` event to signal the transaction has a cross chain message to validate. + +The following fields are required for validating a cross chain message: + +| Name | Type | Description | +| -------- | ---------- | -------------------------------------------------------------------------- | +| `_id` | Identifier | A [`Identifier`] pointing to the initiating message. | +| `_msgHash` | `bytes32` | The keccak256 hash of the message payload matching the initiating message. | + +```solidity +validateMessage(Identifier calldata _id, bytes32 _msgHash) +``` + ### Interop Start Timestamp The Interop Start Timestamp represents the earliest timestamp which an initiating message (identifier) can have to be @@ -146,7 +148,7 @@ that timestamp into the pre-determined storage slot. ### `ExecutingMessage` Event The `ExecutingMessage` event represents an executing message. It MUST be emitted on every call -to `executeMessage`. +to `executeMessage` and `validateMessage`. ```solidity event ExecutingMessage(bytes32 indexed msgHash, Identifier identifier); @@ -196,11 +198,61 @@ function executeMessage(Identifier calldata _id, address _target, bytes calldata Note that the `executeMessage` function is `payable` to enable relayers to earn in the gas paying asset. +An example of encoding a cross chain call directly in an event. However realize the +[L2ToL2CrossDomainMessenger](#l2tol2crossdomainmessenger) predeploy provides a cleaner and user +friendly abstraction for cross chain calls. + +```solidity +contract MyCrossChainApp { + function sendMessage() external { + bytes memory data = abi.encodeCall(MyCrossChainApp.relayMessage, (1, address(0x20))); + + // Encoded payload matches the required calldata by omission of an event topic + assembly { + log0(add(data, 0x20), mload(data)) + } + } + + function relayMessage(uint256 value, address recipient) external { + // Assert that this is only executed directly from the inbox + require(msg.sender == Predeploys.CrossL2Inbox); + } +} +``` + +An example of a custom entrypoint utilizing `validateMessage` to consume a known +event. Note that in this example, the contract is consuming its own event +from another chain, however **any** event emitted from **any** contract is consumable! + +```solidity +contract MyCrossChainApp { + event MyCrossChainEvent(); + + function sendMessage() external { + emit MyCrossChainEvent(); + } + + function relayMessage(Identifier calldata _id, bytes calldata _msg) external { + // Example app-level validation + // - Expected event via the selector (first topic) + // - Assertion on the expected emitter of the event + require(MyCrossChainEvent.selector == _msg[:32]); + require(_id.origin == address(this)); + + // Authenticate this cross chain message + CrossL2Inbox.validateMessage(_id, keccak256(_msg)); + + // ABI decode the event message & perform actions. + // ... + } +} +``` + ### Deposit Handling Any call to the `CrossL2Inbox` that would emit an `ExecutingMessage` event will reverts if the call is made in a [deposit context](./derivation.md#deposit-context). -The deposit context status can be determined by callling `isDeposit` on the `L1Block` contract. +The deposit context status can be determined by calling `isDeposit` on the `L1Block` contract. In the future, deposit handling will be modified to be more permissive. It will revert only in specific cases where interop dependency resolution is not feasible. @@ -219,16 +271,14 @@ properties about the `_msg`. | `EXPIRY_WINDOW` | `uint256(7200)` | The `L2ToL2CrossDomainMessenger` is a higher level abstraction on top of the `CrossL2Inbox` that -provides features necessary for secure transfers ERC20 tokens between L2 chains. +provides general message passing, utilized for secure transfers ERC20 tokens between L2 chains. Messages sent through the `L2ToL2CrossDomainMessenger` on the source chain receive both replay protection as well as domain binding, ie the executing transaction can only be valid on a single chain. ### `relayMessage` Invariants -- Only callable by the `CrossL2Inbox` - The `Identifier.origin` MUST be `address(L2ToL2CrossDomainMessenger)` - The `_destination` chain id MUST be equal to the local chain id -- The `CrossL2Inbox` cannot call itself ### `sendExpire` Invariants @@ -276,18 +326,12 @@ The following function is used for sending messages between domains: function sendMessage(uint256 _destination, address _target, bytes calldata _message) external; ``` -It creates an initiating message that is represented by an anonymous event: +It emits a `SentMessage` event with the necessary metadata to execute when relayed on the destination chain. ```solidity -assembly { - log0(add(_data, 0x20), mload(_data)) -} +event SentMessage(uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message);`` ``` -The `_data` is an ABI encoded call to `relayMessage`. The event is done with Yul so that an extra layer -of ABI encoding as `bytes` is not wrapped around `relayMessage` call. The exact calldata meant to be passed -to the `L2ToL2CrossDomainMessenger` on the remote domain is included in the log. - An explicit `_destination` chain and `nonce` are used to ensure that the message can only be played on a single remote chain a single time. The `_destination` is enforced to not be the local chain to avoid edge cases. @@ -317,42 +361,33 @@ first failed along with its source chain id are stored. This is needed for calculation of the failed message's expiry. The source chain id is also required to simplify the function signature of `sendExpire`. -```solidity -function relayMessage(uint256 _destination, uint256 _source, uint256 _nonce, address _sender, address _target, bytes memory _message) external payable { - require(msg.sender == address(CROSS_L2_INBOX)); - require(_destination == block.chainid); - require(CROSS_L2_INBOX.origin() == address(this)); - - bytes32 messageHash = keccak256(abi.encode(_destination, _source, _nonce, _sender, _target, _message)); - require(sentMessages[messageHash] == false); +A message is relayed by providing the [identifier](./messaging.md#message-identifier) to a `SentMessage` +event and its corresponding [message payload](./messaging.md#message-payload). - assembly { - tstore(CROSS_DOMAIN_MESSAGE_SENDER_SLOT, _sender) - tstore(CROSS_DOMAIN_MESSAGE_SOURCE_SLOT, _source) - } +```solidity +function relayMessage(ICrossL2Inbox.Identifier calldata _id, bytes calldata _sentMessage) external payable { + require(_id.origin == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + CrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_id, keccak256(_sentMessage)); - sentMessages[messageHash] = true; + // log topics + (bytes32 selector, uint256 _destination, address _target, uint256 _nonce) = + abi.decode(_sentMessage[:128], (bytes32,uint256,address,uint256)); - bool success = SafeCall.call({ - _target: _target, - _value: msg.value, - _calldata: _message - }); + require(selector == SentMessage.selector); + require(_destination == block.chainid); - if (!success) { - emit FailedRelayedMessage(messageHash); + // log data + (address _sender, bytes memory _message) = abi.decode(_sentMessage[128:], (address,bytes)); - if (failedMessages[messageHash].timestamp == 0) { - failedMessages[messageHash] = FailedMessage({timestamp: block.timestamp, sourceChainId: _source}); - } + bool success = SafeCall.call(_target, msg.value, _message); - return; + if (success) { + successfulMessages[messageHash] = true; + emit RelayedMessage(_source, _nonce, messageHash); + } else { + emit FailedRelayedMessage(_source, _nonce, messageHash); } - - successfulMessages[messageHash] = true; - delete failedMessages[messageHash]; - emit RelayedMessage(messageHash); -}; +} ``` Note that the `relayMessage` function is `payable` to enable relayers to earn in the gas paying asset. diff --git a/specs/protocol/isthmus/overview.md b/specs/protocol/isthmus/overview.md index 1dbd3641e..ec7bf0827 100644 --- a/specs/protocol/isthmus/overview.md +++ b/specs/protocol/isthmus/overview.md @@ -15,4 +15,4 @@ This document is not finalized and should be considered experimental. ## Consensus Layer -- [Interop](../interop/overview.md) +- [Interop](../../interop/overview.md)