A Hyperlane implementation for a new chain architecture is comprised of the following:

  1. Contracts: expose the interface for application developers to send and receive messages with
  2. Agents: operate the protocol by adding security and relaying messages
  3. Applications: applications that use the protocol and demonstrate its capabilities

Before getting started here, it is recommended to review the protocol documentation.

If you would like to dive deeper, check out some of the Hyperlane implementations that are currently available:

1. Contracts

Below describes the onchain contract spec for the Hyperlane protocol. It uses solidity types for familiarity but everything should be generalizable to other languages.

  • address should be interpreted as the local chain’s address type.
  • payable describes a function that allows callers to pass native tokens.
  • Ensure events are properly emitted for all important state changes.
  • Clearly document any deviations from the reference implementation and their rationale.

Considerations

Byte representation:

  • Be aware that different chains may have different native types for representing bytes. For example, StarkNet uses felt252 for contract addresses, which may not fully accommodate 32-byte addresses used in other chains.
  • Implement proper conversion and validation mechanisms when dealing with cross-chain addresses.

Serialization:

  • Pay close attention to how message fields are serialized, especially for variable-length data like the message body.
  • Ensure that the serialization method used (e.g., abi.encodePacked equivalent) behaves consistently across different chain implementations.

Message

The message is the core data structure used by the Hyperlane protocol. It is a packed data structure that contains all the information needed to route a message from one domain to another.

