本文不涉及晦涩的数学推导,只从“格式”与“使用场景”两个维度,提炼出开发者最该掌握的 核心知识点,让你在任何语言环境里,都能一眼看出 标准 ECDSA 与 Ethereum Sign Message 的差别。
为什么会有两份签章?
在我们产品的实际业务里,经常遇到两大类“签名”场景:
- 链上身份认证
把签完名的消息发进智能合约,再用ecrecover恢复原地址,完成“谁签名、谁操作”的校验。 - 链下数据确权
仅对一条 JSON 数据或一段文本做签名,证明“这是我本人签的”,常用于登录、投票、KYC 等流程。
由于我们代码栈横跨 iOS / Android / 后端 C# / Java / Node.js,每个生态的 ECDSA 实现又不尽相同,早期大家还分不清“标准 signature”和“Web3 provider 的 sign message”到底差在哪,踩坑无数。于是,本文应运而生。
core 关键词:ECDSA、r s v、public key recovery、Ethereum 签名、JSON RPC sign
1. r、s、v 三兄弟的故事
- r、s:一份标准 ECDSA 签名的“真正数据”就只有这两段。
- v:解决“四点重合”问题的索引位——在链上用它从不知道地址的签名里,反向推出到底是哪把 public key。
不少人在文档里第一次看到 “3045……” 开头的一大串 HEX,总会抓瞎。记住这三步即可:
- r、s 各自 32 字节
- v 只占 1 字节
- 这三个玩意儿拼在一起就是 Ethereum 多数 API 里的 65 字节签名字节串。
如果你看到 71、72、73 字节的签名,那是标准 ECDSA 输出的 DER 格式。它把长度标记、标签字节全算了进去,不是 Ethereum 业务想要的数据格式,要先做 ASN.1 → r||s 的转换。
2. Ethereum 为什么要“魔改” v 值?
如果你把 eth_sign 结果打到控制台,往往看到 v 值是 27 / 28,或者 37 / 38——看上去跟 ECDSA 完全不是一回事。实际逻辑是:
- 前置规则:链上
ecrecover要节省 gas,不能跑 4 次还原。于是 Ethereum 把还原结果位置(0,1,2,3)就这 4 种可能性,作为 v 塞进去。 - 人性包装:为了让开发者一眼分辨,给 0/1 直接 +27,于是出现 27、28。
- EIP-155:防止跨链重放,引入链 ID。再 +35 / +36,于是主网看到 37、38。
一句话总结:v 在标准 ECDSA 里没有,在 Ethereum 里就是 recovery-id + 修正常量。
数据格式差异速查
| 场景 | 是否包含 v | 起始字节 | 用途 |
|---|---|---|---|
| 标准 ECDSA (DER) | 无 | 30 | 通用签名,需手动还原公钥 |
| Ethereum Sign | 有 | 1b / 1c | 直接用 web3.eth.ecrecover |
| Ledger / Trezor | 有 | 同上 | 硬件钱包返回也是 65 字节串 |
👉 遇到格式错乱?先确认 DER 与 r||s||v 别搞串,再用这条快速验证工具。
3. 代码层面的坑
3.1 iOS / Android 原生库
iOS 的 SecKeyRawSign 默认输出 71~73 字节 DER,想接 Ethereum 需利用 ASN.1 Parser 手动拆分 r、s,再对接 v 的计算逻辑。
Android 的 Signature 类同理,差别在于 BouncyCastle 有封装好的 ECDSASigner 帮忙把 DER 拆成 r、s。
3.2 后端 C# / Java
- C#:
BouncyCastle的ECDsaSigner直接给出BigInteger r、s;v 的逻辑由你根据链 ID 和要恢复地址自行 +27 / +35 / +36。 - Java:用
web3j的直接Sign.signMessage就走完 v、r、s三步;但别忘了设置chainId,否则测试网抛错。
👉 调试签名始终 0x 开头零填充?查看这份零填充快速修复清单,5 分钟排查。
案例小课堂
假设我们要在 Hardhat 做单元测试,对一条文本 "login_at_1689913157" 做签名,然后调用合约 ecrecover 验证。
格式统一
在 dApp 侧用eth_sign,拿到 65 字节:0x8e7e52…(64 字节 r+s)+ 1b- 链上验证
solidity 合约:ecrecover(hash, v, r, s),直接得回地址,顺势校验。
FAQ:3 分钟解惑合集
Q1:为什么有了 r、s 还要 v?
A:ECDSA 恢复算法一次给你 1~4 个候选公钥,必须用 v 指定“是其中哪一个”。
Q2:我的 Java 端签名发到合约总是失败?
A:多半忘记 +27/+35。参考 Sign.signPrefixedMessage 源码,只有在 chainId ≠ NONE 时才走 EIP-155。
Q3:DER 格式能否直接在 Ethereum 转 Ethernet?
A:不行。需先拆 ASN.1,拿到 64 字节 r||s,再补 1 字节 v。
Q4:前端钱包返回 personal_sign,跟 eth_sign 有区别吗?
A:前缀字符串不同。personal_sign 会多加 "\x19Ethereum Signed Message:\n32",防止交易重放。后端计算 hash 时要保持完全一致。
Q5:为什么 r、s 有时会溢出 32 bytes?
A:算法里 r、s 定义为 0 < r, s < n。非常罕见接近上限时出现首位为 1(0x80 以上),为防止被解读为负数的补码,编码器会给 r 或 s 前缀 0x00,导致变成 33 bytes。后端要先去掉前导零再恢复。
小结
- 记住 标准 ECDSA = DER、无 v,Ethereum Sign = r||s||v、直接 recover。
- 拿到签名先数字节:71~73 是 DER,65 是 Ethereum。
- 看见 27/28、37/38 就不要惊讶,那只是 v 的多种变身。
掌握了这些格式差异,无论 iOS、Android 还是 Node.js,你都能“指哪儿打哪儿”,把一道签名难题拆成三条公式,既安全又优雅。