Solidity - 验证签名如何实现

今天,我们来讲一下,可能大部分合约开发工作人员都不太掌握的验签,本文完全免费公开,图文完全原创,干货满满。

如果已经是Solidity的中级开发水平,我们是可以完全根据业务需求来完成代码的开发工作了。剩下的,就已经是思路的问题了。只要思路明晰,那么就不会有写不出来的合约代码。今天,我们来讲一下,可能大部分合约开发工作人员都不太掌握的验签,本文完全免费公开,图文完全原创,干货满满,学习完之后感觉瞬间顿悟,已经完全掌握此技术点的,别忘了点赞,收藏,关注。有任何问题,欢迎评论区留言。如果您是区块链小白,想要入门区块链,掌握Solidity合约开发,可以查看我以往的文章,也可以关注我的微信公众号:Subkie。

首先,我们得搞清楚,什么是验证签名

验证签名,其实就是验证一笔签名的钱包地址是否就是当前的用户钱包地址,如果不一样的话,验签就不通过。其具体的工作流程是:将数据或者消息进行哈希打包之后,再将打包的消息进行以太坊签名消息,以太坊签名消息是为了防止交易被误签,之后用户将以太坊签名消息过后的消息与使用内联汇编从签名中得到的rsv值利用ecrecover方法返回用户的钱包地址,如果得到的钱包地址与预期的钱包地址一致,则验证成功。(如果面试能讲出来这个,人家绝对觉得你完全ok,哈哈)

我们通过下面的流程图来加深一下记忆。 image.png

接下来,我们直接上代码。这份代码有着完整的中文注释,方便大家节省时间,更简单快速理解代码含义。

// 文章:https://www.wtf.academy/solidity-application/Signature/

contract MyNumberSign {

    /*
     * 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash
     * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * _tokenId: 0
     * 对应的消息msgHash: 0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229
     */
    function getMessageHash(address _account,uint _tokenId) public pure returns(bytes32){
        return keccak256(abi.encodePacked(_account,_tokenId));
    }

    /**
     * @dev 返回 以太坊签名消息
     * `hash`:消息 getMessageHash()的返回值
     * 遵从以太坊签名标准:https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
     * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191`
     * 添加"\x19Ethereum Signed Message:\n32"字段,防止签名的是可执行交易。
     * 
     */
    function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) {
        // 哈希的长度为32
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }

    // @dev 从_msgHash和签名_signature中恢复signer地址
    // _msgHash:toEthSignedMessageHash()的返回值
    // _signature:getMessageHash()的返回值的签名
    function recoverSigner(bytes32 _msgHash, bytes memory _signature) public pure returns (address){
        // 检查签名长度,65是标准r,s,v签名的长度
        require(_signature.length == 65, "invalid signature length");
        bytes32 r;
        bytes32 s;
        uint8 v;
        // 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值
        assembly {
            /*
            前32 bytes存储签名的长度 (动态数组存储规则)
            add(sig, 32) = sig的指针 + 32
            等效为略过signature的前32 bytes
            mload(p) 载入从内存地址p起始的接下来32 bytes数据
            */
            // 读取长度数据后的32 bytes
            r := mload(add(_signature, 0x20))
            // 读取之后的32 bytes
            s := mload(add(_signature, 0x40))
            // 读取最后一个byte
            v := byte(0, mload(add(_signature, 0x60)))
        }
        // 使用ecrecover(全局函数):利用 msgHash 和 r,s,v 恢复 signer 地址
        return ecrecover(_msgHash, v, r, s);
    }

    /**
     * @dev 通过ECDSA,验证签名地址是否正确,如果正确则返回true
     * _msgHash为消息的hash:toEthSignedMessageHash()的返回值
     * _signature为签名
     * _signer为签名地址
     */
    function verify(bytes32 _msgHash, bytes memory _signature, address _signer) public pure returns (bool) {
        return recoverSigner(_msgHash, _signature) == _signer;
    }

}

看懂了以上的代码结构后,我们其实就可以用这份代码来做我们的白名单需求了,只要某个用户的钱包地址验证成功,就通过我们的白名单审查。有这样的白名单审核的方式,是不是感觉特别爽。比我们使用默克尔数链下生成root的方式是不是简单很多。

