L1-L2 Messaging

Layer 2的一个重要特征是它能与Layer 1交互。

Starknet has its own L1-L2 messaging system, which is different from its consensus mechanism and the submission of state updates on L1. Messaging is a way for smart-contracts on L1 to interact with smart-contracts on L2 (or the other way around), allowing us to do "cross-chain" transactions. For example, we can do some computations on a chain and use the result of this computation on the other chain.

在Starknet上,所有的桥接都使用 L1-L2 消息传递。假设你想要将以太坊上的代币桥接到Starknet上。你只需要将代币存入L1桥接合约,这将自动触发在L2上铸造相同代币。L1-L2 消息传递的另一个很好的用例是DeFi池化

在Starknet上,重要的是要注意消息系统是异步非对称

  • Asynchronous: this means that in your contract code (being Solidity or Cairo), you can't wait the result of the message being sent on the other chain within your contract code execution.
  • Asymmetric: sending a message from Ethereum to Starknet (L1->L2) is fully automated by the Starknet sequencer, which means that the message is being automatically delivered to the target contract on L2. However, when sending a message from Starknet to Ethereum (L2->L1), only the hash of the message is sent on L1 by the Starknet sequencer. You must then consume the message manually via a transaction on L1.

让我们来详细了解一下。

The StarknetMessaging Contract

L1-L2 消息传递系统的关键组件是StarknetCore合约。它是部署在以太坊上的一组Solidity合约,用于使Starknet正常运行。StarknetCore 的合约之一被称为StarknetMessaging,它负责在Starknet和以太坊之间传递消息。StarknetMessaging遵循一个接口,其中包含了一些函数,允许向L2发送消息,在L1上从L2接收消息以及取消消息。

interface IStarknetMessaging is IStarknetMessagingEvents {

    function sendMessageToL2(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload
    ) external returns (bytes32);

    function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload)
        external
        returns (bytes32);

    function startL1ToL2MessageCancellation(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload,
        uint256 nonce
    ) external;

    function cancelL1ToL2Message(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload,
        uint256 nonce
    ) external;
}

Starknet消息传送合约接口

In the case of L1->L2 messages, the Starknet sequencer is constantly listening to the logs emitted by the StarknetMessaging contract on Ethereum. Once a message is detected in a log, the sequencer prepares and executes a L1HandlerTransaction to call the function on the target L2 contract. This takes up to 1-2 minutes to be done (few seconds for ethereum block to be mined, and then the sequencer must build and execute the transaction).

L2->L1 messages are prepared by contracts execution on L2 and are part of the block produced. When the sequencer produces a block, it sends the hash of each message prepared by contracts execution to the StarknetCore contract on L1, where they can then be consumed once the block they belong to is proven and verified on Ethereum (which for now is around 3-4 hours).

Sending Messages from Ethereum to Starknet

如果你想从 Ethereum 向 Starknet 发送消息,你的 Solidity 合约必须调用 StarknetMessaging 合约的 sendMessageToL2 函数。要在 Starknet 上接收这些消息,你需要用 #[l1_handler] 属性注解可从 L1 调用的函数。

Let's take a simple contract taken from this tutorial where we want to send a message to Starknet. The _snMessaging is a state variable already initialized with the address of the StarknetMessaging contract. You can check all Starknet contract and sequencer addresses here.

// Sends a message on Starknet with a single felt.
function sendMessageFelt(
    uint256 contractAddress,
    uint256 selector,
    uint256 myFelt
)
    external
    payable
{
    // We "serialize" here the felt into a payload, which is an array of uint256.
    uint256[] memory payload = new uint256[](1);
    payload[0] = myFelt;

    // msg.value must always be >= 20_000 wei.
    _snMessaging.sendMessageToL2{value: msg.value}(
        contractAddress,
        selector,
        payload
    );
}

The function sends a message with a single felt value to the StarknetMessaging contract. Please note that if you want to send more complex data you can. Just be aware that your Cairo contract will only understand felt252 data type. So you must ensure that the serialization of your data into the uint256 array follow the Cairo serialization scheme.

It's important to note that we have {value: msg.value}. In fact, the minimum value we've to send here is 20k wei, due to the fact that the StarknetMessaging contract will register the hash of our message in the storage of Ethereum.

In addition to those 20k wei, since the L1HandlerTransaction executed by the sequencer is not tied to any account (the message originates from L1), you must also ensure that you pay enough fees on L1 for your message to be deserialized and processed on L2.

The fees of the L1HandlerTransaction are computed in a regular manner as it would be done for an Invoke transaction. For this, you can profile the gas consumption using starkli or snforge to estimate the cost of your message execution.

sendMessageToL2的签名是:

function sendMessageToL2(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload
    ) external override returns (bytes32);

