摘要

这是一个用于对类型化结构化数据(而不仅仅是字节串)进行哈希和签名的标准。它包括:

  • 理论框架:用于确保编码函数的正确性
  • 结构化数据规范:与 Solidity 结构体类似且兼容
  • 安全哈希算法:用于这些结构体的实例
  • 安全纳入机制:将这些实例纳入可签名消息集合
  • 可扩展的域分离机制
  • 新的 RPC 调用eth_signTypedData
  • EVM 中哈希算法的优化实现(可选)

不包含重放攻击保护

动机

如果只关心字节串,数据签名是一个已解决的问题。但在现实世界中,我们关心的是复杂且有意义的消息。对结构化数据进行哈希处理并非易事,错误会导致系统安全属性的丧失

因此,“不要自己发明加密算法”(don‘t roll your own crypto)的格言在此适用。我们需要使用经过同行评审、充分测试的标准方法。本 EIP 旨在成为这个标准

本 EIP 旨在提高链下消息签名在链上使用的可用性。链下消息签名因其节省 Gas 和减少链上交易数量而被广泛采用。目前,已签名的消息对用户来说是一个不透明的十六进制字符串,几乎没有关于消息的上下文信息

本提案概述了一种编码数据及其结构的方案,允许在签名时向用户显示信息以供验证

本协议提出前的签名样式: 本协议提出后的签名样式


规范

明白,你是要 Markdown 语法的笔记,但内容保持学术/规范风格,不要花里胡哨。下面是 纯 Markdown、可直接粘到笔记里的版本

可签名消息集合的扩展

以太坊原本支持对以下两类数据进行签名:

  • 交易(𝕋)
  • 任意字节串(𝔹⁸ⁿ)

EIP-712 引入了结构化数据(𝕊)作为新的可签名对象,因此可签名消息集合扩展为:

𝕋 ∪ 𝔹⁸ⁿ ∪ 𝕊

不同类型消息的编码规则

为了便于哈希与签名,不同类型的消息在签名前会被编码为字节串。

1. 交易(𝕋)

encode(transaction) = RLP_encode(transaction)

交易签名直接对交易的 RLP 编码结果进行签名,这是以太坊最基础的交易签名方式,用于转账和合约调用。

2. 普通字节串消息(𝔹⁸ⁿ)

encode(message) =
"\x19Ethereum Signed Message:\n"  len(message)  message

其中 len(message) 是消息字节长度的非零填充 ASCII 十进制编码

该编码方式对应 eth_sign / personal_sign,通过加入固定前缀,防止普通消息被误解释为交易数据

3. 结构化数据(𝕊,EIP-712)

encode(domainSeparator, message) =
"\x19\x01"  domainSeparator  hashStruct(message)

说明:

  • \x19\x01:EIP-712 固定前缀,用于区分结构化数据签名
  • domainSeparator:域分隔符,用于唯一标识合约和链,防止跨链、跨合约重放攻击
  • hashStruct(message):对结构化数据按照其类型定义进行哈希

类型化结构化数据 𝕊 的定义

下面是把你给的这一整段 连贯整理成一份 Markdown 笔记,保持规范语气,结构清晰,便于后续复习 EIP-712。


Definition of Typed Structured Data 𝕊

为了定义结构化数据(𝕊)的全集,规范首先从可接受的数据类型入手进行定义。这些类型在设计上与 ABIv2 中的类型体系高度相关,并且与 Solidity 的类型系统非常接近。为便于说明,规范采用 Solidity 的类型记法来解释相关定义

该标准是 针对以太坊虚拟机(EVM) 设计的,但其目标是 不依赖任何具体的高层编程语言。只要遵循相同的类型、编码和哈希规则,不论使用何种语言,都可以生成与链上验证逻辑一致的结构化数据签名

示例

struct Mail {
    address from;
    address to;
    string contents;
}

结构体类型(Struct Type)

  • 结构体类型由一个合法的标识符作为名称
  • 包含零个或多个成员变量
  • 每个成员变量由 成员类型成员名称 组成

成员类型(Member Type)

成员类型可以是以下三类之一:

  • 原子类型(Atomic Type)
  • 动态类型(Dynamic Type)
  • 引用类型(Reference Type)

原子类型(Atomic Types)

原子类型包括:

  • bytes1bytes32
  • uint8uint256
  • int8int256
  • bool
  • address

这些类型与 Solidity 中的定义一致。需要注意的是:

  • 不存在 uintint 这样的别名,必须显式指定位宽
  • 合约地址始终使用 address 类型
  • 不支持定点数(fixed-point numbers)
  • 未来版本的标准可能会引入新的原子类型

