链上的验签车间:ecrecover 预编译怎么从签名倒推公钥 图 1
链上的验签车间:ecrecover 预编译怎么从签名倒推公钥 · 图 1

把椭圆曲线运算搬进每条交易都能敲的门

以太坊的区块执行环境里住着一小组“预编译合约”:它们不是字节码,而是客户端代码里直接实现的函数,固定占据地址空间最前面的几号门牌。一号门牌住的就是 ecrecover——签名恢复函数。任何合约在执行中都能花固定的三千二百五十燃气的名义价格调用它(黄皮书对第一号预编译标定的基础开销),把一段消息哈希与一组签名参数递进去,换回签名者的公钥。

它做的事用一句话说清:给一条 secp256k1 曲线上的签名,不直接告诉你谁签的,但凭签名本身可以算出两个候选公钥,再靠一个一比特的“奇偶值”在两者之间二选一。这正是比特币同款曲线的设计——签名时用的临时随机数落在曲线上一个点,这个点的纵坐标有奇偶两种可能,恢复算法把数学上的两条路径全部算出来,奇偶值负责挑对的那条。

链上的验签车间:ecrecover 预编译怎么从签名倒推公钥 图 2
链上的验签车间:ecrecover 预编译怎么从签名倒推公钥 · 图 2

输入输出的契约与失败的样子

调用一号门牌时,输入被拼成一百二十八字节:前三十二字节是消息哈希,接下来三十二字节是签名的 r 分量,再三十二字节是 s 分量,最后一字节是奇偶值(历史上写作 v,取值二十七或二十八;自从 EIP-1559 引入新交易格式后规范字段改名为 yParity,只留一比特语义)。返回是右对齐补零的六十四字节公钥——注意,是公钥本体,不是地址。合约若想要地址,得自己对公钥取 keccak256 再截后二十字节。

失败行为值得单列:输入非法——r 或 s 落在曲线阶的范围之外、奇偶值不是零或一、或者根本恢复不出有效点——一号门牌不抛异常,而是安静地返回三十二字节的零。合约开发者必须自己检查返回值,否则“验证失败”会伪装成“签名者地址是零地址”,这在检查映射表时可能意外放行。这一条是链上验签事故里最高频的坑。

高位 s 与两把钥匙配一把锁

同一条消息理论上存在两个合法签名:(r, s) 与 (r, n−s) 共享同一个 r,只是 s 互为补数,恢复出的公钥一个是另一个的“镜像”。如果协议不挑,攻击者拿到你的签名就能合法改写出第二个,重放到还没升级签名的旧逻辑里。以太坊从 Homestead 硬分叉起在交易验证层面强制要求 s 不超过曲线阶的一半(EIP-2 把这一条写进了规则),从此链上只认“低位 s”签名,签名工具库也默认做这个折叠。但预编译本身不做这个检查——合约里自行验签时,要么依赖钱包端的规范,要么在代码里补上这道判断,否则等于把改写面留给了对手。

另一个经典误用是哈希层:直接把字符串喂给 ecrecover 的调用方,如果哈希没有带前缀或用与签名端不一致的哈希函数,恢复出的地址就会“看似成功、实则对不上”。链上签名授权的主流方案(EIP-712 结构化数据)本质上就是在统一这层哈希的构造规则。

什么时候该用、什么时候别用

ecrecover 适合的场景很具体:一次性授权(比如代付gas的元交易里验证用户意图)、跨合约的签名回执、无需登记公钥的轻量准入。它不需要签名方在链上有任何账户,也不需要事先注册,验完即弃。反过来,需要批量验签、频繁轮换密钥、或者对燃气预算敏感的场景,越来越多方案转向其他路线:链上登记公钥映射后改用哈希比对、或者把验证逻辑挪到协议外。

对普通用户,这枚车间的存在解释了一件日常小事:为什么钱包给一条消息签名不需要你付gas——签名本身只是曲线运算,上链验证才烧燃气;而 dapp 提示“该签名免费但需要你确认”,就是在你的设备上做一次与一号门牌对称的运算。

快速问答

问:ecrecover 能验证“消息内容正确”吗? 答:不能。它只回答“这个哈希是不是这个签名者签的”,消息内容怎么构造、是否合规,全靠上层协议自己约定。

问:返回零一定代表签名无效吗? 答:不一定。存在公钥恰好恢复为零的理论输入,但实践中返回零几乎总是输入非法或签名损坏,规范语义上应视为失败并要求重签。

问:奇偶值和比特币里的恢复标识是一回事吗? 答:机制同源,编码不同。以太坊交易里历史上用 27/28 编码,比特币的压缩公钥签名常用 31-34 段编码,跨链工具转换时要显式处理。

风险提示:本文讨论链上验签机制,不构成投资建议;使用自定义签名逻辑前请进行安全审计。