struct Message {    // The version of the origin and destination Mailboxes    uint8 version,    // A nonce to uniquely identify the message on its origin Mailbox    uint32 nonce,    // Domain of origin chain    uint32 origin,    // Address of sender on origin chain    bytes32 sender,    // Domain of destination chain    uint32 destination,    // Address of recipient on destination chain    bytes32 recipient,    // Raw bytes of message body    bytes body}

Mailbox

The mailbox is the entrypoint for developers to send and receive messages from. Make sure that localDomain is immutable to prevent unauthorized changes that could compromise cross-chain security.

Implementations:

In addition to default and custom hooks, Hyperlane introduces the concept of a Required Hook that is used for post processing of ALL dispatches. Make sure to call the required hook before the default or custom hook.

dispatch

Dispatches a message to the destination domain and recipient.

function dispatch(    // Domain of destination chain    uint32 destination,    // Address of recipient on destination chain as bytes32    bytes32 recipient,    // Raw bytes content of message body    bytes body) returns (    // The message ID inserted into the Mailbox's merkle tree    bytes32 messageId);

Dispatches a message to the destination domain and recipient, and provides metadata for the default hook.

function dispatch(    // Domain of destination chain    uint32 destination,    // Address of recipient on destination chain as bytes32    bytes32 recipient,    // Raw bytes content of message body    bytes body,    // Metadata used by the default post dispatch hook    bytes defaultHookMetadata) returns (    // The message ID inserted into the Mailbox's merkle tree    bytes32 messageId);

Dispatches a message to the destination domain and recipient, and provides metadata for a custom hook to use instead of the default.

function dispatch(    // Domain of destination chain    uint32 destination,    // Address of recipient on destination chain as bytes32    bytes32 recipient,    // Raw bytes content of message body    bytes body,    // Metadata used by the custom post dispatch hook    bytes customHookMetadata,    // Custom hook to use instead of the default    IPostDispatchHook customHook) returns (    // The message ID inserted into the Mailbox's merkle tree    bytes32 messageId);

process

Attempts to deliver message to its recipient. Verifies message via the recipient’s ISM using the provided metadata.

function process(    // Metadata used by the ISM to verify message.    bytes metadata,    // Byte packed message    bytes message);

latestDispatchedId

Returns the latest dispatched message ID used for auth in post-dispatch hooks.

function latestDispatchedId() public view returns (bytes32);

Message Recipient

A contract that wants to receive a message must expose the following handler.

function handle(    // Domain of origin chain    uint32 origin,    // Address of sender on origin chain    bytes32 sender,    // Raw bytes content of message body    bytes body);

They may optionally specify a security module to verify messages before they are handled.

function IInterchainSecurityModule() returns (address);

After implementing these three contracts, you can reach your first milestone to test, mocking a message transfer, by calling a Mailbox’s dispatch function to send a message to a recipient and assert that the recipient received the message. See a Foundry test case here.

Interchain Security Module

Interchain security modules are used to verify messages before they are processed.

moduleType

Returns an enum that represents the type of security model encoded by this ISM.

enum ModuleType {    UNUSED,    ROUTING,    AGGREGATION,    LEGACY_MULTISIG,    MERKLE_ROOT_MULTISIG,    MESSAGE_ID_MULTISIG,    NULL, // used with relayer carrying no metadata    CCIP_READ}function moduleType() returns (ModuleType);

Relayers infer how to fetch and format metadata from this type.

verify

Defines a security model responsible for verifying interchain messages based on the provided metadata.

function verify(    // Off-chain metadata provided by a relayer, specific    // to the security model encoded by the module    // (e.g. validator signatures)    bytes metadata,    // Hyperlane encoded interchain message    bytes message) returns (    // True if the message was verified    bool success);

Static module management - Our Solidity implementation defines MultisigISMs as static and part of the bytecode which might not be possible in other chains. You may need to implement dynamic modules for the ISM instead.

Validator Announce

Validators announce their signature storage location so that the relayer can fetch and verify their signatures.

announce

Announces a validator signature storage location

function announce(    address validator,      // The address of the validator    string storageLocation, // Information encoding the location of signed checkpoints    bytes signature         // The signed validator announcement) external returns (bool);

getAnnouncedStorageLocations

Returns a list of all announced storage locations

function getAnnouncedStorageLocations(    address[] _validators   // The list of validators to get storage locations for) external view returns (    string[][]              // A list of registered storage metadata);

Multisig ISM

Implements a security module that checks if the metadata provided to verify satisfies a quorum of signatures from a set of configured validators.

It is a common error when implementing this ISM to allow a single validator’s signature to be passed multiple times and errantly achieve quorum. Take care to ensure validators cannot be double counted and add a negative test case for this. See solidity test_verify_revertWhen_duplicateSignatures for example.

Metadata

To be used with the MESSAGE_ID_MULTISIG module type implementation in the relayer.

The metadata must be formatted as follows:

struct MultisigMetadata {    // The address of the origin mailbox    bytes32 originMailbox;    // The signed checkpoint root    bytes32 signedCheckpointRoot;    // The concatenated signatures of the validators    bytes signatures;}

validatorsAndThreshold

Returns the set of validators responsible for verifying message and the number of signatures required.

Can change based on the content of _message

function validatorsAndThreshold(    // Hyperlane formatted interchain message    bytes message) returns (    // The array of validator addresses    address[] validators,    // The number of validator signatures needed    uint8 threshold);

After implementing the MultisigISM, you reach the second milestone to test that your Mailbox only processes after a recipient’s ISM returns true. You can test that with a TestISM that you can statically set to accept or reject any message. See a Foundry test case here.

Interchain Gas Paymaster

The gas paymaster is used to pay for the gas required in message processing on the destination chain. This is not strictly required if the relayer is willing to subsidize message processing.

Implement robust checks for sufficient gas payment, considering chain-specific token handling. Our solidity implementation charges native message value but for other chains, you may need to charge a specific token and scale the gas overhead and tokenExchangeRate accordingly.

payForGas

Deposits msg.value as a payment for the relaying of a message to its destination chain.

Although you can specify a refundAddress, overpayment may not be refunded to the message sender if you are composing hooks together.

function payForGas(    // The ID of the message to pay for.    bytes32 messageId,    // The domain of the message's destination chain.    uint32 destination,    // The amount of destination gas to pay for.    uint256 gasAmount,    // The local address to refund any overpayment to.    address refundAddress) payable;

GasPayment

Emitted when a payment is made for a message’s gas costs.

event GasPayment(    bytes32 messageId,    uint32 destinationDomain,    uint256 gasAmount,    uint256 payment);

DestinationGasConfigSet

Emitted when the gas oracle for a remote domain is set.

event DestinationGasConfigSet(    uint32 remoteDomain, // remote domain    address gasOracle,   // gas oracle    uint96 gasOverhead   // destination gas overhead);

2. Agents

Below describes the agent spec for a new chain implementation. The rust implementations hope to support all chains, but the spec is intended to be chain agnostic.

Message Indexing

All agents must index messages from the origin mailbox. In the solidity mailbox, we emit an event for each message dispatched. Other chains may have different ways of surfacing this information, but the agent must be able to get message content reliably and with consistent ordering — see the message indexer trait.

Validator

In addition to indexing messages dispatched from the mailbox, validators produce attestations for the messages they observe to be used on the destination chain for security.

Checkpoint

Validators produce attestations called checkpoints from the Mailbox which commit via merkle root to all dispatched message IDs.

pub struct Checkpoint {    /// The mailbox address    pub mailbox_address: H256,    /// The mailbox chain    pub mailbox_domain: u32,    /// The checkpointed root    pub root: H256,    /// The index of the checkpoint    pub index: u32,}

Validators use the latest checkpoint method on the mailbox trait to get the latest checkpoint from the mailbox and submit signatures to some highly available storage using the checkpoint syncer trait.

Checkpoint with Message ID

Validators use indexed messages to join the checkpoint with the corresponding message ID emitted from the mailbox.

pub struct CheckpointWithMessageId {    /// existing Hyperlane checkpoint struct    #[deref]    pub checkpoint: Checkpoint,    /// hash of message emitted from mailbox checkpoint.index    pub message_id: H256,}

They also publish these augmented checkpoints on their syncer.

You can test your validator by configuring it with a chain with the above contracts and observe that it creates valid signatures.

Relayer

In addition to indexing messages dispatched from the mailbox, a relayer processes messages on the destination chain. This requires building metadata that satisfies the message recipient’s ISM verification requirements, and signing transactions that process the message on the destination mailbox.

Metadata Builders

Each module type implies a different metadata format for message verification to succeed. A Relayer will need each module trait (eg multisig) to be implemented.

Message Processor

The relayer will attempt to process messages on the destination mailbox (see message processor). If

  • the message recipient ISM returns an unknown module type
  • module type is known but metadata fails to verify
  • metadata verifies but dry running (gas estimation) message processing fails

then the message will be kicked to an exponential backoff retry queue. The relayer relies on implementations of the mailbox and ism traits for these checks.

Gas Payment Enforcement

The Relayer may also require gas payment for a specific message ID on the origin chain before processing the message on the destination chain. To do this, they must have an IGP deployed with their address set as beneficiary and index gas payment events. See gas payment enforcement trait. We recommend to start with no gas payment enforcement policy and then gradually support more restrictive ones.

Testing

Once you have implemented an MVP of the relayer, you should create an end-to-end test that:

  1. Spins up local origin and destination chains.
  2. Deploys your contracts onto both chains.
  3. Run validators for the origin chain.
  4. Run a relayer between both chains.
  5. Observe that upon dispatch of a message of the origin chain, the validator observes the message, creates a signature and the relayer appropriately processes your message via the ISM that specifies the validator on the destination chain.

See this end-to-end test on the Rust codebase for inspiration.

After validating the agents with local end-to-end tests, it is recommended that you also run end-to-end tests with real testnets.

3. Applications

Warp Routes

Token router application that routes tokens between domains on demand.

transferRemote

Transfers amountOrId token to recipient on destination domain.

function transferRemote(    // The Domain of the destination chain.    uint32  destination,    // The address of the recipient on the destination chain.    bytes32 recipient,    // The amount or identifier of tokens to be sent to the remote recipient.    uint256 amountOrId) returns (    // The identifier of the dispatched message.    bytes32 messageId);

Transfer Message

To be interoperable with Warp Routes on other chains, the body of a transfer message must be a byte packed TransferMessage struct.

struct TransferMessage {    // The recipient of the remote transfer    bytes32 recipient;    // An amount of tokens or a token identifier to be transferred    uint256 amountOrId;    // Optional metadata e.g. NFT URI information    bytes   metadata;}