动态类型(Dynamic Types)

动态类型包括:

  • bytes
  • string

在类型声明层面,它们与原子类型类似,但在实际编码过程中具有不同的处理方式


引用类型(Reference Types)

引用类型包括:

  • 数组(Array)
  • 结构体(Struct)

数组可以是:

  • 固定长度数组:Type[n]
  • 动态长度数组:Type[]

结构体通过其名称引用其他结构体。标准支持递归结构体类型,即结构体中可以包含对自身或其他结构体的引用


结构化数据集合 𝕊

结构化数据集合 𝕊 包含所有结构体类型的所有实例。 只要一个数据实例符合上述类型系统的定义,就属于 EIP-712 所定义的结构化数据范畴


hashStruct 的定义

hashStruct 函数定义如下: hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s)) 其中 typeHash = keccak256(encodeType(typeOf(s)))

注意typeHash 是给定结构体类型的常量,无需在运行时计算

encodeType 的定义

结构体的类型被编码为:name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")" 其中每个成员写作 type ‖ " " ‖ name

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)

encodeData 的定义

结构体实例的编码是 enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ),即成员值编码按照它们在类型中出现的顺序连接。每个编码的成员值正好是 32 字节长

编码规则

  • 原子值
    • boolfalsetrue 分别编码为 uint256 值 0 和 1
    • address:编码为 uint160
    • 整数值:符号扩展至 256 位,并按大端序编码
    • bytes1bytes31:作为数组处理,从开头(索引 0)到结尾(索引长度-1),在末尾用零填充至 bytes32,并按从头到尾的顺序编码。这对应于它们在 ABI v1 和 v2 中的编码
  • 动态值 (bytesstring):编码为其内容的 keccak256 哈希值
  • 数组值:编码为其内容 encodeData 串联的 keccak256 哈希值(即 SomeType[5] 的编码与包含五个 SomeType 类型成员的结构体相同)
  • 结构体值:递归编码为 hashStruct(value)对于循环数据,此操作未定义

domainSeparator 的定义

domainSeparator = hashStruct(eip712Domain) 其中 eip712Domain 的类型是一个名为 EIP712Domain 的结构体,包含以下一个或多个字段。协议设计者只需包含对其签名域有意义的字段,未使用的字段则从结构体类型中省略

标准字段(建议顺序)

  1. string name:签名域的可读名称,例如 DApp 或协议的名称
  2. string version:签名域的当前主版本号。不同版本的签名不兼容
  3. uint256 chainId:EIP-155 链 ID。如果不匹配当前活动链,用户代理应拒绝签名
  4. address verifyingContract:将验证签名的合约地址。用户代理可以进行合约特定的钓鱼防护
  5. bytes32 salt:协议的消歧义盐值。这可以作为最后的域分隔符

注意

  • 未来扩展可以添加具有新用户代理行为约束的新字段
  • 用户代理应接受 EIP712Domain 类型指定的任何顺序的字段
  • EIP712Domain 字段应按上述顺序排列,跳过任何不存在的字段。未来添加的字段必须按字母顺序排列,并放在上述字段之后

eth_signTypedData JSON RPC 规范

方法 eth_signTypedData 被添加到以太坊 JSON-RPC 中,与 eth_sign 类似

eth_signTypedData

签名方法通过 sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))) 计算以太坊特定签名

注意:用于签名的地址必须已解锁

参数

  1. Address - 20 字节 - 将签名消息的账户地址
  2. TypedData - 要签名的类型化结构化数据

TypedData JSON 模式定义

{
  "type": "object",
  "properties": {
    "types": {
      "type": "object",
      "properties": {
        "EIP712Domain": {"type": "array"}
      },
      "additionalProperties": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "name": {"type": "string"},
            "type": {"type": "string"}
          },
          "required": ["name", "type"]
        }
      },
      "required": ["EIP712Domain"]
    },
    "primaryType": {"type": "string"},
    "domain": {"type": "object"},
    "message": {"type": "object"}
  },
  "required": ["types", "primaryType", "domain", "message"]
}

返回

DATA:签名。与 eth_sign 一样,它是一个以 0x 开头的十六进制编码的 65 字节数组。它按大端序编码黄皮书附录 F 中的 r, s, v 参数。字节 0…32 包含参数 r,字节 32…64 包含参数 s,最后一个字节包含参数 v。注意,参数 v 包含了 EIP-155 指定的链 ID

示例

请求:

curl -X POST \
  --data '{
    "jsonrpc": "2.0",
    "method": "eth_signTypedData",
    "params": [
      "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
      {
        "types": {
          "EIP712Domain": [
            { "name": "name", "type": "string" },
            { "name": "version", "type": "string" },
            { "name": "chainId", "type": "uint256" },
            { "name": "verifyingContract", "type": "address" }
          ],
          "Person": [
            { "name": "name", "type": "string" },
            { "name": "wallet", "type": "address" }
          ],
          "Mail": [
            { "name": "from", "type": "Person" },
            { "name": "to", "type": "Person" },
            { "name": "contents", "type": "string" }
          ]
        },
        "primaryType": "Mail",
        "domain": {
          "name": "Ether Mail",
          "version": "1",
          "chainId": 1,
          "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
        },
        "message": {
          "from": {
            "name": "Cow",
            "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
          },
          "to": {
            "name": "Bob",
            "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
          },
          "contents": "Hello, Bob!"
        }
      }
    ],
    "id": 1
  }'

结果:

{
  "id":1,
  "jsonrpc": "2.0",
  "result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
}

personal_signTypedData

还应有一个相应的 personal_signTypedData 方法,它接受账户密码作为最后一个参数

Web3 API 规范

向 Web3.js 版本 1 添加了两个方法,分别与 web3.eth.signweb3.eth.personal.sign 方法对应

web3.eth.signTypedData

web3.eth.signTypedData(typedData, address [, callback]) 使用特定账户签名类型化数据。该账户需要解锁。

参数、返回、示例: 同上

web3.eth.personal.signTypedData

web3.eth.personal.signTypedData(typedData, address, password [, callback])web3.eth.signTypedData 相同,但增加了一个 password 参数,类似于 web3.eth.personal.sign

原理阐述

  1. 编码扩展encode 函数扩展了一个处理新类型的情况。编码的首字节区分不同情况。这是为了防止与 RLP 编码的交易等发生冲突
  2. 域分隔符 (domainSeparator)
    • 防止碰撞:不同 DApp 可能设计出相同的结构体。域分隔符保证了签名不会碰撞
    • 支持多用例:同一结构体实例在同一 DApp 中可用于多个不同的签名场景(例如,需要 fromto 双方签名),通过提供不同的域分隔符来区分
    • 替代方案考量:仅使用目标合约地址作为域分隔符可以解决第一个问题,但无法解决第二个用例
  3. 类型哈希 (typeHash)
    • 设计目标:旨在成为 Solidity 中的编译时常量
    • 被拒绝的替代方案
      • ABIv2 函数签名(4字节):碰撞风险高,长度不足
      • 256位 ABIv2 函数签名:捕获类型信息,但语义信息不足,已导致 ERC-20 与 ERC-721 transfer 函数碰撞
      • 扩展的 256位 ABIv2 签名(含参数和结构体名):字符串长度可能呈指数增长,且不允许递归结构体类型
      • 包含 NatSpec 文档:使文档变更成为破坏性更改,过于冗长
  4. 数据编码 (encodeData)
    • 设计目标:便于在 Solidity 中实现 hashStruct,并允许在 EVM 中进行高效的内存原地计算
    • 被拒绝的替代方案
      • 紧凑打包:需要复杂的 EVM 打包指令,不支持原地计算
      • ABIv2 编码:本身不满足确定性安全标准(同一数据有多个有效编码),不支持原地计算
      • typeHash 移出 hashStruct 并与域分隔符合并:效率更高,但会使 Solidity 的 keccak256 哈希函数的语义失去单射性
      • 支持循环数据结构:当前标准针对树状结构优化。支持循环结构需要维护路径栈,实现复杂,且会破坏组合性(成员值的哈希将依赖于路径)。未来可以以兼容的方式扩展标准来定义循环数据的哈希

向后兼容性

  • RPC 调用、Web3 方法和 SomeStruct.typeHash 参数目前是未定义的。定义它们不应影响现有 DApp 的行为
  • Solidity 表达式 keccak256(someInstance)someInstance 是结构体 SomeStruct 的实例)是有效语法。它当前计算的是实例内存地址的 keccak256 哈希。此行为应被视为危险。依赖于当前行为的 DApp 应被视为已损坏

安全考量

  1. 重放攻击:本标准仅涉及签名消息和验证签名。在实际应用中,签名消息通常用于授权操作。实现者必须确保当应用看到相同的签名消息两次时行为正确(例如,拒绝重复消息或授权操作应是幂等的)。如何实现是应用特定的,不在本标准范围内
  2. 抢先交易攻击:可靠广播签名的机制是应用特定的。当签名被广播到区块链以供合约使用时,应用必须防范抢先交易攻击(攻击者拦截签名并在原始预期使用之前将其提交给合约)。应用应能在攻击者首先提交签名时正确行为(例如,拒绝它或产生与签名者预期完全相同的效果)