以太坊712签名

25 阅读5分钟

本文介绍一下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完整流程

整个流程如下

  1. 定义好用到的types和types对应的具体的值,EIP712Domain一般是固定的
  2. 计算EIP712Domain的TypeHash
  3. 使用abi.encode(domainTypeHash, ...domain相关值)得到domain相关的类型和值的abi encode字节编码
  4. sha256("domain abi encode")得到domain相关的hash256
  5. 计算要签名的结构的TypeHash(struct hash),在上面的例子中为PERMIT_TYPEHASH
  6. 使用abi.encode(structHahs, ...要签名的结构体的值,按顺序)得到要签名的数据的类型和具体值的abi encode编码
  7. sha256("struct abi encode")得到要签名数据的hash值
  8. 拼接前缀和两个hash值相关的字节,然后取hash256,得到finalHash256, keccak256("\x19\x01" || domainSeparator || structHash(message))
  9. sign(finalHash256)得到rsv
  10. 合约端使用相同的方式,计算出最终要签名的hash256,finalHash256ByContract
  11. recover(finalHash256ByContract, rsv)得到签名的公钥,然后可以得到签名所用的地址,验签