quentangle

Posted on Jan 10, 2023Read on Mirror.xyz

使用ECDSA签名来进行NFT铸造白名单校验

翻译:团长(https://twitter.com/quentangle_

什么是签名?

在进入代码之前,我认为更重要的是我们应该先尝试去理解其中的机制。我将尽我所能以这样的方式来解释签名,无论你的背景如何,是否有技术背景,在本文的结论中,你将有效地理解签名以及它们保证矿工真实性和包容性的方式。

在现实世界中,我们知道重要的文件,如银行贷款,肯定需要借款人在贷款人提供的某种形式的文件上签名,以有效地承认他们参与了某种形式的交易。在这之后,你会期望借款人从贷款人那里收到一些钱,因为一切都检查都验证了。

是的,我知道! 现实世界并不完美,只要花费一定的时间和精力,签名是可以伪造的,但核心概念仍然是一样的。一些文件(信息a message)是由贷款人(验证人the verifier)提供给借款人(签字人the signer)的。请在脑海中记下这3个术语,因为你会在文章中看到它们经常被提及。

链上交易也不例外。你可以把自己想象成一个信息提供者,向签字人提供一些数据。这些数据通常在签署前被处理过,通常是变成一个bytes32的哈希值(消息message)。然后这个哈希值被签名者使用签名者的私钥有效地签名,最终返回一个签名。从这一点开始,哈希值和签名都被提供给智能合约进行验证,在那里执行一些复杂的代码,并使用这两个信息恢复签名者的公钥(钱包地址)。

需要注意的重要事项

在继续之前,我认为澄清上一段中的一些要点是非常重要的。它们是…

  • 签名者只是一个普通的以太坊钱包,但它应该是一个被抛弃的钱包,从不用于在以太坊主网上进行交易。这样做的原因是,在某些时候,这个钱包的私钥需要存储在本地机器的RAM或ROM中的某个地方,这一点稍后会详细说明。

  • 为了完全验证签名,签名者的钱包地址在部署时或在稍后的时间点设置时被存储在智能合约中,并与恢复的地址进行比较。

  • 在向智能合约提供哈希值和签名之前的一切都在链外完成。由于我们将在白名单铸造的背景下研究签名,其中所有参与的钱包地址和造币数量都是提前知道的,这将是一个后端较少的实现。在公开铸造背景下实现签名将与下面提供的实现方式不同。

  • 本文展示的签名方式只需要将签名传递给铸造功能,因为在签名前创建的原始哈希值将被复制到链上。

背景介绍

说了这么多,我们现在可以开始研究技术层面的东西,以便更深入地了解签名和它们的工作方式。让我们首先提供一些我们的白名单方案的背景和它必须满足的要求。

  • 所有参与的钱包地址的知识是提前知道的。

  • 白名单地址最多可以通过一次交易或多次交易铸造3个代币。

  • 该实现必须防止签名重放攻击

签名脚本

如前所述,我们所采取的方法将是后端少(不需要API),只需要将签名传递给我们的智能合约,而不是签名和哈希。在我们的签名脚本中,我们将使用单一的关键信息来生成我们的bytes32哈希值,这将是白名单上的铸币者钱包地址,它将被存储在addresses.json文件中。

在这个哈希值生成过程中使用调用者钱包地址的好处是,我们知道msg.sender的值是不可改变的,不能被改变。由于哈希值将在链上使用该值进行复制,仅此一点就可以保护我们免受非白名单用户通过签名重放攻击的方式来利用我们的合约,即有效的签名被非白名单用户重复使用。

从sign()方法返回的签名对象

签名脚本本身是非常简单的,它只是重新创建一个字符串类型的消息,其中包含白名单的钱包地址,左侧没有0x前缀。然后使用Web3包的sign()方法,用签名者的私钥对该消息进行签名,最终返回一个对象(图2),包含原始消息、原始消息的哈希值、签名和其他各种信息。然后,这些数据的签名密钥值被放入一个JSON对象中,刚刚被签名的钱包地址作为我们的密钥。然后,这个JSON对象被写入某个任意命名的文件中,以后我们将在我们的前端使用。让我们把它称为signatures.json

运行脚本

技术启示#1

所以在这一点上,你可能有几个问题。为什么地址要存储在右侧?为什么左手边有24个前导零?让我解释一下。

在我继续之前,需要注意的是,bytes32类型变量的长度为64个字符(00=1字节),与uint256类型变量的大小相同,都是256位。最终,我们需要创建一个bytes32类型的消息。在设计上,Solidity是一种以big-endian格式存储字节的语言。这就意味着最重要的字节将被存储在最重要的内存位置。

由于Ethereum钱包地址的长度为40个十六进制字符,没有0x前缀,而一个十六进制字符相当于4个比特,我们可以确定,将地址存储在我们的bytes32变量中只会消耗32个可用字节中的20个(40 * 4 = 160比特/20字节)。由于00相当于一个字节,而原来的32个字节中有12个是未使用的,这就解释了为什么我们的信息有24个前导零。

网站实现

坦白讲我的前端经验极其有限,因为我在工作期间所做的大部分工作都与后端和/或智能合约严格相关。虽然我不能提供一个可视化的例子,但我将从白名单用户的角度向你解释流程。

  • 之前提到的signatures.json文件存储在你的前端。

  • 一个用户在你的白名单申请期间访问你的造币页面,并连接他们的钱包。在连接完成后,你的前端代码会获取连接的钱包地址,并尝试从signatures.json文件中返回相关签名。

  • 如果找到了签名,启用铸币按钮,并在铸币交易中提供这个签名作为参数。

  • 如果没有与所连接的钱包地址相关联的签名,请保持禁用铸币按钮,因为这不是一个白名单用户。

核实链上的签名

有趣的部分开始了,我们现在可以开始看看如何使用Open Zeppelin ECDSA库在链上验证这个签名以确认铸造者的加入。让我们先注意一下允许该库在此合约中使用的关键变化,请随时关注智能合约中的行号。

  • 第7行 — 导入声明,允许ECDSA库在我们的智能合约中使用。

  • 第11行 — ECDSA库与bytes32类型的变量的关联。

  • 第20行 — 定义_signerAddress的地址类型的私有变量。

  • 第25/27行 — 在构造函数中为_signerAddress变量设置一个值,这是可选的,可以在以后的时间点上用setter方法来完成。

  • 第35–40行 — 这是神奇发生的地方。这5行代码负责在链上复制我们的原始预签名信息,将该信息转换为以太坊签名信息,恢复签名者的公钥(钱包地址),并确认恢复的签名者地址与我们预期的签名者相符。

神奇发生的地方

让我们先关注一下第38行,同时参考一下图2。在这行代码执行后,bytes32的值将与消息的密钥值相同,这个前缀消息的keccak256哈希值(第35行)将与所示对象中的messageHash密钥匹配。在这个哈希值上调用ECDSA .recover()方法,记得我们将ECDSA库与bytes32类型的变量相关联,我们可以提供作为第二个参数传递给铸币函数的签名,以成功恢复签署此消息的账户的公钥(钱包地址)。

技术启示#2

所以在这一点上,你可能有一些很多疑问。关于为什么要在复制的消息前加上 \x19Ethereum Signed Message:\n32 这个前缀,我建议你看看 @RicMoo 写的这篇文章的开头,他以一种容易理解的方式解决了这个问题。这个前缀也没有包含在我们的签名脚本中,是因为Web3包的sign()方法会自动给我们传递的消息加上这个前缀。另外,你可能还会问自己,msg.sender值的铸造是怎么回事?让我解释一下。

计算机有时确实会说No

之所以msg.sender的值先是转成uint160然后是uint256,而不是直接投给uint256,是因为…嗯…你根本无法将一个地址类型直接转成uint256。然而,还有一点需要解释的是。如果你还记得之前提到的,一个没有0x前缀的Ethereum钱包地址正好是160比特长。因此,根据Solidity文档(图7),我们可以把msg.sender的值投到一个uint160。从这一点出发,我们可以把这个uint160转成uint256,然后再转成bytes32。这样做也为我们提供了正确的编码方式。

地址类型转换

最后的冲刺

鉴于上述所有情况都很顺利,并且向该函数提供了有效的签名,白名单上的用户将能够成功地铸造其分配的代币数量。该函数执行时不发生逆转的唯一方法是提供一个有效的签名,该签名与链上生成的消息有关,该消息来自msg.sender值。这将有效地消除非白名单用户调用此函数的任何机会,因为签名钱包账户没有被泄露,用户可以签署自己的消息。

结束语

你已经有了,关于如何有效使用NFT白名单铸造的签名的解释。在结束这篇文章之前,我只想对Jeffrey Scholz表示特别感谢。杰夫一直是这个领域的一个伟大的思想领袖,他写了关于使用签名的NFT白名单铸造的不同策略的惊人文章,我强烈建议查看他的 “NFT铸造节省gas的硬核方法 ”系列的第三部分。

总结一下吧。像您一样,我也在日复一日地学习,写这篇文章的动机源于想分享我进入Solidity世界的学习之旅。我希望您和我一样喜欢这篇文章,并期待着在下一篇文章中看到您。如果您学到了新的东西,或者喜欢阅读它,请随时鼓掌,并在Twitter上关注我,我经常在那里分享我学到的东西和我目前正在做的事情的更新。下一篇见! ✌️

原文链接:https://medium.com/@ItsCuzzo/using-signatures-ecdsa-for-nft-whitelists-ba0a4d070e92

NFT