话不多说,我们举个例子。

比如:验证某个用户是否存在于白名单内。 我们可以将用户地址和用户的tokenId使用keccak256(abi.encodePacked(_account,_tokenId))方法进行打包加密后得到一个bytes32的值。2.将值添加以太坊签名消息防止签名误签,得到新的bytes32的值。3.将打包加密后得到的bytes32的值作为signature,使用内联汇编从签名中获得v,r,s的值,利用ecrecover方法将获得的v,r,s的值和添加以太坊签名消息后的值,返回一个钱包账户地址,像这样:ecrecover(以太坊签名消息后的值,v,r,s)。4.将得到的钱包账户地址与预期的地址相比较即可知道验签是否成功。

在remix-ide上,部署流程是这样的:

1.调用getMessageHash参数传入:
_account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
_tokenId: 1
结果:0x887610ccbf6ff730a639c5ec66d671b53ea0e4b57e1d0365ac1312d4da91ee70
2.调用toEthSignedMessageHash 参数传入
0x887610ccbf6ff730a639c5ec66d671b53ea0e4b57e1d0365ac1312d4da91ee70
结果:
0xc2eae610249f4309ee22bc01a83dbd9a0bada4490fd5be61b12482be0ff2b295
3.调用recoverSigner 参数传入
_msgHash: 0xc2eae610249f4309ee22bc01a83dbd9a0bada4490fd5be61b12482be0ff2b295
_signature:
0xb055edd2c56afd2e8ff99612eb2c35748371be7f35df95a297cc97face7bc3567ef2fa28de2bdfe70daf699db6488d099f1e4462f8984870110d1963496645741c
结果:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// _msgHash:toEthSignedMessageHash()的返回值
// _signature:getMessageHash()的返回值的签名
4.调用verify 参数传入
_msgHash:
0xc2eae610249f4309ee22bc01a83dbd9a0bada4490fd5be61b12482be0ff2b295
_signature:
0xb055edd2c56afd2e8ff99612eb2c35748371be7f35df95a297cc97face7bc3567ef2fa28de2bdfe70daf699db6488d099f1e4462f8984870110d1963496645741c
_signer:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
结果:true
// _msgHash:toEthSignedMessageHash()的返回值

好的,至此,我们验证签名就讲完了。下面补充另一个知识点,我们还可以用ECDSA的方式来验证签名。合约代码示例放在这里,大家自行研究哦。

contract SignatureNFT is ERC721 {
    address immutable public signer; // 签名地址
    mapping(address => bool) public mintedAddress;   // 记录已经mint的地址

    // 构造函数,初始化NFT合集的名称、代号、签名地址
    constructor(string memory _name, string memory _symbol, address _signer)
    ERC721(_name, _symbol)
    {
        signer = _signer;
    }

    // 利用ECDSA验证签名并mint
    function mint(address _account, uint256 _tokenId, bytes memory _signature)
    external
    {
        bytes32 _msgHash = getMessageHash(_account, _tokenId); // 将_account和_tokenId打包消息
        bytes32 _ethSignedMessageHash = ECDSA.toEthSignedMessageHash(_msgHash); // 计算以太坊签名消息
        require(verify(_ethSignedMessageHash, _signature), "Invalid signature"); // ECDSA检验通过
        require(!mintedAddress[_account], "Already minted!"); // 地址没有mint过
        _mint(_account, _tokenId); // mint
        mintedAddress[_account] = true; // 记录mint过的地址
    }

    /*
     * 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash
     * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * _tokenId: 0
     * 对应的消息: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
     */
    function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
        return keccak256(abi.encodePacked(_account, _tokenId));
    }

    // ECDSA验证,调用ECDSA库的verify()函数
    function verify(bytes32 _msgHash, bytes memory _signature)
    public view returns (bool)
    {
        return ECDSA.verify(_msgHash, _signature, signer);
    }

}

觉得文章不错的话,点赞,关注,评论。感谢您的支持。

点赞 3
收藏 6
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
心辰说区块链
心辰说区块链
0xc15d...f612
区块链技术从业者!