跨链消息传递 - OpenZeppelin 文档

本文档介绍了跨链消息传递的标准 ERC-7786,它通过定义源和目标网关来实现跨链通信,解决了跨链互操作性的难题。开发者可以使用该标准构建跨链应用,并通过 Axelar 网络发送和接收跨链消息。此外,Open Bridge 通过多重签名验证提高跨链消息传递的可靠性。

跨链消息传递

构建合约的开发者可能需要跨链功能。为了实现这一点,多个协议已经实现了它们自己跨链处理操作的方式。

@norswap跨链互操作性报告 中概述了这些桥的多样性,该报告提出了 7 个桥类别的一种分类法。这种多样性使得开发者难以设计跨链应用程序,因为缺乏可移植性。

本指南将教你如何遵循 ERC-7786 来建立跨链的消息传递网关,而不用考虑底层桥是什么。开发者可以实现处理跨链消息的网关合约,并连接他们想要的任何跨链协议(或自己实现)。

ERC-7786 网关

为了以一种简单且非主观的方式解决可组合性的不足,ERC-7786 提出了一个用于实现将消息传递到其他链的网关的标准。这种通用的方法具有足够的表达力,可以启用新型的应用程序,并且可以使用标准化属性来适应任何桥分类法或特定的桥接口。

消息传递概述

该 ERC 定义了一个源网关和一个目标网关。两者都是实现一种协议以发送消息并分别处理其接收的合约。这两个过程由 ERC-7786 规范明确标识,因为它们定义了两个网关的最低要求。

  • 源链上,合约实现了一个标准的 sendMessage 函数,并发出一个 MessagePosted 事件,以表明该消息应由底层协议中继。

  • 目标链上,网关接收消息,并通过调用 executeMessage 函数将其传递给接收者合约。

智能合约开发者只需要担心实现 IERC7786GatewaySource 接口以在源链上发送消息,以及 IERC7786GatewaySourceIERC7786Receiver 接口以在目标链上接收此类消息。

Axelar 网络入门

要开始发送跨链消息,开发者可以使用由 Axelar 网络提供支持的双工网关入门。这将允许合约利用 Axelar 中继器在目标链上的自动执行来发送或接收跨链消息。

// contracts/MyCustomAxelarGatewayDuplex.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {AxelarGatewayDuplex, AxelarExecutable} from "@openzeppelin/community-contracts/crosschain/axelar/AxelarGatewayDuplex.sol";
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";

abstract contract MyCustomAxelarGatewayDuplex is AxelarGatewayDuplex {
    /// @dev 使用 Axelar 网关和初始所有者初始化合约。
    constructor(IAxelarGateway gateway, address initialOwner) AxelarGatewayDuplex(gateway, initialOwner) {}
}

有关双工网关如何工作的更多详细信息,请参见下面的 如何使用 Axelar 网络发送和接收消息

开发者可以使用 registerChainEquivalenceregisterRemoteGateway 函数注册受支持的链和目标网关

跨链通信

发送消息

源网关的接口足够通用,它允许包装一个自定义协议来验证消息。根据用例,开发者可以实现任何链下机制来读取标准 MessagePosted 事件,并将其传递到目标链上的接收者。

// contracts/MyERC7786GatewaySource.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC7786GatewaySource} from "@openzeppelin/community-contracts/interfaces/IERC7786.sol";
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol";

