本文介绍一下712签名,理解其最核心的原理
传统签名
eth_sign和personal_sign
eth_sign就是给什么签什么,很不安全,给你签的可能是某个转账行为的hash
personal_sign是默认给你在要签名的内容前加前缀,所以不会出现签的是一笔转账交易这种情况, 但是还是不够清晰,如果给你签的是一个hash,你也不知道这个hash背后是什么东西
712签名
712签名最主要的是支持签结构化的数据,也就是JSON,同时有强类型约束,同时可以指定链合约等防止签名用在不同的链上或合约里
Demo
一个712签名的数据的结构如下
const typedData = {
domain: {
name: "MyDApp",
version: "1",
chainId: 1,
verifyingContract: "0x123456..."
},
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
],
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
},
primaryType: "Permit",
message: {
owner,
spender,
value,
nonce,
deadline
}
}
domain
其中types里有EIP712Domain和Permit两个类型,其中EIP712Domain主要是指定了这个签名能用在什么地方, chainId指定只能用在这个链上避免跨链重放,verifyingContract指定只能用在这个合约,避免用在其他合约,还有name和version,表示只能用在这个版本,domain里是具体的EIP712Domain类型定义的值
Type message
其他的type,比如Permit是你定义的其他类型,然后有一个primaryType字段指定你的这个入口类型是什么, 这里是Permit,然后message是你真正要用到的数据,对应的是primaryType指定的类型
序列化编码
712签名就是如何处理上面的这个typeData,对它进行序列化编码,最后得到一个256位的hash值, 然后使用以太签名对这个hash值进行签名,得到rsv
712最终要签名的数据如下
keccak256(
"\x19\x01" || domainSeparator || structHash(message)
)
其中||是拼接操作符,表示将对应的字节拼接起来
"\x19\x01"这两个字节是一个固定前缀,表示这是一个712签名,和personal_sign的前缀是一个意思
domainSeparator是domain相关信息的hash256,structHash是结构化数据对应的hash256
将这三个值拼接起来,再计算一个最终要用于ESDSA签名的hash256
domainSeparator
domainSeparator的计算方式如下
keccak256( abi.encode( DOMAIN_TYPEHASH, name, version, chainId, verifyingContract ) )
keccak256是hash256算法,abi.encode是合约的ABI Encode算法,这个算法理解为是合约专用的一种编码方法就行了,可以编码也可以解码
里面的值是EIP712Domain对应的值,顺序不能乱, 然后这里需要注意的是有一个DOMAIN_TYPEHASH,这个hash是DOMAIN_TYPEHASH这个类型的哈希, 所以712里不仅对用到的数据签名,对数据对应的类型也需要计算进去一起签名, 可以认为DOMAIN_TYPEHASH是EIP712Domain对应的一个唯一的身份ID, 其计算方式如下
bytes32 constant DOMAIN_TYPEHASH =
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
得到的也是一个hash256,所以这样的话即使你实际签名的值相同,但是代表的类型不同,签出来最终的签名也是不一样的
TYPEHASH
上面的DOMAIN_TYPEHASH是一个Type Hash,Type Hash的计算方式都是一样的
bytes32 TYPEHASH = keccak256("StructName(type1 name1,type2 name2,...)");
把定于的类型拼接成一个字符串,然后取hash256,对应的名字顺序都不能错,空格也不能多不能少
structHash
理解了domainSeparator,structHash就好理解了,也就是要签名的结构话数据的类型和数据的一个hash, 在这个例子里如下
keccak256(
abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
)
)
其中PERMIT_TYPEHASH的计算方式如下
bytes32 constant PERMIT_TYPEHASH =
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
712完整流程
整个流程如下
- 定义好用到的types和types对应的具体的值,EIP712Domain一般是固定的
- 计算EIP712Domain的TypeHash
- 使用abi.encode(domainTypeHash, ...domain相关值)得到domain相关的类型和值的abi encode字节编码
- sha256("domain abi encode")得到domain相关的hash256
- 计算要签名的结构的TypeHash(struct hash),在上面的例子中为PERMIT_TYPEHASH
- 使用abi.encode(structHahs, ...要签名的结构体的值,按顺序)得到要签名的数据的类型和具体值的abi encode编码
- sha256("struct abi encode")得到要签名数据的hash值
- 拼接前缀和两个hash值相关的字节,然后取hash256,得到finalHash256, keccak256("\x19\x01" || domainSeparator || structHash(message))
- sign(finalHash256)得到rsv
- 合约端使用相同的方式,计算出最终要签名的hash256,finalHash256ByContract
- recover(finalHash256ByContract, rsv)得到签名的公钥,然后可以得到签名所用的地址,验签