Skip to main content

Canonical message service

Linea message service architecture

What is it?

The canonical message service is a combination of smart contracts and other protocols which work together to pass "arbitrary messages"--that is, user-specified data--between Linea and other networks.

What does it do?

If you've ever used a bridge between two blockchains, you may be used to what feels like a fairly restrictive experience; you can only send certain tokens, for example. The canonical message service itself isn't like an end-user bridge interface. It's a system through which data and assets can be permissionlessly and reliably transferred from one blockchain to another. The Service, as a whole, receives requests to move something from one network to the other, and then carries that request out, delivering the message as submitted to an established smart contract on the destination network.

One of the most important things that the Message Service transfers is information about the current state of the Ethereum network, from Ethereum to Linea, and in return, an updated Merkle tree and a zk-proof from Linea to Ethereum, every time Linea reports back about activity on the network. In other words, the canonical message service transmits the rollup data.

However, the Service is not limited or restricted to use by Linea's core functionality. It is general-purpose, public infrastructure which can be used by developers, integrated into dapps, and triggered by end users.

Linea bridge architecture

How does it do it?

The canonical message service consists of three main elements: two smart contracts, and the Postbots service in between. As you may have guessed already, the smart contracts are on Linea and Ethereum, and are almost exactly the same. They allow for ETH to be minted on the target network, for example, though they are not limited to that.

A user initiates a network-to-network transfer by executing a call on one of the contract's methods--that is, invoking a function built into the smart contract. The user could do this on their own, if they have the knowledge of how to interact with a smart contract directly, or they could do so through a frontend. If properly formulated, the smart contract will accept the request from the user, and pass it off to the Postbots.

The Postbots (sometimes referred to as postmen) are one part of Linea that are currently centralized, but will be decentralized. The Postbots are essentially actors that "listen" for calls being made to one of the contracts, either on Linea or Ethereum, and pass the information submitted to the other network.

Once the information is delivered to the destination smart contract, the code contained in the request is executed. If the message being transferred carried orders to mint tokens, users can either choose to manually pull the transferred assets out of the destination end of the bridge, or pay up-front and allow the assets to be pushed directly to the destination address.

There's an additional layer of logic, though, which serves to ensure that the message delivered to the L2 is valid. Essentially, a message is sent from Ethereum, relayed through the Message Service, and is delivered to the Linea smart contract. That smart contract checks the message received against the list of messages sent on the Ethereum side, verifies that it exists on the L1, and only then accepts it as a valid message:

Linea message service verification

Technical reference

The message service is responsible for cross-chain messages between Ethereum and Linea, which:

  • Allows a contract on the source chain to safely interact with a contract on the target chain (e.g. L1TokenBridge triggering mint on the L2TokenBridge),
  • Is responsible for bridging ETH (native currency on L1 and L2)
  • Supports:
    • push: auto-execution on target layer if a fee is paid
    • pull: users / protocols responsible for triggering the transaction

Message service contracts

How to use

Workflow

  1. Dapp calls sendMessage(...) on the origin layer using the proxy contract at one of the testnet addresses above.
    • Args:
      • _to: the destination address on the destination chain
      • _fee: the message service fee on the origin chain
        • An optional field used to incentivize a Postman to perform claimMessage(...) automatically on the destination chain (not available when bridging from L2 to L1, or for non-ETH transfers)
      • _calldata: a flexible field that is generally created using abi.encode(...)
  2. Dapp uses the Postman SDK to simplify the execution of messages on the destination layer by:
    • Triggering the delivery
      • If messages don't get delivered by the postman, the message can be manually claimed by calling, with the parameters detailed in the interface below, one of:
      • L2: claimMessage
      • L1: claimMessageWithProof
        You can also use the SDK to claim messages.
    • Receiving the delivery in the dapp smart contract
      • This triggers claimMessage(...) on the destination layer that will call _to with _calldata and a value equal to.
      • The dapp smart contract can inherit from MessageServiceBase.sol to:
        • Verify that the call comes from the MessageService onlyMessagingService
        • Verify that the sender on the origin chain comes from a trusted contract (usually the dapp sibling contract) using onlyAuthorizedRemoteSender()
Proxy contract

