跳到主要内容

实施指南

一个新的链架构的 Hyperlane 实现由以下部分组成:

  1. 合约:为应用程序开发者提供发送和接收消息的接口
  2. 代理:通过增加安全性和中继消息来操作协议
  3. 应用程序:使用该协议并展示其能力的应用程序

在开始之前,建议先查看协议文档。

信息

如果您想深入了解,可以查看一些当前可用的 Hyperlane 实现:

1. 合约

以下描述了 Hyperlane 协议的链上合约规范。它使用 Solidity 类型以便于理解,但所有内容都应可推广到其他语言。

  • address 应被解释为本地链的地址类型。
  • payable 描述允许调用者传递本地代币的函数。
  • 确保所有重要状态变化都正确发出事件。
  • 清楚地记录任何与参考实现的偏差及其理由。

一些需要考虑的事项

字节表示:

  • 请注意,不同链可能有不同的本地类型来表示字节。例如,StarkNet 使用 felt252 作为合约地址,这可能无法完全容纳其他链使用的 32 字节地址。
  • 在处理跨链地址时,实施适当的转换和验证机制。

序列化:

  • 请密切关注消息字段的序列化方式,特别是对于可变长度数据(如消息体)。
  • 确保所使用的序列化方法(例如,abi.encodePacked 等效)在不同链实现中表现一致。

消息

消息是 Hyperlane 协议使用的核心数据结构。它是一个打包的数据结构,包含了从一个域路由消息到另一个域所需的所有信息。

struct Message {
// 原始和目标邮箱的版本
uint8 version,
// 唯一标识消息的随机数
uint32 nonce,
// 原链的域
uint32 origin,
// 原链上发送者的地址
bytes32 sender,
// 目标链的域
uint32 destination,
// 目标链上接收者的地址
bytes32 recipient,
// 消息体的原始字节
bytes body
}

邮箱

邮箱是开发者发送和接收消息的入口点。确保 localDomain 是不可变的,以防止未经授权的更改,这可能会危及跨链安全。

实现:

信息

除了默认和自定义钩子,Hyperlane 引入了 必需钩子 的概念,用于对所有调度进行后处理。确保在默认或自定义钩子之前调用必需钩子。

调度

将消息调度到目标域和接收者。

function dispatch(
// 目标链的域
uint32 destination,
// 目标链上接收者的地址(以 bytes32 表示)
bytes32 recipient,
// 消息体的原始字节内容
bytes body
) returns (
// 插入到邮箱的默克尔树中的消息 ID
bytes32 messageId
);

将消息调度到目标域和接收者,并提供默认钩子的元数据。

function dispatch(
// 目标链的域
uint32 destination,
// 目标链上接收者的地址(以 bytes32 表示)
bytes32 recipient,
// 消息体的原始字节内容
bytes body,
// 默认后调度钩子使用的元数据
bytes defaultHookMetadata
) returns (
// 插入到邮箱的默克尔树中的消息 ID
bytes32 messageId
);

将消息调度到目标域和接收者,并提供自定义钩子使用的元数据,以替代默认钩子。

function dispatch(
// 目标链的域
uint32 destination,
// 目标链上接收者的地址(以 bytes32 表示)
bytes32 recipient,
// 消息体的原始字节内容
bytes body,
// 自定义后调度钩子使用的元数据
bytes customHookMetadata,
// 自定义钩子以替代默认钩子
IPostDispatchHook customHook
) returns (
// 插入到邮箱的默克尔树中的消息 ID
bytes32 messageId
);

处理

尝试将 message 交付给其接收者。通过接收者的 ISM 验证 message,使用提供的 metadata

function process(
// ISM 用于验证消息的元数据。
bytes metadata,
// 字节打包的消息
bytes message
);

latestDispatchedId

返回用于后调度钩子身份验证的最新调度消息 ID。

function latestDispatchedId() public view returns (bytes32);

消息接收者

想要接收消息的合约必须公开以下处理程序。

function handle(
// 原链的域
uint32 origin,
// 原链上发送者的地址
bytes32 sender,
// 消息体的原始字节内容
bytes body
);

