Skip to main content

Delivering a Message

Message delivery

After a message is sent by the Router, the MessageSendRequested event is intercepted by the Equito Validators, which generate signatures necessary for validating these messages across different chains. DApps can collect these signatures to produce a proof to be submitted to the Router for message delivery.

On the destination chain, the Router receives messages through the deliverMessages call. It's responsible for verifying and storing cross-chain messages for later execution.

/// @notice Delivers messages to be stored for later execution.
/// @param messages The list of messages to be delivered.
/// @param verifierIndex The index of the verifier used to verify the messages.
/// @param proof The proof provided by the verifier.
function deliverMessages(
EquitoMessage[] calldata messages,
uint256 verifierIndex,
bytes calldata proof
) external;

Under the hood, deliverMessages will perform various security checks, for example ensuring the destination chain selector matches the chain selector of the Router, and that the message has not been previously delivered or executed. Moreover, the proof is used to verify the authenticity of the message list against a certain Verifier.
Only the messages that pass this validation process are stored in the Router for later execution. However, the presence of an invalid message in the batch does not prevent the rest of the messages from being delivered: only the invalid messages are discarded.

Message verification and ECDSAVerifier

Verifiers implement the IEquitoVerifier interface, are stored in the Router in an array which can be upgraded by the Equito Governance, allowing for smooth protocol upgrades. The verifyMessages function is designed to verify that a batch of messages has been effectively confirmed.

/// @notice Verifies a set of Equito messages using the provided proof.
/// @param messages The array of Equito messages to verify.
/// @param proof The proof provided to verify the messages.
/// @return True if the messages are verified successfully, otherwise false.
function verifyMessages(
EquitoMessage[] calldata messages,
bytes calldata proof
) external returns (bool);

The first implementation provided for IEquitoVerifier is ECDSAVerifier, which verifies a batch of signatures against the Validator Set and a hash of the messages, checking each ECDSA signature to confirm it belongs to a validator and ensures that the cumulative number of unique validators meets or exceeds a predefined threshold of 70%.

Upon successfully verifying the messages, the Router contract emits a MessageDelivered event for each valid message stored.

/// @notice Emitted when a messages has successfully been delivered, ready to be executed.
/// @param messageHash The hash of the message that has been delivered.
event MessageDelivered(bytes32 messageHash);

Message execution

When a message is delivered, it is ready for execution: anyone can call the executeMessage function for previously delivered message, which will be routed to the appropriate receiver contracts, where the logic for message execution is implemented.

/// @notice Executes a stored message.
/// @param message The message to be executed.
/// @param messageData The data of the message to be executed.
function executeMessage(
EquitoMessage calldata message,
bytes calldata messageData
) external payable;

After message and data validation, the receiveMessage function is called on each receiver contract. This function is part of the IEquitoReceiver interface, which is implemented by the EquitoApp contract. If you went through the code in Router.sol or ECDSAVerifier.sol you might have noticed those contracts are also implementing this interface: it is a way to enable these contracts to receive messages from the Equito Governance using the Equito Protocol.

After messages are executed, thet are removed from storage, and a MessageExecuted event is emitted.

/// @notice Emitted when a message has successfully been executed.
/// @param messageHash The hash of the message that has been executed.
event MessageExecuted(bytes32 messageHash);

Combined delivery and execution

While separate message delivery and execution can be useful in different use-cases, most of the times they can be executed within the same transaction. For this reason, the deliverAndExecuteMessage function is provided, which combines the delivery and execution of one message in a single call.

/// @notice Verify and execute a message with the appropriate receiver contract.
/// @param message The message to be executed.
/// @param messageData The data of the message to be executed.
/// @param verifierIndex The index of the verifier used to verify the message.
/// @param proof The proof to provide to the verifier.
function deliverAndExecuteMessage(
EquitoMessage calldata message,
bytes calldata messageData,
uint256 verifierIndex,
bytes calldata proof
) external payable;

Message reception

EquitoApp contract

EquitoApp abstracts lots of the complexity of message reception and execution, providing a simple interface for dApps to implement their own logic. The EquitoApp contract is designed to receive messages from the Router and execute them. It implements the IEquitoReceiver interface, which defines the receiveMessage function that is called by the Router when a message is executed.

/// @notice Receives a cross-chain message from the Router Contract.
/// It is a wrapper function for the `_receiveMessage` functions, that need to be overridden.
/// Only the Router Contract is allowed to call this function.
/// @param message The Equito message received.
/// @param messageData The data of the message received.
function receiveMessage(
EquitoMessage calldata message,
bytes calldata messageData
) external payable override onlyRouter {
/// ...
}

The first peculiarity of the receiveMessage function is the onlyRouter modifier, which ensures that only the Router contract can call this function. This is a security measure to prevent unauthorized calls and forged messages from being executed.

EquitoApp includes dedicated functions to manage messages, distinguishing between those from trusted peer contracts processed via _receiveMessageFromPeer, and messages from non-peer contracts handled by _receiveMessageFromNonPeer. You can learn more about peers in the Build your first Equito App guide.

/// @notice The logic for receiving a cross-chain message from a peer.
/// @param message The Equito message received.
/// @param messageData The data of the message received.
function _receiveMessageFromPeer(
EquitoMessage calldata message,
bytes calldata messageData
) internal virtual {}

/// @notice The logic for receiving a cross-chain message from a non-peer.
/// The default implementation reverts the transaction.
/// @param message The Equito message received.
/// @param messageData The data of the message received.
function _receiveMessageFromNonPeer(
EquitoMessage calldata message,
bytes calldata messageData
) internal virtual {
revert Errors.InvalidMessageSender();
}

The implementation of these functions depends on the specific requirements of EquitoApp. They can include logic to validate the received message, update contract state, emit events, or send response messages.

IEquitoReceiver interface

On the contrary of EquitoApp, the IEquitoReceiver interface does not implement any check on the message sender, leaving the implementation of the receiveMessage function to the contract that implements the interface. This allows for more flexibility in the message reception logic, but also requires the contract to implement its own security checks.

This is how the implementation looks like in Router, where the check is performed manually:

/// @notice Receives a cross-chain message from the Router contract.
/// @param message The Equito message received.
/// @param messageData The data of the message received.
function receiveMessage(
EquitoMessage calldata message,
bytes calldata messageData
) external override onlySovereign(message) {
if (msg.sender != address(this)) {
revert Errors.InvalidRouter(msg.sender);
}

// ...
}