摘要
这是一个用于对类型化结构化数据(而不仅仅是字节串)进行哈希和签名的标准。它包括:
- 理论框架:用于确保编码函数的正确性
- 结构化数据规范:与 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)
原子类型包括:
bytes1到bytes32uint8到uint256int8到int256booladdress
这些类型与 Solidity 中的定义一致。需要注意的是:
- 不存在
uint或int这样的别名,必须显式指定位宽 - 合约地址始终使用
address类型 - 不支持定点数(fixed-point numbers)
- 未来版本的标准可能会引入新的原子类型
动态类型(Dynamic Types)
动态类型包括:
bytesstring
在类型声明层面,它们与原子类型类似,但在实际编码过程中具有不同的处理方式
引用类型(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 字节长
编码规则:
- 原子值:
bool:false和true分别编码为uint256值 0 和 1address:编码为uint160- 整数值:符号扩展至 256 位,并按大端序编码
bytes1到bytes31:作为数组处理,从开头(索引 0)到结尾(索引长度-1),在末尾用零填充至bytes32,并按从头到尾的顺序编码。这对应于它们在 ABI v1 和 v2 中的编码
- 动态值 (
bytes和string):编码为其内容的keccak256哈希值 - 数组值:编码为其内容
encodeData串联的keccak256哈希值(即SomeType[5]的编码与包含五个SomeType类型成员的结构体相同) - 结构体值:递归编码为
hashStruct(value)。对于循环数据,此操作未定义
domainSeparator 的定义
domainSeparator = hashStruct(eip712Domain)
其中 eip712Domain 的类型是一个名为 EIP712Domain 的结构体,包含以下一个或多个字段。协议设计者只需包含对其签名域有意义的字段,未使用的字段则从结构体类型中省略
标准字段(建议顺序):
string name:签名域的可读名称,例如 DApp 或协议的名称string version:签名域的当前主版本号。不同版本的签名不兼容uint256 chainId:EIP-155 链 ID。如果不匹配当前活动链,用户代理应拒绝签名address verifyingContract:将验证签名的合约地址。用户代理可以进行合约特定的钓鱼防护bytes32 salt:协议的消歧义盐值。这可以作为最后的域分隔符
注意:
- 未来扩展可以添加具有新用户代理行为约束的新字段
- 用户代理应接受
EIP712Domain类型指定的任何顺序的字段EIP712Domain字段应按上述顺序排列,跳过任何不存在的字段。未来添加的字段必须按字母顺序排列,并放在上述字段之后
eth_signTypedData JSON RPC 规范
方法 eth_signTypedData 被添加到以太坊 JSON-RPC 中,与 eth_sign 类似
eth_signTypedData
签名方法通过 sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))) 计算以太坊特定签名
注意:用于签名的地址必须已解锁
参数
Address- 20 字节 - 将签名消息的账户地址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.sign 和 web3.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
原理阐述
- 编码扩展:
encode函数扩展了一个处理新类型的情况。编码的首字节区分不同情况。这是为了防止与 RLP 编码的交易等发生冲突 - 域分隔符 (
domainSeparator):- 防止碰撞:不同 DApp 可能设计出相同的结构体。域分隔符保证了签名不会碰撞
- 支持多用例:同一结构体实例在同一 DApp 中可用于多个不同的签名场景(例如,需要
from和to双方签名),通过提供不同的域分隔符来区分 - 替代方案考量:仅使用目标合约地址作为域分隔符可以解决第一个问题,但无法解决第二个用例
- 类型哈希 (
typeHash):- 设计目标:旨在成为 Solidity 中的编译时常量
- 被拒绝的替代方案:
- ABIv2 函数签名(4字节):碰撞风险高,长度不足
- 256位 ABIv2 函数签名:捕获类型信息,但语义信息不足,已导致 ERC-20 与 ERC-721
transfer函数碰撞 - 扩展的 256位 ABIv2 签名(含参数和结构体名):字符串长度可能呈指数增长,且不允许递归结构体类型
- 包含 NatSpec 文档:使文档变更成为破坏性更改,过于冗长
- 数据编码 (
encodeData):- 设计目标:便于在 Solidity 中实现
hashStruct,并允许在 EVM 中进行高效的内存原地计算 - 被拒绝的替代方案:
- 紧凑打包:需要复杂的 EVM 打包指令,不支持原地计算
- ABIv2 编码:本身不满足确定性安全标准(同一数据有多个有效编码),不支持原地计算
- 将
typeHash移出hashStruct并与域分隔符合并:效率更高,但会使 Solidity 的keccak256哈希函数的语义失去单射性 - 支持循环数据结构:当前标准针对树状结构优化。支持循环结构需要维护路径栈,实现复杂,且会破坏组合性(成员值的哈希将依赖于路径)。未来可以以兼容的方式扩展标准来定义循环数据的哈希
- 设计目标:便于在 Solidity 中实现
向后兼容性
- RPC 调用、Web3 方法和
SomeStruct.typeHash参数目前是未定义的。定义它们不应影响现有 DApp 的行为 - Solidity 表达式
keccak256(someInstance)(someInstance是结构体SomeStruct的实例)是有效语法。它当前计算的是实例内存地址的keccak256哈希。此行为应被视为危险。依赖于当前行为的 DApp 应被视为已损坏
安全考量
- 重放攻击:本标准仅涉及签名消息和验证签名。在实际应用中,签名消息通常用于授权操作。实现者必须确保当应用看到相同的签名消息两次时行为正确(例如,拒绝重复消息或授权操作应是幂等的)。如何实现是应用特定的,不在本标准范围内
- 抢先交易攻击:可靠广播签名的机制是应用特定的。当签名被广播到区块链以供合约使用时,应用必须防范抢先交易攻击(攻击者拦截签名并在原始预期使用之前将其提交给合约)。应用应能在攻击者首先提交签名时正确行为(例如,拒绝它或产生与签名者预期完全相同的效果)