应用与以太坊交互所需的一切——提供者、合约、ABI 编解码、ENS——ethers 都已涵盖,而大多数 JavaScript 代码正是用它的 Wallet 和 SigningKey 类来签名的。曲线运算来自 @noble/curves;ethers 6.17.0 锁定了它的 1.2.0 版本。
wallet.signMessage() 实现了 personal_sign:它在消息前加上 \x19Ethereum Signed Message:\n 和消息长度(EIP-191),用 keccak-256 计算哈希,并返回 65 字节的 r ‖ s ‖ v,其中 v = 27 或 28。本次检查用一个独立的 keccak 实现核对了摘要,并用 @noble/curves 的 RFC 6979 输出核对了签名,因此同一密钥和同一消息总是得到相同的字节。要对已经算好的哈希签名——例如 EIP-712 摘要或交易哈希——应使用 SigningKey.sign(),它只接受恰好 32 字节的输入,且不添加前缀;其他输入都会报错 invalid digest length。
ethers 对输入的布局很宽松,对规范形式却很严格。verifyMessage() 能从 v = 27/28、v = 0/1 以及 64 字节的 EIP-2098 紧凑形式中恢复出签名者,但拒绝了镜像得到的 high-S 签名,报错 non-canonical s; use ._s——尽管它在数学上是有效的。由不对 s 做规范化的 OpenSSL、云 KMS 或硬件模块生成的签名,必须先转换为 low-S(s → n − s,并翻转 v),ethers 才会接受;以太坊签名验证工具可以显示签名属于哪种形式。
钱包登录中的大多数验证问题与曲线毫无关系:消息相差一个换行符或末尾空格,EIP-712 域中的链 ID 不同,或者是智能合约钱包,其签名需要用 ERC-1271 而不是 ecrecover 验证。用 EIP-712 查看器查看类型化数据,用 SIWE 验证工具检查完整的登录消息。
示例对一条登录消息签名、恢复出地址并展示紧凑形式;比较时应比较恢复出的地址,切勿比较签名字节,因为同一个签名有多种有效编码。