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

  • optimism
  • 发布于 2025-06-02 18:47
  • 阅读 5

本文介绍了Relayer.sol,一个用于在Forge测试环境中进行多链端到端测试的工具。通过使用Relayer.sol,开发者可以在Forge测试中模拟跨链消息传递,无需外部relayer或脚本,简化了跨链应用的测试流程。文章还介绍了如何配置Supersim,编写跨链测试,以及一些高级用法和常见问题。

Faina Shalts
2025年6月

Relayer.sol 为你的 Forge 测试设置带来一流的多链端到端测试。通过继承抽象的 Relayer 测试助手,你的测试可以启动多个网络的 fork,发出真实的 L2ToL2CrossDomainMessenger 事件,并在同一个 forge test 进程中传递它们 —— 无需外部 relayer 或脚本粘合代码。在这篇文章中,你将学习如何:

  • 从运行的 Supersim 端点连接 forked RPC URLs

  • 在多个链上部署和执行合约

  • 通过一次调用传递所有消息(或仅传递一个子集)

  • 像其他单元测试一样,断言目标链上的状态变化

Superchain Interop 承诺改变去中心化应用程序开发的范例。这项令人兴奋的改进将允许在 Superchain interop 集中的链之间进行低延迟、无缝的消息传递和资产桥接。当你寻求为这个未来构建时,你需要提升你的跨链测试工作流程。

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

Superchain 的 interop 工作流程是事件驱动的:合约在 L2ToL2CrossDomainMessenger 上调用 sendMessage(),messenger 发出 SentMessage,一个链下 relayer 拾取该事件并向目标链提交一个执行交易 - 只有这样,payload 才会运行。

Relayer.sol 将该 relayer 逻辑直接嵌入到 Forge 中,消除了你在本地 CI 运行中尝试的不可靠的休眠或手动 cast 命令。

Relayer.sol 将跨链测试从“运行两个不相交的测试套件并希望桥能工作”转变为“在 Forge 内部证明消息确实在链 B 上执行”。它消除了外部依赖,减少了样板代码,并使你的测试环境更接近主网的现实。

Relayer.sol 显著简化了跨链测试流程

项目设置

1. 添加 interop lib

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

启动一个原生的 Superchain

supersim

Supersim 启动三个本地 anvil 节点,预先部署 Superchain interop 合约,并暴露 JSON-RPC 端点:

ID RPC URL
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>"

需要 devnet 吗? 如果你想试用 interop devnet,或者通过更改你的 foundry.toml 以指向这些 devnet 端点,将你的项目从本地开发升级到 devnet,请跳过 Supersim(有关最新的端点,请参阅文档):

[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 – 将 Supersim RPC URLs 传递给 Relayer,以便它可以映射
     * chainIds ↔ forkIds 在幕后。
     */
    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 标识符
    uint256 l2aFork;
    uint256 l2bFork;

    // ──────────────── 2. 合约Handle ────────────────

    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. 在每个链上部署合约
        vm.selectFork(l2aFork);
        src = new Counter();
        vm.selectFork(l2bFork);
        dst = new Counter();
    }

    function testIncrementAcrossChains() public {
        // 3. 在源链上构建消息
        vm.selectFork(l2aFork);
        L2ToL2CrossDomainMessenger(payable(CDM_ADDR)).sendMessage(
            address(dst),
            abi.encodeCall(dst.increment, ()),
            100_000
        );

        // 4. 传递刚刚记录的所有内容
        relayAllMessages();

        // 5. 断言目标链
        vm.selectFork(l2bFork);
        assertEq(dst.count(), 1);
    }
}

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

步骤 1:Fork 网络

vm.createFork 克隆远程状态;vm.selectFork 切换活动 fork。保留返回的 IDs。每当你需要在链之间跳转时,都需要它们。

步骤 2:自动记录日志

Relayer 构造函数调用 vm.recordLogs() 意味着捕获每个发出的事件。你不需要自己添加这个;只需继承这个助手!

步骤 3:发出一个 interop 消息

L2ToL2CrossDomainMessenger.sendMessage 为你提供开箱即用的重放保护和域绑定。像在链上一样使用它。

步骤 4:在测试中传递

  • relayAllMessages() 通过 vm.getRecordedLogs() 拉取缓冲区,筛选 SentMessage,并在正确的目的地 fork 上重新执行每个消息。

  • 需要更多控制?vm.log[] 的切片传递给 relayMessages(),并决定哪些事件被传递。

步骤 5:断言状态

一旦传递完成,切换到目的地 fork(再次 vm.selectFork("l2a"))并像任何其他单元测试一样进行断言。因为一切都在进程中同步运行,所以没有竞争条件或轮询循环。

💡要查看此测试模式的实际应用,请查看这里

高级模式

精细化传递

有时你只想传递事件的子集。因为 SentMessage 日志不嵌入源链,所以你 必须 传递产生这些日志的 sourceChainId

Vm.Log[] memory logs = vm.getRecordedLogs();
relayMessages(slice(logs, 1, 3), 901); // only the second and third messages

当你需要缓存记录的日志以用于传递以外的其他用途时,此功能特别有用。因为 vm.getRecordedLogs() 消耗缓冲区,你可以获取一次,存储它,在原始事件上运行自定义断言/解码/模糊测试,然后将相同的切片(或过滤的子切片)馈送到 relayMessages() 中。这使你可以传递你关心的项目,重新传递同一消息以测试重放保护路径,或保留日志以用于覆盖率指标 - 所有这些都无需丢失数据或需要额外的链上发送。

Promise 测试(实验性)

Interop 库还公开了一个 Promise 原语来保证传递语义;早期的测试套件位于 Promise.t.sol 中。期待该助手很快获得一流的 Promise 实用程序!

常见陷阱

  • 缺少预部署: 在没有 messenger 的本地节点上运行将会回滚。坚持使用 Supersim 和 devnets!

  • 日志缓冲区消耗: 每次调用 vm.getRecordedLogs() 都会 消耗 缓冲区。如果你需要多次传递,请缓存它。

整合

只需不到 40 行样板代码,你现在就可以进行实际的、确定性的多链测试,这些测试可以在几秒钟内运行:

supersim &. # 一次性启动
forge test -vvvv. # 一切都在本地传递

在幕后,Forge:

  1. 在 supersim 上 fork 两个 L2

  2. 执行你的源链交易

  3. 在目标 fork 上重放日志

  4. 断言后置条件

……所有这些都不需要离开 EVM 或依赖外部基础设施。这就是 Relayer.sol 的力量。试一试,破坏一些消息,并在你的跨链逻辑进入生产环境之前使其加强。祝你测试愉快!

进一步阅读

在加密货币领域以及在 OP Labs,我们拥有一项与众不同的工作。风险巨大,我们试图做的事情的复杂性也是如此。但是,有时,简单的流程可以发挥重要作用,并使我们能够进一步发展。

在 OP Labs,我们正在招聘 准备在技术和安全的最前沿工作的人员。如果你想在一个世界级的团队中从事世界级的项目,请联系我们

  • 原文链接: optimism.io/blog/end-to-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
optimism
optimism
江湖只有他的大名,没有他的介绍。