Skip to content

feat: l2tol2cdm RelayedMessage return value #227

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

Merged
merged 6 commits into from
Apr 11, 2025
Merged
Changes from 2 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
85 changes: 85 additions & 0 deletions ecosystem/l2tol2cdm-relayedmessage-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 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 emmitted when interacting with the OP-Stack predeploys.

## Goals

- Faciliate cross chain async programming

# Problem Statement + Context

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

- Bridge Funds
- Invoke calldata on via 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 -> brige)

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 within the `relayMessage` function. 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.

```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, 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)

## 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 the 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

The extra gas cost associated with including a variable sized input into `RelayedMessage`. There's a gas cost of 8 per non-zero bytes (4 for zeros) in log data. This is not a real concern for a couple of reasons

1. Dynamically sized return values are generally discouraged, and it is good practice in solidity to ensure bounds here when writing contracts.
2. The caller to `relayMessage` is already gas-sensitive to the captured return data since it returns this to the caller
3. Just as these gas costs are a concern in a single-chain development experience, the developer looking to query a contract cross chain should also be aware of the gas costs of invoking any dynmically sized return values.