3. 跨链消息验证合约

跨链消息验证合约 学习卡片

任务说明

本案例需要编写一个Solidity合约,用于验证来自其他区块链的消息签名,确保消息来源可信。核心场景为:跨链桥或跨链协议在收到外部链提交的消息时,需要验证该消息确实由受信任的验证者签名,且消息内容未被篡改。本合约实现基于ECDSA的签名验证,支持验证者公钥管理,并针对跨链消息特有的字段(源链ID、目标链ID、消息类型、nonce)进行结构化哈希后再验签,防止跨链重放攻击。

合约实现

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/**
 * @title CrossChainMessageVerifier
 * @notice 验证来自其他链的消息签名,确保消息来源可信
 */
contract CrossChainMessageVerifier {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;

    // 受信任的验证者地址 => 是否有效
    mapping(address => bool) public validVerifiers;

    // 已消费的nonce(防止同一消息重复执行)
    mapping(bytes32 => bool) public usedNonces;

    // 本合约所属的链ID(部署时设定)
    uint256 public immutable thisChainId;

    // 事件:验证者状态变更
    event VerifierUpdated(address indexed verifier, bool valid);
    // 事件:消息验证通过
    event MessageVerified(
        uint256 indexed sourceChainId,
        uint256 indexed targetChainId,
        bytes32 indexed messageType,
        uint256 nonce,
        address verifier
    );

    modifier onlyVerifier() {
        require(validVerifiers[msg.sender], "Caller is not a valid verifier");
        _;
    }

    constructor(uint256 _thisChainId, address[] memory _initialVerifiers) {
        thisChainId = _thisChainId;
        for (uint256 i = 0; i < _initialVerifiers.length; i++) {
            validVerifiers[_initialVerifiers[i]] = true;
        }
    }

    /**
     * @notice 添加或移除验证者(仅现有验证者可操作)
     */
    function setVerifier(address _verifier, bool _valid) external onlyVerifier {
        require(_verifier != address(0), "Zero address");
        validVerifiers[_verifier] = _valid;
        emit VerifierUpdated(_verifier, _valid);
    }

    /**
     * @notice 验证跨链消息签名
     * @param _sourceChainId 源链ID
     * @param _targetChainId 目标链ID(必须等于本链ID)
     * @param _messageType 消息类型标识(如keccak256("TRANSFER"))
     * @param _nonce 消息唯一序号(防重放)
     * @param _payload 业务负载(任意字节)
     * @param _signature 验证者对以上字段的签名
     * @return 验证通过返回true
     */
    function verifyMessage(
        uint256 _sourceChainId,
        uint256 _targetChainId,
        bytes32 _messageType,
        uint256 _nonce,
        bytes calldata _payload,
        bytes calldata _signature
    ) external returns (bool) {
        // 目标链必须为本链
        require(_targetChainId == thisChainId, "Wrong target chain");

        // 构造跨链消息哈希(EIP-191兼容)
        bytes32 messageHash = keccak256(
            abi.encodePacked(
                _sourceChainId,
                _targetChainId,
                _messageType,
                _nonce,
                keccak256(_payload)
            )
        );
        bytes32 ethSignedHash = messageHash.toEthSignedMessageHash();

        // 恢复签名者
        address signer = ethSignedHash.recover(_signature);
        require(validVerifiers[signer], "Invalid signer");

        // 防重放:nonce全局唯一
        bytes32 nonceKey = keccak256(abi.encodePacked(_sourceChainId, _nonce));
        require(!usedNonces[nonceKey], "Nonce already used");
        usedNonces[nonceKey] = true;

        emit MessageVerified(_sourceChainId, _targetChainId, _messageType, _nonce, signer);
        return true;
    }

    /**
     * @notice 离线计算消息哈希(供外部链验证者签名使用)
     */
    function getMessageHash(
        uint256 _sourceChainId,
        uint256 _targetChainId,
        bytes32 _messageType,
        uint256 _nonce,
        bytes calldata _payload
    ) public pure returns (bytes32) {
        return keccak256(
            abi.encodePacked(
                _sourceChainId,
                _targetChainId,
                _messageType,
                _nonce,
                keccak256(_payload)
            )
        );
    }
}

