Starknet消息机制

Starknet消息机制

翻译: julei

原文:

https://docs.starknet.io/docs/L1-L2%20Communication/messaging-mechanism

L2 → L1 消息

L2 上的合约可以通过 L2→L1 消息传递协议与 L1 上的合约进行异步交互。

在执行一个交易时,L2合约可通过send_message_to_L1()系统调用发送 L2→L1 消息。之后,Starknet系统就会将消息参数(包含 L1 上的接收者合约和相关数据)附加到L2的世界状态更新中。

代码示例:

let (message_payload : felt*) = alloc()
// 消息Payload。可增加。like msg_payload[1] ...
assert message_payload[0] = <payload_parameter> 

assert_lt_felt(to_address, ETH_ADDRESS_BOUND)
assert_not_zero(to_address)
send_message_to_l1(to_address=to_address, payload_size=1, payload=message_payload)

在包含了此交易的L2世界状态更新后,并且在L1验证后,此消息的哈希会被存储在 StarkNet Core Contract ( L1) 中(引用计数+1),并发送一个LogMessageTo的L1事件(包含消息参数)。

稍后,L1 上的收件人可以消费这条消息。这是通过调用StarkNet 核心合约的consumeMessageFromL2 ()方法来完成的。 Starknet核心合约会验证Hash值是否对应于存储的消息,并且调用者确实是 L1 上的接收者。之后,StarkNet Core Contract 中消息哈希的引用计数减1。

上述流程如下图所示:

L2 → L1 消息的结构由下式给出:

// L2 → L1 消息
struct L2L1Message {
  felt fromAddress;
  felt toAddress;
  Payload payload;
}

L2 → L1 消息的哈希值在 L1 上计算方法如下:

keccak256(
  abi.encodePacked(
   FromAddress,
   ToAddress,
   Payload.length,
   Payload
  )
);

L1 → L2 消息

L1 上的合约可以通过 L1→L2 消息传递协议与 L2 上的合约进行异步交互。该协议包括以下阶段:

  • 1. L1 合约向 StarkNet 上的 L2 合约发起消息。它通过sendMessageToL2使用消息参数调用 StarkNet Core Contract 上的函数来实现。 1.1 StarkNet Core Contract 对消息参数进行散列处理,并更新 L1→L2 消息映射,以表明确实发送了具有此散列的消息。事实上,L1 合约记录了发送方支付的费用。有关更多信息,请参阅L1 → L2 消息费用。

  • 2. 然后将消息解码为 StarkNet 交易,该交易调用l1_handler目标合约上的装饰器注解的函数。L2 上的此类事务称为L1 处理程序事务。

     2\.1. StarkNet Sequencer在看到发送消息的交易的足够 L1 确认后,启动相应的 L2 交易。
    
     2\.2. L2 事务调用相关的 l1_handler。
    
  • 3. 将上一步创建的 L1 Handler 交易添加到证明中。

  • 4. Core 合约收到状态更新

  • 5. 消息从核心合约的存储中清除。至此,消息被处理。 上述流程如下图所示:

一条 L1→L2 消息包括:

L1 发件人地址 StarkNet 上的收件人合约地址 功能选择器 调用数据数组 消息随机数 消息随机数 消息 nonce 在 L1 上的 StarkNet Core 合约上维护,并且每当消息发送到 L2 时都会被碰撞。它用于避免在 L1 上多次发送同一消息引起的不同 L1 处理程序事务之间的哈希冲突(见下文)。

L1 → L2 消息的取消机制

想象一下用户将资产从 L1 转移到 L2 的场景。该流程从用户将资产发送到 StarkNet 网桥和相应的 L1→L2 消息生成开始。现在,假设 L2 消息消费不起作用(可能由于 dApp 的 Cairo 合约中的bug)。这可能导致用户永远失去对其eth资产。

为了降低这种风险,我们允许发起 L1→L2 消息的合约取消它——在声明意图并等待适当的时间之后。用户首先调用StarkNet Core Contract的startL1ToL2MessageCancellation() 方法来开始取消流程。然后,在延迟五天后,用户调用消cancelL1ToL2Message()来完成取消流程。延迟的原因是为了保护Sequencer免受以重复发送和取消消息形式的 DOS 攻击——在它包含在 L1 之前,使包含相应 L1 处理程序激活的 L2 块无效。请注意,此流程应仅用于边缘情况,例如第 2 层合同上的错误阻止消息消费。

L1 → L2 消息Gas费用

L1 → L2 消息在 L2 上引发交易,与常规交易不同,该交易不是由账户发送的。这需要一种不同的机制来支付交易费用,否则定序器没有动力将 L1 处理程序交易包含在一个块中。

为了避免在发送消息时必须与 L1 和 L2 交互,L1 → L2 消息是在 L1 上支付的,通过调用sendMessageToL2StarkNet Core 合约上的支付函数发送 ETH。定序器收取这笔费用以换取处理消息。定序器在使用此消息更新 L1 状态时全额收取费用。

注意

在消息的整个生命周期中,相关gas费用被锁定在核心合约中。如果由于任何原因未处理该消息,则取消该消息也会将费用退还给发件人。

费用本身的计算方式与“常规”L2 交易相同。您可以使用CLI估算 L1 → L2 消息费用。

消息结构定义和Hash的计算

为了完整起见,我们给出 L1 上的消息和 L2 上的相关交易的精确结构定义。

struct L2L1Message {
   EthereumAddress FromAddress;
   felt ToAddress;
   felt Selector;
   feltList<felt> Payload;
   felt Nonce;
};

struct L1HandlerTransaction {
   felt Version;
   felt ContractAddress;
   felt Selector;
   List<felt> Calldata;
   felt Nonce;
};

在L1上,消息的哈希值x计算方法如下:

keccak256(
  abi.encodePacked(
    uint256(FromAddress),
    ToAddress,
    Nonce,
    Selector,
    Payload.length,
    Payload
  )
);

在L2上,使用下述方法计算L1 Handler Transaction的Hash:

l1_hander_tx_hash = h(
  "l1_handler",version,
  contract_address,
  entry_point_selector,
  h(calldata),
  max_fee,
  chain_id,
nonce)

其中:

“l1_handler” 是一个常量前缀,以字节 (ASCII) 编码,采用bigendian。

chain_id是一个常数值,它指定了这个交易被发送到的网络。请参阅链 ID。

h()是Pedersen Hash

Calldata中的发件人地址

在l1_handler交易中,calldata 的第一个元素始终是发送者的以太坊地址。