Send an L2 to L1 message

Send an L2 to L1 message

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 representationopen in new window: 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.

Send a message

Two transactions are required:

  • An L2 transaction which sends a message of arbitrary length.
  • An L1 read; implemented by a getter function on an L1 smart contract.
  1. Import the zkSync Era library or contract containing the required functionality.

  2. Get a Contract object that represents the L1Messenger contract.

  3. Transform the request into a raw bytes array.

  4. Use the sendToL1open in new window function from the IL1Messenger.solopen in new window interface, passing the message as a raw bytes array.

Each sent message emits an L1MessageSentopen in new window event.

event L1MessageSent(address indexed _sender, bytes32 indexed _hash, bytes _message);

function sendToL1(bytes memory _message) external returns (bytes32);

4.1 The return value from sendToL1 is the keccak256 hash of the message bytes.

Prove the result

  1. The proveL2MessageInclusionopen in new window function returns a boolean parameter indicating whether the message was sent successfully to L1.
function proveL2MessageInclusion(
    uint256 _blockNumber,
    uint256 _index,
    L2Message memory _message,
    bytes32[] calldata _proof
) public view returns (bool) {
    return _proveL2LogInclusion(_blockNumber, _index, _L2MessageToLog(_message), _proof);

Parameter details

  • _blockNumber: L1 batch number in which the L2 block was included; retrievable using the getBlock method.
  • _index: Index of the L2 log in the block; returned as id by the zks_getL2ToL1LogProof method. _message: Parameter holding the message data. It should be an object containing: - sender: Address that sent the message from L2. - data: Message sent in bytes. - txNumberInBlock: Index of the transaction in the L2 block; returned as transactionIndex with getTransactionReceiptopen in new window on an Ethers Provider object.
  • _proof: Merkle proof of the message inclusion; retrieved by observing Ethereum or using the zks_getL2ToL1LogProof method of the zksync-web3 API.


// The Example contract below sends its address to L1 via the Messenger system contract.
//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);

Example output

Sending message to L1 with text Some L2->L1 message
L2 trx hash is  0xb6816e16906788ea5867bf868693aa4e7a46b68ccd2091be345e286a984cb39b
Waiting for transaction to finalize...
Getting L2 message proof for block 5382192
Proof is:  {
  id: 14,
  proof: [
  root: '0xbc872eb80a7d5d35dd16283c1b1a768b1e1c36404000edaaa04868c7d6a5907c'
L1 Index for Tx in block :>>  32
L1 Batch for block :>>  77512
Retrieving proof for batch 77512, transaction index 32 and proof id 14
Result is :>>  true