关键设计说明

合约将跨链消息的五个关键字段(源链ID、目标链ID、消息类型、nonce、payload哈希)打包后计算keccak256哈希,再通过toEthSignedMessageHash转换为以太坊签名消息格式,使用ECDSA恢复签名者地址。只有恢复出的地址在validVerifiers映射中为true时,签名才被接受。nonce以(sourceChainId, nonce)组合作为键记录消费状态,防止同一源链的相同消息被重复执行,同时允许不同源链使用相同nonce值。目标链ID必须等于部署时的thisChainId,确保消息不会被错误投递到其他链。

部署与验证者初始化

// 部署示例(假设本链ID为1,初始验证者为0x1234...)
// CrossChainMessageVerifier verifier = new CrossChainMessageVerifier(
//     1,
//     new address[](0)  // 初始无验证者,部署后由首个验证者添加
// );
// 注意:首个验证者需通过setVerifier自行添加(需先部署一个带初始验证者的实例)

实际部署时建议在构造函数中传入至少一个初始验证者地址,否则合约将无人可操作setVerifier。例如:

address[] memory initial = new address[](1);
initial[0] = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B;
CrossChainMessageVerifier verifier = new CrossChainMessageVerifier(1, initial);

验证流程

// 1. 外部链的验证者离线计算消息哈希
bytes32 msgHash = verifier.getMessageHash(
    56,                    // 源链ID(如BSC)
    1,                     // 目标链ID(本链)
    keccak256("TRANSFER"), // 消息类型
    1001,                  // nonce
    payloadBytes           // 业务数据
);

// 2. 验证者使用自己的私钥对msgHash签名(在外部链的离线环境中完成)
// 得到签名 signatureBytes

// 3. 本链上任何用户调用verifyMessage提交消息与签名
bool ok = verifier.verifyMessage(
    56,
    1,
    keccak256("TRANSFER"),
    1001,
    payloadBytes,
    signatureBytes
);
// ok == true 表示签名有效且消息来源可信

验证方法与判定标准

单元测试验证:使用Hardhat或Foundry编写测试,按以下步骤执行:

  1. 生成两个测试账户:verifier(验证者)和attacker(攻击者);
  2. 部署合约,将verifier设为初始验证者;
  3. 使用verifier的私钥对合法消息哈希签名,调用verifyMessage,断言返回true且事件MessageVerified被触发;
  4. 使用attacker的私钥对同一消息签名,调用verifyMessage,断言回退(revert)且错误信息为"Invalid signer";
  5. 使用verifier签名但将_targetChainId改为其他值,断言回退"Wrong target chain";
  6. 使用verifier签名合法消息后,再次以相同参数调用,断言回退"Nonce already used";
  7. 篡改_payload内容后使用原签名调用,断言回退"Invalid signer"(因为payload哈希改变导致签名不匹配)。

判定标准:上述7项测试全部通过,即证明合约能正确验证来自可信验证者的签名、拒绝伪造签名、防止跨链重放和消息篡改,满足“确保消息来源可信”的目标。

跨链消息验证合约 — 签名验证流程 外部链(如BSC, ChainID=56) 验证者(持有私钥) 地址: 0xAb58...aeC9B 构造消息字段 sourceChainId=56, targetChainId=1 messageType, nonce, payload 本链(ChainID=1) CrossChainMessageVerifier validVerifiers 映射 usedNonces 映射 调用者(Relayer/用户) ① 构造消息 ② 离线签名 消息哈希 ③ 提交消息 + 签名 ④ 调用verifyMessage 验证逻辑 1. targetChainId == thisChainId? 2. 计算消息哈希 3. ECDSA.recover 恢复签名者 4. validVerifiers[signer] == true? 5. nonce 未使用过? 验证通过 ✓ 攻击者(伪造签名) 地址: 0xAtt...acker ✗ 伪造签名提交 ✗ 判定标准: 合法验证者签名 → 通过 伪造签名/篡改消息 → 拒绝 重复nonce → 拒绝