什么是EIP
EIP-712则是代表712号提案,主要内容是对类型化结构化数据进行散列和签名的标准,而不仅仅是字节串。
为什么要使用EIP712
该EIP主要针对两个问题:
1. 提高链下消息签名在链上使用的可用性,节省gas;
2. 让用户知道他们在给什么数据进行签名。
在传统的dapp签名中,用户看到的往往是一串十六进制的数据,如下图:
这样使得签名消息在实用性和安全性取得重大进步,让用户对dapp的签名内容有所了解,而不是稀里糊涂的在恶意dapp签署签名。
规范和概念
EIP712文档中阐述了很多前置的签名和定义,但这些都可以忽略,下面直接讲解EIP712的规范和概念。内容很干,可以结合代码看。
NO.1
EIP712最终的可签名的hash生成公式
encode(domainSeparator : bytes32, message : Struct) = "x19x01" ‖ domainSeparator ‖ hashStruct(message)
可以看到encode接收两个参数,一个是domainSeparator,一个是message,而message的类型则是EIP712Struct。
所以这里的encode处理就是将"x19x01"、domainSeparator和hashStruct(message)拼接在一起。
domainSeparator、message和hashStruct将在下面讲解。
NO.2
Struct定义
struct Mail {
address from;
address to;
string contents;
}
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(
"x19x01",
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''))
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'x19x01', 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 原文始发于微信公众号(零鉴科技):以太坊标准——EIP712
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论