使用 Relayer.sol 进行端到端的多链测试

原文|End-to-EndMultichainTestingwithRelayer.solRelayer.sol为你的Forge测试环境带来了原生的多链端到端测试能力。通过继承抽象的Relayer测试辅助类,你的测试可以:启动多个网络的分叉(fork)、真实地发送L2ToL2

Screenshot-2025-06-18-at-3.09.38---PM (1).png

原文|End-to-End Multichain Testing with Relayer.sol

Relayer.sol 为你的 Forge 测试环境带来了原生的多链端到端测试能力。通过继承抽象的 Relayer 测试辅助类,你的测试可以:启动多个网络的分叉(fork)、真实地发送 L2ToL2CrossDomainMessenger 事件、并在同一个 forge test 流程中完成消息的中继传输——无需额外的 relayer 或脚本粘合。在本文中你将学到如何:

  • 使用 Supersim 节点运行的 RPC 接口配置多链分叉
  • 在多个链上部署合约并进行交互
  • 用一次调用中继所有消息(或只中继部分)
  • 和普通单元测试一样,断言目标链上的状态变化

Superchain Interop(超级链互操作)有望彻底改变去中心化应用开发的范式。它将实现低延迟、无缝的跨链消息传递与资产桥接,而为了构建适应这一未来的应用,你需要升级自己的跨链测试工作流。

为什么你的测试中需要一个 relayer?

超级链的跨链交互流程是事件驱动的:

某个合约调用 L2ToL2CrossDomainMessenger.sendMessage(),消息器发出 SentMessage 事件,然后一个链下 relayer 监听该事件并向目标链发送执行交易——只有这样,消息载荷才会真正执行。

Relayer.sol 将这一 relayer 逻辑直接内嵌到 Forge 中,无需不稳定的 sleep 等待,也不需要手动 cast 命令,非常适合本地 CI 流程中的自动化测试。

最终,Relayer.sol 将跨链测试从“分别跑两个测试套件,然后祈祷桥接成功”转变为“在 Forge 内部验证:这个消息确实在链 B 上成功执行了”。它去除了外部依赖,减少了样板代码,并使你的测试环境更加贴近主网现实。

image (4).png

Relayer.sol 极大简化了跨链测试流程

项目设置

1. 添加互操作库

forge install ethereum-optimism/interop-lib

Relayer.sol 位于 repo https://github.com/ethereum-optimism/interop-lib 的 src/test/Relayer.sol 下。

2. 启动 Supersim 并将 Foundry 指向它

安装 Supersim

brew install ethereum-optimism/tap/supersim      # macOS/Linux

启动一个原版超级链

supersim 

Supersim 启动三个本地 anvil 节点,预先部署 Superchain 互作合约,并公开 JSON-RPC 端点:

Chain  链 ID RPC URL  RPC 网址
L1(主网) 900 http://127.0.0.1:8545
L2-A 901 http://127.0.0.1:9545
L2-B 902 http://127.0.0.1:9546

有关 Supersim 以及自定义本地开发工作流程的各种方式的更多信息,请查看 Supersim 文档 。

在 foundry.toml 中告诉 Foundry 这些 RPC

[rpc_endpoints]
l2a = "<http://127.0.0.1:9545>"
l2b = "<http://127.0.0.1:9546>"

需要测试网? 如果您想尝试互作的测试网,请跳过 Supersim,或者通过更改 foundry.toml 以指向这些测试网端点,将您的项目从本地开发升级到测试网(请参阅文档以获取最新的端点):

[rpc_endpoints]
devnet0 = "<https://interop-alpha-0.optimism.io>"
devnet1 = "<https://interop-alpha-1.optimism.io>"

编写跨链的测试

以下是参考 CrossChainIncrementer.t.sol 测试的精简版本:

contract IncrementerTest is Relayer {
    /**
     * 0. Constructor – pass Supersim RPC URLs to Relayer so it can map
     *    chainIds ↔ forkIds under the hood.
     */
    constructor() Relayer(_rpcUrls()) {}

    function _rpcUrls() internal view returns (string[] memory urls) {
        urls = new string[](2);
        urls[0] = vm.rpcUrl("l2a"); // source
        urls[1] = vm.rpcUrl("l2b"); // destination
    }

    // 1. Fork identifiers
    uint256 l2aFork;
    uint256 l2bFork;

    // ──────────────── 2. Contract handles ────────────────
    Counter src;              // lives on l2a fork
    Counter dst;              // lives on l2b fork

    function setUp() public {
        // Foundry forks
        l2aFork = vm.createFork(vm.rpcUrl("l2a"));
        l2bFork = vm.createFork(vm.rpcUrl("l2b"));

        // 2. Deploy contracts on each chain
        vm.selectFork(l2aFork);
        src = new Counter();

        vm.selectFork(l2bFork);
        dst = new Counter();
    }

    function testIncrementAcrossChains() public {
        // 3. Build a message on the source chain
        vm.selectFork(l2aFork);
        L2ToL2CrossDomainMessenger(payable(CDM_ADDR)).sendMessage(
            address(dst),
            abi.encodeCall(dst.increment, ()),
            100_000
        );

        // 4. Relay everything that was just logged
        relayAllMessages();

        // 5. Assert on the destination chain
        vm.selectFork(l2bFork);
        assertEq(dst.count(), 1);
    }
}

