# L2 -> L1 communication

This section describes the interface for interaction with Ethereum from L2. It assumes that you are already familiar with the basic concepts of working with L2 -> L1 communication. If you are new to this topic, you can read the conceptual introduction here.

# Structure

Unlike L1 -> L2 communication, it is impossible to directly initialize transactions from L2 to L1. However, you can send an arbitrary-length message from zkSync to Ethereum and then handle the received message on an L1 smart contract. From the L2 side, to send a message, a special system contract should be called. It accepts only the bytes of the message that is sent to the zkSync smart contract on Ethereum. From the L1 side, the zkSync smart contract provides an interface to prove that the message was sent to L1 and included in a zkSync block.

# Sending a message on L2

The message sender will be determined from context.

function sendToL1(bytes memory _message) external returns (bytes32 messageHash);
  • _message is a parameter that contains the raw bytes of the message

This function sends a message from L2 and returns the keccak256 hash of the message bytes. The message hash can be used later to get a proof that the message was sent on L1. Its use is optional and is for convenience purposes only.

More information about the messenger can be found in the system contracts section.

# Examples

# Solidity

//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);
    }
}

# Prove inclusion of the message into the L2 block

The following function returns a boolean value that indicates that a message, with such parameters, was sent to L1.

    struct L2Message {
        address sender;
        bytes data;
    }

    function proveL2MessageInclusion(
        uint32 _blockNumber,
        uint256 _index,
        L2Message calldata _message,
        bytes32[] calldata _proof
    ) external view returns (bool);
  • _blockNumber is a parameter that points to the L2 block number in which the message was sent.
  • _index is a parameter that contains the position of the message in the L2 block. It can be obtained from observing Ethereum or received via API.
  • _message is a parameter that contains the full information of the message, including the raw bytes of payload and sender address.
  • _proof is a parameter that contains the merkle proof of the message inclusion. It can be restored either from observing Ethereum or received via API.

# Examples

# Sending message from L2 to L1 using zksync-web3

import { Wallet, Provider, Contract, utils } from "zksync-web3";
import { ethers } from "ethers";

const TEST_PRIVATE_KEY = "0xc8acb475bb76a4b8ee36ea4d0e516a755a17fad2e84427d5559b37b544d9ba5a";

async function main() {
  const zkSyncProvider = new Provider("https://zksync2-testnet.zksync.dev");
  const ethereumProvider = ethers.getDefaultProvider("goerli");
  const wallet = new Wallet(TEST_PRIVATE_KEY, zkSyncProvider, ethereumProvider);

  const contract = new ethers.Contract(utils.L1_MESSENGER_ADDRESS, utils.L1_MESSENGER, wallet);

  const someString = ethers.utils.toUtf8Bytes("Some L2->L1 message");
  const tx = await contract.sendToL1(someString);
  const receipt = await tx.waitFinalize();

  // Get proof that the message was sent to L1
  const msgProof = await zkSyncProvider.getMessageProof(receipt.blockNumber, wallet.address, ethers.utils.keccak256(someString));
}

# Example of L1 message processing contract

//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(uint32 => 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
    uint32 _l2BlockNumber,
  // Message index, that can be received via API
    uint256 _index,
  // The message that was sent from l2
    bytes calldata _message,
  // Merkle proof for the message
    bytes32[] calldata _proof
  ) external returns (bytes32 messageHash) {
    // check that the message has not been processed yet
    require(!isL2ToL1MessageProcessed(_l2BlockNumber, _index));

    IZkSync zksync = IZkSync(_zkSyncAddress);
    address someSender = 0x19a5bfcbe15f98aa073b9f81b58466521479df8d;
    L2Message message = L2Message({sender: someSender, data: _message});

    bool success = zksync.proveL2MessageInclusion(
      _l2BlockNumber,
      _index,
      message,
      _proof
    );
    require(success, "Failed to prove message inclusion");

    // Mark message as processed
    isL2ToL1MessageProcessed(_l2BlockNumber, _index) = true;
  }
}

Last Updated: 7/5/2022, 11:03:10 AM