abstract contract MyERC7786GatewaySource is IERC7786GatewaySource {
    error UnsupportedNativeTransfer();

    /// @inheritdoc IERC7786GatewaySource
    function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) {
        return false;
    }

    /// @inheritdoc IERC7786GatewaySource
    function sendMessage(
        bytes calldata recipient, // 二进制可互操作地址
        bytes calldata payload,
        bytes[] calldata attributes
    ) external payable returns (bytes32 sendId) {
        require(msg.value == 0, UnsupportedNativeTransfer());
        // 使用 `if () revert` 语法来避免访问 attributes[0](如果它为空)
        if (attributes.length > 0)
            revert UnsupportedAttribute(attributes[0].length < 0x04 ? bytes4(0) : bytes4(attributes[0][0:4]));

        // 发出事件
        sendId = bytes32(0); // 显式设置为 0。可用于后处理
        emit MessageSent(
            sendId,
            InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
            recipient,
            payload,
            0,
            attributes
        );

        // (可选)如果这是一个适配器,则将消息发送到协议网关进行处理
        // 这可能需要用于跟踪目标网关地址和链标识符的逻辑

        return sendId;
    }
}
该标准使用 CAIP-2 标识符表示链,并使用 CAIP-10 标识符表示帐户,以增强与非 EVM 链的互操作性。考虑在合约库中使用 Strings 库来处理这些标识符。

接收消息

为了在目标链上成功处理消息,需要一个目标网关。尽管 ERC-7786 没有为目标网关定义标准接口,但它要求在接收消息时调用 executeMessage

每个跨链消息协议都已经提供了一种通过规范桥或中间合约接收消息的方式。开发者可以轻松地将接收合约包装到网关中,该网关按照 ERC 的要求调用 executeMessage 函数。

为了在自定义智能合约上接收消息,OpenZeppelin 社区合约提供了一个 ERC7786Receiver 实现,供开发者继承。这样,你的合约就可以接收通过已知目标网关中继的跨链消息。

// contracts/MyERC7786ReceiverContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";
import {ERC7786Receiver} from "@openzeppelin/community-contracts/crosschain/utils/ERC7786Receiver.sol";

contract MyERC7786ReceiverContract is ERC7786Receiver, AccessManaged {
    constructor(address initialAuthority) AccessManaged(initialAuthority) {}

    /// @dev 检查给定的实例是否为已知网关。
    function _isKnownGateway(address /* instance */) internal view virtual override returns (bool) {
        return true;
    }

    /// @dev 用于接收跨链消息的内部端点。
    function _processMessage(
        address gateway,
        bytes32 receiveId,
        bytes calldata sender,
        bytes calldata payload,
        bytes[] calldata attributes
    ) internal virtual override restricted {
        // 在这里处理消息
    }
}

标准的接收接口抽离了底层协议。这样,合约就可以通过兼容 ERC-7786 的网关(或通过适配器)发送消息,并在目标链上接收到消息,而无需担心协议实现细节。

Axelar 网络

除了 AxelarGatewayDuplex 之外,该库还提供了一个 IERC7786GatewaySource 接口的实现,称为 AxelarGatewaySource,它作为适配器用于发送符合 ERC-7786 的消息

该实现采用一个本地网关地址,该地址必须对应于 Axelar 的原生网关,并且具有以下机制:

  • 跟踪 Axelar 链名称和 CAIP-2 标识符之间的等效性

  • 使用其 CAIP-2 标识符为每个网络记录一个目标网关

AxelarGatewaySource 实现可以直接使用

// contracts/MyERC7786ReceiverContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {AxelarGatewaySource} from "@openzeppelin/community-contracts/crosschain/axelar/AxelarGatewaySource.sol";
import {AxelarGatewayBase} from "@openzeppelin/community-contracts/crosschain/axelar/AxelarGatewayBase.sol";
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";

abstract contract MyCustomAxelarGatewaySource is AxelarGatewaySource {
    /// @dev 使用 Axelar 网关和初始所有者初始化合约。
    constructor(IAxelarGateway gateway, address initialOwner) Ownable(initialOwner) AxelarGatewayBase(gateway) {}
}

对于目标网关,该库提供了一个 AxelarExecutable 接口的适配器,用于接收消息并将它们中继到 IERC7786Receiver

// contracts/MyCustomAxelarGatewayDestination.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {AxelarGatewayDestination, AxelarExecutable} from "@openzeppelin/community-contracts/crosschain/axelar/AxelarGatewayDestination.sol";
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";

