背景

在目前NFT概念国内外火爆的背景下,涌现了很多项目,特别是公链以太坊上,社区与新团队更是层出不穷,让人眼花缭乱。

而一个新项目上线的成功与否,往往与其社区支持力度息息相关。现在很多新项目方为了拥有更多的热度,人为的设置了白名单这个玩法和门槛,于是我们可以看到Discord频道里的人们为了肝白,可以绞尽脑汁、废寝忘食。毕竟,拿到了白名单的人,是会被承诺可以提前pre mint,对于热门项目来说,这几乎是个稳赚不赔的投资。

而对于ERC721标准协议的内容来说,并没有白名单这个说法,那么从技术的角度来说,是怎么实现这个功能的呢。实际上,这里还是个逐渐演进的过程。

白名单

最早,在有项目方开始逐渐使用白名单机制的时候,由于白名单一般只给出几百个,所以实现方式还是比较原始的。而因为当时普遍的项目架构都是前端网页调用智能合约就完事了,并没有引入后端进来,所以做法往往是把白名单地址列表由项目方直接写入到合约中,然后用户在发起pre mint请求时,方法里会判断用户地址是否在该地址列表中。

这种方式从原理上当然没有问题,而且也体现了区块链不可篡改、公开透明的特性。不过由于以太坊上高昂的gas费,以及目前白名单人数一般都是数以千计,所以为了自身成本考虑,几乎所有项目都逐渐放弃了这种方式,而是改用另外两种机制:

  1. 链下(即后端服务)对单个白名单地址签名,合约只需存储签名地址。
  2. 对白名单地址列表整体构建Merkle树,合约只需存储Merkle的root hash。

本文对第一种链下签名,链上验证的方式进行阐述与实践。需要用户对solidity语言和区块链概念有一定的了解。
整体流程大致为:

  1. 用户在前端网页操作发起pre mint时,弹出信息提示用户对该请求进行签名
  2. 请求(包含地址、签名、签名内容)发到后端,校验签名后,查询地址是否在白名单列表中。
  3. 如果确实存在,由后端特定地址的私钥对用户地址进行签名,然后把该签名返回给前端。
  4. 前端调用钱包,把后端返回的签名数据作为参数传给合约pre mint方法
  5. 合约验证该签名确实是后端特定地址签署的,并且内容与用户地址吻合,则通过校验,并且保存该地址到合约中,避免用户重复发起。

合约

在流行的第三方库OpenZeppelin中,实际上已经实现了合约验证的方法,用户的自定义合约里只有引入ECDSA这个library即可。验证签名的源代码如下:

function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {// Check the signature length// - case 65: r,s,v signature (standard)// - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._if (signature.length == 65) {bytes32 r;bytes32 s;uint8 v;// ecrecover takes the signature parameters, and the only way to get them// currently is to use assembly.assembly {r := mload(add(signature, 0x20))s := mload(add(signature, 0x40))v := byte(0, mload(add(signature, 0x60)))}return tryRecover(hash, v, r, s);} else if (signature.length == 64) {bytes32 r;bytes32 vs;// ecrecover takes the signature parameters, and the only way to get them// currently is to use assembly.assembly {r := mload(add(signature, 0x20))vs := mload(add(signature, 0x40))}return tryRecover(hash, r, vs);} else {return (address(0), RecoverError.InvalidSignatureLength);}}
}

具体的验证逻辑不在此详述,方法里主要的逻辑是涉及到了签名的结构,因为以太坊中签名是由r、s、v长度固定三部分构成,所以这里通过长度来还原,然后可以还原出签署该签名的地址。
注意到方法参数,第一个名为hash,固定长度32字节,也就是说后端应对某个hash值(后文会提到)进行签名。

校验签名的合约示例代码如下:

address signer = 0xXXXX;function _verify(bytes32 dataHash, bytes memory signature, address account) private pure returns (bool) {return dataHash.toEthSignedMessageHash().recover(signature) == account;
}function pubVerify(bytes memory signature, bytes32 msgHash) public view returns (bool) {bool r = _verify(msgHash, signature, signer);return r;
}

注意:

  1. signer为写在合约里的后端特定地址
  2. recover方法包装了上文的tryRecover
  3. 对签名原文msgStr进行了hash(即keccak256)后,又使用了toEthSignedMessageHash方法进行处理。这是因为由于多链的存在,以太坊规范里要求拼接一段的前缀。在OpenZeppelin库的方法源代码如下:
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s));
}
  1. 上面有2个重载方法,如果是第一种,在拼接之后,又对整体取了一次hash,所以对应后端也要做2次hash。如果是第二种,则不需要对原文取hash,直接传入即可。但是由于交易参数公开透明,也为了保护用户的隐私,往往不希望传原文,所以这里我们选择第一种。

后端

后端整体逻辑上是根据合约的验证流程,对原文数据进行签名处理。这里选用java中常用的web3j库来处理。整体签名代码如下:

import org.web3j.crypto.*;
import org.web3j.utils.Numeric;
import org.web3j.crypto.Sign.SignatureData;
public static String sign(String msg,  String pwd, String path){try {Credentials ownerCredentials = WalletUtils.loadCredentials(pwd, path);byte[] sha3Msg = Hash.sha3(msg.getBytes());Sign.SignatureData signMessage = Sign.signPrefixedMessage(sha3Msg, ownerCredentials.getEcKeyPair());byte[] signatureBytes = new byte[65];System.arraycopy(signMessage.getR(),0, signatureBytes,0, signMessage.getR().length);System.arraycopy(signMessage.getS(),0, signatureBytes,32, signMessage.getS().length);signatureBytes[64] = signMessage.getV()[0];return Numeric.toHexString(signatureBytes);}catch (Exception e){log.error(e.getMessage());}return null;}

步骤为:

  1. 通过密码和keystore文件路径加载本地钱包,即前文提到的后端特定地址。
  2. 对原文取hash(即Hash.sha3,等同于合约中的keccak256)
  3. 通过私钥,对原文进行带特定前缀的签名
  4. 使用签名的rsv字段,构建签名的16进制字符串
  5. 把16进制的签名以及原文hash返回给前端即可

优势

不仅是白名单,只要是需要项目方提供数据的场景,都可以用这种链下签名,链上验证的方式。而很多链游就是这么做的,比如游戏内很多赚取游戏币(Token)的场景,本身是没有上链的,只是和传统游戏一样,保存在了后端的数据库中,而当用户真正想要提取币,转到交易市场的时候,就可以提交请求,由游戏后端服务器来查询该用户可以提取的数量,签名后发到合约里进行链上Token的转移。

缺陷

这种签名验证方式,需要项目方把keystore文件保存在服务器上,而密码很多项目方都直接是写在配置文件中,甚至直接把私钥保存到服务器上,安全性得不到保障,一旦泄漏,攻击者可以发起任何签名相关的攻击请求。

参考

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol

https://blog.csdn.net/topc2000/article/details/119921231

