Hyperlane 深度解析(3):构建跨链通信实践—乒乓球示例
本文将通过一个实际示例,演示如何通过Hyperlane 协议发送跨链消息 。
你将学习如何:
- 部署与 Hyperlane Mailbox 兼容的合约
- 部署可信中继器 ISM (跨链安全模块)
- 运行一个简单的中继器
- 发送跨链消息
示例概览
在此示例中,您将在两个不同的链上部署类似的合约。链 A 上的合约将使用 Hyperlane 协议 向链 B 发送一条消息,链 B 上的合约将处理此消息并回复链 A。
合约主要功能:
sendPing
:通过调用Hyperlane的方法 来发起跨链消息IMailbox.dispatch
。enrollRemoteRouter
:在继承的Router
合约中,注册来自另一条链的合约。setInterchainSecurityModule
:设置合同的 ISM。_handle
:处理来自邮箱的传入消息(继承的Router
合约handle
函数调用的内部函数)。
开发环境准备
-
创建并导航到新目录:
mkdir hyperlane-pingpong && cd hyperlane-pingpong
-
初始化 Hardhat 项目并安装依赖项:
npx hardhat init
-
添加
@hyperlane-xyz/core
为依赖项:npm install -D @hyperlane-xyz/core
测试代币
确保你有足够的测试令牌Arbitrum Sepolia
,并且Sapphire Testnet
获取更多信息:
Sapphire Testnet
来自Oasis Faucet 的测试代币。Arbitrum Sepolia
来自 Alchemy 的Faucet的ETH 代币。
将网络添加到 Hardhat
打开hardhat.config.ts
并添加 Arbitrum Sepolia 和 Sapphire 测试网。
hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const accounts = process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [];
const config: HardhatUserConfig = {
solidity: "0.8.24",
networks: {
'arbitrum-sepolia': {
url: 'https://arbitrum-sepolia-rpc.publicnode.com',
chainId: 421614,
accounts,
},
'sapphire-testnet': {
url: "https://testnet.sapphire.oasis.io",
accounts,
chainId: 23295, // 0x5aff
},
},
};
export default config;
乒乓球合约
在此示例中,我们利用了HyperlaneRouter
中的包装器。这具有以下优势:
- 合约与Hyperlane的MailboxClient和IMessageRecipient接口兼容 。
- 支持注册其他链的路由器。
- 支持设置自定义 ISM。
-
Ping.sol
创建一个名为Arbitrum Sepolia 的新文件 -
将以下合同粘贴到其中:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
// ============ External Imports ============
import {Router} from "@hyperlane-xyz/core/contracts/client/Router.sol";
/*
* @title Ping
* @dev You can use this simple app as a starting point for your own application.
*/
contract Ping is Router {
// A generous upper bound on the amount of gas to use in the handle
// function when a message is processed. Used for paying for gas.
uint256 public constant HANDLE_GAS_AMOUNT = 50_000;
// A counter of how many messages have been sent from this contract.
uint256 public sent;
// A counter of how many messages have been received by this contract.
uint256 public received;
// Keyed by domain, a counter of how many messages that have been sent
// from this contract to the domain.
mapping(uint32 => uint256) public sentTo;
// Keyed by domain, a counter of how many messages that have been received
// by this contract from the domain.
mapping(uint32 => uint256) public receivedFrom;
// ============ Events ============
event SentPing(
uint32 indexed origin,
uint32 indexed destination,
string message
);
event ReceivedPing(
uint32 indexed origin,
uint32 indexed destination,
bytes32 sender,
string message
);
event HandleGasAmountSet(
uint32 indexed destination,
uint256 handleGasAmount
);
constructor(address _mailbox) Router(_mailbox) {
// Transfer ownership of the contract to deployer
_transferOwnership(msg.sender);
setHook(address(0));
}
// ============ External functions ============
/**
* @notice Sends a message to the _destinationDomain. Any msg.value is
* used as interchain gas payment.
* @param _destinationDomain The destination domain to send the message to.
* @param _message The message to send.
*/
function sendPing(
uint32 _destinationDomain,
string calldata _message
) public payable {
sent += 1;
sentTo[_destinationDomain] += 1;
_dispatch(_destinationDomain, bytes(_message));
emit SentPing(
mailbox.localDomain(),
_destinationDomain,
_message
);
}
/**
* @notice Fetches the amount of gas that will be used when a message is
* dispatched to the given domain.
*/
function quoteDispatch(
uint32 _destinationDomain,
bytes calldata _message
) external view returns (uint256) {
return _quoteDispatch(_destinationDomain, _message);
}
// ============ Internal functions ============
/**
* @notice Handles a message from a remote router.
* @dev Only called for messages sent from a remote router, as enforced by Router.sol.
* @param _origin The domain of the origin of the message.
* @param _sender The sender of the message.
* @param _message The message body.
*/
function _handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _message
) internal override {
received += 1;
receivedFrom[_origin] += 1;
emit ReceivedPing(
_origin,
mailbox.localDomain(),
_sender,
string(_message)
);
}
} -
Pong.sol
创建一个名为Sapphire Testnet 的新文件 -
将以下合同粘贴到其中:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
// ============ External Imports ============
import {Router} from "@hyperlane-xyz/core/contracts/client/Router.sol";
/*
* @title Pong
* @dev You can use this simple app as a starting point for your own application.
*/
contract Pong is Router {
// A generous upper bound on the amount of gas to use in the handle
// function when a message is processed. Used for paying for gas.
uint256 public constant HANDLE_GAS_AMOUNT = 50_000;
// A counter of how many messages have been sent from this contract.
uint256 public sent;
// A counter of how many messages have been received by this contract.
uint256 public received;
// Keyed by domain, a counter of how many messages that have been sent
// from this contract to the domain.
mapping(uint32 => uint256) public sentTo;
// Keyed by domain, a counter of how many messages that have been received
// by this contract from the domain.
mapping(uint32 => uint256) public receivedFrom;
// ============ Events ============
event SentPing(
uint32 indexed origin,
uint32 indexed destination,
string message
);
event ReceivedPing(
uint32 indexed origin,
uint32 indexed destination,
bytes32 sender,
string message
);
event HandleGasAmountSet(
uint32 indexed destination,
uint256 handleGasAmount
);
constructor(address _mailbox) Router(_mailbox) {
// Transfer ownership of the contract to deployer
_transferOwnership(msg.sender);
setHook(address(0));
}
// ============ External functions ============
/**
* @notice Sends a message to the _destinationDomain. Any msg.value is
* used as interchain gas payment.
* @param _destinationDomain The destination domain to send the message to.
* @param _message The message to send.
*/
function sendPing(
uint32 _destinationDomain,
string calldata _message
) public payable {
sent += 1;
sentTo[_destinationDomain] += 1;
_dispatch(_destinationDomain, bytes(_message));
emit SentPing(
mailbox.localDomain(),
_destinationDomain,
_message
);
}
/**
* @notice Fetches the amount of gas that will be used when a message is
* dispatched to the given domain.
*/
function quoteDispatch(
uint32 _destinationDomain,
bytes calldata _message
) external view returns (uint256) {
return _quoteDispatch(_destinationDomain, _message);
}
// ============ Internal functions ============
/**
* @notice Handles a message from a remote router.
* @dev Only called for messages sent from a remote router, as enforced by Router.sol.
* @param _origin The domain of the origin of the message.
* @param _sender The sender of the message.
* @param _message The message body.
*/
function _handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _message
) internal override {
received += 1;
receivedFrom[_origin] += 1;
emit ReceivedPing(
_origin,
mailbox.localDomain(),
_sender,
string(_message)
);
// send return message
sendPing(
_origin,
string(_message)
);
}
}
ISM合同
在当前状态下,如果您从 Sapphire Testnet
向Arbitrum Sepolia
发送消息,邮箱的默认 ISM将不会接受该消息。您可以在Arbitrum Sepolia
上部署和注册自定义 ISM 以使其正常工作。
Hyperlane的一个简单默认 ISM是TrustedRelayerISM,它在传递消息之前检查中继器地址。
-
创建一个名为
TrustedRelayerIsm.sol
-
将以下合同粘贴到其中:
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "@hyperlane-xyz/core/contracts/interfaces/IInterchainSecurityModule.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Message} from "@hyperlane-xyz/core/contracts/libs/Message.sol";
import {Mailbox} from "@hyperlane-xyz/core/contracts/Mailbox.sol";
import {PackageVersioned} from "@hyperlane-xyz/core/contracts/PackageVersioned.sol";
contract TrustedRelayerIsm is IInterchainSecurityModule, PackageVersioned {
using Message for bytes;
uint8 public immutable moduleType = uint8(Types.NULL);
Mailbox public immutable mailbox;
address public immutable trustedRelayer;
constructor(address _mailbox, address _trustedRelayer) {
require(
_trustedRelayer != address(0),
"TrustedRelayerIsm: invalid relayer"
);
require(
Address.isContract(_mailbox),
"TrustedRelayerIsm: invalid mailbox"
);
mailbox = Mailbox(_mailbox);
trustedRelayer = _trustedRelayer;
}
function verify(
bytes calldata,
bytes calldata message
) external view returns (bool) {
return mailbox.processor(message.id()) == trustedRelayer;
}
}
部署合约
分别在 Sapphire Testnet
和 Arbitrum Sepolia
两个不同的链上部署 Ping 和 Pong 合约。
将 Pong 部署到 Sapphire Testnet
-
deploypong.ts
在以下位置创建部署脚本scripts/
:import { ethers } from "hardhat";
async function main() {
// deployed mailbox on Sapphire Testnet
const mailbox = "0x79d3ECb26619B968A68CE9337DfE016aeA471435";
const PongFactory = await hre.ethers.getContractFactory("Pong");
const pong = await PongFactory.deploy(mailbox);
const pongAddr = await pong.waitForDeployment();
console.log(`Pong deployed at: ${pongAddr.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
}); -
运行部署:
pnpm hardhat run scripts/deploypong.ts --network sapphire-testnet
将 Ping 部署到 Arbitrum Sepolia
-
deployping.ts
在以下位置创建部署脚本scripts/
:import { ethers } from "hardhat";
async function main() {
// default mailbox on Arbitrum Sepolia
const mailbox = "0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8";
const PingFactory = await ethers.getContractFactory("Ping");
const ping = await PingFactory.deploy(mailbox);
const pingAddr = await ping.waitForDeployment();
console.log(`Ping deployed at: ${pingAddr.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
}); -
运行部署:
pnpm hardhat run scripts/deployping.ts --network arbitrum-sepolia
部署 ISM 到 Arbitrum Sepolia
-
deployISM.ts
在以下位置创建部署脚本scripts/
:import { ethers } from "hardhat";
async function main() {
// default mailbox on Arbitrum Sepolia
const mailbox = "0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8";
const trustedRelayer = "0x<your relayer address>";
const trustedRelayerISM = await ethers.deployContract(
"TrustedRelayerIsm",
[mailbox, trustedRelayer]
);
await trustedRelayerISM.waitForDeployment();
console.log(`TrustedRelayerISM deployed to ${trustedRelayerISM.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
}); -
运行部署:
pnpm hardhat run scripts/deployISM.ts --network arbitrum-sepolia
合约设置
注册路由器 Router
由于我们将路由器包装器用于我们的 Ping Pong 合约,因此我们需要登记对方合约的合约地址。