Send a message from L2 to L1

Learn how to send an arbitrary message from a smart contract on ZKsync to another contract in Ethereum.

This how-to guide explains how to send an arbitrary message from ZKsync to Ethereum L1.

What you'll learn:

  • How L2-L1 communication works
  • How to send a message from ZKsync to Ethereum L1.
  • How to verify the message on Ethereum L1.
GitHub

Tools:

  • - zksync-ethers
  • - zksync-contracts
smart contractshow-toL2-L1 communication
Last Updated: May 09, 2024

It is impossible to send transactions directly from L2 to L1.

Instead, you can send arbitrary-length messages from ZKsync Era to Ethereum, and then handle the received message on Ethereum with an L1 smart contract.

What is a message?

  • A message is like an event on Ethereum.
  • The difference is that a message publishes data on L1.
  • Solidity representation: solidity struct L2Message { address sender; bytes data; uint256 txNumberInblock; }
Verification and confirmation is possible using Ethereum data. However, ZKsync Era has an efficient request proof function which does the same.

Common use cases

Along with ZKsync Era's built-in censorship resistance that requires multi-layer interoperability, there are some common use cases that need L2 to L1 transaction functionality, such as:

  • Bridging funds from L2 to L1.
  • Layer 2 governance.

Set up

  1. Create a project folder and cd into it
    mkdir message-l2
    cd message-l2
    
  2. Initialise the project:
    npm init -y
    
  3. Install the required dependencies:
    npm install -D zksync-ethers ethers typescript dotenv @matterlabs/zksync-contracts @types/node
    
  4. Install ts-node globally to execute the scripts that we're going to create:
    npm install -g ts-node
    
  5. In the root folder, add a .env file with the private key of the wallet to use:
    WALLET_PRIVATE_KEY=0x..;
    

Send the message

To send a message from L2 to L1, we are going to interact with the ZKsync messenger contract.

Both the address and ABI are provided in the utils.L1_MESSENGER_ADDRESS and utils.L1_MESSENGER of zksync-ethers. The method we're using is sendToL1 and we're passing the message in UTF8 bytes format.

Create a 1.send-message.ts file in the root directory with the next script:

Change the MESSAGE variable around line 10 and execute the script by running:

ts-node 1.send-message.ts

You should see the following output:

Sending message to L1 with text {MESSAGE}
L2 trx hash is  0x926efb47c374478191645a138c5d110e6a6a499ea542e14bcb583918646f7db5
Check https://sepolia.explorer.zksync.io/tx/0x926efb47c374478191645a138c5d110e6a6a499ea542e14bcb583918646f7db5

Retrieve the message transaction details

In order to continue, we need the transaction that sent the message to be included in a batch and sent to L1. This time varies depending on the network activity and could be around one hour.

For the next steps we'll need information about the L2 block and L1 batch the transaction was included into. Create a 2.get-tx-details.ts file in the root directory with the next script:

This script retrieves the transaction receipt. The fields we're interested in are:

  • blockNumber: the L2 block number the transaction was included into.
  • index: the index of the transaction in the L2 block.
  • l1BatchNumber: the L1 batch number the transaction was included into.
  • l1BatchTxIndex: the index of the transaction in the L1 batch.

Enter the transaction hash from the previous step in the TX_HASH variable and run the script with:

ts-node 2.get-tx-details.ts

You'll get the following output:

Getting L2 tx details for transaction 0x7be3434dd5f886bfe2fe446bf833f09d1be08e0a644a4996776fec569c3801a0
L2 transaction included in block 2607311 with index 0
L1 batch number is 9120 and tx index in L1 batch is 953
Check https://sepolia.explorer.zksync.io/tx/0x7be3434dd5f886bfe2fe446bf833f09d1be08e0a644a4996776fec569c3801a0 for more details

Retrieve the message proof

To retrieve the proof that the message was sent to L1, create a 3.get-proof.ts file in the root directory with the next script:

The getLogProof method requires the L2 transaction hash and the L2 transaction index, both of which are included in the transaction receipt that we retrieved in the previous step.

Enter the hash and index in the TX_HASH and L2_TX_INDEX variables and run the script with:

ts-node 3.get-proof.ts

You'll get an output similar to this:

Getting L2 message proof for transaction 0x7be3434dd5f886bfe2fe446bf833f09d1be08e0a644a4996776fec569c3801a0 and index 0
Proof is:  {
  id: 15,
  proof: [
    '0x871b381c5abfd7365d19ef7bf2b9bd80912b6728a4475dfbaf2f2c652f9912b6',
    '0x505e3c0e95b3f2c18a11630874013b527820b729cf8443da3b39c0f029a5d354',
    '0x1d49feee54b5d52f361196a133e1265481afae3fcc3ccfae74ef5df0f0c1bad6',
    '0x71c3b4937077cd356e32d3f5c413eddff25caf93542a6fa05f0b1c046b6c59d5',
    '0x33d776ccbbe67db6aaf1ab61ec564d406b33f7f9d12c587a85104077d13cecd3',
    '0x1798a1fd9c8fbb818c98cff190daa7cc10b6e5ac9716b4a2649f7c2ebcef2272',
    '0x66d7c5983afe44cf15ea8cf565b34c6c31ff0cb4dd744524f7842b942d08770d',
    '0xb04e5ee349086985f74b73971ce9dfe76bbed95c84906c5dffd96504e1e5396c',
    '0xac506ecb5465659b3a927143f6d724f91d8d9c4bdb2463aee111d9aa869874db',
    '0x124b05ec272cecd7538fdafe53b6628d31188ffb6f345139aac3c3c1fd2e470f',
    '0xc3be9cbd19304d84cca3d045e06b8db3acd68c304fc9cd4cbffe6d18036cb13f',
    '0xfef7bd9f889811e59e4076a0174087135f080177302763019adaf531257e3a87',
    '0xa707d1c62d8be699d34cb74804fdd7b4c568b6c1a821066f126c680d4b83e00b',
    '0xf6e093070e0389d2e529d60fadb855fdded54976ec50ac709e3a36ceaa64c291'
  ],
  root: '0x98ebb6d15a0274a2a40bf7ca42d1576c994f29e23155c10597cd5a0c9ed7e367'
}

Verify the message transaction proof

Once we have a proof that the message was sent from L2, we can verify that it was actually included in L1. Create a 4.prove-inclusion.ts file in the root directory with the next script:

This scripts interacts with the proveL2MessageInclusion method of the ZKsync contract on L1. This method requires the following parameters:

  • l1BatchNumber: Batch number the L2 transaction was included into.
  • proofId: the id of the proof retrieved in step 3.
  • messageInfo: an object including:
    • txNumberInBatch: the index of the transaction in the L1 batch.
    • sender: the address of the account that sent the transaction.
    • data: the message formated in UTF8 bytes format.
  • proof: the proof retrieved in step 3.

Enter all these details in the SENDER, MESSAGE, L1_BATCH_NUMBER, L1_BATCH_TX_INDEX and PROOF variables and execute the script with:

ts-node 4.prove-inclusion.ts

You'll get the following output:

Retrieving proof for batch 9120, transaction index 953 and proof id 15
Result is :>>  true

This indicates the proof is valid and the message was actually included in L1.


Made with ❤️ by the ZKsync Community