W3

EIP-712 签名验证台

EIP-712 是「让用户在钱包里读得懂自己签的是什么」的标准。 问题是读懂之后仍然会出事:签名里少一个 chainId、少一个 nonce、少一个合约地址,签名就变成了万能通行证。这一页让你亲手把这些字段删掉,看摘要怎么变、验证怎么「成功但错了」。

这个页面上的签名全部在你自己浏览器的内存里完成,不会发给本站后端,也不会发到任何网络。 默认用一个临时生成的测试钱包,刷新页面就换一个。 绝对不要在这里粘贴持有真实资产的钱包私钥——需要验证真实钱包时,用下面的「浏览器钱包」方式,私钥始终留在钱包插件里。

选一个场景

最值得理解的一个:签一次名就完成授权,不用发交易、不用付 gas。 value 是「授权额度」,一旦签出去,spender 就能把 owner 的这笔钱转走。把它改一位数字试试——digest 会完全变样,旧签名立刻失效。这就是「签名绑定到确切内容」的含义。

用哪个身份签

点「签这个结构」拿到签名。之后随便改上面任何一个 JSON, 这一块会实时显示签名和内容对不上的地方。

域字段自检

namename 已设置,用于区分同名协议的不同版本
versionversion 已设置,协议升级时可以换一个值把旧签名作废
chainIdchainId = 1,签名被钉死在这一条链上
verifyingContractverifyingContract = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,签名只对这一个合约有效
salt没有 salt。这不是缺陷——name + version + chainId + verifyingContract 的组合通常已经够用
推导出的 EIP712Domain 类型(顺序是规范定死的,字段顺序会影响域分隔符)
EIP712Domain = [{ name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }]

签之前该检查的五件事

1
domain 有没有 chainId

缺了就能跨链重放。用上面那个「危险示例」预设点一下「换一条链」,你会看到 digest 变了但一份缺失 chainId 的签名在别处照样能用。

2
domain 有没有 verifyingContract

缺了就能在别的合约上消费。同一个团队部署在两个链上的同款合约,如果没有 verifyingContract,签给 A 的授权可以在 B 上花掉。

3
链下有没有 nonce 且链上有没有检查

签名本身不防重放。防重放靠的是合约里记账「这个 nonce 用过了」。签名工具只能替你确认 nonce 存在且没被用过,检查它有没有被消费是链上合约的责任。

4
deadline / validBefore 是不是太长

没有期限或者期限很远,等于留下了一张长期有效的支票。把上面的 value 改成你愿意承担的上限,deadline 改成够用的最短时间。

5
钱包里显示的字段和你要签的是不是同一件事

这是 EIP-712 的全部意义:让你在签名前看见自己在签什么。如果钱包弹窗里的 to / value 和你预期的不一样,立刻取消——尤其是「授权额度」被设成无限大的那种。

这个工具不替你做的事

  • · 不连链。这里算出来的 digest 和签名,不会去链上验证那个合约是否真的会接受它。 合约可能有额外的检查(allowlist、pause、余额要求),签名有效不等于交易成功。
  • · 不判断「该不该签」。它能把内容摊开给你看,但 value 填 100 万还是 1 块,只有你自己知道。
  • · 不保存任何东西到服务器。签名、私钥、JSON 全在浏览器内存里。用「粘贴私钥」方式时唯一落盘的是 localStorage 里那份私钥——所以那个输入框旁边才放了清除按钮。
  • · 不代理浏览器钱包。走「浏览器钱包」时,是钱包插件在签名,本页只是把载荷递给它, 私钥不会离开插件。

三个反直觉的地方,建议按顺序试一遍

  1. 1. 改一个字节,摘要全变,但验证不会「报错」。verifyTypedData 在消息被改过之后不会抛异常, 它照样返回一个地址——只是那个地址不是签名者。 所以正确的写法永远是「先恢复地址,再和期望的签名者比对」, 而不是「没抛异常就算通过」。平台上就是靠这一点让无数人签下了自己没打算签的东西。
  2. 2. 往 message 里加一个没在 types 里声明的字段,摘要不变。EIP-712 只哈希 types 里声明过的字段。你以为「多写了一个字段让签名更严格」, 实际上它被完全忽略了——签名范围由 types 决定,不由你写了几行 JSON 决定。 所以验签的一方必须用自己那份 types 去还原, 绝不能用提交方随请求一起发来的 types。
  3. 3. domain 字段的顺序是规范定死的,不是随便排的。域分隔符按 name → version → chainId → verifyingContract → salt 的顺序逐字段计算。顺序不同,同一个 domain 对象算出来的分隔符就不同, 签名会验证失败——而且失败得毫无线索,看起来就像「钱包签错了」。

这个页面拿你的私钥做什么

「粘贴私钥签名」那一档的存在,是为了让你不用装钱包也能看清签名流程。 私钥只存在你这台机器的 localStorage 里,不会发送到任何服务器——签名与验证全部在浏览器里由 ethers 完成。 用完随时点「清除」。任何情况下都不要把真实资产的私钥粘到网页上,包括这一页:要试就用随机生成的测试钱包

相关:permit 为什么能「一次签名完成授权」,EIP-2612 标准页有完整字段说明;本页实现的规则原文在EIP-712 标准页;签名重放导致损失的完整攻击链在漏洞库的「签名与重放」分类里;签之前该逐项确认什么,看审计检查清单的「签名与重放」组。