ECDSA 签名中的 Recovery 参数详解

in #dev6 days ago

1. ECDSA 签名的基本结构

ECDSA 签名包含两部分:

  • r:32 字节
  • s:32 字节

标准签名是 64 字节。在 Steem/Bitcoin 中,会在前面加一个 recovery 字节,形成 65 字节的紧凑签名(compact signature)。

[recovery_byte(1字节)] + [r(32字节)] + [s(32字节)] = 65字节

2. 为什么需要 Recovery 参数?

从签名恢复公钥时,需要知道:

  • 使用哪个候选点(4 个可能)
  • 公钥是压缩还是非压缩

Recovery 参数用于编码这些信息。

3. Recovery 参数的含义(0-3)

椭圆曲线上,给定 r 值,可能的 y 坐标有 2 个(y 和 -y),且可能使用 r 或 r+n(n 是曲线阶数),因此共有 4 种组合:

recovery = 0: 使用 r,y 是偶数
recovery = 1: 使用 r,y 是奇数
recovery = 2: 使用 r+n,y 是偶数
recovery = 3: 使用 r+n,y 是奇数

4. 压缩 vs 非压缩公钥

  • 非压缩:33 字节(0x04 + x + y)
  • 压缩:33 字节(0x02/0x03 + x,y 由奇偶性确定)

Steem 使用压缩格式。

5. Recovery 字节的编码方式

编码公式:

recovery_byte = base + compressed_flag + recovery_param

其中:

  • base = 27(历史约定)
  • compressed_flag = 0(非压缩)或 4(压缩)
  • recovery_param = 0, 1, 2, 3

因此:

  • 非压缩:27 + 0 + (0-3) = 27-30
  • 压缩:27 + 4 + (0-3) = 31-34

6. 代码中的实现

C++ 代码(steem):

// 签名生成(总是使用压缩格式)
result.begin()[0] = 27 + 4 + recid;  // 31-34

// 签名恢复
if (nV >= 31) {
    // 压缩格式
    EC_KEY_set_conv_form(my->_key, POINT_CONVERSION_COMPRESSED);
    nV -= 4;  // 去掉压缩标志
}
// 然后使用 nV - 27 作为 recovery 参数 (0-3)

dsteem:

// 期望 recovery 字节是 31-34
const recovery = buffer.readUInt8(0) - 31;  // 得到 0-3

steem-js next版本(修复后):

// 生成签名时
const i = calcPubKeyRecoveryParam(...);  // 得到 0-3
return new Signature(r, s, i + 31);  // 编码为 31-34

// 解析签名时
const recoveryByte = buffer.readUInt8(0);  // 31-34
const recovery = recoveryByte - 31;  // 得到 0-3

7. 实际例子

假设:

  • recovery 参数 = 1
  • 使用压缩格式

那么:

recovery_byte = 27 + 4 + 1 = 32

签名格式:

[32] + [r的32字节] + [s的32字节]

恢复时:

recovery_byte = 32
compressed_flag = 32 >= 31 ? 4 : 0  // 是压缩的
recovery_param = 32 - 31 = 1  // 或者 (32 - 27) & 3 = 1
Sort:  

Upvoted! Thank you for supporting witness @jswit.