一
什么是EIP
EIP(Ethereum Improvement Proposals)是向以太坊社区提供信息或描述以太坊或其流程或环境的新功能的设计文档。
EIP 作为一个中心角色,记载以太坊的变化并且记载在以太坊中。它们是人们提议、辩论和适应变化的途径。
有各种不同类型的EIP,其中包括会影响共识并需要网络升级的用于底层协议更改的核心EIP,以及用于应用程序标准的ERC。
例如,创建token 的标准,ERC20 或 ERC721 允许应用程序使用相同的规则处理所有 token,使得创建互操作应用程序更加容易。
EIP-712则是代表712号提案,主要内容是对类型化结构化数据进行散列和签名的标准,而不仅仅是字节串。
二
为什么要使用EIP712
该EIP主要针对两个问题:
1. 提高链下消息签名在链上使用的可用性,节省gas;
2. 让用户知道他们在给什么数据进行签名。
在传统的dapp签名中,用户看到的往往是一串十六进制的数据,如下图:
而EIP712强调了一种对数据及其结构进行编码的方案,该方案允许在签名时将其显示给用户进行验证,让用户清楚的知道他们将要签署什么样的数据,如下图所示:
这样使得签名消息在实用性和安全性取得重大进步,让用户对dapp的签名内容有所了解,而不是稀里糊涂的在恶意dapp签署签名。
三
规范和概念
NO.1
EIP712最终的可签名的hash生成公式
encode(domainSeparator : bytes32, message : Struct) = "\\x19\\x01" ‖ domainSeparator ‖ hashStruct(message)
可以看到encode接收两个参数,一个是domainSeparator,一个是message,而message的类型则是EIP712Struct。
所以这里的encode处理就是将"\x19\x01"、domainSeparator和hashStruct(message)拼接在一起。
domainSeparator、message和hashStruct将在下面讲解。
NO.2
Struct定义
EIP712 Struct与常见的struct类似,我们可以定义一个简单的Mail Struct:
`struct Mail {` `address from;` `address to;` `string contents;` `}`
对于struct的要求如下:
a. 一个struct必须具有有效标识符作为名称,并且包含零个或者多个成员变量。成员变量具有成员类型和名称;
b. 成员类型可以是原子类型、动态类型或引用类型;
c. 原子类型是 bytes1 到 bytes32、uint8 到 uint256、int8 到 int256、bool 和 address。这些对应于它们在 Solidity 中的定义。请注意,没有别名 uint 和 int。请注意,合约地址始终是纯地址。标准不支持定点数。该标准的未来版本可能会添加新的原子类型;
d. 动态类型是字节和字符串。这些类似于用于类型声明的原子类型,但它们在编码中的处理方式不同;
e. 引用类型是数组和结构。数组要么是固定大小的,要么是动态的,分别用 Type[n] 或 Type[] 表示。结构是通过名称引用其他结构。该标准支持递归结构类型;
NO.3
hashStruct函数定义
hashStruct函数的定义为:
hashStruct(s : Struct) = keccak256(typeHash ‖ encodeData(s))
而typeHash函数为
typeHash = keccak256(encodeType(typeOf(s)))
所以
hashStruct(s : Struct) = keccak256(keccak256(encodeType(typeOf(s))) ‖ encodeData(s))
NO.4
encode Type 函数定义
encodeType函数定义:
encodeType(s : Strcut) = s.name ‖ "(" ‖ s.member₁ ‖ "," ‖ s.member₂ ‖ "," ‖ … ‖ s.memberₙ ")"
其中member = type ‖ " " ‖ name。
举个例子会更清晰点,对上面的 Mail进行encodeType:
encodeType(Mail) = Mail(address from,address to,string contents)
记得成员顺序是根据结构体定义时出现的顺序是一致的。
如果结构体中引用了其它结构体,则将后续引用的结构体按名称排序附加到编码中,例子如下:
Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)
NO.5
encode Data 函数定义
encodeData = enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)
编码的成员值按照它们在类型中出现的顺序串联。每个编码的成员值正好是 32 字节长。
编码中对于不同的类型有不同的规定,原子值编码如下:
a. Boolean false 和 true 分别编码为 uint256 值 0 和 1;
b. Address被编码为 uint160;
c. 整数值符号扩展为 256 位并以大端顺序编码;
d. bytes1 到 bytes31 是具有开头(索引 0)和结尾(索引长度 - 1)的数组,它们在 bytes32 的末尾补零并按从头到尾的顺序编码;上面的这些对应于它们在 ABI v1 和 v2 中的编码,即采用abi.encode(value₁, value₂, ..., valueₙ)。动态类型如下:
e. bytes 和 string 类型将会进行keccak256散列;
f. array 先进行encodeData处理,再进行keccak256散列。
所以对encodeData的实现大概如下:
encodeData(s) = abi.encode(a, b, c, keccak256(d), e, keccak256(f))
NO.6
domainSepator 函数定义
四
实例和实现
上面讲了那么多概念和定义,比较干,但这也是必要的。根据定义我们可以写出实现的代码,也是比较简单的。
接下来我会以两种代码形式,一种傻瓜式,一种是简便式。傻瓜式代码适用于不太想去了解各种定义细节,根据合约代码直接构造签名哈希。简便式代码适用于对EIP712比较熟悉的人,根据规则套入变量即可。
以CakeToken的delegateBySig为例,代码如下:
`bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");``bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");``string public name = "PancakeSwap Token";``function delegateBySig(` `address delegatee,` `uint nonce,` `uint expiry,` `uint8 v,` `bytes32 r,` `bytes32 s``)` `external``{` `bytes32 domainSeparator = keccak256(` `abi.encode(` `DOMAIN_TYPEHASH,` `keccak256(bytes(name())),` `getChainId(),` `address(this)` `)` `);`` ` `bytes32 structHash = keccak256(` `abi.encode(` `DELEGATION_TYPEHASH,` `delegatee,` `nonce,` `expiry` `)` `);`` ` `bytes32 digest = keccak256(` `abi.encodePacked(` `"\x19\x01",` `domainSeparator,` `structHash` `)` `);`` ` `address signatory = ecrecover(digest, v, r, s);` `require(signatory != address(0), "CAKE::delegateBySig: invalid signature");` `require(nonce == nonces[signatory]++, "CAKE::delegateBySig: invalid nonce");` `require(now <= expiry, "CAKE::delegateBySig: signature expired");` `return _delegate(signatory, delegatee);` `}`
1、傻瓜式代码
主要讲解附在代码注释中了:
`from web3 import Web3``from eth_account import Account``from eth_abi import encode_abi``import json``import requests``import time`` ``w3 = Web3(Web3.HTTPProvider('https://bsc-mainnet.web3api.com/v1/xxxxx'))``# contract addr``contract_addr = '0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82'``# bscscan abi key``api_key = 'xxxxxx'``# abi url``get_abi_url_temp = 'https://api.bscscan.com/api?module=contract&action=getabi&address={0}&apikey=' + api_key`` ``private_key = 'xxxx'``op_account = Account.from_key(private_key)``user_addr = op_account.address`` `` ``def get_contract_abi(contract_address, step=0):` `'''` `get contract abi from bsc api` `:param contract_address:` `:param step:` `:return:` `'''` `get_abi_url = get_abi_url_temp.format(contract_address)` `try:` `res = requests.get(get_abi_url).text` `res = json.loads(res)` `abi_res = res['result']` `return abi_res` `except Exception as e:` `print(e)`` `` ``def to_32byte_hex(val):` `return Web3.toHex(Web3.toBytes(val).rjust(32, b'\0'))`` `` ``abi = get_contract_abi(contract_addr)``# contract instance``contract = w3.eth.contract(address=contract_addr, abi=abi)`` ``# 过期时间,防止被利用``expire_time = int(time.time()) + 600``# 防止用户在同一个合约的不同授权出现一样的结果``user_nonces = contract.functions.nonces(user_addr).call()``chain_id = w3.eth.chain_id``# 随便挑选个地址``delegation_addr = '0xe2C8f362154aacE6144Cb9d96f45b9568e0Ea721'`` ``# -----------替换开始-----------``# 按照合约里的变量赋值``DOMAIN_TYPEHASH = Web3.keccak(text="EIP712Domain(string name,uint256 chainId,address verifyingContract)");``DELEGATION_TYPEHASH = Web3.keccak(text="Delegation(address delegatee,uint256 nonce,uint256 expiry)")`` ``# 进行abi encode``domain_abi_encode = encode_abi(['bytes32', 'bytes32', 'uint', 'address'], [DOMAIN_TYPEHASH, Web3.keccak(text='PancakeSwap Token'), chain_id, contract_addr]).hex()``delegation_abi_encode = encode_abi(['bytes32', 'address', 'uint256', 'uint256'], [DELEGATION_TYPEHASH, delegation_addr, user_nonces, expire_time]).hex()`` ``domain_separator = Web3.keccak(hexstr=domain_abi_encode)``struct_hash = Web3.keccak(hexstr=delegation_abi_encode)`` ``# get signable hash``msg = Web3.solidityKeccak(['bytes', 'bytes32', 'bytes32'], [b'\x19\x01', domain_separator, struct_hash]).hex()``# -----------替换结束-----------``print(msg)``# sign the eip712 hash``attribDict = w3.eth.account.signHash(msg, private_key=private_key)``# 或者使用``'''``signable_msg = SignableMessage(version=b'\x01', header=domain_seperator, body=struct_hash)``attribDict1 = w3.eth.account.sign_message(signable_message=signable_msg, private_key=private_key)``'''`` ``r = to_32byte_hex(attribDict['r'])``s = to_32byte_hex(attribDict['s'])``v = attribDict['v']`` `` ``print("user_addr: " + user_addr)``print("delegation: " + delegation_addr)``print("expiry: " + str(expire_time))``print("nonces: " + str(user_nonces))``print("r: " + r)``print("s: " + s)``print("v: " + str(v))`` ``# 发送交易``unsigned_tx = contract.functions.delegateBySig(delegation_addr, user_nonces, expire_time, v, r, s).buildTransaction({` `'gas': 100000,` `'gasPrice': w3.toWei('6', 'gwei'),` `'nonce': w3.eth.get_transaction_count(user_addr),` `'chainId': w3.eth.chain_id``})`` ``signed_tx = op_account.sign_transaction(unsigned_tx)``tx = w3.eth.send_raw_transaction(signed_tx.rawTransaction).hex()`` ``print("transaction hash: " + tx)`` ``# 获取ecrecover后的签名者并验证是否与签名者相同``transaction_log = w3.eth.wait_for_transaction_receipt(tx)``signatory = transaction_log.logs[0].topics[1].hex()`` ``print("signatory: " + signatory)`
从上述代码可以看出,该代码就是将solidity中的代码复现一遍,好处就是可以按照合约代码来,不需要做过多的理解,坏处就是不直观和繁琐。
2、简便式
利用已经有的 eip712-struct 库安装 eip712-structs。
pip install eip712-structs
使用 eip712-structs 库的代码如下:
`from eip712_structs import EIP712Struct, Address, String, Uint, Bytes`` ``class EIP712Domain(EIP712Struct):` `name = String()` `chainId = Uint(256)` `verifyingContract = Address()`` `` ``# 如果遇到保留关键字作为名称,可以使用:``# setattr(Delegation, 'from', Address())``# my_struct.values['from'] = user_addr``class Delegation(EIP712Struct):` `delegatee = Address()` `nonce = Uint(256)` `expiry = Uint(256)`` `` ``my_domain = EIP712Domain(name='PancakeSwap Token', chainId=chain_id, verifyingContract=contract_addr)``my_struct = Delegation(delegatee=delegation_addr, nonce=user_nonces, expiry=expire_time)`` ``msg = Web3.keccak(hexstr=my_struct.signable_bytes(my_domain).hex()).hex()`
上述代码就能很直接和轻松的获取可签名的msg,将该代码放到傻瓜式代码中的替换处,运行就可获得同样的结果。
五
总结
EIP712是一个用比较简单的方法实现可视化的结构数据签名,使用domainSepatator作为域分割符,让用户在不同的合约、不同的链上签出来的数据是不同的;再在 message 中添加自定义的数据和标识符,让用户知道自己在签署什么,并保证时效和唯一。
参考: