NFT签名验证

说明

NFT项目中必不可少的是白名单的验证。白名单的验证一般都是有两种做法,Merkle Tree和签名验证。本篇文章主要展示签名验证的做法。

在此之前,我们需要明确一个概念。账户一般都有公钥和私钥。我们平时用作地址转账的就是公钥,而合约部署这种就需要用到我们的私钥。公钥和私钥是一一对应的。私钥可以推导出公钥,但是公钥却不能推导出公钥。

如何证明某个地址(公钥)是某个人的呢?我们给他一段信息,让他用自己的私钥进行签名。然后我们得到这个签名结合这个人的地址(公钥),即可判断这个签名的信息是不是正确的。概括下:

带验证的人: 私钥 + 明文 = 签名信息
验证方法: 明文 + 签名信息 = 公玥

如果 公玥 和 私钥对的上,那么就说明这个签名是由这个人签名的。

签名验证示例

关于合约验证的方法,参考 OpenZeppelin 的 ECDSA 库来进行示范。

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

这个方法是用来验证签名的。此方法接受两个参数:

  • hash,需要验证的信息的hash值(可以理解为明文)
  • signature,签名信息

通过(address recovered, RecoverError error) = tryRecover(hash, signature); 尝试还原出签名的地址。如果还原成功,那么就返回这个地址。如果还原失败,那么就抛出错误。

如果我们最后验证了这个签名的地址和私钥的地址所对应的公玥是一致的,那么就说明这个签名是由这个人签名的。

验证步骤

NFT实际项目中的白名单的验证步骤如下:

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

实际案例

配合前面说的验证步骤,下面的这张图片也更加容易理解。

在此过程中,我们假设需要假设以下信息:

  • 用户的地址: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
  • 合约的地址: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
  • 合约owner的地址:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

整体的思路是,使用Python代码对用户进行签名,合约判断是不是对应的实际用户.一般情况下.如果签名信息仅仅是对合约发送者进行签名,这可能会造成签名信息被重复利用从而造成重放攻击。因此我们一般在实际项目开发中都会加上一些其他的信息,比如合约本身的地址(这样签名就确认只能应用于这个合约),或者盐(随机数确保随机)。在本例中会加上合约地址作为签名信息的一部分.

Python签名验证的代码:

1
2
3
4
5
6
7
8
9
def sign_address():
w3 = Web3(HTTPProvider(your_rpc_url))
base_message = Web3.soliditySha3(['address','address'],
[Web3.toChecksumAddress("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512") # contract address
,Web3.toChecksumAddress('0x70997970c51812dc3a010c7d01b50e0d17dc79c8')]) # msg.sender
private_key = '' # contract owner private key
message = encode_defunct(primitive=base_message)
signed_message = w3.eth.account.sign_message(message, private_key=private_key)
print(signed_message.signature.hex())

得到最终的签名信息是: 0x125521c93d39fbed27f4df56e424c3dd8c8b26143276ee59525157e3582cac95067ab4008f7e82086ccd8eb73a48358d19faa39acab9ab6f7d9ae2e30b5d42341b

合约的代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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(address(this), msg.sender));
// 调用 recover 验证签名地址
address signer = messageHash.toEthSignedMessageHash().recover(signature);
return signer;
}
}

我们使用cast call的方式调用合约的verify(bytes)方法校验我们的签名是否正确:

1
cast call --rpc-url your_rpc_url 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "verify(bytes)(bool)" 0x125521c93d39fbed27f4df56e424c3dd8c8b26143276ee59525157e3582cac95067ab4008f7e82086ccd8eb73a48358d19faa39acab9ab6f7d9ae2e30b5d42341b --from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8

其中:

  • 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512,是合约地址
  • verify(bytes)(bool), 函数原型
  • 0x125521c93d39fbed27f4df56e424c3dd8c8b26143276ee59525157e3582cac95067ab4008f7e82086ccd8eb73a48358d19faa39acab9ab6f7d9ae2e30b5d42341b 待校验的签名信息
  • 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 表示msg.sender

调用的方法返回True,说明签名校验成功。

除了使用cast call的方式调用合约的方法外,还可以创建TestSig.t.sol,编写测试用例来验证签名是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/TestSig.sol";

contract TestSigTest is Test {
TestSig public testsig;
function setUp() public {
vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
vm.setNonce(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,1);
testsig = new TestSig();
}

function testsign() public {
bytes memory _word = hex"125521c93d39fbed27f4df56e424c3dd8c8b26143276ee59525157e3582cac95067ab4008f7e82086ccd8eb73a48358d19faa39acab9ab6f7d9ae2e30b5d42341b";
vm.prank(0x70997970C51812dc3A010C7d01b50e0d17dc79C8);
bool result = testsig.verify(_word);
console2.log(result);
}
}

同样返回的结果也是true.

总结

本篇文章就是对于nft使用签名校验的原理说明以及一个简单的案例展示。

参考:

https://mirror.xyz/xyyme.eth/-e1FodE7HZcwhw60VuGnbUue2SfCN4kn6JVg0JjCFS4

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/09/15/nft-verify/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow