BTC/ETH 签名机制简介

前景提示

用于区块链的椭圆曲线公式: \[ y^2 \equiv x^3+ax +b\ (\mod\ p) \]

其中:

1
2
3
4
5
6
7
8
9
10
11
a = 0
b = 7
# 非压缩
G = 04
79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
# 压缩
G = 02
79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
p = 2^{256}-2^{32}-2^{9}-2^{8}-2^{7}-2^{6}-2^{4}-1
= FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
以上参数值 BTC 和 ETH 都在使用,所以实际应用场景下的椭圆曲线函数为: \[ y^2 \equiv x^3+7 \]

BTC 的交易签名及验签逻辑

公私钥生成

$ ECDSA(Pri)=Pub $

细节

$ Pub=Pri G $

签名

$ Hash=txHash $

$ Sign(Pri, Hash)=R,S $

细节

  1. 找一个随机数 k $ Point=kG $

  2. 计算 R 和 S,若任意一个为 0,则返回第 1 步 $ R=Point.x  mod p $ $ S=(k^{-1}(Hash+RPri)) mod p $

  3. 拼接 R 和 S 后的结果即为最终的签名结果

验签

$ F(Pub, S)=R $

细节

$ Point=((S^{-1} Hash)p)G+((S^{-1} R)p)Pub $

只要 $ R=Point.x  ( p) $ ,那么验签就通过

极简证明过程(实际证明过程比较复杂,涉及椭圆曲线点乘及模运算等知识):

\[ \begin{aligned} Point &=((S^{-1}\cdot Hash)\bmod \ p)\cdot G+((S^{-1}\cdot R)\bmod \ p)\cdot Pub \hspace{100cm}\\ &=((S^{-1}\cdot Hash)\bmod \ p)\cdot G+((S^{-1}\cdot R)\bmod \ p)\cdot Pri\cdot G\\ &=S^{-1}G((Hash+R\cdot Pri)\bmod \ p)\\ &=S^{-1}GSk\\ &=Gk\\ &=Point \end{aligned} \]

知识点: 1. 私钥的本质就是一个 32 字节的随机数 2. 公钥的本质就是一个点(私钥点乘 G 的结果),但是一般都会将其序列化表示出来,x 坐标和 y 坐标各 32 字节,所以完整的公钥表示需要 64 个字节。由于椭圆曲线是参数固定的曲线,可以由 x 计算出 y ,又由于椭圆曲线是关于 x 轴对称的,所以需要多一个字节表示奇偶性显式地指明是哪一个点,于是也可以用 33 个字节简要表示公钥 3. k 必须是随机的,否则可以根据两次针对不同内容的签名还原出私钥。攻击案例参考 PS3 破解 - 另外很多区块链的签名算法库会遵循 RFC6979 ,这个规范将 私钥签名内容 一起派生出来的值作为 k 作为签名的随机数。(比如 go-ethereumrust-bitcoin ) - Go 的官方底层库还是使用的最初的随机 k

ETH 的交易签名及验签逻辑

跟 btc 的不太一样:

  1. 为了防止跨链重放攻击(EIP155),生成的交易 hash 里“包含”了链 id 的信息,另外虽然签出来的结果大致一样,但是各自的格式不一样:
    • BTC:30 44 02 20 r 02 20 s
    • ETH:r a0 s a0 v (在 raw tx 的最后)
  2. ETH 不用暴露公钥,而是在验签的环节利用上面的 BTC 验签公式反向还原出公钥,然后求出地址,与发送交易的地址做字节比对

ETH 的消息签名及验签逻辑

签名

eth_sign

因为没有做任何转换,直接签的原文,存在可能的欺诈风险

personal_sign

Hash=Keccak256(“19Ethereum Signed Message:” + Keccak256(m))

EIP191

EIP191 提案的其中一种数据结构的具体实现就是 EIP712,这里不详细介绍

验签

与交易验签逻辑一致

关于 v

ETH 签名里用于辅助恢复公钥的 v 字段历史:

背景

由于 secp256k1 曲线沿 x 轴对称,签名结果里的 r 字段代表的 x 坐标,这就会对应两个候选点。再加上由于阈值不可能无限,所以区块链里面的 secp256k1 算法都是最后带上了取模运算限定了上下限,那么由于模运算的特性这样的 x 坐标就可能会有两个,这就导致最后的候选点可能会出现 4 个。

如果每次验签都要循环遍历最多 4 次才能得到正确的答案显然会影响验签效率,所以需要额外引入一个辅助参数 v

发展

  1. 原始签名出来的结果里的 v 字段只有 0 或 1 两个值用于区分奇偶

  2. 在 ETH 的 Frontier 和 Homestead 阶段对这个 v 值 + 27(参考了 BTC 的设计)

    issue 里的对话里可知,这个数字 27 参考自 BTC 钱包,过去看了下,代码也是随性,直接硬编码贴了个 27,而这个 Python 版的 BTC 钱包想必也是参考了 BTC 源码来实现的,再顺藤摸瓜去查了下 BTC 源码,也同样是直接硬编码,找不到任何缘由。

    个人猜测应该是 BTC 签名结果的第一个字节用于区分使用场景的,因为:
    • 0x00 已经被 BTC 地址的第一个字节占用了
    • 0x02 和 0x03 也被 BTC 的压缩格式的公钥的第一个字节使用了
    • 0x04 被 BTC 旧版本的非压缩格式的公钥的第一个字节使用了

    所以签名后的结果理论上就应该顺延至 0x05 起头,但不知为啥这次决定反向操作从尾部开始,再加上工程习惯,选择了从 31(\(31=2^5-1\))开始,遇到压缩格式的公钥时,再减去 0x04 腾出位置于是就有了现在的这个魔数 27 了?🤔

  3. 后来因为重放问题,在 EIP155 提案里重新设定了 v 的取值:+ CHAIN_ID * 2 + 35

  4. EIP1559 ,这个 v 又重新回到了 0 和 1 的时代,chainid 含义不再被嵌入到 v 的值里,被单独提出来作为一个独立的字段嵌入到交易结构里

    尽管有 EIP155 和 EIP1559 可以杜绝跨 evm 链的重放攻击,但当下的 ETH 依旧支持不满足上述两个协议的交易上链,意味着当下的一些老旧的交易还是可以被重放。详情可以参见前段时间的因交易重放攻击而导致的 2000w OP 被盗事故