L1 processing
-
An L1 entity, either a smart contract or an externally owned account (EOA), sends a deposit transaction to
L1CrossDomainMessenger
, usingsendMessage
. This function accepts three parameters:_target
, target address on L2._message
, the L2 transaction’s calldata, formatted as per the ABI of the target account._minGasLimit
, the minimum gas limit allowed for the transaction on L2. Note that this is a minimum and the actual amount provided on L2 may be higher (but never lower) than the specified gas limit. The actual amount provided on L2 is often higher because the portal contract on L2 performs some processing before submitting the call to_target
.
-
The L1 cross domain messenger calls its own
_sendMessage
function. It uses these parameters:_to
, the destination address, is the messenger on the other side. In the case of deposits, this is always0x4200000000000000000000000000000000000007
._gasLimit
, the gas limit. This value is calculated using thebaseGas
function._value
, the ETH that is sent with the message. This amount is taken from the transaction value._data
, the calldata for the call on L2 that is needed to relay the message. This is an ABI encoded call torelayMessage
.
-
_sendMessage
calls the portal’sdepositTransaction
function. Note that other contracts can also calldepositTransaction
directly. However, doing so bypasses certain safeguards, so in most cases it’s a bad idea. -
The
depositTransaction
function runs a few sanity checks, and then emits aTransactionDeposited
event.
L2 processing
-
The
op-node
component looks forTransactionDeposited
events on L1. If it sees any such events, it parses them. -
Next,
op-node
converts thoseTransactionDeposited
events into deposit transactions. -
In most cases, user deposit transactions call the
relayMessage
function ofL2CrossDomainMessenger
. -
relayMessage
runs a few sanity checks and then, if everything is good, calls the real target contract with the relayed calldata.
Denial of service (DoS) prevention
As with all other L1 transactions, the L1 costs of a deposit are borne by the transaction’s originator. However, the L2 processing of the transaction is performed by the Optimism nodes. If there were no cost attached, an attacker could submit a transaction that had high execution costs on L2, and that way perform a denial of service attack. To avoid this DoS vector,depositTransaction
, and the functions that call it, require a gas limit parameter.
This gas limit is encoded into the TransactionDeposited
event, and used as the gas limit for the user deposit transaction on L2.
This L2 gas is paid for by burning L1 gas here.
Replaying messages
Deposit transactions can fail due to several reasons:- Not enough gas provided.
- The state on L2 does not allow the transaction to be successful.
Replays in action
L1 vs L2 network clarificationThis tutorial involves two different networks:
- L1: Ethereum Sepolia testnet (
https://sepolia.infura.io/v3/YOUR_KEY
) - L2: OP Sepolia testnet (
https://sepolia.optimism.io
)
-
Call
stopChanges
, using this Foundry command: -
Verify that
getStatus()
returns false, meaning changes are not allowed, and see the value ofgreet()
using Foundry. Note that Foundry returns false as zero. -
Get the calldata.
You can use this Foundry command:
Or just use this value:
-
Send a greeting change as a deposit from L1 (Ethereum Sepolia) to L2 (OP Sepolia).
Use these commands:
The transaction will be successful on L1 (Ethereum Sepolia), but then emit a fail event on L2 (OP Sepolia).
-
The next step is to find the hash of the failed relay. There are several ways to do this:
Method A: Using Etherscan Internal Transactions
Look in the internal transactions of the destination contract, and select the latest one that appears as a failure. It should be a call to
L2CrossDomainMessenger
at address0x420...007
. Method B: Using Contract Events (if internal transactions aren’t visible) If you can’t see internal transactions on Etherscan, check the L2CrossDomainMessenger contract events and look forFailedRelayedMessage
events with your contract address. Method C: Using cast to query failed messagesIf the latest internal transaction is a success, it probably means your transaction hasn’t relayed yet. Wait until it is, that may take a few minutes. -
Get the transaction information using Foundry.
Wait for the failed relay transactionMake sure you wait for the deposit to be processed on L2 and fail before proceeding. This can take 2-5 minutes. You should see a failed transaction in one of the methods from step 5.
-
Call
startChanges()
to allow changes using this Foundry command:Don’t do this prematurelyIf you callstartChanges()
too early, it will happen when the message is relayed to L2, and then the initial deposit will be successful and there will be no need to replay it. -
Verify that
getStatus()
returns true, meaning changes are not allowed, and see the value ofgreet()
. Foundry returns true as one. -
Now send the replay transaction.
Why do we need to specify the gas limit?The gas estimation mechanism tries to find the minimum gas limit at which the transaction would be successful. However,
L2CrossDomainMessenger
does not revert when a replay fails due to low gas limit, it just emits a failure message. The gas estimation mechanism considers that a success.To get a gas estimate, you can use this command:That address is a special case in which the contract does revert. -
Verify the greeting has changed:
Debugging
To debug deposit transactions, you can ask the L2 cross domain messenger for the state of the transaction.-
Look on Etherscan to see the
FailedRelayedMessage
event. SetMSG_HASH
to that value. -
To check if the message is listed as failed, run this:
To check if it is listed as successful, run this: