以太坊验签如何通过签名反推公钥:原理、流程与代码实战

·

1. ECDSA 的魔法:不用公钥也能验签

在大多数区块链系统中,数字签名 的作用类似传统网银的U盾:证明“我是我”,却不必把公钥随身携带。以太坊主打的是 椭圆曲线数字签名算法(ECDSA)——它有一个非常性感的特性:公钥可以从签名中还原。这意味着在网络中传播的只有 签名(r、v、s),而无需额外传输 64~65 字节的 公钥(或地址);接收方只需根据这 3 组字节,就能还原公钥、校验签名是否有效。这不仅节省带宽,还降低了系统复杂度和攻击面。

核心关键词:ECDSA、数字签名、公钥恢复、以太坊

2. 拆解签名字段:r、s、v 各肩负什么使命?

只要手头有这三个字段,再知道“签的是哪段数据”,任何人都能把 公钥 算回来。

3. 手动还原公钥的整体流程

3.1 明确 sign hash

交易签名 or personal_sign 调用时都会对 message 做一次 Keccak-256 哈希:hash = keccak256(message)

3.2 送入 ecrecover 预编译合约

以太坊内置 0x01 地址的预编译合约 ecrecover,输入参数:

input = [hash (32 bytes)] + [v (32 bytes)] + [r (32 bytes)] + [s (32 bytes)]

调用 staticcall(gas, 0x01, input, 128, ret, 32),合约返回未压缩 64 字节公钥(或 0 表示非法签名)。

3.3 计算地址

对上一步得到的公钥再走一次 Keccak-256,取后 20 字节即 以太坊地址。把该地址与交易中 from 地址比对,即可完成验签。

4. Go 与 Solidity 示例代码

4.1 Go 实现(基于 go-ethereum)

package main

import (
    "fmt"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
)

func main() {
    hash := crypto.Keccak256([]byte("hello world"))
    sig := []byte{...} // 65 字节签名: r(32) + s(32) + v(1)
    v := sig[64]
    if v < 27 {
        v += 27
    }
    pubKey, err := crypto.SigToPub(hash, append(sig[:64], byte(v-27)))
    if err != nil {
        panic(err)
    }
    addr := crypto.PubkeyToAddress(*pubKey)
    fmt.Println("Recovered address:", addr.Hex())
}

4.2 Solidity 实现

pragma solidity ^0.8.0;

library ECDSA {
    function recover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address) {
        require(v == 27 || v == 28, "Invalid v");
        require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "Invalid s");
        return ecrecover(hash, v, r, s);
    }
}

借助以上函数即可在链上验证签名的 from 字段是否匹配恢复后的地址。

👉 一图看懂 ECDSA 签名重建公钥全过程,点进来对照实战!

5. 常见误区:为什么签名只有 65 字节?

许多人误以为签名内含公钥。实际上,65 字节仅由 r(32) + s(32) + v(1) 组成,不含其他数据。能把“曲线点”压缩得如此紧凑,靠的正是 椭圆曲线公钥恢复算法

6. 原子拆解:ecrecover 的底层细节

当调用 ecrecover 时,EVM 内部的闭源(circuit)流程如下:

  1. 借助 v 位,选定两条候选曲线。
  2. 使用 r 在椭圆曲线上找到两个同余点,再用 s 与消息哈希做一次倍点运算。
  3. 通过点加/点减恢复完整公钥,最后哈希取后 20 字节回报率地址。

通俗讲:v 让还原不再“二选一”,而是“一拍即中”。

7. 与 EIP-155 的恩怨情仇

若交易中使用了 EIP-155 链 ID 保护v 的值会加上 2 * chainID + 35。在链外恢复公钥时需先还原原始 v,否则签名将被判无效。
链上合约无须操心,因为 ecrecover 会忽略 EIP-155 的保护位。

👉 链上链下地址不一致?教你 30 秒校准 EIP-155 恢复值!

8. FAQ:那些你最想问的问题

Q1:如果 r、s 或 v 写错,ecrecover 会返回空地址吗?

对。ecrecover 遇到无法恢复的场景(错误签名、点在曲线上不存在等)会返回 0x0 地址,这是一种快速失败机制。

Q2:Solidity 能否一次还原两个待选公钥?

理论上可行,但 Solidity 内不存在点乘/点加库,你需要自己用预编译合约或链下完成。通常不建议在链上操作复数公钥,因 gas 不可控。

Q3:为什么同样的消息同一私钥不同时间签名会得到不同公钥?

不会的。只要 vrs 一致,恢复出的公钥就是恒定的那一支。之所以会看到“不同签名”,是因为随机数 k 每次不同,所以 r、s 是会变化的。

Q4:可以用该机制做匿名登录吗?

可以,这正是 “以太坊登录(Sign-in with Ethereum)” 的核心思路。后端收到签名后还原地址,比对数据库即可完成免密码登录,兼顾匿名与安全。

Q5:Go 代码里为什么要把 v-27?

golang 的库默认 v 取值范围是 0 或 1,而大多数交易/钱包的 v 是 27/28,故需减 27 对齐。

Q6:私钥丢了还能用签名反推吗?

不能。签名恢复只能得到 公钥,逆向无法取得 私钥。这是椭圆曲线离散对数难题的根基。

9. 知识卡片总结

牢记一句话:签名是通行证,公钥是身份证,但身份证其实一直藏在通行证里。