一个 2016 年留下的口子
EIP-155 给交易签名引入链 ID,目的是防止同一份签名在别的链上被重复执行,并把 v 值的算法定为链号的函数(编号历史见 签过的消息会因链号改变作废吗:EIP-1959 与 1965 的链ID历史之问)。但早期规范留了余地:链号为 0 的交易被视为有效,等于开发者可以签出一笔”到哪条链都成立”的交易。这个可重放特性当年是特性而非漏洞——测试网络与工具链需要它。几年里 EVM 兼容链爆炸式增长,多数 forks 自以太坊客户端代码、共用地址体系,同一个地址、同一串 nonce 序列在很多条链上成立,口子于是翻转成风险。

3788 写的重放路径与规则
EIP-3788 在 2021 年 9 月提出,动机部分把攻击链讲得很直白:多数钱包界面不把链号暴露给用户,签名人常常并不知道自己刚签的载荷里 chainId 写的是几;恶意或出错的一方引导用户签一笔链号为 0 的转账,这笔合法签名会同时被主网和任何一条兼容链接受,一条链上执行成功,另一条链上它依然可以进块——用户在同一条 nonce 上被扣了两次款。提案的规格只有一条规则:自某升级块起,节点把自己配置里的链号与交易里的链号逐一比对,不相等即拒绝,链号为 0 不再豁免。状态至今 Stagnant,从未作为独立升级激活;各客户端如今以何种版本规则对待链号不符的交易,需以各客户端发布的版本说明为准,本文不做未核实的断言。
重放成立的三个前提
把提案描述的攻击拆成条件清单:同一密钥在多链可被使用;两条链上发送者 nonce 恰好相等;交易本身在所有目标链语义一致(同样的 to 与 data 在两边都指向目标合约)。三个条件全中,重放才有完整效果。这解释了为什么常见重放目标往往是”授权加转账”这类结构简单的载荷,也解释了普通用户为什么难以自查到密码学层——你无法用肉眼从签名反推链号,正确动作只能前移到签名之前。
用户侧的自查动作
签名弹窗出现时把链号当金额一样核对:先确认界面显示的网络名与预期一致,再展开详情检查 chain ID 数值;显示为 0 或干脆缺失链号的签名请求一律拒绝。跨链工具发起的批量签名尤其要看这条,因为其中混入旧格式交易的概率高于钱包内普通转账。平时用 eth_chainId(见 eth_chainId 怎么确认你现在在正确的链上?)确认当前 RPC 指向,遇到添加陌生网络的弹窗按网络配置单逐项核对(见 钱包弹出的网络配置单怎么逐项核对:chainId、RPC 与小数位);链号本身的取值区间与分配逻辑另见 链号多大才算安全:EIP-2294 划出的两段区间怎么来的。多链操作后做一次对账:在每条涉及的链上按同一 nonce 查一次该序号的最终内容,被重放的序号会立刻显形。
提案史给出的两条教训
第一,安全债务常常写得很体面:链号 0 的豁免当年有明确用途,环境变了才暴露危险,因此看到”向后兼容”的设计要问一句它兼容的是哪一年的世界。第二,协议层拒绝是最干净的收口,但收口前生态往往早已自发收敛——今天大多数钱包不会再产出链号为 0 的新签名,风险残留在旧工具与恶意页面里。还有一个容易混淆的点值得澄清:3788 管的是交易层的链号校验,与 EIP-712 结构化签名在消息里嵌入链号防重放(另文有述)是两条互补的防线——前者保护”这笔转账不会在别处再执行一次”,后者保护”这份授权书不会被换到别的链上兑现”,签名的两类载荷都需要各自核对链号。教训落在用户侧就一句话:链号是签名内容的一部分,核对它不需要任何专业工具,只需要在按下确认键之前多看一眼。本文为机制解读,不构成投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。