L1-L2 messaging mechanism
Starknet’s ability to interact with L1 is crucial. Messaging is the mechanism that enables this interaction, enabling cross-chain transactions.
For example, you can perform computations on L2 and use the result on L1.
Bridges on Starknet use the L1-L2 messaging mechanism. Consider that you want to bridge tokens from Ethereum to Starknet. You deposit your tokens in the L1 bridge contract, which automatically triggers the minting of the same token on L2. Another good use case for L1-L2 messaging is Defi pooling. For more information, see DeFi pooling on StarkWare’s site and dApps on https://www.starknet.io.
Be aware that the messaging mechanism is asynchronous and asymmetric.
-
Asynchronous: Your contract code, whether Cairo or Solidity, cannot await the result of the message being sent on the other chain within your contract code’s execution.
-
Asymmetric: Sending a message from Ethereum to Starknet, L1→L2, is fully automated by the Starknet sequencer, so the message is automatically delivered to the target contract on L2. However, when sending a message from Starknet to Ethereum, L2→L1, the sequencer only sends the hash of the message. You must then consume the message manually using a transaction on L1.
L2 → L1 messages
Contracts on L2 can interact asynchronously with contracts on L1 using the L2→L1 messaging protocol.
The protocol consists of the following stages:
-
During the execution of a transaction, a contract on Starknet sends a message from L2 to L1 by calling the
send_message_to_L1
syscall. -
The sequencer attaches the message parameters to the block that includes the syscall invocation. The message parameters include the address of the sender on L2, the address of the recipient contract on L1, and the message data.
For example:
let mut payload: Array<felt252> = ArrayTrait::new(); let to_address: EthAddress = 1_felt252.try_into().unwrap(); payload.append(1); // potentially add more elements to payload (payload[1], payload[2], etc.) send_message_to_l1_syscall(to_address: to_address.into(), payload: payload.span());
-
The prover proves the state update that includes this transaction.
-
The sequencer updates the L1 state.
-
The message is stored on L1 in the Starknet Core Contract and a counter on the Core Contract increases by one.
-
The
processMessage
function, which is part of the Starknet Core Contract, emits theLogMessageToL1
event, which contains the message parameters. -
The message recipient on L1 can access and consume the message by calling the
consumeMessageFromL2
function, which includes the message parameters within the transaction. This function, which is part of the Starknet Core Contract, verifies the following:-
The hashes of the L2 sent message parameters, now stored on the Core Contract, and the L1 received message parameters, are the same.
-
The entity calling the function is indeed the recipient on L1.
In such a case, the counter corresponding to the message hash in the Starknet Core Contract decreases by one. For more information, see the
consumeMessageFromL2
function inStarknetMessaging.sol
.
-
L2→L1 Messaging mechanism illustrates this flow:
L2 → L1 message structure
The structure of an L2 → L1 message is described as follows under MSG_TO_L1
in the Starknet API JSON RPC specification:
from_address (felt252 )
|
The address of the L2 contract sending the message. |
to_address (EthAddress )
|
The target L1 address the message is sent to. |
payload (Array<felt252> )
|
The payload of the message. |
L2 → L1 message hashing
The hash of an L2 → L1 message is computed on L1 as follows:
keccak256(
abi.encodePacked(
FromAddress,
uint256(ToAddress),
Payload.length,
Payload
)
);
Sending an L2 to L1 message always incurs a fixed cost of 20,000 gas, because the hash of the message being sent must be written to L1 storage in the Starknet Core Contract. |
L1 → L2 messages
Contracts on L1 can interact asynchronously with contracts on L2 using the L1→L2 messaging protocol.
The protocol consists of the following stages:
-
An L1 contract induces a message to an L2 contract on Starknet by calling the
sendMessageToL2
function on the Starknet Core Contract with the message parameters.The Starknet Core Contract hashes the message parameters and updates the L1→L2 message mapping to indicate that a message with this hash was indeed sent. The L1 contract records the fee that the sender paid. For more information, see L1 → L2 message fees.
-
The message is then decoded into a Starknet transaction that invokes a function annotated with the
l1_handler
decorator on the target contract. Transactions like this on L2 are called L1 handler transactions.-
The Starknet sequencer, upon receiving enough L1 confirmations for the transaction that sent the message, initiates the corresponding L2 transaction.
-
The L2 transaction invokes the relevant
l1_handler
function.
-
-
The L1 Handler transaction that was created in the previous step is added to a proof.
-
The Core Contract receives the state update.
-
The message is cleared from the Core Contract’s storage to consume the message. Clearing the Core Contract’s storage does the following:
-
incurs a fixed cost of 5,000 gas
-
emits an L1 event logging the message consumption
-
At this point, the message is handled.
An L1→L2 message consists of the following:
-
L1 sender’s address
-
L2 recipient’s contract address
-
Function selector
-
Calldata array
-
Message nonce
The message nonce is maintained on the Starknet Core Contract on L1, and is incremented whenever a message is sent to L2. The nonce is used to avoid a hash collision between different L1 handler transactions that is caused by the same message being sent on L1 multiple times.
For more information, see L1→L2 structure.
L1 → L2 message cancellation
The flow described here should only be used in edge cases such as bugs on the Layer 2 contract preventing message consumption. |
Consider that Alice sends an L1 asset to a Starknet bridge to transfer it to L2, which generates the corresponding L1→L2 message. Now, consider that the L2 message consumption doesn’t function, which might happen due to a bug in the dApp’s Cairo contract. This bug could result in Alice losing custody of their asset forever.
To mitigate this risk, the contract that initiated the L1→L2 message can cancel it by declaring the intent to cancel, waiting five days, and then completing the cancellation. This delay protects the sequencer from a DoS attack in the form of repeatedly sending and canceling a message before it is included in L1, rendering the L2 block which contains the activation of the corresponding L1 handler invalid.
The steps in this flow are as follows:
-
The user that initiated the L1→L2 message calls the
startL1ToL2MessageCancellation
function in the Starknet Core Contract. -
The user waits five days until she can finalize the cancellation.
-
The user calls the
cancelL1ToL2Message
function.
L1 → L2 message fees
An L1 → L2 message induces a transaction on L2, which, unlike regular transactions, is not sent by an account. This calls for a different mechanism for paying the transaction’s fee, for otherwise the sequencer has no incentive of including L1 handler transactions inside a block.
To avoid having to interact with both L1 and L2 when sending a message, L1 → L2 messages are payable on L1, by sending ETH with the call to the payable function sendMessageToL2
on the Starknet Core Contract.
The sequencer takes this fee in exchange for handling the message. The sequencer charges the fee in full upon updating the L1 state with the consumption of this message.
The fee itself is calculated in the same manner as "regular" L2 transactions. You can use the CLI to get an estimate of an L1 → L2 message fee.
L1 → L2 structure
For completeness, L1 → L2 structure describes the precise structure of both the message as it appears on L1 and the induced transaction as it appears on L2.
FromAddress | ToAddress | Selector | Payload | Nonce | |
---|---|---|---|---|---|
|
|
|
|
|
L1 → L2 hashing
The hash of the message is computed on L1 as follows:
keccak256(
abi.encodePacked(
uint256(FromAddress),
ToAddress,
Nonce,
Selector,
Payload.length,
Payload
)
);
Version | ContractAddress | Selector | Calldata | Nonce | |
---|---|---|---|---|---|
|
|
|
|
|
The hash of the corresponding L1 handler transaction on L2 is computed as follows:
l1_handler_tx_hash = ℎ(
"l1_handler",
version,
contract_address,
entry_point_selector,
ℎ(calldata),
chain_id,
nonce
)
Where:
-
l1_handler
is a constant prefix, encoded in bytes (ASCII), as big-endian. -
chain_id
is a constant value that specifies the network to which this transaction is sent. -
h is the Pedersen hash
In an |
Additional resources
-
send_message_to_L1
syscall -
sendMessageToL2
function on the Starknet Core Contract -
For more information on how messaging works within the Starknet Core Contract, including details on coding, see L1-L2 Messaging in The Cairo Book: The Cairo Programming Language