让我们来了解一下这里发生了什么。

第 1 步:分叉网络

vm.createFork 会克隆远程链的状态;vm.selectFork 用于切换当前激活的链分叉。请保留返回的 ID —— 在你跨链切换时将会用到它们。

第 2 步:自动记录日志

Relayer 构造函数调用 vm.recordLogs() 表示每个触发的事件都会被捕获。你无需手动添加这一逻辑,只需继承这个辅助类即可!

第 3 步:发出互操作消息

L2ToL2CrossDomainMessenger.sendMessage 为您提供开箱即用的重放保护和域绑定。完全按照您在链上的方式使用它。

第 4 步:在测试中中继

  • relayAllMessages() 通过 vm.getRecordedLogs() 获取日志缓冲区,筛选出 SentMessage 事件,并在对应的目标分叉上重新执行每条消息。
  • 需要更多控制?你可以将 vm.log[] 的一个切片传给 relayMessages(),手动决定哪些事件需要中继。

第 5 步:断言状态

一旦消息中继完成,切换回目标分叉(再次调用 vm.selectFork("l2a")),然后像普通单元测试一样进行断言。由于所有操作都是在同一进程中同步执行的,不会出现竞态条件或轮询循环的问题。、

要查看此测试模式的实际效果,请查看此处 !

高级用法

细粒度中继

有时你只想中继部分事件。由于 SentMessage 日志中不包含来源链的信息,你需要手动传入生成这些日志的 sourceChainId

Vm.Log[] memory logs = vm.getRecordedLogs();
relayMessages(slice(logs, 1, 3), 901); // 仅中继第二条和第三条消息

这个功能在你需要缓存日志并进行中继以外的操作时特别有用。因为 vm.getRecordedLogs() 每次调用都会清空缓冲区,你可以先获取一次,存储下来,然后对原始事件进行自定义断言、解码、模糊测试等操作,最后将相同的切片(或过滤后的子集)传入 relayMessages()。这允许你仅中继感兴趣的消息,重复中继以测试重放保护机制,或保留日志用于覆盖率统计 —— 无需额外的链上事件,也不会丢失数据。

Promise 测试(实验性)

Interop 库还提供了一个 Promise 原语,用于保障消息交付的语义;早期测试套件可见于 Promise.t.sol。未来该辅助模块将添加更多原生的 Promise 工具!

常见陷阱

  • 缺少预部署合约:如果你在本地节点运行,且没有部署 messenger 合约,则会回退。建议使用 Supersim 或 devnet 测试网络。
  • 日志缓冲区被消费vm.getRecordedLogs() 每次调用都会清空缓冲区。如需多次使用,请务必缓存结果。

整合应用

只需不到 40 行模板代码,你就能实现真实、确定性的多链测试,几秒内运行完成:

supersim &         # 一次性启动 Supersim
forge test -vvvv   # 所有消息都在本地中继并测试

在幕后,Forge 会:

  1. fork 两条 L2 链(使用 Supersim)
  2. 执行源链上的交易
  3. 在目标链 fork 上重放日志
  4. 断言目标链的状态变化(后置条件)

这一切都发生在 EVM 内部,无需依赖外部基础设施 —— 这就是 Relayer.sol 的强大之处。用它试一试,打破几条跨链消息,在上线前让你的逻辑变得坚不可摧。祝你测试愉快!

延伸阅读

在加密行业,尤其是 OP Labs,我们的工作独一无二。尽管面临的挑战巨大,复杂性惊人,但有时,简单的流程就能带来质的飞跃。

OP Labs 正在招聘愿意在技术与安全的最前沿工作的优秀人才。如果你想加入一个世界级的团队,参与世界级的项目,欢迎联系我们


Join us

微信公众号: Optimism 中文

Twitter: https://x.com/Optimismzh

Telegramhttps://t.me/optimism_cn

Medium: https://medium.com/@optimismcn

微信群:公众号后台回复 【加群】


点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
该文章收录于 Optimism 中文力量
10 订阅 66 篇文章

0 条评论

请先 登录 后评论
OP 中文力量
OP 中文力量
Optimism 中文社区 公众号:Optimism 中文 TG:https://t.me/optimism_cn Twitter:https://x.com/Optimismzh