From 249d4c02726a840962ee305aba3cb5b38e38d5c7 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 21 Apr 2025 17:26:47 +0800 Subject: [PATCH 1/2] callbacks to sender --- protocol/callbacks-to-sender.md | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 protocol/callbacks-to-sender.md diff --git a/protocol/callbacks-to-sender.md b/protocol/callbacks-to-sender.md new file mode 100644 index 00000000..6a68cf97 --- /dev/null +++ b/protocol/callbacks-to-sender.md @@ -0,0 +1,107 @@ +# Callbacks to Sender: Design Doc + +| | | +| ------------------ | ---------- | +| Author | Jeff Huang | +| Created at | 2025-04-21 | +| Initial Reviewers | | +| Need Approval From | | +| Status | Draft | + +## Purpose + +To help developers better manage `sender`'s state consistency that is contingent on whether `target`'s execution is successful or reverted. + +## Summary + +This feature introduces an overload of `sendMessage()` with custom `onSuccess` and/or `onRevert` callbacks which is automatically invoked by `L2ToL2CrossDomainMessenger` after relaying message to `target`. + +This enables cross-chain state changes to be atomic without introducing logical coupling between `sender` and `target` (i.e. `target` does not need to know the implementation details on `sender`). Also, this is all achieved through the same single user transaction to `sendMessage()`. + +## Problem Statement + Context + +Interops often lead to state changes on both `source` and `destination` that must be consitent. A simple example is `sender` contract locks user fund on `source` and request `target` to debit the approperiate amount on `destination`. Currently there is no mechanism to ensure the two actions occur atomically, so a failed debit for any reason would lead chain states to be inconsistent. + +One way around this is for `source` to cache actions on its side and provide an additional function that `target` can call to roll them back. However, this introduces a tight coupling between `source` and `target` while also require `target` to self-call with a `try/catch` to capture any revert. + +## Proposed Solution + +The proposed solution is to expand `SendMessage` event and overload `sendMessage()` with `onSuccess` and/or `onRevert` callbacks that `L2ToL2CrossDomainMessenger` will automatically invoke depending on what `target.call()` returns. + +```solidity +contract L2ToL2CrossDomainMessenger { + event SentMessage( + uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message, bytes onSuccess, bytes onRevert + ); + + function sendMessage(uint256 _destination, address _target, bytes calldata _message) external returns (bytes32 messageHash_) { + return sendMessage(_destination, _target, _message, "", ""); + } + + function sendMessage(uint256 _destination, address _target, bytes calldata _message, bytes calldata _onSuccess, bytes calldata _onRevert) + external returns (bytes32 messageHash_) + { + // ... + + emit SentMessage(_destination, _target, nonce, msg.sender, _message, _onSuccess, _onRevert); + } + + function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) + external payable nonReentrant returns (bytes memory returnData_) + { + // ... + + (uint256 destination, address target, uint256 nonce, address sender, bytes memory message, bytes memory onSuccess, bytes memory onRevert) = + _decodeSentMessagePayload(_sentMessage); + + // ... + + bool success; + (success, returnData_) = target.call{ value: msg.value }(message); + + if (success) { + if (onSuccess.length > 0) sendMessage(_id.chainId, sender, onSuccess); + } else { + if (onRevert.length > 0) { + sendMessage(_id.chainId, sender, onSuccess); + } else { + assembly { + revert(add(32, returnData_), mload(returnData_)) + } + } + } + } +} +``` + +### Resource Usage + +It adds additional bytes to `L2ToL2CrossDomainMessenger` calldata and `SendMessage` event. + +### Single Point of Failure and Multi Client Considerations + +TBD + +## Failure Mode Analysis + +TBD + +## Impact on Developer Experience + +This is a quality-of-life feature that should improve developer experience. + +- Ensure `sender` state is consistent with `target` execution outcome +- Reduce self-calling and boilerplate code on `target` +- Decouple `sender` from `target`, `sender` can specify its own callback signatures and not confined to some interface that `target` requires + +## Alternatives Considered + +[Promise](https://github.com/ethereum-optimism/supersim/blob/main/contracts/src/Promise.sol) has some similarities but is only intended for the happy execution path. + +[Return data in relayed message event](https://github.com/ethereum-optimism/optimism/pull/14599) could also potentially be used but requires a second transaction to query `CrossL2Inbox` and breaks atomicity. + +## Risks & Uncertainties + +- Currently any revert during `target.call()` will cause `L2ToL2CrossDomainMessenger` to also revert, changes in proposed solution will "swallow" the revert if `onRevert` is specified. Uncertain if this would cause any down stream problem. + +TBD From 8babc6c658a157da44a5958994e726f453c90474 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 21 Apr 2025 17:32:53 +0800 Subject: [PATCH 2/2] workding tweak --- protocol/callbacks-to-sender.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/callbacks-to-sender.md b/protocol/callbacks-to-sender.md index 6a68cf97..77a8a191 100644 --- a/protocol/callbacks-to-sender.md +++ b/protocol/callbacks-to-sender.md @@ -10,11 +10,11 @@ ## Purpose -To help developers better manage `sender`'s state consistency that is contingent on whether `target`'s execution is successful or reverted. +To help developer experience by enabling atomicity on cross-chain state changes between `sender` on `source` and `target` on `destination`. ## Summary -This feature introduces an overload of `sendMessage()` with custom `onSuccess` and/or `onRevert` callbacks which is automatically invoked by `L2ToL2CrossDomainMessenger` after relaying message to `target`. +This feature introduces an overload of `sendMessage()` with custom `onSuccess` and/or `onRevert` callbacks which is automatically invoked by `L2ToL2CrossDomainMessenger` after relaying message to `target`, depending on the outcome. This enables cross-chain state changes to be atomic without introducing logical coupling between `sender` and `target` (i.e. `target` does not need to know the implementation details on `sender`). Also, this is all achieved through the same single user transaction to `sendMessage()`.