# NFT白名单校验-签名验证

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-04-01

---

前面的[文章](https://mirror.xyz/xyyme.eth/kz25RzXxUDyWr9oqOWUF0M9g0Q0ke0vxlkhJ0ffT7kk)我们介绍过，NFT 白名单校验主要有两种方式：

*   Merkle Tree
    
*   签名验证
    

这篇文章我们就来介绍一下签名验证的实现方法。我们这篇文章不会注重于签名的数学原理，仅仅对于链上与链下交互的实现做介绍。

在开始之前，我们先要了解一个基础知识：我们平时使用的区块链地址，都分为私钥和地址。私钥可以生成地址，而反过来，地址不能生成私钥。那么如何证明一个地址是属于某一个人呢？我们可以给他一段信息，让他用私钥对这段信息进行签名。我们拿到这段签名，再结合这个地址，就可以证明，这段信息是否是由这个地址对应的私钥签名，从而也就证明了地址的所有权。

### 验证步骤

明白了这个简单的原理之后，我们来看看实际项目中的白名单签名验证是什么步骤：

1.  项目方后台数据库中保存所有的白名单用户
    
2.  用户在网站连接钱包后，前端将用户地址发送给后端
    
3.  后端检查该地址是否是白名单用户，如果是，则用后端的管理员私钥对地址进行签名
    
4.  后端返回签名数据，同样，前端会将签名数据传给合约验证
    
5.  合约验证通过，则用户可以白名单 mint
    

接下来我们看看，合约中具体应该怎样实现。我们这里使用 OpenZeppelin 的 [ECDSA](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol) 库来进行示范。在实际使用中，主要调用的方法为：

    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        (address recovered, RecoverError error) = tryRecover(hash, signature);
        _throwError(error);
        return recovered;
    }
    

方法接收两个参数：

*   hash → 原始待签名信息的哈希值
    
*   signature → 签名数据
    

返回值是对这段信息签名的地址，也就是我们前面在步骤 3 中所说的管理员地址。也就是说，我们需要将管理员的地址设置在合约中进行比对，注意管理员的地址一定不能作为参数传入，否则用任意一个地址作签名之后再传入这个地址作验证，那结果一定是满足的。

对于这段代码调用的 tryRecover 中的逻辑我们就不再详述了，里面涉及到了数学原理，感兴趣的同学可以自行看看[代码](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol)。

### 代码

接下来我们利用 ECDSA 来实现验证逻辑：

    pragma solidity 0.8.13;
    
    import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
    
    contract TestSig {
    
        // 使用 using 方法，就可以直接使用 bytes32 类型调用方法
        using ECDSA for bytes32;
    
        address public owner;
    
        constructor() {
            // 管理员地址（仅测试，不要对这个地址转账）
            owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
        }
    
        function verify(bytes memory signature) public view returns (bool) {
            // 验证签名者是否是管理员
            return recoverSigner(signature) == owner;
        }
    
        function recoverSigner(bytes memory signature) public view returns (address) {
            // 注意这里待哈希的内容需要与链下签名方法保持一致即可
            // 可以加盐或者其他数据来保持唯一性，防止重放攻击
            // 这里简单起见，仅对调用者的地址进行哈希签名
            bytes32 messageHash = keccak256(abi.encodePacked(msg.sender));
            // 调用 recover 验证签名地址
            address signer = messageHash.toEthSignedMessageHash().recover(signature);
            return signer;
        }
    }
    

链上逻辑实现后，我们还要在链下对信息进行签名。我们使用 `ethers` 的 JavaScript 库进行签名操作，需要安装 `ethers` 的依赖：

> npm install --save-dev ethers