A proxy contract is one that simply points towards the actual "implementation" contracts. This model is beneficial as it allows the implementation contracts to be upgraded independently of the proxy, allowing contract upgrades without having to start afresh and lose the proxy contract's history. When the implementation contracts are updated, the proxy contract is simply amended to point towards the new implementation contract addresses.

Interface IMessageService.sol

IMessageService.sol
pragma solidity ^0.8.19;

interface IMessageService {
/**
* @dev Emitted when a message is sent.
* @dev We include the message hash to save hashing costs on the rollup.
*/
event MessageSent(
address indexed _from,
address indexed _to,
uint256 _fee,
uint256 _value,
uint256 _nonce,
bytes _calldata,
bytes32 indexed _messageHash
);

/**
* @dev Emitted when a message is claimed.
*/
event MessageClaimed(bytes32 indexed _messageHash);

/**
* @dev Thrown when fees are lower than the minimum fee.
*/
error FeeTooLow();

/**
* @dev Thrown when fees are lower than value.
*/
error ValueShouldBeGreaterThanFee();

/**
* @dev Thrown when the value sent is less than the fee.
* @dev Value to forward on is msg.value - _fee.
*/
error ValueSentTooLow();

/**
* @dev Thrown when the destination address reverts.
*/
error MessageSendingFailed(address destination);

/**
* @dev Thrown when the destination address reverts.
*/
error FeePaymentFailed(address recipient);

/**
* @notice Sends a message for transporting from the given chain.
* @dev This function should be called with a msg.value = _value + _fee. The fee will be paid on the destination chain.
* @param _to The destination address on the destination chain.
* @param _fee The message service fee on the origin chain.
* @param _calldata The calldata used by the destination message service to call the destination contract.
*/
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable;

/**
* @notice Deliver a message to the destination chain.
* @notice Is called automatically by the Postman, dApp or end user.
* @param _from The msg.sender calling the origin message service.
* @param _to The destination address on the destination chain.
* @param _value The value to be transferred to the destination address.
* @param _fee The message service fee on the origin chain.
* @param _feeRecipient Address that will receive the fees.
* @param _calldata The calldata used by the destination message service to call/forward to the destination contract.
* @param _nonce Unique message number.
*/
function claimMessage(
address _from,
address _to,
uint256 _fee,
uint256 _value,
address payable _feeRecipient,
bytes calldata _calldata,
uint256 _nonce
) external;

/**
* @notice Returns the original sender of the message on the origin layer.
* @return The original sender of the message on the origin layer.
*/
function sender() external view returns (address);
}

Abstract contract MessageServiceBase.sol

MessageServiceBase.sol
// SPDX-License-Identifier: OWNED BY Consensys Software Inc.
pragma solidity ^0.8.19;

import "./interfaces/IMessageService.sol";

/**
* @title Base contract to manage cross-chain messaging.
* @author Consensys Software Inc.
*/
abstract contract MessageServiceBase {
IMessageService public messageService;
address public remoteSender;

uint256[10] private __base_gap;

/**
* @dev Thrown when the caller address is not the message service address
*/
error CallerIsNotMessageService();

/**
* @dev Thrown when remote sender address is not authorized.
*/
error SenderNotAuthorized();

/**
* @dev Thrown when an address is the default zero address.
*/
error ZeroAddressNotAllowed();

/**
* @dev Modifier to make sure the caller is the known message service.
*
* Requirements:
*
* - The msg.sender must be the message service.
*/
modifier onlyMessagingService() {
if (msg.sender != address(messageService)) {
revert CallerIsNotMessageService();
}
_;
}

/**
* @dev Modifier to make sure the original sender is allowed.
*
* Requirements:
*
* - The original message sender via the message service must be a known sender.
*/
modifier onlyAuthorizedRemoteSender() {
if (messageService.sender() != remoteSender) {
revert SenderNotAuthorized();
}
_;
}

/**
* @notice Initializes the message service and remote sender address
* @dev Must be initialized in the initialize function of the main contract or constructor
* @param _messageService The message service address, cannot be empty.
* @param _remoteSender The authorized remote sender address, cannot be empty.
**/
function _init_MessageServiceBase(address _messageService, address _remoteSender) internal {
if (_messageService == address(0)) {
revert ZeroAddressNotAllowed();
}

if (_remoteSender == address(0)) {
revert ZeroAddressNotAllowed();
}

messageService = IMessageService(_messageService);
remoteSender = _remoteSender;
}
}