abstract contract MyCustomAxelarGatewayDestination is AxelarGatewayDestination {
    /// @dev 使用 Axelar 网关和初始所有者初始化合约。
    constructor(IAxelarGateway gateway, address initialOwner) AxelarExecutable(address(gateway)) {}
}

开放桥

ERC7786OpenBridge 是一个特殊的网关,它实现了 IERC7786GatewaySourceIERC7786Receiver 接口。它提供了一种同时跨多个桥发送消息的方式,并确保通过基于阈值的确认系统进行消息传递。

该桥维护一个已知网关列表和一个确认阈值。发送消息时,它会广播到所有注册的网关,接收消息时,它需要最少数量的确认才能执行消息。这种方法通过确保跨多个桥正确传递和验证消息来提高可靠性。

发送消息时,桥会跟踪来自每个网关的消息 ID,以维护消息跨不同桥的传递记录:

function sendMessage(
    string calldata destinationChain,
    string memory receiver,
    bytes memory payload,
    bytes[] memory attributes
) public payable virtual whenNotPaused returns (bytes32 outboxId) {

    // ... 初始化变量并准备有效负载 ...

    // 在所有网关上发布
    Outbox[] memory outbox = new Outbox[](_gateways.length());
    bool needsId = false;
    for (uint256 i = 0; i < outbox.length(); ++i) {
        address gateway = _gateways.at(i);
        // 发送消息
        bytes32 id = IERC7786GatewaySource(gateway).sendMessage(
            destinationChain,
            bridge,
            wrappedPayload,
            attributes
        );
        // 如果有 ID,则跟踪它
        if (id != bytes32(0)) {
            outbox[i] = Outbox(gateway, id);
            needsId = true;
        }
    }

    // ... 处理消息跟踪和返回值 ...
}

在接收端,桥实现了一个基于阈值的确认系统。只有在收到来自网关的足够确认后才会执行消息,从而确保消息的有效性并防止重复执行。executeMessage 函数处理此过程:

function executeMessage(
    string calldata /*messageId*/, // 网关特定的,空或唯一
    string calldata sourceChain, // CAIP-2 链标识符
    string calldata sender, // CAIP-10 帐户地址(不包括链标识符)
    bytes calldata payload,
    bytes[] calldata attributes
) public payable virtual whenNotPaused returns (bytes4) {

    // ... 验证消息格式并提取消息 ID ...

    // 如果调用首先来自受信任的网关
    if (_gateways.contains(msg.sender) && !tracker.receivedBy[msg.sender]) {
        // 计算接收到的次数
        tracker.receivedBy[msg.sender] = true;
        ++tracker.countReceived;
        emit Received(id, msg.sender);

        // 如果已执行,则正常退出
        if (tracker.executed) return IERC7786Receiver.executeMessage.selector;
    } else if (tracker.executed) {
        revert ERC7786OpenBridgeAlreadyExecuted();
    }

    // ... 验证发送者并准备要执行的 payload ...

    // 如果准备好执行,但尚未执行
    if (tracker.countReceived >= getThreshold()) {
        // 防止重入
        tracker.executed = true;

        // ... 准备执行上下文并验证状态 ...
        bytes memory call = abi.encodeCall(
            IERC7786Receiver.executeMessage,
            (uint256(id).toHexString(32), sourceChain, originalSender, unwrappedPayload, attributes)
        );

        (bool success, bytes memory returndata) = receiver.parseAddress().call(call);

        // ... 处理结果 ...
    }

    return IERC7786Receiver.executeMessage.selector;
}

该桥被设计为可配置的。作为 Ownable 合约,它允许所有者管理受信任网关的列表并调整确认阈值。_gateways 列表和阈值最初是在合约部署期间使用 _addGateway_setThreshold 函数设置的。所有者可以根据需要更新这些设置,以适应不断变化的需求或添加新的网关。

← Paymasters

Utilities →

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

0 条评论

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