# NFT白名单校验-签名验证 **Published by:** [xyyme.eth](https://paragraph.com/@xyyme/) **Published on:** 2022-04-01 **URL:** https://paragraph.com/@xyyme/nft-2 ## Content 前面的文章我们介绍过,NFT 白名单校验主要有两种方式:Merkle Tree签名验证这篇文章我们就来介绍一下签名验证的实现方法。我们这篇文章不会注重于签名的数学原理,仅仅对于链上与链下交互的实现做介绍。 在开始之前,我们先要了解一个基础知识:我们平时使用的区块链地址,都分为私钥和地址。私钥可以生成地址,而反过来,地址不能生成私钥。那么如何证明一个地址是属于某一个人呢?我们可以给他一段信息,让他用私钥对这段信息进行签名。我们拿到这段签名,再结合这个地址,就可以证明,这段信息是否是由这个地址对应的私钥签名,从而也就证明了地址的所有权。验证步骤明白了这个简单的原理之后,我们来看看实际项目中的白名单签名验证是什么步骤:项目方后台数据库中保存所有的白名单用户用户在网站连接钱包后,前端将用户地址发送给后端后端检查该地址是否是白名单用户,如果是,则用后端的管理员私钥对地址进行签名后端返回签名数据,同样,前端会将签名数据传给合约验证合约验证通过,则用户可以白名单 mint接下来我们看看,合约中具体应该怎样实现。我们这里使用 OpenZeppelin 的 ECDSA 库来进行示范。在实际使用中,主要调用的方法为: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 中的逻辑我们就不再详述了,里面涉及到了数学原理,感兴趣的同学可以自行看看代码。代码接下来我们利用 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 的使用方法可以参考文档。 加盐的方法与上面类似,小幽灵(The Weirdo Ghost Gang)这个项目代码的白名单验证实现中就加入的盐的信息:总结签名验证的数学逻辑比较复杂,但是已经有成熟的库将其封装好了,对于我们实际使用来说,难度不大,基本都是套路,多看看代码,多写几遍就很熟练了。参考(这几篇文章和视频的内容都很精华,时间充裕的话建议都读一遍)Using Signatures (ECDSA) for NFT WhitelistsUsing 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.comTutorial: Digital Signatures & NFT AllowlistsTutorial: 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 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 ## Publication Information - [xyyme.eth](https://paragraph.com/@xyyme/): Publication homepage - [All Posts](https://paragraph.com/@xyyme/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@xyyme): Subscribe to updates - [Twitter](https://twitter.com/xyymeeth): Follow on Twitter