他们可以选择性地指定一个安全模块,以在处理之前验证消息。

function interchainSecurityModule() returns (address);
信息

在实现这三个合约后,您可以通过调用 Mailboxdispatch 函数发送消息到接收者,并断言接收者已收到消息,从而达到第一个里程碑。请参见 Foundry 测试用例

跨链安全模块

跨链安全模块用于在处理之前验证消息。

moduleType

返回一个枚举,表示此 ISM 编码的安全模型类型。

enum ModuleType {
UNUSED,
ROUTING,
AGGREGATION,
LEGACY_MULTISIG,
MERKLE_ROOT_MULTISIG,
MESSAGE_ID_MULTISIG,
NULL, // 用于不携带元数据的中继器
CCIP_READ
}

function moduleType() returns (ModuleType);

中继器推断如何从此类型获取和格式化元数据。

verify

定义一个安全模型,负责根据提供的元数据验证跨链消息。

function verify(
// 中继器提供的离线元数据,特定于模块编码的安全模型
// (例如,验证者签名)
bytes metadata,
// Hyperlane 编码的跨链消息
bytes message
) returns (
// 如果消息已被验证,则为真
bool success
);
信息

静态模块管理 - 我们的 Solidity 实现将 MultisigISMs 定义为静态并作为字节码的一部分,这在其他链中可能无法实现。您可能需要为 ISM 实现动态模块。

验证者公告

验证者公告其签名存储位置,以便中继器可以获取和验证其签名。

announce

公告验证者签名存储位置

function announce(
address validator, // 验证者的地址
string storageLocation, // 编码签名检查点位置的信息
bytes signature // 签名的验证者公告
) external returns (bool);

getAnnouncedStorageLocations

返回所有公告存储位置的列表

function getAnnouncedStorageLocations(
address[] _validators // 要获取存储位置的验证者列表
) external view returns (
string[][] // 注册存储元数据的列表
);

多签 ISM

实现一个安全模块,检查提供的元数据是否满足一组配置验证者的签名法定人数。

元数据

用于与 MESSAGE_ID_MULTISIG 模块类型实现一起使用的元数据。

元数据必须格式化如下:

struct MultisigMetadata {
// 原邮箱的地址
bytes32 originMailbox;
// 签名的检查点根
bytes32 signedCheckpointRoot;
// 验证者的连接签名
bytes signatures;
}

validatorsAndThreshold

返回负责验证消息的验证者集合和所需的签名数量。

可以根据 _message 的内容进行更改

function validatorsAndThreshold(
// Hyperlane 格式的跨链消息
bytes message
) returns (
// 验证者地址的数组
address[] validators,
// 所需的验证者签名数量
uint8 threshold
);
信息

在实现 MultisigISM 后,您可以达到第二个里程碑,以测试您的邮箱仅在接收者的 ISM 返回 true 后才处理。您可以使用一个 TestISM 来测试,您可以静态设置以接受或拒绝任何消息。请参见 Foundry 测试用例

跨链 Gas 支付者

Gas 支付者用于支付目标链上消息处理所需的 Gas 。这在中继器愿意补贴消息处理时并不是严格要求的。

注意

实施对足够 Gas 支付的强健检查,考虑链特定的代币处理。我们的 Solidity 实现收取本地消息值,但对于其他链,您可能需要收取特定代币,并相应地调整 Gas 开销和 tokenExchangeRate

payForGas

将 msg.value 存入作为支付消息转发到其目标链的费用。

注意

尽管您可以指定 refundAddress,但如果您将钩子组合在一起,过度支付 可能不会退还 给消息发送者。

function payForGas(
// 要支付的消息 ID。
bytes32 messageId,
// 消息目标链的域。
uint32 destination,
// 要支付的目标 Gas 数量。
uint256 gasAmount,
// 本地地址以退还任何过度支付。
address refundAddress
) payable;

GasPayment

当为消息的 Gas 费用支付时发出。

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

DestinationGasConfigSet

当设置远程域的 Gas 预言机时发出。

event DestinationGasConfigSet(
uint32 remoteDomain, // 远程域
address gasOracle, // Gas 预言机
uint96 gasOverhead // 目标 Gas 开销
);

