Skip to content

Commit

Permalink
docs: Updates to Messaging mechanism (#1244)
Browse files Browse the repository at this point in the history
* Clarifying messaging-mechanism.adoc.

* Merge branch 'dev' into steve/L1-L2_messaging

* Removed extraneous space.

* Use Cairo for source block label.

* Added messaging-reference.adoc

* Merge branch 'dev' into steve/L1-L2_messaging

* Merge branch 'dev' into steve/L1-L2_messaging

* Merge branch 'dev' into steve/L1-L2_messaging

* More questions.

* Minor edits.

* Merge branch 'dev' into steve/L1-L2_messaging

* Close some open questions.

* Merge branch 'steve/L1-L2_messaging' of github.com:starknet-io/starknet-docs into steve/L1-L2_messaging

* Clarification edits.

* Clarification edits.

* Merge branch 'master' into steve/L1-L2_messaging

* Edits.

* Edits.

* Edits.

* Edits.

* Merge branch 'main' into steve/L1-L2_messaging

* Update components/Starknet/modules/architecture_and_concepts/pages/Network_Architecture/messaging-mechanism.adoc

* Merge branch 'main' into steve/L1-L2_messaging

* More edits.

* Merge branch 'main' into steve/L1-L2_messaging

* Updates implemented in PR #857, minus questions/comments that are still unanswered or TBD.

* Added intro content from the Cairo Book topic on messaging.

* Added intro content from the Cairo Book topic on messaging.

* SME comments

* Added fixed 5,000 gas fee for clearing Core Contract storage after L1->L2 message is handled.

Co-Authored-By: JameStark <[email protected]>
  • Loading branch information
stoobie and JameStark authored Jun 6, 2024
1 parent f30ef5a commit 2adb157
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
** State
*** xref:Network_Architecture/starknet-state.adoc[Starknet state]
*** xref:Network_Architecture/on-chain-data.adoc[Data availability]

** xref:Network_Architecture/messaging-mechanism.adoc[L1-L2 messaging]
*** xref:Network_Architecture/messaging-mechanism.adoc[L1-L2 messaging mechanism]
** Accounts
*** xref:Accounts/introduction.adoc[What is an account?]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
[id="messaging_mechanism"]
= L1-L2 messaging
= 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 link:https://starkware.co/resource/defi-pooling/[DeFi pooling] on StarkWare's site and link:https://www.starknet.io/en/ecosystem/dapps[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.

[id="l2-l1_messages"]
== L2 L1 messages
== L2 -> L1 messages

Contracts on L2 can interact asynchronously with contracts on L1 via the L2->L1 messaging protocol.
Contracts on L2 can interact asynchronously with contracts on L1 using the L2->L1 messaging protocol.

During the execution of a Starknet transaction, a contract on Starknet sends an L2→L1 message by calling the https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/common/messages.cairo#L4[`send_message_to_L1`] syscall. The message parameters (which contain the recipient contract on L1 and the relevant data) are then attached to the relevant state update that includes this syscall invocation.
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:

[source,rs]
+
[source,cairo]
----
let mut payload: Array<felt252> = ArrayTrait::new();
let to_address: EthAddress = 1_felt252.try_into().unwrap();
Expand All @@ -20,30 +34,40 @@ payload.append(1);
send_message_to_l1_syscall(to_address: to_address.into(), payload: payload.span());
----

After the state update that included this transaction is proved and the L1 state is updated, the message is stored on L1 in the Starknet Core Contract (and the relevant counter is increased), and the `LogMessageToL1` event (which contains the message parameters) is emitted.
. 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 the `LogMessageToL1` event, which contains the message parameters.
. The message recipient on L1 can access and consume the message by calling the link:https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L119[`consumeMessageFromL2`] function, which includes the message parameters within the transaction.
This function, which is part of the Starknet Core Contract, verifies the following:

Later, the recipient address on L1 can access and consume the message as part of an L1 transaction by re-supplying the message parameters.
* 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.
+
// We need to separate out these functions into a reference.
In such a case, the counter corresponding to the message hash in the Starknet Core Contract decreases by one. For more information, see the link:https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L130C7-L130C7#[`consumeMessageFromL2`] function in `StarknetMessaging.sol`.

This is done by calling https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L119[`consumeMessageFromL2`] in the Starknet Core Contract, who verifies that the hash corresponds to a stored message and that the caller is indeed the recipient on L1. In such a case, the reference count of the message hash in the Starknet Core Contract decreases by 1.
xref:#diagram_l2-l1_messaging_mechanism[] illustrates this flow:

The above flow is illustrated in the following diagram:
[#diagram_l2-l1_messaging_mechanism]
.L2->L1 Messaging mechanism
image::l2l1.png[L2->L1 message mechanism]

image::l2l1.png[l2l1]
=== L2 -> L1 message structure

[id="structure_and_hashing_l2-l1"]
=== L2 → L1 structure and hashing
// xref:#structure_l2-l1[] illustrates the structure of an L2 -> L1 message.

As demonstrated above, the structure of an L2 L1 message is given by:
The structure of an L2 -> L1 message is described as follows under `MSG_TO_L1` in the link:https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_api_openrpc.json[Starknet API JSON RPC] specification:

.L2 → L1 Message
[%autowidth.stretch]
|===
| FromAddress | ToAddress | Payload
[horizontal,labelwidth="30",role="stripes-odd"]
`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.

| `FieldElement` | `EthereumAddress` | `Payload`
|===
[#hashing_l2-l1]
=== L2 -> L1 message hashing

The hash of an L2 L1 message is computed on L1 as follows:
The hash of an L2 -> L1 message is computed on L1 as follows:

[source,js]
----
Expand All @@ -57,81 +81,103 @@ keccak256(
);
----

NOTE: As the hash of the message being sent needs to be written to L1 storage (in the Starknet Core Contract) there is always a fixed 20k gas cost associated with sending an L2 to L1 message.

[NOTE]
====
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.
====

[id="l1-l2-messages"]
== L1 → L2 messages
== L1 -> L2 messages

Contracts on L1 can interact asynchronously with contracts on L2 using the _L1->L2_ messaging protocol.

Contracts on L1 can interact asynchronously with contracts on L2 via the L1→L2 messaging protocol. The protocol consists of the following stages:
The protocol consists of the following stages:

. An L1 contract initiates a message to an L2 contract on Starknet. It does so by calling the link:https://github.com/starkware-libs/cairo-lang/blob/54d7e92a703b3b5a1e07e9389608178129946efc/src/starkware/starknet/solidity/IStarknetMessaging.sol#L13[`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. In fact, the L1 contract records the fee that the sender paid. For more information, see xref:#l1-l2-message-fees[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 functions_*.
.. The Starknet sequencer, upon seeing enough L1 confirmations for the transaction that sent the message, initiates the corresponding L2 transaction.
.. The L2 transaction invokes the relevant `l1_handler`.
. An L1 contract induces a message to an L2 contract on Starknet by calling the link:https://github.com/starkware-libs/cairo-lang/blob/54d7e92a703b3b5a1e07e9389608178129946efc/src/starkware/starknet/solidity/IStarknetMessaging.sol#L13[`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 xref:#l1-l2-message-fees[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 state update is received on the Core contract.
. the message is cleared from the Core contract's storage. At this point, the message is handled.
. 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.

// The above flow is illustrated in the following diagram:
// THIS IMAGE IS WRONG & MISLEADING AND THUS COMMENTED OUT UNTIL FIXED
// #THIS IMAGE IS WRONG & MISLEADING#

// image::l1l2.png[l1l2]

An L1L2 message consists of:
An L1->L2 message consists of the following:

* The L1 sender address
* The recipient contract address on Starknet
* L1 sender's address
* L2 recipient's contract address
* Function selector
* Calldata array
* Message nonce

+
[NOTE]
====
*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.
The message nonce is maintained on the Starknet Core Contract on L1, and is bumped whenever a message is
sent to L2. It is used to avoid hash collisions between different L1 handler transactions that are induced by the same message being sent on L1 multiple times (see xref:structure_and_hashing_l1-l2[below]).
For more information, see xref:#l1_l2_message_structure[L1->L2 structure].
====

[id="l2-l1_message_cancellation"]
=== L1 L2 message cancellation
=== L1 -> L2 message cancellation

Imagine a scenario where a user transfers an asset from L1 to L2. The flow starts with the user sending the asset to a Starknet bridge and the corresponding L1→L2 message generation. Now, imagine that the L2 message consumption doesn't function (this might happen due to a bug in the dApp's Cairo contract). This could result in the user losing custody over their asset forever.
[NOTE]
====
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.

To mitigate this risk, we allow the contract that initiated the L1→L2 message to cancel it after declaring the intent and waiting a suitable amount of time.
The steps in this flow are as follows:

The user starts by calling https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L134[`startL1ToL2MessageCancellation`] with the relevant message parameters in the Starknet Core Contract. Then, after a five days delay, the user can finalize the cancellation by calling https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L147[`cancelL1ToL2Message`].
. The user that initiated the L1->L2 message calls the https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L134[`startL1ToL2MessageCancellation`] function in the Starknet Core Contract.
. The user waits five days until she can finalize the cancellation.
. The user calls the https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L147[`cancelL1ToL2Message`] function.

The reason for the delay is to protect 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.

Note that this flow should only be used in edge cases such as bugs on the Layer 2 contract preventing message consumption.

[id="l1-l2-message-fees"]
=== L1 L2 message fees
=== 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.
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.
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 xref:../Network_Architecture/fee-mechanism.adoc#overall_fee[same manner] as
"regular" L2 transactions. You can use the xref:documentation:cli:starkli.adoc#starknet-estimate_fee[CLI] to get an estimate of an L1 L2 message fee.
"regular" L2 transactions. You can use the xref:documentation:cli:starkli.adoc#starknet-estimate_fee[CLI] to get an estimate of an L1 -> L2 message fee.

[id="structure_and_hashing_l1-l2"]
=== L1 → L2 structure and hashing
[#l1_l2_message_structure]
=== L1 -> L2 structure

For completeness, we describe the precise structure of both the message as it appears on L1 and the induced transaction as it appears on L2.
For completeness, xref:#l1_l2_message_structure[] describes the precise structure of both the message as it appears on L1 and the induced transaction as it appears on L2.

.L1 → L2 Message
[#L1-L2_message_structure]
.L1 -> L2 message structure
[%autowidth.stretch]
|===
| FromAddress | ToAddress | Selector | Payload | Nonce |

| `EthereumAddress` | `FieldElement` | `FieldElement` | `List+++<FieldElement>+++` | `FieldElement` |
|===

[#hashing_l1-l2]
=== L1 -> L2 hashing

The hash of the message is computed on L1 as follows:

[source,js]
Expand Down Expand Up @@ -174,11 +220,17 @@ l1_handler_tx_hash = ℎ(

Where:

- stem:[\text{l1_handler}] is a constant prefix, encoded in bytes (ASCII), with big-endian.
- stem:[\text{chain_id}] is a constant value that specifies the network to which this transaction is sent.
- stem:[$$h$$] is the xref:../Cryptography/hash-functions.adoc#pedersen_hash[Pedersen] hash
- `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 xref:../Cryptography/hash-functions.adoc#pedersen_hash[Pedersen] hash

[NOTE]
====
In an `l1_handler` transaction, the first element of the calldata is always the Ethereum address of the sender.
====

== Additional resources

* xref:Smart_Contracts/system-calls-cairo1.adoc#send_message_to_L1[`send_message_to_L1`] syscall
* link:https://github.com/starkware-libs/cairo-lang/blob/54d7e92a703b3b5a1e07e9389608178129946efc/src/starkware/starknet/solidity/IStarknetMessaging.sol#L13[`sendMessageToL2`] function on the Starknet Core Contract
* For more information on how messaging works within the Starknet Core Contract, including details on coding, see link:https://book.cairo-lang.org/ch16-04-L1-L2-messaging.html[L1-L2 Messaging] in _The Cairo Book: The Cairo Programming Language_

0 comments on commit 2adb157

Please sign in to comment.