# NFT白名单校验-数字签名 **Published by:** [cyptoJune](https://paragraph.com/@cyptojune/) **Published on:** 2022-09-02 **URL:** https://paragraph.com/@cyptojune/nft-3 ## Content 如果大家用过钱包,相信对于签名都不会陌生。Metamask钱包签名以太坊使用的数字签名算法叫双椭圆曲线数字签名算法(ECDSA),基于双椭圆曲线“私钥-公钥”对的数字签名算法。它主要有三种作用:身份认证:证明签名方时私钥的持有人不可否认:发送方不能否认发送过这个消息完整性:消息在传输过程中无法被修改 在openzeppelin中提供了现成的ECDSA.sol合约,具体大家可以去官网看创建签名 创建签名分为两步,第一步是打包消息,第二步是计算以太坊签名消息打包消息: 在以太坊的ECDSA标准中,被签名的消息是一组数据的keccak256哈希,为bytes32类型。我们可以把任何想要签名的内容利用abi.encodePacked()函数打包,然后用keccak256()计算哈希,作为消息。 /* * 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 * _tokenId: 0 * 对应的消息msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c */ function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){ return keccak256(abi.encodePacked(_account, _tokenId)); } 计算以太坊签名消息: 消息可以是能被执行的交易,也可以是其他任何形式。为了避免用户误签了恶意交易,EIP191提倡在消息前加上"\x19Ethereum Signed Message:\n32"字符,并再做一次keccak256哈希,作为以太坊签名消息。经过toEthSignedMessageHash()函数处理后的消息,不能被用于执行交易。 [ /** * @dev 返回 以太坊签名消息 * `hash`:消息哈希 * 遵从以太坊签名标准: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) internal pure returns (bytes32) { // 哈希的长度为32 return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); }]( /** * @dev 返回 以太坊签名消息 * `hash`:消息哈希 * 遵从以太坊签名标准: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) internal pure returns (bytes32) { // 哈希的长度为32 return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); })签名工具 一般我们用两种方法进行签名,一种是钱包签名,另外一种是脚本签名。钱包签名 日常操作中,大部分用户都是通过这种方式进行签名。在获取到需要签名的消息之后,我们需要使用metamask钱包进行签名。metamask的personal_sign方法会自动把消息哈希转换为以太坊签名消息,然后发起签名。所以我们只需要输入消息哈希hash和签名者钱包account即可。需要注意的是输入的签名者钱包account需要和metamask当前连接的account一致才能使用。脚本签名 利用web3.py签名: 批量调用中更倾向于使用代码进行签名,具体的代码可自行搜索,下面我给出一段.py的参考代码 [from web3 import Web3, HTTPProviderfrom eth_account.messages import encode_defunct private_key = "0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b" address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" rpc = 'https://rpc.ankr.com/eth' w3 = Web3(HTTPProvider(rpc)) #打包信息 msg = Web3.solidityKeccak(['address','uint256'], [address,0]) print(f"消息:{msg.hex()}") #构造可签名信息 message = encode_defunct(hexstr=msg.hex()) #签名 signed_message = w3.eth.account.sign_message(message, private_key=private_key) print(f"签名:{signed_message['signature'].hex()}")](from web3 import Web3, HTTPProvider from eth_account.messages import encode_defunct private_key = "0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b" address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" rpc = 'https://rpc.ankr.com/eth' w3 = Web3(HTTPProvider(rpc)) #打包信息 msg = Web3.solidityKeccak(['address','uint256'], [address,0]) print(f"消息:{msg.hex()}") #构造可签名信息 message = encode_defunct(hexstr=msg.hex()) #签名 signed_message = w3.eth.account.sign_message(message, private_key=private_key) print(f"签名:{signed_message['signature'].hex()}")) 验证签名上面讲解的是一些数字签名的基本知识,回到本文中心,我们探讨如何在实际项目中利用数字签名进行Nft的白名单的发放。我们知道,我们平时使用的区块链地址,**都分为私钥和地址。私钥可以生成地址,而反过来,地址不能生成私钥。那么如何证明一个地址是属于某一个人呢?我们可以给他一段信息,让他用私钥对这段信息进行签名。我们拿到这段签名,再结合这个地址,就可以证明,这段信息是否是由这个地址对应的私钥签名,从而也就证明了地址的所有权。** 上面这段原理理解之后,就可以运用到实际项目开发中。 验证逻辑项目方后台数据库中保存所有的白名单用户用户在网站连接钱包后,前端将用户地址发送给后端后端检查该地址是否是白名单用户,如果是,则用后端的管理员私钥对地址进行签名后端返回签名数据,同样,前端会将签名数据传给合约验证合约验证通过,则用户可以白名单 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 中所说的管理员地址。也就是说,我们需要将管理员的地址设置在合约中进行比对。通过上面的验证逻辑,可分为链上和链下进行。链上部分 链上部分主要是对签名数据进行验证,看是否是管理员的私钥进行签名。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]); 总结: ## Publication Information - [cyptoJune](https://paragraph.com/@cyptojune/): Publication homepage - [All Posts](https://paragraph.com/@cyptojune/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@cyptojune): Subscribe to updates