代码：

    const ethers = require('ethers');
    const main = async () => {
        // 管理员地址与私钥（仅测试，不要对这个地址转账）
        const owner = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266';
        const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
        const signer = new ethers.Wallet(privateKey);
        console.log(signer.address)
    
        // 由于我们之前是使用了 msg.sender 进行签名
        // 因此这里需要改为合约的调用地址
        let message = '0xxxxx';
        // 计算信息的哈希值
        let messageHash = ethers.utils.solidityKeccak256(['address'], [message]);
        console.log("Message Hash: ", messageHash);
        // 由于 ethers 库的要求，需要先对哈希值数组化
        let messageBytes = ethers.utils.arrayify(messageHash);
        console.log("messageBytes: ", messageBytes);
        // 签名
        let signature = await signer.signMessage(messageBytes);
        console.log("Signature: ", signature);
    }
    const runMain = async () => {
        try {
            await main();
            process.exit(0);
        }
        catch (error) {
            console.log(error);
            process.exit(1);
        }
    };
    runMain();
    

这段代码就可以为我们生成签名数据，我们将签名数据传入合约的 `verify` 方法，就可以验证签名信息的准确性。

我们这里的签名信息仅仅是对合约发送者进行签名，这可能会造成签名信息被重复利用从而造成重放攻击。因此我们一般在实际项目开发中都会加上一些其他的信息，比如合约本身的地址（这样签名就确认只能应用于这个合约），或者盐（随机数确保随机）。

如果加上合约本身的地址作源信息，那么代码就需要改成：

    // Solidity
    bytes32 messageHash = keccak256(abi.encodePacked(address(this), msg.sender));
    
    // Javascript
    let messageHash = ethers.utils.solidityKeccak256(['address', 'address'], [message, message]);
    

关于 `solidityKeccak256` 的使用方法可以参考[文档](https://docs.ethers.io/v5/api/utils/hashing/#utils-solidityKeccak256)。

加盐的方法与上面类似，小幽灵（The Weirdo Ghost Gang）这个项目[代码](https://etherscan.io/address/0x9401518f4ebba857baa879d9f76e1cc8b31ed197)的白名单验证实现中就加入的盐的信息：

![](https://storage.googleapis.com/papyrus_images/5c4392102db5044e5fe1d482ac582153794411c6de9634a18b34e8b7520a1418.png)

![](https://storage.googleapis.com/papyrus_images/2e7687a198931feabb65c7013b3058c4a9fa27d71701f9cb10f02b15216b6cdc.png)

### 总结

签名验证的数学逻辑比较复杂，但是已经有成熟的库将其封装好了，对于我们实际使用来说，难度不大，基本都是套路，多看看代码，多写几遍就很熟练了。

### 参考（这几篇文章和视频的内容都很精华，时间充裕的话建议都读一遍）

[

Using Signatures (ECDSA) for NFT Whitelists
-------------------------------------------

Using Signatures (ECDSA) for NFT Whitelists Introduction In my previous article, I covered the concept of Merkle Trees and discussed how they played a critical role in ensuring both minter ...

https://medium.com

![](https://storage.googleapis.com/papyrus_images/7bc3621bce3caf7fbf041a62250fb3b8f1a2b8370332677a4cbfa9d702368881.gif)

](https://medium.com/@ItsCuzzo/using-signatures-ecdsa-for-nft-whitelists-ba0a4d070e92)

[

Tutorial: Digital Signatures & NFT Allowlists
---------------------------------------------

Tutorial: Digital Signatures & NFT Allowlists Disclaimer A previous version of this article used the term whitelist instead of allowlist. Although they refer to the same thing, we have decided to ...

https://medium.com

![](https://storage.googleapis.com/papyrus_images/35e0466e3fd457afde6d0fb7422be7e2d48a70322c66c0d44be532d67fac6800.png)

](https://medium.com/scrappy-squirrels/tutorial-creating-nft-whitelists-7d6233f6d359)

[![]({{DOMAIN}}/editor/youtube/play.png)](https://www.youtube.com/watch?v=rtyaD-RASbQ)

[

What is the equivalent of keccak256 in solidity?
------------------------------------------------

I am going to get the same value that is produced by keccak256 in solidity. This is the code in my solidity file and I want to get the same value in the javascript file using ethers or web3. bytes3...

https://ethereum.stackexchange.com

![](https://storage.googleapis.com/papyrus_images/dc7b2a51adecb9025c3c259ca1207f5f2fc8ebc314cfd83fd79e6d0cba01a644.png)

](https://ethereum.stackexchange.com/questions/122989/what-is-the-equivalent-of-keccak256-in-solidity)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/nft-2)*
