Messaging
Interop messaging marks the first step of universal interoperability for the Elastic Network. Interop messaging enables sending and verifying messages across ZKsync chains via ZKsync Gateway.
An interop message consists of arbitrary data and has two simple properties:
- Anyone can send a message.
- Anyone can verify that a given message was successfully sent on some chain.
One example use case for interop messaging would be to unlock experiences on a chain based on activities on another chain.
For a full step-by-step tutorial for sending and verifying interop messages using zksync-ethers
,
check out the interop messages guide.
Sending a message
To send a message, you can call the sendToL1
function on the L1Messenger
contract, which is pre-deployed on every ZKsync chain at address 0x00..008008
:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.28;
import { IL1Messenger } from '@matterlabs/zksync-contracts/contracts/system-contracts/interfaces/IL1Messenger.sol';
contract InteropSendMessage {
address constant L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR = 0x0000000000000000000000000000000000008008;
IL1Messenger public L1Messenger = IL1Messenger(L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR);
function sendMessage(bytes calldata _message) public {
L1Messenger.sendToL1(_message);
}
}
The message itself has no destination chain or address. It’s simply a payload created by a user or contract that gets broadcast.
There is no expiration for when messages can be verified. They remain verifiable indefinitely.
The function name sendToL1
was kept from previous versions for simplicity, although it does not restrict messages from only being verified on the L1.
These messages are the foundation to unlock more complex crosschain activities in the future, like bridging assets and operate with contracts across different chains.
Lifecycle of sending a message
- Send a Message: The message is sent via the
sendToL1
method in the L1Messenger contract. The naming is leftover from a previous method, but this message first gets sent to Gateway. - Batch created: The transaction gets included in a batch, which is sent to Gateway.
- Chain-batch root added to Gateway Message Root: The chain-batch root (which includes the log for the message)
is added to Gateway’s global message root.
The
MessageRoot
is a contract that collects messages from different chains and aggregates them into a single Merkle tree. - Event emitted: ZKsync Gateway’s MessageRoot contract emits an event indicating that the interop root was updated.
- Event detected: EthWatch detects the event from a ZKsync chain, stores it in its database,and includes it in the next batch’s bootloader state.
- New Root gets stored: The bootloader calls the
L2InteropRootStorage
contract to update its stored interop roots. - Dependency verification: The latest batch’s dependency roots are verified against ZKsync Gateway’s
MessageRoot
. At this point, the interop root for the batch is confirmed.
Verifying a message
Messages can be verified using the proveL2MessageInclusionShared
method
in the L2 message verification contract deployed at 0x..10009
on each ZKsync chain.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.28;
import { IMessageVerification } from '@matterlabs/zksync-contracts/contracts/l1-contracts/state-transition/chain-interfaces/IMessageVerification.sol';
import { L2Message } from '@matterlabs/zksync-contracts/contracts/l1-contracts/common/Messaging.sol';
contract InteropVerification {
address constant L2_MESSAGE_VERIFICATION_ADDRESS = 0x0000000000000000000000000000000000010009;
IMessageVerification public l2MessageVerifier = IMessageVerification(L2_MESSAGE_VERIFICATION_ADDRESS);
function checkVerification(
uint256 _sourceChainId,
uint256 _l1BatchNumber,
uint256 _l2MessageIndex,
L2Message calldata _l2MessageData,
bytes32[] calldata _proof
) public view returns (bool) {
bool result = l2MessageVerifier.proveL2MessageInclusionShared(
_sourceChainId,
_l1BatchNumber,
_l2MessageIndex,
_l2MessageData,
_proof
);
return result;
}
}
Lifecycle of verifying a message
- Proof Submission: On any ZKsync chain that uses Gateway, the user calls proveL2MessageInclusionShared
at the
L2_MESSAGE_VERIFICATION_ADDRESS
(0x..10009
), supplying the message data and a proof of inclusion. Note, that this step could be done in the same batch as the dependency verification. - Final Verification: The
L2InteropRootStorage
contract is triggered to verify the corresponding interop root inclusion on the chain where the proof was submitted.
For more detail about how interoperability works at the protocol level, visit the protocol specifications documentation.