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编写测试,按以下步骤执行:
- 生成两个测试账户:
verifier(验证者)和attacker(攻击者); - 部署合约,将
verifier设为初始验证者; - 使用
verifier的私钥对合法消息哈希签名,调用verifyMessage,断言返回true且事件MessageVerified被触发; - 使用
attacker的私钥对同一消息签名,调用verifyMessage,断言回退(revert)且错误信息为"Invalid signer"; - 使用
verifier签名但将_targetChainId改为其他值,断言回退"Wrong target chain"; - 使用
verifier签名合法消息后,再次以相同参数调用,断言回退"Nonce already used"; - 篡改
_payload内容后使用原签名调用,断言回退"Invalid signer"(因为payload哈希改变导致签名不匹配)。
判定标准:上述7项测试全部通过,即证明合约能正确验证来自可信验证者的签名、拒绝伪造签名、防止跨链重放和消息篡改,满足“确保消息来源可信”的目标。