Solidity合约中签名验证的一点实践相关推荐

  1. 面向开发者:Yul 与 Solidity 合约比较

    面向开发者:Yul 与 Solidity 合约比较 概述 在本文中,我将使用Remix IDE,并将提供一些带有完整源代码的要点.虽然我将解释本文中使用的每个操作码,但最好阅读文档并在手边准备一个操作 ...

  2. 区块链智能合约solidity的中的一些关键字

    目  录 pragma mapping msg对象 block对象 contract constructor struct 数据地址 地址类型 address payable revert 以下场景使 ...

  3. Solidity合约记录——(三)如何在合约中对操作进行权限控制

    合约中一般会有多种针对不同数据的操作:例如对于存证内容的增加.更新及查询,若不进行一套符合要求的权限控制,事实上整个合约在真实环境下是没有多少使用价值的.那么应当如何对合约的权限进行划分?我们针对So ...

  4. solidity智能合约中tx.origin的正确使用场景

    简介 tx.origin是Solidity的一个全局变量,它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址.在智能合约中使用此变量进行身份验证会使合约容易受到类似网络钓鱼的攻击. 但针对tx. ...

  5. 以太坊智能合约开发:Solidity语言中变量的存储位置与作用域

    在Solidity中,有一些数据类型是引用类型,如: 数组(string和bytes是特殊的数组,也是引用类型) 结构体(struct) 映射(mapping) 在Solidity中使用引用类型的时候 ...

  6. 以太坊智能合约中随机数预测

    一.前言 作为首次币发行(ICO)的平台,以太坊已经获得了极大的普及. 但是,它不仅仅用于 ERC20 通证,轮盘,彩票和纸牌游戏都可以使用以太坊区块链实现. 与任何区块链实施一样,以太坊是不可逆的, ...

  7. 以太坊智能合约开发-《精通以太坊智能合约开发》学习总结实践

    文章目录 一.初探以太访智能合约 1. remix小demo 2. 写智能合约用的编程语言 二.以太坊核心概念 1. 交易/事务( Transaction ) 2. 区块 3. 共识协议:工作量证明( ...

  8. 来自智能合约中的威胁:去中心化应用安全威胁Top10榜单

    NCC Group 发起了一个名为 2018 年去中心化应用安全 Top10 ( Decentralized Application Security Project)的项目.据悉,该项目会与类似于 ...

  9. 阿里妈妈品牌广告中的 NLP 算法实践

    导读:本次分享的主题为阿里妈妈品牌广告中的 NLP 算法实践,主要内容包括: 1. 品牌广告业务模式与技术架构的简要介绍 2. NLP 算法在品牌搜索广告中的实践,以两个具体的算法问题展开:品牌意图识 ...

最新文章

  1. 中小企业网络结构设计1(华为版)
  2. yum提示Error: rpmdb open failed
  3. ecshop支持mysql5.5吗,centos5.5 安装配置 ecshop【nginx + php + mysql】
  4. OpenCASCADE:Modeling Algorithms模块几何工具之来自约束的曲线和曲面
  5. why we need getCoreClasses()
  6. 从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
  7. jzoj3362,bzoj3758-[NOI2013模拟]数数【分段打表,背包,状压】
  8. 使用Git推送代码到GitHub远程仓库
  9. 公众号openid能做用户识别_四川养老公众号开发哪里能做
  10. 什么舱位_飞机的舱位究竟是怎么一回事儿
  11. idea如何恢复默认的keymap
  12. 程序员理想中的工作环境是什么样的?
  13. MT【217】韦达定理应用
  14. 【推荐系统】User-Item CF:GC-MC
  15. SkyWalking Agent数据采集和上报原理浅析
  16. Hadoop-HDFS
  17. 权限管理系统(包括审批流程)数据库设计图
  18. 爬虫(八十八)lxml库的用法
  19. Matlab遗传算法求函数最大值
  20. 七牛云 转码_七牛上传视频并转码

热门文章

  1. 202x年基于区块链的生鲜食品溯源解决方案(专业完整版).pdf
  2. 【转】饭统网倒闭:不创新、不放权就是作死
  3. python -- 内存管理
  4. 调薪之后该思考的问题
  5. 欢迎参加 2015 Autodesk 产品开发培训课程 (2015/8/17开始)
  6. Adversarial Sample misleading the Model(生成对抗样本迷惑模型)
  7. 百度官方:网站被降权后会立即恢复吗?
  8. 数据库/SQL初学者看过来!试试资深DBA推荐的八款Web版SQL工具,免费又简单!
  9. 「JavaSE」-基础语法
  10. 浪潮服务器没有报警灯开机无显示,开机后电源正常,显示器不亮,也没有自检的声音,而且开机后也没...