Skip to content

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项目要为用户提供白名单铸造权限:

  1. 项目方签名:项目方用自己持有的私钥,为每个白名单用户的地址和可铸造的TokenID生成一个签名。
  2. 用户提交:用户调用合约的mint函数,提交自己的地址、TokenID和收到的签名。
  3. 合约验证:合约使用上述流程验证签名。如果通过,则为用户铸造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;
    }
    
}