长亭百川云 - 文章详情

以太坊标准——EIP712

零鉴科技

60

2024-07-14

什么是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签署签名。

规范和概念

EIP712文档中阐述了很多前置的签名和定义,但这些都可以忽略,下面直接讲解EIP712的规范和概念。内容很干,可以结合代码看。

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))

所以hashStruct函数就是,给定一个Struct,首先将其encodeType进行哈希,并与其encodeData拼接成为一个字符串,最后再对这个字符串进行哈希。

那么接下来就讲解一下什么是encodeType和encodeData。

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 函数定义

‍‍‍domainSeparator 是 EIP712 中非常重要的概念,它的作用主要是保证不同的合约和链上的签名是不同的、隔离的。   

domainSepatator的公式如下:   

domainSeparator = hashStruct(eip712Domain)    

eip712Domain类型是Struct,并具有以下一个或多个字段: 

a. string name, 签名域的名称;   

b. string version,签名域的主要版本,现在都是1;   

c. uint256 chainId;    

d. address verifyingContract, 将要验证该签名的合约; 

e. bytes32 salt。   

需要注意的一点是,eip712Domain 里的成员必须按照上面的顺序,新增字段的添加必须按字母顺序排列并在上述字段之后。 ‍‍‍

实例和实现

上面讲了那么多概念和定义,比较干,但这也是必要的。根据定义我们可以写出实现的代码,也是比较简单的。

接下来我会以两种代码形式,一种傻瓜式,一种是简便式。傻瓜式代码适用于不太想去了解各种定义细节,根据合约代码直接构造签名哈希。简便式代码适用于对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 中添加自定义的数据和标识符,让用户知道自己在签署什么,并保证时效和唯一。

参考:

1.https://eips.ethereum.org/EIPS/eip-712

2.https://github.com/ConsenSysMesh/py-eip712-structs

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2