EIP-712 的域分隔符:给签名上“户口”,让它跨不了链、串不了场 图 1
EIP-712 的域分隔符:给签名上“户口”,让它跨不了链、串不了场 · 图 1

你在 A 链上给某协议签了一枚授权,同样的字节要是拿到 B 链、或者被搬去另一份合约用,会发生什么?签名只是一串字节,本身不认识场景——这正是重放攻击的土壤。EIP-712 给出结构化解法:签名摘要里预先焊死一份“身份信息”,也就是域分隔符(domain separator),让一枚签名天然只属于一个链、一个合约、一套字段格式。标准文本自 2017 年创建,状态 Final,钱包侧的 eth_signTypedData 系列接口就是它的门面(消息签名的用户侧流程见 签署消息)。

摘要的三层结构

一枚 EIP-712 签名最终签的是 keccak256(0x19 || 0x01 || 域分隔符 || 结构哈希):前缀沿用 EIP-191 的框架,表明“这不是交易”。结构哈希把每个字段的类型名字典序拼成 typehash 再编码取值,字段名顺序一变哈希全变,机器读得出“谁、给谁、多少、到什么时候”。而域分隔符自身也是一个结构哈希,字段来自固定菜单:应用名(name)、版本(version)、钱包验证的合约地址(verifyingContract)、链 ID(chainId),外加一个自定义盐值(salt)。域里每一项都是防线:verifyingContract 让签名不能喂给“克隆合约”,chainId 让跨链复用直接对不上摘要,name 与 version 则挡住同合约新旧版本之间的格式漂移。签名摘要与场景的关系,就此从约定变成算术。

域之外:nonce 与 deadline 的补刀

域分隔符管“空间上的独一无二”,管不了“同一场景内被用两次”。所以业务结构体里通常还埋两道闸:nonce 让合约记录每人已消费到第几号,签过的下一份自然换摘要;deadline 给签名设过期时间。ERC-2612 的 Permit(免 gas 授权,风险面见 Permit 授权)就是“域 + nonce + deadline”三件套的标准示范。合约实现还会处理部署时序的边角:链 ID 在合并事件后可能变化,域分隔符需要按当前链重算——OpenZeppelin 的 EIP712 基类用“缓存域 + 版本判断”处理这类情况,思路与 EIP-1559 时代交易类型演进一样,都是标准落地后补的账。想核对某合约到底声明了哪个域,可以走 ERC-5267 的域查询接口,不必靠肉眼猜。

用户与开发者各看什么

用户侧的收益是可读性:钱包能把“授权某合约支取 5000 枚 USDC,有效期至某日”渲染成人话而不是十六进制乱码;反过来说,遇到钱包只能显示原始哈希或字段名全是 bytes32 unknown 的请求,要警觉——那意味着对方没按可读规范来,你签的是盲签。开发者侧的对账清单很短:域里每个字段是否与部署环境一致;struct 是否含 nonce/deadline;合约验证签名时是否重算域并逐字节比对;同一枚签名的消费是否绑定唯一 nonce。任何一环缺失,域分隔符就退化成装饰。

快速问答

问:EIP-712 签名能证明钱包地址的所有权吗?能作为证据,但它是“应用级签名”,直接当通用身份证明不如标准签名消息方案严谨。问:fork 出一条新链后旧签名还有效吗?只要新链沿用同一链 ID 编码(或旧签名域里的 chainId 恰好匹配),理论上存在复用面——这也是链 ID 必须全局唯一的原因。问:salt 字段什么时候用?链 ID 或合约地址无法唯一界定场景时(多版本共用合约)引入。问:EIP-712 与 eth_sign 什么区别?后者是裸哈希盲签,安全水位低一档,能用前者就不要退回去。

常见误区

一是以为“用了 TypedData 就不会重放”——域防跨场景,场景内重复消费靠 nonce。二是把 verifyingContract 当成可选装饰,漏填等于门没锁。三是钱包把字段名显示对了就放心,字段值语义(比如 value 的单位是不是含 18 位小数)仍要核对,可参考ERC-20 decimals那个经典精度坑。

小结

域分隔符做的事,本质是给每枚签名上户口:生在哪个链、哪份合约、哪个版本,死于哪个时间、第几号。签名安全由此从“希望用户看懂十六进制”转成“让错误的场景在哈希层根本拼不出来”。对普通用户,一句口诀就够:钱包能显示结构化字段就用结构化签名,显示不出内容的签名,不值得签。

风险提示:本文只解释签名标准机制,不构成投资建议;签名即授权,请在理解字段含义后操作。