solidity 签名验证算法
Solidity 中的签名验证核心是椭圆曲线数字签名算法(ECDSA),它允许智能合约验证一段数据是否由特定的私钥持有者签发。这在白名单、代币铸造等场景中至关重要。其核心原理可以概括为“链下签名,链上验证”。
下面这个表格总结了ECDSA签名验证流程中的关键角色和步骤。
| 步骤 | 角色 | 关键动作 | 说明 |
|---|---|---|---|
| 1. 生成签名(链下) | 用户/前端 | 创建消息哈希:将待签名的数据(如地址、数量)打包并计算哈希。 | 确保数据格式一致。 |
添加以太坊前缀:在哈希前添加特定字符\x19Ethereum Signed Message:\n32并再次哈希。 | 防止签名被恶意用于交易。 | ||
使用私钥签名:对处理后的哈希进行签名,得到 r, s, v 三个组件。 | 签名结果长度通常为65字节。 | ||
| 2. 验证签名(链上) | 智能合约 | 重组消息哈希:在合约内,用相同规则对传入的数据进行打包和哈希计算。 | 必须与链下步骤完全一致。 |
调用 ecrecover | 这是最核心的环节。ecrecover 是一个内置函数,它接收消息哈希、签名组件 r, s, v,并计算出一个以太坊地址。 | ||
比对地址:将 ecrecover 返回的地址与预设的合法签名者地址进行比对。 | 如果一致,则签名有效。 |
🔑 核心组件与安全实践
签名构成 (r, s, v)
一个完整的ECDSA签名由三部分组成:
- r (32字节):签名的一部分,代表椭圆曲线点的一个坐标值。
- s (32字节):签名的另一部分。
- v (1字节,恢复标识符):用于在验证时确定使用哪个具体的椭圆曲线点,其值通常是27或28。 在合约中解析签名时,需要正确地从65字节的签名数据中提取出这三部分。
谨防签名重放攻击
这是签名验证中最主要的安全风险。攻击者可以拦截并重复使用一个有效的签名。防御策略包括:
- 记录已使用的签名:使用映射(mapping)记录每个签名是否已被使用,防止同一签名被多次提交。
- 加入上下文信息:在签名消息中包含Nonce(一次性编号) 和 ChainID(区块链ID),确保签名仅能在特定交易和特定链上使用一次,有效防范跨链重放攻击。
💡 一个简单的应用场景:NFT白名单
假设一个NFT项目要为用户提供白名单铸造权限:
- 项目方签名:项目方用自己持有的私钥,为每个白名单用户的地址和可铸造的TokenID生成一个签名。
- 用户提交:用户调用合约的
mint函数,提交自己的地址、TokenID和收到的签名。 - 合约验证:合约使用上述流程验证签名。如果通过,则为用户铸造NFT,并将该签名标记为已使用;否则交易失败。
简单来看个demo
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract VerifySig{
function getMessageHash(string memory _message) public pure returns (bytes32){
return keccak256(abi.encodePacked(_message));
}
function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32){
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32",_messageHash));
}
function recover(bytes32 _ethSignedMessageHash, bytes memory _sig) public pure returns (address){
(bytes32 r , bytes32 s, uint8 v) = _split(_sig);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
function _split(bytes memory _sig) internal pure returns (bytes32 r, bytes32 s, uint8 v){
require(_sig.length == 65, "invalid signature length");
assembly {
r := mload(add(_sig, 32))
s := mload(add(_sig, 64))
v := byte(0, mload(add(_sig, 96)))
}
}
function verify(address _signer, string memory _message, bytes memory _sig) external pure returns(bool){
bytes32 messageHash = getMessageHash(_message);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recover(ethSignedMessageHash, _sig) == _signer;
}
}

