-
Notifications
You must be signed in to change notification settings - Fork 41
feat: incentivized cross domain message delivery #272
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
base: l2tol2cdm-gasreceipt
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,162 @@ | ||||||
# [Incentivized Relays MVP]: Design Doc | ||||||
|
||||||
| | | | ||||||
| ------------------ | -------------------------------------------------- | | ||||||
| Author | _Hamdi Allam_ | | ||||||
| Created at | _2025-04-21_ | | ||||||
| Initial Reviewers | _Reviewer Name 1, Reviewer Name 2_ | | ||||||
| Need Approval From | _Reviewer Name_ | | ||||||
| Status | _Draft / In Review / Implementing Actions / Final_ | | ||||||
|
||||||
## Purpose | ||||||
|
||||||
<!-- This section is also sometimes called “Motivations” or “Goals”. --> | ||||||
|
||||||
<!-- It is fine to remove this section from the final document, | ||||||
but understanding the purpose of the doc when writing is very helpful. --> | ||||||
|
||||||
We want to preserve a single transaction experience in the Superchain event event when a transaction spawns asynchronous cross chain invocations. | ||||||
|
||||||
## Summary | ||||||
|
||||||
<!-- Most (if not all) documents should have a summary. | ||||||
While the length will likely be proportional to the length of the full document, | ||||||
the summary should be as succinct as possible. --> | ||||||
|
||||||
With an incentivation framework, we can ensure permissionless delivery of cross domain messages by any relayer. Very similar to solvers fulfilling cross chain [intents](https://www.erc7683.org/) that are retroactively paid by the settlement system. | ||||||
|
||||||
This settlement system is built outside external to the core protocol contracts, with this iteration serving as a functional MVP to start from. Any cut scope significantly simplifies implementation and can be re-added as improvements in further versions. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
## Problem Statement + Context | ||||||
|
||||||
<!-- Describe the specific problem that the document is seeking to address as well | ||||||
as information needed to understand the problem and design space. | ||||||
If more information is needed on the costs of the problem, | ||||||
this is a good place to that information. --> | ||||||
|
||||||
In order to pay relayers for delivering cross chain messages, relayers need to reimburse themselves for gas used during delivery. Since cross domain messages can span N hops, i.e A->B->C, this settlement system must ensure all costs are paid by the same fee payer. Thus all callbacks and transitive messages are implicitly incentivized by the originating transaction. | ||||||
|
||||||
## Proposed Solution | ||||||
|
||||||
<!-- A high level overview of the proposed solution. | ||||||
When there are multiple alternatives there should be an explanation | ||||||
of why one solution was picked over other solutions. | ||||||
As a rule of thumb, including code snippets (except for defining an external API) | ||||||
is likely too low level. --> | ||||||
|
||||||
An initial invariant that works well in establishing the fee payer is the `tx.origin` of the originating transaction. This is the same invariant that holds for 4337 and 7702 sponsored transaction. | ||||||
|
||||||
The changes proposed in [#266](https://github.com/ethereum-optimism/design-docs/pull/266), provides the foundation creating the first iteration of this settlement system -- without enshrinment in the core protocol contracts. The `RelayedMessageGasReceipt` includes contextual information on the gas consumed, and the propogated (`tx.origin`, `rootMessageHash`,`callDepth`) upon nested cross domain messages that can be used to appropriately charge `tx.origin`. | ||||||
|
||||||
This settlement system is a permissionless CREATE2 deployment, `L2ToL2CrossDomainGasTank`, where tx senders can hold an ETH deposit, used to asynchronously pay relayers for delivered messages. | ||||||
|
||||||
```solidity | ||||||
contract L2ToL2CrossDomainGasTank { | ||||||
uint256 constant MAX_DEPOSIT = 0.01 ether; | ||||||
|
||||||
mapping(address => uint256) public balanceOf; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are your thoughts on having a second mapping keying balance by root message hash?
This could still enforce a
Then in claim you would have to deduct cost from both balance mappings:
The main benefit of However, a potential downside I see of the wdyt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea this is a great point! I was actually thinking of having support for this not in replacement but for 3rdparty integrator that wants a different fee payer than The reason it can't replace is that this requires native integration with someone using the L2ToL2CrossDomainMessenger. You'd have to wrap every One easy usecase. Lets say Layerzero wants to integrate this feature. Someone sends a message from Solana -> OPM that then spawns a bunch of operations. You dont want the LZ relayer from Sol->OPM to be the fee payer. However, the LZ DVN that integrates the L2ToL2CDM can use this api to specifically just push whatever gas was provided from the original tx as the deposit for the given root msg hash. Then everything just works. And if the provided gas wasn't enough, the person can top up the given root msg hash with more funds Does this make sense? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 100% agree that this first version should actually include this, i'll add it today There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
when you say additive API, are you saying that the gas tank will support balance tracking and claiming by tx.origin and by msg hash? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by default claiming by tx.origin unless the If the rootMsgHash has a balance set, i think it should not claim by tx.origin. Is there a scenario where you think it should fallback to tx.origin after depleting the funds associated with the msg hash? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, if we are going to support balances by tx.origin and by msgHash then we should fallback to tx.origin. Otherwise, a bad actor could potentially try to make it more difficult for a msg to relay by setting a very small balance to any msg hash they want to prevent from being relayed, and then the user would have to intervene and increase the balance on that msg hash in order to get it to relay. The other option here is to just simplify things and only support msgHash balances. Even though it will always require an extra deposit, can't that be simplified by something like 7702? |
||||||
|
||||||
function deposit() nonReentrant external payable { | ||||||
uint256 amount = msg.value; | ||||||
require(amount > 0); | ||||||
|
||||||
uint256 newBalance = balanceOf[msg.sender] + amount; | ||||||
require(newBalance < MAX_DEPOSIT); | ||||||
|
||||||
balanceOf[msg.sender] = newBalance; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be good to emit a deposit event |
||||||
} | ||||||
} | ||||||
``` | ||||||
|
||||||
We cap the deposits in order to cut scope in supporting withdrawals. This makes our first interation super simple since as withdrawal support introduces a race between deposited funds and a relayer compensating themselves for message delivery. With a relatively low max deposit, we eliminate the risk of a large amount of stuck funds for a given account whilst the amount being sufficient enough to cover hundreds of sub-cent transactions. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we planning to launch this to mainnet without withdrawal support? My understanding was that prior to launching to mainnet withdrawal support would be a requirement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes I think that's right Harry! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all the way till mainnet is an open question. This lets us get to a first version and maybe we have time to iterate on a version with withdrawal support for mainnet |
||||||
|
||||||
Relayers that delivered messages can compensate themselves by pushing through the `RelayedMessageGasReceipt` event, validated with Superchain interop. A tx sender with no deposit makes this feature a no-op as no relayer will auto-deliver the message. The user but either relay messages themselves or can rely on a 3rdparty provider. | ||||||
|
||||||
```solidity | ||||||
contract L2ToL2CrossDomainGasTank { | ||||||
|
||||||
mapping(bytes32 => bool) claimed; | ||||||
|
||||||
function claim(Identifier calldata id, bytes calldata payload) nonReentrant external { | ||||||
require(id.origin == address(messenger)); | ||||||
ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(id, keccak256(payload)); | ||||||
|
||||||
// parse the receipt | ||||||
require(payload[:32] == RelayedMessageGasReceipt.selector); | ||||||
(bytes32 rootMsgHash, address txOrigin, uint256 callDepth, address relayer, uint256 relayCost) = _decode(payload); | ||||||
|
||||||
// ensure message was sent from this chain | ||||||
require(messenger.sentMessages[rootMsgHash]); | ||||||
|
||||||
// ensure unclaimed, and mark the claim | ||||||
bytes32 claimId = keccak256(abi.encode(rootMsgHash, callDepth)); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't it possible to have two messages with the same rootMsgHash and callDepth? For example, if a message goes from A -> B and then from B two more messages are kicked off (B -> C and B -> D), then I believe C and D will both have the same rootMsgHash and callDepth. why not just use msgHash here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep i really should just use the msg hash here. Idk why i'm creating a new unique identifier when one exists There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we use full msgHash + chainId + receipt blockNumber; this guarantees uniqueness even for identical sub-calls (A→B then A→C), right? Just thinking of ways to further protect against double spend. |
||||||
require(!claimed[claimId]); | ||||||
|
||||||
claimed[claimId] = true | ||||||
|
||||||
// compute total cost | ||||||
uint256 claimCost = CLAIM_OVERHEAD * block.basefee; | ||||||
uint256 cost = relayCost + claimCost; | ||||||
require(balanceOf[txOrigin] >= cost); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. even if there is not enough balance to cover should we still let the relayer claim? I could see a scenario where the relayer has already relayed a message and even though the gas tank doesnt have the full balance to cover the cost, the relayer still wants to claim the remaining funds in the gas tank. wdyt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wouldn't the tx not go through if there's not enough balance prior? I guess its possible that the simulated gas price and the actual are different, is that the scenario you had in mind? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gave this some more thought - If the relayer spent gas it should be able to drain whatever funds are present if the cost > what's in the vault I feel. Should we add PartialClaim ability? do a |
||||||
|
||||||
// compensate the relayer | ||||||
balanceOf[txOrigin] -= cost; | ||||||
new SafeSend{ value: cost }(payable(relayer)); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we emit an event after this? might be helpful for tracking which messages have been claimed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea agreed, an event here is good There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another thing I thought of, wouldnt someone be able to claim the same gas receipt multiple times with this? do we need a mapping of claimed msgs to prevent this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lmfao yes good call 🤣 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated with the claim id There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is another scenario we will want to guard against for 3rd party relayers:
should we have a permissioned set of relayers that can claim from the gas tank in order to prevent this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tremarkley interesting! curious how we could account for this. permissioned set of relayers doesn't feel like the right approach. or at least its not a long-term solution. |
||||||
} | ||||||
} | ||||||
``` | ||||||
|
||||||
A claim is uniquely identified by the `(rootMsgHash, callDepth)` tuple. | ||||||
|
||||||
As relayers deliver messages, they can claim the cost against the original tx sender deposit with the gas tank. This design introduces some off-chain complexity for the relayer. | ||||||
|
||||||
1. The relayer must simulate the call and ensure the emitted tx origin has a deposit that will cover the cost on the originating chain. | ||||||
2. The relayer should also track the pending claimable `RelayedMessageGasReceipt` of the `rootMsgHash` callstack to maximize the likelihood that the held deposit is sufficient. | ||||||
3. The relayer should claim the receipt within a reasonable time frame to ensure they are compensated. An unrelated relayer not checking (2) might claim newer transitive message that depletes the funds. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we add |
||||||
|
||||||
Integrating the `callDepth` is left out of scope in this first iteration to keep things simple. Later iterations may incorporate this to allow for a fee payer to set or have a global constant `max_call_depth` to cap how much a single call stack can claim against a deposit. | ||||||
|
||||||
### Resource Usage | ||||||
|
||||||
<!-- What is the resource usage of the proposed solution? | ||||||
Does it consume a large amount of computational resources or time? --> | ||||||
|
||||||
As a CREATE2 deployment, this settlement framework does not affect the core protocol contracts. All contract operations are on fixed-sized fields bounding the gas consumption of this contract. | ||||||
|
||||||
### Single Point of Failure and Multi Client Considerations | ||||||
|
||||||
<!-- Details on how this change will impact multiple clients. Do we need to plan for changes to both op-geth and op-reth? --> | ||||||
|
||||||
No external contract calls are made during the deposit and claim pathways, with the `nonReentrant` modifier also applied to protect against re-entrancy attacks as ETH is transferred between the gas tank and the caller. | ||||||
|
||||||
## Failure Mode Analysis | ||||||
|
||||||
<!-- Link to the failure mode analysis document, created from the fma-template.md file. --> | ||||||
|
||||||
_pending_. Important to note that holding no deposit is makes this feature a no-op. Users leveraging 3rdparty relayers or different infrastructure can continue to do so without affect. The failure mode here is simply an unused gas tank. The `RelayedMessageGasReceipt` emmitted by the messenger can be ignored or consumed by a different party for their own purposes. | ||||||
|
||||||
## Impact on Developer Experience | ||||||
|
||||||
<!-- Does this proposed design change the way application developers interact with the protocol? | ||||||
Will any Superchain developer tools (like Supersim, templates, etc.) break as a result of this change? --> | ||||||
|
||||||
When sending a transaction that involves cross chain interactions, the frontend should simulate these interactions and ensure the gas tank has appropriate funds to pay. With `multicall`, a single transaction can bundle together the funding operation with the transaction if neededed. | ||||||
|
||||||
The infrastructure required for make cross chain tx simulation as simple as possible must also be taken into consideration. Since a cross chain tx requires a valid `CrossL2Inbox.Identifier`, there's already added complexity here in simulating side effects where dependencies have not yet been executed. However, frontends can liberally make deposits without full simulation as the settlement system only charges what was used. | ||||||
|
||||||
## Alternatives Considered | ||||||
|
||||||
<!-- List out a short summary of each possible solution that was considered. | ||||||
Comparing the effort of each solution --> | ||||||
|
||||||
1. Rely on 3rdparty Bridge/Relay providers. See the problem statement in [#266](https://github.com/ethereum-optimism/design-docs/pull/266). | ||||||
2. Offchain attribution. Schemes can be derived with web2-esque approaches such as API keys. With a special tx-submission endpoint, being able to tag cross chain messages with off-chain accounts to thus charge for gas used. This might a good fallback mechanism to have in place. | ||||||
|
||||||
## Risks & Uncertainties | ||||||
|
||||||
<!-- An overview of what could go wrong. | ||||||
Also any open questions that need more work to resolve. --> | ||||||
|
||||||
1. This incentive framework is insufficient and unused. This is not a problem as this settlement framework is not enshrined in the protocol and is a simple CREATE2 deployment. Entirely new frameworks can be derived to replace this, leaving this unused. | ||||||
|
||||||
2. Also important to note that holding no deposit is makes this feature a no-op. Users leveraging 3rdparty relayers or different infrastructure can continue to do so without affect. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.