ERC-7920: 复合 EIP-712 签名
一种使用单个签名对多个类型化数据消息进行签名,通过将它们编码到 Merkle 树中的方案
Authors | Sola Ogunsakin (@sola92) |
---|---|
Created | 2025-03-20 |
Discussion Link | https://ethereum-magicians.org/t/composite-eip-712-signatures/23266 |
Requires | EIP-20, EIP-712 |
Table of Contents
摘要
此 ERC 提供了一种使用单个签名对多个类型化数据消息进行签名的标准,方法是将它们编码到 Merkle 树中。这允许组件独立验证消息,而无需完全了解其他消息。通过将签名提示的数量减少到一个,同时保留 EIP-712 标准的安全性和灵活性,它提供了显着的 UX 改进。
此 ERC 还使应用程序能够灵活地单独或聚合验证消息。这开辟了新的验证方式:例如,应用程序可以要求消息 (x
) 仅在与消息 (y
) 组合签名时才有效。
动机
随着生态系统向无 ETH 交易发展,用户经常需要快速连续地签署多个链下消息。通常,需要第一个签名来获得精确的花费额度(通过 Permit2,ERC-2612 等),然后是后续消息来指导资金的使用。这会产生摩擦性的用户体验,因为每个签名都需要单独的钱包交互,并使用户对总体上批准的内容感到困惑。
目前的解决方案存在明显的缺点:
- 预先批准 ERC-20 额度:花费会产生安全漏洞
- 将多个消息合并为单个消息:阻止独立的可验证性。如果不了解整个批次,则无法验证每个消息
- 单独的签名请求:会在用户体验中产生摩擦
此 ERC 具有以下目标:
单个签名
单个签名应涵盖多个消息
隔离验证
无需了解其他消息,即可独立验证消息
人类可读
应保留 EIP-712 的可读性优势。让钱包和用户了解正在签名什么。
规范
概述
复合签名方案使用 Merkle 树将多个类型化数据消息一起哈希到一个根下。用户仅签署 Merkle 根。该过程如下所述。
生成复合签名
-
对于一组消息
[m₁, m₂, ..., mₙ]
,使用 EIP-712 的encode
对每个消息进行编码,并计算其哈希值:hashₙ = keccak256(encode(mₙ))
-
在 Merkle 树中使用这些消息哈希作为叶节点,并计算
merkleRoot
-
签署 Merkle 根。
signature = sign(merkleRoot)
验证过程
要验证单个消息 mₓ
是否包含在复合签名中:
-
验证
merkleRoot
上的签名:recoveredSigner = ecrecover(merkleRoot, signature) isValidSignature = (recoveredSigner == expectedSigner)
-
计算消息
mₓ
的叶节点,并使用证明验证其到 Merkle 根的路径:leaf = keccak256(encode(mₓ)) isValidProof = _verifyMerkleProof(leaf, merkleProof, merkleRoot)
其中 _verifyMerkleProof()
定义为:
function _verifyMerkleProof(
bytes32 leaf,
bytes32[] calldata proof,
bytes32 merkleRoot
) internal pure returns (bool) {
bytes32 computedRoot = leaf;
for (uint256 i = 0; i < proof.length; ++i) {
if (computedRoot < proof[i]) {
computedRoot = keccak256(abi.encode(computedRoot, proof[i]));
} else {
computedRoot = keccak256(abi.encode(proof[i], computedRoot));
}
}
return computedRoot == merkleRoot;
}
当且仅当 (1) 和 (2) 都成功时,消息才会被验证。
isVerified = isValidSignature && isValidProof
eth_signTypedData_v5
JSON RPC 方法的规范。
此 ERC 向 Ethereum JSON-RPC 添加了一个新方法 eth_signTypedData_v5
。此方法允许使用上述规范通过单个签名对多个类型化数据消息进行签名。签名帐户必须事先解锁。
此方法返回:签名、Merkle 根和证明数组(每个证明对应于一个输入消息)。
参数
Address
- 签名帐户TypedData | TypedDataArray
- 来自 EIP-712 的单个 TypedData 对象或TypedData
对象数组。
返回值
{
signature: `0x${string}`; // Hex 编码的 65 字节签名(与 eth_sign 格式相同)
merkleRoot: `0x${string}`; // 32 字节 Merkle 根作为十六进制字符串
proofs: Array<Array<`0x${string}`>>; // Merkle 证明数组(每个输入消息一个)
}
示例
请求:
{
"jsonrpc": "2.0",
"method": "eth_signTypedData_v5",
"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!"
}
},
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Transfer": [
{
"name": "amount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
]
},
"primaryType": "Transfer",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"amount": "1000000000000000000",
"recipient": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
}
}
]
],
"id": 1
}
结果:
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"signature": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c",
"merkleRoot": "0x7de103665e21d6c9d9f82ae59675443bd895ed42b571c7f952c2fdc1a5b6e8d2",
"proofs": [
["0x4bdbac3830d492ac3f4b0ef674786940fb33481b32392e88edafd45d507429f2"],
["0x95be87f8abefcddc8116061a06b18906f32298a4644882d06baff852164858c6"]
]
}
}
理由
选择使用 Merkle 树来捆绑消息提供了以下额外的好处:
链上高效验证
_verifyMerkleProof
的运行时为 O(log2(N))
,其中 N 是已签名的消息数。
灵活的验证模式
应用程序可以要求将消息组合在一起签名以增强安全性。
N=1
向后兼容性
单个消息包的 Merkle 签名等于 eth_signTypedData_v4
。无需链上更改。
向后兼容性
当消息数量为 1 时,eth_signTypedData_v5
生成的签名与 eth_signTypedData_v4
相同,因为 merkleRoot == keccak256(encode(message))
。这允许 eth_signTypedData_v5
作为 eth_signTypedData_v4
的直接替代品,而无需更改链上验证。
参考实现
eth_signTypedData_v5
eth_signTypedData_v5
的参考实现可以在 assets directory 中找到。
验证器
链上验证器的 Solidity 实现可以在 assets directory 中找到。
Merkle
参考 Merkle 树可以在 assets directory 中找到。
安全考虑
重放保护
此 ERC 侧重于生成复合消息并验证其签名。它不包含防止重放的机制。开发人员必须确保他们的应用程序可以处理两次接收同一消息。
部分消息验证
在验证期间,必须注意确保所有这些检查都通过:
- Merkle 根上的 EIP-712 签名有效
- Merkle 证明对于根有效
用户理解
钱包必须向用户传达他们正在一次签名多个消息。钱包必须在签名前显示所有消息类型。
为确保批量签名请求易于理解,建议将最大消息数限制为 10。
Merkle 树构建
Merkle 树应以一致的方式构建。
- 哈希函数必须是
keccak256
- 为确保可预测/一致的证明大小,实现必须用零哈希填充叶子以达到下一个 2 的幂以确保平衡。令
n
为消息数。在构建树之前,计算最小的k
,使得2^(k-1) < n ≤ 2^k
。将零哈希插入消息列表中,直到消息列表等于2^k
。 - 为确保隐式验证路径,在构建父哈希之前,必须按字典顺序对对进行排序。
版权
版权及相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Sola Ogunsakin (@sola92), "ERC-7920: 复合 EIP-712 签名 [DRAFT]," Ethereum Improvement Proposals, no. 7920, March 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7920.