2. 代理

以下描述了新链实现的代理规范。Rust 实现希望支持所有链,但该规范旨在与链无关。

消息索引

所有代理必须从原邮箱索引消息。在 Solidity 邮箱中,我们 为每条消息发出事件。其他链可能有不同的方式来显示此信息,但代理必须能够可靠地获取消息内容,并保持一致的顺序 -- 请参见 消息索引器 特性。

验证者

除了索引从邮箱调度的消息外,验证者还会为他们观察到的消息生成证明,以便在目标链上用于安全性。

检查点

验证者从邮箱生成称为检查点的证明,通过默克尔根提交所有调度的消息 ID。

pub struct Checkpoint {
/// 邮箱地址
pub mailbox_address: H256,
/// 邮箱链
pub mailbox_domain: u32,
/// 检查点根
pub root: H256,
/// 检查点的索引
pub index: u32,
}

验证者使用 邮箱特性 上的最新检查点方法从邮箱获取最新检查点,并使用 检查点同步器特性 提交签名到某个高可用存储。

带消息 ID 的检查点

验证者使用索引消息将检查点与从邮箱发出的相应消息 ID 连接起来。

pub struct CheckpointWithMessageId {
/// 现有的 Hyperlane 检查点结构
#[deref]
pub checkpoint: Checkpoint,
/// 从邮箱检查点.index 发出的消息的哈希
pub message_id: H256,
}

他们还会在其同步器上发布这些增强的检查点。

提示

您可以通过将验证者配置为具有上述合约的链来测试它,并观察它创建有效的签名。

中继器

除了索引从邮箱调度的消息外,中继器还会在目标链上处理消息。这需要构建满足消息接收者的 ISM 验证要求的元数据,并签署在目标邮箱上处理消息的交易。

元数据构建器

每个模块类型暗示了不同的元数据格式,以使消息验证成功。中继器将需要实现每个模块特性(例如 多签)。

消息处理器

中继器将尝试在目标邮箱上处理消息(请参见 消息处理器)。如果

  • 消息接收者 ISM 返回未知模块类型
  • 模块类型已知但元数据验证失败
  • 元数据验证通过但干运行( Gas 估算)消息处理失败

则消息将被送入指数退避重试队列。中继器依赖于 邮箱ism 特性的实现来进行这些检查。

Gas 支付强制执行

中继器还可能要求在处理目标链上的消息之前,先在原链上为特定消息 ID 支付 Gas 。为此,他们必须部署一个 IGP,并将其地址设置为受益人,并索引 Gas 支付事件。请参见 Gas 支付强制执行特性。我们建议从不强制 Gas 支付政策开始,然后逐渐支持更严格的政策。

测试

一旦您实现了中继器的 MVP,您应该创建一个端到端测试:

  1. 启动本地原链和目标链。
  2. 将您的合约部署到两个链上。
  3. 为原链运行验证者。
  4. 在两个链之间运行中继器。
  5. 观察在原链调度消息时,验证者观察到消息,创建签名,并且中继器通过指定在目标链上的验证者的 ISM 适当地处理您的消息。

请参见 Rust 代码库中的端到端测试 以获取灵感。

提示

在使用本地端到端测试验证代理后,建议您还在真实测试网中运行端到端测试。

3. 应用程序

Warp 路由

代币路由应用程序,根据需要在域之间路由代币。

transferRemote

amountOrId 代币转移到目标域的 recipient

function transferRemote(
// 目标链的域。
uint32 destination,
// 目标链上接收者的地址。
bytes32 recipient,
// 要发送到远程接收者的代币数量或标识符。
uint256 amountOrId
) returns (
// 调度消息的标识符。
bytes32 messageId
);

转移消息

为了与其他链上的 Warp 路由互操作,转移消息的 body 必须是一个字节打包的 TransferMessage 结构。

struct TransferMessage {
// 远程转移的接收者
bytes32 recipient;
// 要转移的代币数量或代币标识符
uint256 amountOrId;
// 可选元数据,例如 NFT URI 信息
bytes metadata;
}