L2 -> L1通信
L2 -> L1通信
本节描述了从L2与Ethereum互动的接口。它假定你已经熟悉了与L2->L1通信工作的基本概念。如果你是这个话题的新手,你可以阅读概念性介绍这里。
Note
请注意,在新的0.13.0
SDK中,API层是用气体操作的。ergs的概念只被VM使用。
结构
与L1->L2通信不同,不可能直接将事务从L2初始化到L1。然而,你可以从zkSync向Ethereum发送一个任意长度的消息,然后在L1智能合约上处理收到的消息。要从L2端发送消息,你应该调用[信使系统合约](.../system-contracts.md#L1Messenger)中的sendToL1
方法。它只接受发送到Ethereum上的zkSync智能合约的消息的字节数。
从L1端来看,zkSync智能合约提供了proveL2MessageInclusion
方法,以证明消息被发送到L1并包含在zkSync块中。
从L2向L1发送消息
从L2端发送消息需要用户调用Messenger系统合同中的sendToL1
方法。这个方法只接受被发送到L1上的zkSync智能合约的消息的字节数。
function sendToL1(bytes memory _message) external returns (bytes32 messageHash);
_message
是一个参数,包含信息的原始字节数
Tips
消息的发件人将由上下文决定。
这个函数从L2发送一个消息,并返回消息字节的keccak256哈希值。该消息的哈希值可以在以后用来证明该消息是在L1上发送的。它的使用是可选的,只是为了方便。
关于Messenger的更多信息可以在系统合同部分找到。
例子
zksync-web3
从L2向L1发送消息
使用import { Wallet, Provider, Contract, utils } from "zksync-web3";
import { ethers } from "ethers";
const TEST_PRIVATE_KEY = "<YOUR_PRIVATE_KEY>";
async function main() {
const zkSyncProvider = new Provider("https://zksync2-testnet.zksync.dev");
const wallet = new Wallet(TEST_PRIVATE_KEY, zkSyncProvider);
const messengerContract = new ethers.Contract(utils.L1_MESSENGER_ADDRESS, utils.L1_MESSENGER, wallet);
console.log(`Messenger contract address is ${messengerContract.address}`);
const someString = ethers.utils.toUtf8Bytes("Some L2->L1 message");
console.log(`Sending message from L2 to L1`);
const tx = await messengerContract.sendToL1(someString);
console.log("L2 trx hash is ", tx.hash);
const receipt = await tx.waitFinalize();
console.log(`Transaction included in block ${receipt.blockNumber}`);
// Get proof that the message was sent to L1
const msgProof = await zkSyncProvider.getMessageProof(receipt.blockNumber, wallet.address, ethers.utils.keccak256(someString));
console.log("Proof that message was sent to L1 :>> ", msgProof);
}
try {
main();
} catch (error) {
console.error(error);
}
L2中的智能合约,向L1发送了一条信息
以下合约通过信使系统合约将其地址发送给L1。
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
// Importing interfaces and addresses of the system contracts
import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";
contract Example {
function sendMessageToL1() external returns(bytes32 messageHash) {
// Construct the message directly on the contract
bytes memory message = abi.encode(address(this));
messageHash = L1_MESSENGER_CONTRACT.sendToL1(message);
}
}
证明消息被纳入L2块中
从L1端来看,zkSync智能合约提供了一个接口来证明消息被发送到L1并包含在zkSync块中。
来自邮箱L1合约的proveL2MessageInclusion
函数,返回一个布尔值,表明带有此类参数的消息,被发送到了L1。
struct L2Message {
address sender;
bytes data;
uint256 txNumberInblock;
}
function proveL2MessageInclusion(
uint256 _blockNumber,
uint256 _index,
L2Message calldata _message,
bytes32[] calldata _proof
) external view returns (bool);
下面是对所需参数的详细描述。
_blockNumber
是L1批号,其中包括L2块。它可以用getBlock
方法来检索。_index
是该块中L2日志的索引。它由zksync-web3
API的getMessageProof
方法作为id
返回。_message
是一个参数,包含发送消息的全部信息。它应该是一个包含以下内容的对象。sender
: 从L2发送消息的地址。data
: 发送的消息的字节数。txNumberInBlock
:L2块中交易的索引,使用getTransaction
返回transactionIndex
。
_proof
是一个参数,包含消息包含的Merkle证明。它可以从观察Ethereum或从zksync-web3
API的getMessageProof
方法接收。
重要
请注意,在证明L1的包含性之前,你的交易的L2块必须被验证(因此交易被最终确定)。
例子
L1信息处理合约
下面的合约接收发送到L2信使合约的交易信息,并证明它被包含在L2块中。
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
// Importing zkSync contract interface
import "@matterlabs/zksync-contracts/l1/contracts/zksync/interfaces/IZkSync.sol";
contract Example {
// NOTE: The zkSync contract implements only the functionality for proving that a message belongs to a block
// but does not guarantee that such a proof was used only once. That's why a contract that uses L2 -> L1
// communication must take care of the double handling of the message.
/// @dev mapping L2 block number => message number => flag
/// @dev Used to indicated that zkSync L2 -> L1 message was already processed
mapping(uint256 => mapping(uint256 => bool)) isL2ToL1MessageProcessed;
function consumeMessageFromL2(
// The address of the zkSync smart contract.
// It is not recommended to hardcode it during the alpha testnet as regenesis may happen.
address _zkSyncAddress,
// zkSync block number in which the message was sent
uint256 _l2BlockNumber,
// Message index, that can be received via API
uint256 _index,
// The tx number in block
uint16 _l2TxNumberInBlock,
// The message that was sent from l2
bytes calldata _message,
// Merkle proof for the message
bytes32[] calldata _proof
) external {
// check that the message has not been processed yet
require(!isL2ToL1MessageProcessed[_l2BlockNumber][_index]);
IZkSync zksync = IZkSync(_zkSyncAddress);
address someSender = 0x19A5bFCBE15f98Aa073B9F81b58466521479DF8D;
L2Message memory message = L2Message({sender: someSender, data: _message, txNumberInBlock:_l2TxNumberInBlock});
bool success = zksync.proveL2MessageInclusion(
_l2BlockNumber,
_index,
message,
_proof
);
require(success, "Failed to prove message inclusion");
// Mark message as processed
isL2ToL1MessageProcessed[_l2BlockNumber][_index] = true;
}
}
L2到L1
下面的脚本从L2向L1发送一个消息,检索消息证明,并验证在L1收到的消息是否来自L2区块。
import * as ethers from "ethers";
import { Provider, utils, Wallet } from "zksync-web3";
const TEST_PRIVATE_KEY = "<YOUR_PRIVATE_KEY>";
const MESSAGE = "Some L2->L1 message";
const l2Provider = new Provider("https://zksync2-testnet.zksync.dev");
const l1Provider = ethers.getDefaultProvider("goerli");
const wallet = new Wallet(TEST_PRIVATE_KEY, l2Provider, l1Provider);
async function sendMessageToL1(text: string) {
console.log(`Sending message to L1 with text ${text}`);
const textBytes = ethers.utils.toUtf8Bytes(MESSAGE);
const messengerContract = new ethers.Contract(utils.L1_MESSENGER_ADDRESS, utils.L1_MESSENGER, wallet);
const tx = await messengerContract.sendToL1(textBytes);
await tx.wait();
console.log("L2 trx hash is ", tx.hash);
return tx;
}
async function getL2MessageProof(blockNumber: ethers.BigNumberish) {
console.log(`Getting L2 message proof for block ${blockNumber}`);
return await l2Provider.getMessageProof(blockNumber, wallet.address, ethers.utils.keccak256(ethers.utils.toUtf8Bytes(MESSAGE)));
}
async function proveL2MessageInclusion(l1BatchNumber: ethers.BigNumberish, proof: any, trxIndex: number) {
const zkAddress = await l2Provider.getMainContractAddress();
const mailboxL1Contract = new ethers.Contract(zkAddress, utils.ZKSYNC_MAIN_ABI, l1Provider);
// all the information of the message sent from L2
const messageInfo = {
txNumberInBlock: trxIndex,
sender: wallet.address,
data: ethers.utils.toUtf8Bytes(MESSAGE),
};
console.log(`Retrieving proof for batch ${l1BatchNumber}, transaction index ${trxIndex} and proof id ${proof.id}`);
const res = await mailboxL1Contract.proveL2MessageInclusion(l1BatchNumber, proof.id, messageInfo, proof.proof);
return res;
}
/**
* Full end-to-end of an L2-L1 messaging with proof validation.
* Recommended to run in 3 steps:
* 1. Send message.
* 2. Wait for transaction to finalize and block verified
* 3. Wait for block to be verified and validate proof
*/
async function main() {
// Step 1: send message
const l2Trx = await sendMessageToL1(MESSAGE);
console.log("Waiting for transaction to finalize...");
// Step 2: waiting to finalize can take a few minutes.
const l2Receipt = await l2Trx.waitFinalize();
// Step 3: get and validate proof (block must be verified)
const proof = await getL2MessageProof(l2Receipt.blockNumber);
console.log(`Proof is: `, proof);
const { l1BatchNumber, l1BatchTxIndex } = await l2Provider.getTransactionReceipt(l2Receipt.transactionHash);
console.log("L1 Index for Tx in block :>> ", l1BatchTxIndex);
console.log("L1 Batch for block :>> ", l1BatchNumber);
// IMPORTANT: This method requires that the block is verified
// and sent to L1!
const result = await proveL2MessageInclusion(
l1BatchNumber,
proof,
// @ts-ignore
l1BatchTxIndex
);
console.log("Result is :>> ", result);
process.exit();
}
try {
main();
} catch (error) {
console.error(error);
}