使用ECDSA签名并验证:
什么是ECDSA:
ECDSA可理解为以太坊、比特币对消息、交易进行签名与验证的算法与流程。
流程:
-
签名即
正向算法(消息 + 私钥 + 随机数)= 签名
,其中消息是公开的,私钥是隐私的,经过ECDSA正向算法可得到签名,即r、s、v
-
验证即
反向算法(消息 + 签名)= 公钥
,其中消息是公开的,签名是公开的,经过ECDSA反向算法可得到公钥,然后对比已公开的公钥。
签名交易:
签名方法分类:
可以把签名方法划分为三种:
-
通用消息签名方法;
-
EIP-191标准签名方法;
-
EIP-712标准签名方法;
通用签名方法就是添加了"\x19Ethereum Signed Message:\n"
这个字符串的签名,如metamask的personal_sign
方法和ECDSA库的toEthSignedMessageHash
方法,添加这个字符串只是单纯的为了表明这是以太坊的签名。
EIP-191提出了在签名中加入合约自身的address参数,以防止重放攻击的手法。
主要学习一下EIP-712
EIP-712:
为什么要使用EIP712:
该EIP主要针对两个问题:
-
提高链下消息签名在链上使用的可用性,节省gas;
-
让用户知道他们在给什么数据进行签名。
在传统的dapp签名中,用户看到的往往是一串十六进制的数据,如下图:
而EIP712强调了一种对数据及其结构进行编码的方案,该方案允许在签名时将其显示给用户进行验证,让用户清楚的知道他们将要签署什么样的数据,如下图所示:
EIP-712 结构解析:
对比我们上述"通用消息签名方法"中只是对要签名的参数进行序列化、keccak256、添加"\x19Ethereum Signed Message:\n32"后再次序列化与keccak256、签名
相比,EIP-712是有着结构化上的要求的。
EIP712最终的可签名的hash生成公式:
1 | encode(domainSeparator : bytes32, message : Struct) = "x19x01" ‖ domainSeparator ‖ hashStruct(message) |
这里的encode处理就是将"x19x01"
、domainSeparator
和hashStruct(message)
拼接在一起。
看看
domainSeparator
和hashStruct(meaasge)
具体实现。
签名域domainSeparator:
-
作用主要是保证不同的合约和链上的签名是不同的、隔离的。
两个 DApp 可能会产生相同的结构,这样
Transfer(address from,address to,uint256 amount)
就不应该兼容。通过引入域分隔符,dApp 开发人员可以保证不会出现签名冲突。
-
domainSeparator
由两部分组成,第一部分为对结构体的keccak256(encodeType),第二部分为结构体的具体实现(encodeData); -
domainSeparator
结构体如下所示,一般来说salt(随机数)会省略;
1 | struct EIP712Domain{ |
-
encodeType
与encodeData
都要按照如上的结构体顺序来实现,其中字段可以省略,但不可以颠倒顺序 -
针对string 或者 bytes 等动态类型,即长度不定的类型,其取值为
keccak256(string) 、 keccak256(bytes)
即内容的hash值; -
我们可以看到这里用的是
abi.encode
而非abi.encodePacked
,这是因为domainSeparator
结构体要求每个字段占据256位,以便于前端分割。
1 | DOMAIN_SEPARATOR = keccak256( |
签名对象hashStruct(message):
一般来讲,hashStruct(message)
与domainSeparator
格式相同,也是由两部分组成,第一部分为对自定义结构体的keccak256(encodeType)
,第二部分为自定义结构体的具体实现(encodeData)
;
由注释可知,PERMIT_TYPEHASH就是Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
的hash,注意自定义对象名要首字母大写;
encodeData与encodeType顺序要相同;
1 | // https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol |
小结:
-
签名域domainSeparator
: 该签名可用于哪个链的哪个合约,限定的范围 -
签名对象hashStruct(message)
: 该msg用于哪个具体的函数
其中r,s,v
就相当于是签名
1 | const sig = signature.value; |
Compound委托实例:
通过实例来加深理解。教程: https://medium.com/compound-finance/delegation-and-voting-with-eip-712-signatures-a636c9dfec5e
通过签名功能的用户的一个主要好处是他们可以免费创建签名委托或投票交易,并让受信任的第三方花费 ETH 支付gas费并将其写入区块链。
步骤:
测试代码:https://github.com/jerrychan807/my-awesome-solidity/blob/main/sign/README.md
部署Compound代币合约:
1 | # 启动本地节点 |
部署完Compound代币合约后,编写转账用例,AB用户均持有comp
代币
启动http服务器:
1 | python -m http.server |
用户A在链下创建签名
接着把签名数据给用户B,由用户B去支付gas写入链上。
1 | # 测试结果 |
实现了用户A在链上免费生成签名,再由别的用户付gas fee写到链上。
源码:
1 | // https://etherscan.io/address/0xc00e94Cb662C3520282E6f5717214004A7f26888#code |