Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: l2tol2cdm RelayedMessage return value #227

Merged
merged 6 commits into from
Apr 11, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions ecosystem/l2tol2cdm-relayedmessage-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# L2ToL2CrossDomainMessenger RelayedMessage return event field: Design Doc

| | |
| ------------------ | --------------------------------------- |
| Author | Hamdi Allam |
| Created at | 2025-03-24 |
| Initial Reviewers | Wonderland, Mark Tyneway, Harry Markley |
| Need Approval From | Wonderland, Mark Tyneway, Harry Markley |
| Status | In Review |

## Purpose

The ability to read events with Superchain interop is incredibly powerful. We should make sure we maximize the potential of all the default events emitted when interacting with the OP-Stack predeploys.

## Summary

With an added field to the `RelayedMessage` event emitted by the `L2ToL2CrossDomainMessenger`, we open the door to better async contract development within the Superchain.

## Problem Statement + Context

As we horizontally scale, contract development will look more asynchronous. A lot of cross chain messages are predominantly write-based.

- Bridge Funds
- Invoke calldata on a remotely controlled proxy account

Read-based cross chain messages are less common but will increasingly become so in an asynchronous world. Solidity has a `view` modifier specifically targeted for reading -- it would be natural to also want to also query this state onchain very quickly.
Copy link
Contributor

@tynes tynes Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some usecases, its not safe to read remote data without knowing the timestamp of the read. You could be tricked into reading stale data. Is there an easy way to see the timestamp of the read?

Copy link
Contributor Author

@hamdiallam hamdiallam Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identifier of the remote log plays in nicely here. The consumer on the source chain can choose to reject at whatever latency, the RelayedMessage event if the timestamp of the event is stale

Nothing needs to change on the destination.


- Fetch remote swap quotes
- Query balance/ownership status
- Chain on subsequent write actions, only after success (deposit -> borrow -> bridge)

In order to support use cases like these, custom handler contracts per-application must be written to capture these side effects and return data backwards or chain on additional side effects. For example

```solidity
contract ERC20BalanceGateway {
L2ToL2CrossDomainMessenger messenger;

// Gateway contract that can be used to query ERC20s with a specified handler
function query(IERC20 token, address account, bytes4 selector) external {
uint256 balance = token.balanceOf(account);

// Message sent back with the value as an encoded argument
(address sender, uint256 source) = messenger.crossDomainMessageContext();
messenger.sendMessage(source, sender, abi.encode(selector, balance));
}
}
```

## Proposed Solution

When the `L2ToL2CrossDomainMessenger` invokes a target with calldata, the return data of that call is captured and returned via `relayMessage`. However this return data is only actionable for the caller of this function, typically the relayer, or a faciliating smart contract.

`relayMessage` emits the `RelayedMessage` event on successful execution which itself can be consumed remotely via the `CrossL2Inbox` predeploy. By including the return data in this event, it becomes immediately visible to the sending chain (and all others in the interop set) without any additional message or handler contract.

To limit the gas overhead of this inclusion, the hash of the return data is emitted such that authentication is possible by the consumer with the provided data.

```solidity
contract L2ToL2CrossDomainMessenger {
function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) {
// include return data
(, returnData_) = target.call{ value: msg.value }(message);
emit RelayedMessage(source, nonce, messageHash, keccak256(returnData_));
}
}
```

The `messageHash` known ahead of time can be correlated with the corresponding `RelayedMessage`. Thus the return value of any read function or return value of a write call becomes immediately actionable for the sender, without requiring special integration.
Copy link
Contributor

@tremarkley tremarkley Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to read/validate the RelayedMessage, the identifier of the RelayedMessage is a requirement.
Wouldn't an integration be required for constructing the identifier of the RelayedMessage since you cannot calculate the identifier ahead of time?

Copy link
Contributor Author

@hamdiallam hamdiallam Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by ahead of time here, I meant with the return value of sendMessage, the message hash. You can directly chain the continuation since the caller just needs to assert the msg hash matches with the RelayedMessage event.

I'll make this a bit more clear

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to the message identifier not being able to be calculated ahead of time and not the message hash. The identifier is needed in order to trigger the continuation, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea after relay is when the continuation would be invoked. you need the return value

the identifier isnt computed ahead of time. It can just be consumed right away after emission

Copy link
Contributor

@tremarkley tremarkley Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah and so that identifier and message will need to be fetched or indexed offchain. Do you see this requiring some additional offchain infra to find and “relay” these events back to the source chain? Relay in quotes because it’s not a relay using relayMessage, but you need to have some service that fetches that event and then triggers the continuation on the source chain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, we'll definitely need indexing of pending promises that relayers will have to handle separately. Especially now that the data is hashed.

This plays in nicely for protocols that have to propagate the return data backwards, which incurs extra gas cost


See the Promise Library [Design Doc](https://github.com/ethereum-optimism/design-docs/pull/216)

### Resource Usage

The extra gas cost associated with including a variable sized input into the `RelayedMessage` log. There's a gas cost of 8 per non-zero byte (4 for zeros) in log data.

We cap the overhead by including a hash of the return data that can be authenticated by the consumer of the `RelayedMessage` event. It is up to the relayer of this event to include the appropriate return data for the caller.

### Single Point of Failure and Multi Client Considerations

This proposal doesnt change the callpath of cross domain messenger, thus has changes no impact on how messages are relayed. The L2ToL2CrossDomainMessenger is not used in production outside of the existing devnet. Thus it is a good opportunity to make this change even thought it breaks the event signature for any indexers that may rely on it.

- `RelayedMessage` also is not indexed in the callpath relative to `SentMessage` which is required to relaye a message, thus additionally safe to break right now.

There's some additional gas overhead to relaying messages. A malicious contract can variable return a large amount of data to make it hard to query. Hashing the return data included in the event mitigates the blast radius of the gas increase.

## Alternatives Considered

### Entrypoint

With [Entrypoints](https://github.com/ethereum-optimism/specs/pull/484), we could envision a general purpose entrypoint that simply emits the captured return values

```solidity
contract ReturnEmitterEntrypoint {
function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) {
bytes32 parsedMessageHash;
bytes memory returnData = messenger.relayMessage(_id, _sentMessage);
emit ReturnValue(parsedMessageHash, returnData)
}
}
```

However this limits additional tooling such as a general Promise library. Where every outbound message from the L2ToL2CDM returns a message hash that can behave like a Promise. In this approach, only outbound messages leveraging this entrypoint could.

Entrypoint composition is also a bit undefined. Thus creates more uncertain behavior with a Promise-like abstraction or capturing the return amongst the involvement of different entrypoints.

## Risks & Uncertainties

### Resource Usage

See the [Resource Usage](#resource-usage) section.