以太坊上面签名随处可见,在智能合约中,也需要处理签名数据。
比如在Uniswap V2的代码中,可以传入一段签名数据,代码会判断数据的签名者是否是owner本身。
人们喜欢规范,于是就给智能合约中的签名数据制定了一个数据格式:EIP191。
结论
- EIP191编码是为了定义智能合约中签名数据的格式
- EIP712是EIP191的一种
- EIP712解决了重放攻击和结构体编码规范的问题
EIP191
EIP191的数据格式为:
0x19 <1 byte version> <version specific data> <data to sign>.
- 0x19是前缀
- 1个1字节的版本号
- 版本号特有的数据
- 签名数据本身
有了这个数据格式,人们就会先把数据按照EIP191的格式组装起来,然后进行签名。签名的数据在智能合约中也按照EIP191的格式进行验证。
目前一共有三个版本号:
Version byte | EIP | Description |
0x00 | 191 | Data with intended validator |
0x01 | 712 | Structured data |
0x45 | 191 | personal_sign messages |
version 0x00
这种版本的数据格式为:
0x19 <0x00> <intended validator address> <data to sign>
其中往往是合约地址。这样做的好处是签名仅对某合约有效,一定程度避免重放攻击。
比如你有一段数据“abc“需要签名,在合约地址0xffff中使用,则常见的步骤为:
- 拼接EIP191数据格式: data=0x19 0x00 0xffff abc
- 把拼接好的数据做哈希运算:hash = keccak256(data)
- 签名数据
- 把数据发送给合约,合约调用ecrecover计算出签名人地址
- 验证签名人是否合法
version 0x45
这种版本的数据格式为:
0x19 <0x45 (E)> <thereum Signed Message:\n" + len(message)> <data to sign>
注意,0x45对应的ascii编码就是字母E。这个版本其实是把personal_sign方案纳入到了EIP191中。
version 0x01
这个版本的就是EIP712。也就是EIP712其实也是EIP191的一种。
几个问题
- 为什么采用0x19作为前缀
以太坊中大量使用了RLP格式编码,为了和RLP编码区分,EIP191采用了0x19作为EIP191数据的前缀。
因为RLP编码的数据,如果是0x19作为前缀,其仅代表着一个单字节。不可能像EIP191一样,0x19后面跟着一串数据。因此EIP191数据和RLP编码的数据就能区别开来。
- EIP191中0x00的版本与0x45有什么区别
这是一个历史原因。一开始并没有EIP191格式,人们常用的是最初由 Geth 实现(https://github.com/ethereum/go-ethereum/pull/2940)的 personal_sign方案,数据格式为:
"\x19Ethereum Signed Message:\n" + length(message) + message
而人们往往会对message做哈希运算,因此更常见的格式为:
"\x19Ethereum Signed Message:\n32" + Keccak256(message)
在这基础之上拓展了EIP191,EIP191是以0x19为前缀,再加一个版本号,personal_sign方案并没有版本号,因此就把第一个字母“E”,作为版本号,曲线救国。
EIP712
EIP712是EIP191的一种。
EIP191有几个问题:
- 没有明确防止重访攻击的规定
- message如果是一个结构体,则没有对应的编码规范,开发者可以按照自己的方式进行编码,造成一些外部组件,比如钱包,无法解析编码
EIP712就是为了解决以上两个问题:
- 通过DOMAIN_SEPARATOR设定,防止重放攻击
- 规范对结构体编码的方式
首先直观的对比下差别:

左边是EIP91编码,可以看到message是一堆不可读的字符串,右边是EIP712编码,可以知道结构体的具体数据。
我们来具体看看EIP712如果做到的。
1. 通过DOMAIN_SEPARATOR设定,防止重放攻击
还记得EIP712是EIP191的一部分吗,我们看一下EIP191的数据格式:
0x19 <1 byte version> <version specific data> <data to sign>.
对EIP712而言,1 byte version对应的版本号就是0x01。
version specific data中,则存放着DOMAIN_SEPARATOR的哈希,DOMAIN_SEPARATOR是一个结构体,如下:
struct EIP712Domain{ string name, //用户可读的域名,如DAPP的名字 string version, // 目前签名的域的版本号 uint256 chainId, // EIP-155中定义的chain ID, 如以太坊主网为1 address verifyingContract, // 用于验证签名的合约地址 bytes32 salt // 随机数,这个往往被省略 }
有这个数据,即包括chainID,又包括合约地址,还包括app名字,版本号等数据,不可能被重放攻击了吧。
现在假设我们有一段数据“abc“需要通过EIP712签名,则步骤为:
- 准备前缀。prefix = 0x19 0x01
- 计算DOMAIN_SEPARATOR的哈希
DOMAIN_SEPARATOR_HASH = keccak256( abi.encode( // encodeType keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), // encodeData keccak256(bytes(name)), keccak256(bytes('1')), chainId, address(this) ) );
3. 拼在一起:
0x19 0x01 DOMAIN_SEPARATOR_HASH "abc"。
2. 规范对结构体编码的方式
上面的例子中,讲了DOMAIN_SEPARATOR的作用,然后用了一个非结构体“abc”举例,现在我们用来看看结构体怎么编码。
// Mail 是待签名的结构体 struct Mail { address from; address to; string contents; } //对结构体编码 messageHash = keccak256( abi.encode( keccak256("Mail(address from,address to,string contents)" mail.from, mail.to, keccak256(bytes(mail.contents)) ) );
可以看到messageHash中,把结构体名称,属性名称都编码进去了,因此钱包等第三方能够知道编码的结构体数据结构。解决了结构体编码的规范
再集合前面DOMAIN_SEPARATOR的例子,最终的EIP712编码就为:
0x19 0x01 DOMAIN_SEPARATOR_HASH messageHash
再把EIP712编码的数据求一个哈希,签名,就可以发送给智能合约了验证了。
Ref
回复 cytotec misoprostol 200mcg buy 取消回复