The parameters are as follows:

  • toAddress: The contract address on L2 that will be called.
  • selector: The selector of the function of this contract at toAddress. This selector (function) must have the #[l1_handler] attribute to be callable.
  • payload: The payload is always an array of felt252 (which are represented by uint256 in Solidity). For this reason we've inserted the input myFelt into the array. This is why we need to insert the input data into an array.

在Starknet方面,要接收这条信息,我们需要:

    #[l1_handler]
    fn msg_handler_felt(ref self: ContractState, from_address: felt252, my_felt: felt252) {
        assert(from_address == self.allowed_message_sender.read(), 'Invalid message sender');

        // You can now use the data, automatically deserialized from the message payload.
        assert(my_felt == 123, 'Invalid value');
    }

我们需要为我们的函数添加 #[l1_handler] 属性。L1处理器是一种特殊的函数,只能由L1HandlerTransaction 执行。接收来自L1的事务时不需要特别处理,因为消息会自动由顺序器中继。在你的 #[l1_handler] 函数中,重要的是要验证L1消息的发送者,以确保我们的合约只能接收来自受信任的L1合约的消息。

Sending Messages from Starknet to Ethereum

当从Starknet发送消息到以太坊时,你将需要在Cairo合约中使用 send_message_to_l1 系统调用。该系统调用允许您向L1上的 StarknetMessaging 合约发送消息。与 L1->L2 消息不同的是,L2->L1 消息必须手动消耗,这意味着你的Solidity合约需要显式调用 StarknetMessaging 合约的 consumeMessageFromL2 函数来消耗消息。

要从 L2 向 L1 发送信息,我们在Starknet上要做的是:

        fn send_message_felt(ref self: ContractState, to_address: EthAddress, my_felt: felt252) {
            // Note here, we "serialize" my_felt, as the payload must be
            // a `Span<felt252>`.
            syscalls::send_message_to_l1_syscall(to_address.into(), array![my_felt].span())
                .unwrap();
        }

我们只需构建payload,并将其与 L1 合约地址一起传递给系统调用函数。

On L1, the important part is to build the same payload sent by the L2. Then you call consumeMessageFromL2 in you Solidity contract by passing the L2 contract address and the payload. Please be aware that the L2 contract address expected by the consumeMessageFromL2 is the address of the contract that sends the message on the L2 by calling send_message_to_l1_syscall.

function consumeMessageFelt(
    uint256 fromAddress,
    uint256[] calldata payload
)
    external
{
    let messageHash = _snMessaging.consumeMessageFromL2(fromAddress, payload);

    // You can use the message hash if you want here.

    // We expect the payload to contain only a felt252 value (which is a uint256 in Solidity).
    require(payload.length == 1, "Invalid payload");

    uint256 my_felt = payload[0];

    // From here, you can safely use `my_felt` as the message has been verified by StarknetMessaging.
    require(my_felt > 0, "Invalid value");
}

As you can see, in this context we don't have to verify which contract from L2 is sending the message (as we do on the L2 to verify which contract from L1 is sending the message). But we are actually using the consumeMessageFromL2 of the StarknetCore contract to validate the inputs (the contract address on L2 and the payload) to ensure we are only consuming valid messages.

Note: The consumeMessageFromL2 function of the StarknetCore contract is expected to be called from a Solidity contract, and not directly on the StarknetCore contract. The reason of that is because the StarknetCore contract is using msg.sender to actually compute the hash of the message. And this msg.sender must correspond to the to_address field that is given to the function send_message_to_l1_syscall that is called on Starknet.

重要的是要记住,在L1上我们发送的有效载荷是 uint256,但是在Starknet上的基本数据类型是 felt252;然而,felt252uint256 大约小4位。因此,我们必须注意我们发送的消息有效载荷中包含的值。如果在L1上构建的消息的值超过了最大的felt252,该消息将被卡住,无法在L2上被消耗。

Cairo Serde

Before sending messages between L1 and L2, you must remember that Starknet contracts, written in Cairo, can only understand serialized data. And serialized data is always an array of felt252. In Solidity we have uint256 type, and felt252 are approximately 4 bits smaller than uint256. So we have to pay attention to the values contained in the payload of the messages we are sending. If, on L1, we build a message with values above the maximum felt252, the message will be stuck and never consumed on L2.

例如,在Cairo中,一个实际的 uint256 值表示为类似以下的结构:

struct u256 {
    low: u128,
    high: u128,
}

这将被序列化为两个 felts ,一个用于 low ,另一个用于 high 。这意味着要向Cairo发送一个 u256,你需要从L1发送一个包含两个值的 payload。

uint256[] memory payload = new uint256[](2);
// Let's send the value 1 as a u256 in cairo: low = 1, high = 0.
payload[0] = 1;
payload[1] = 0;

如果你想了解更多关于消息机制的信息,可以访问 Starknet 文档.

You can also find a detailed guide here to test the messaging system locally.