基于链下链上双视角深入解析以太坊签名与验证

概述

你可以前往我的博客获得更好的阅读体验。

本文主要介绍在以太坊中的签名问题,主要涵盖以下内容:

  1. ECDSA公钥密码学的数学原理与代码实现解析
  2. 以太坊客户端对交易信息签名的基本流程与源代码分析
  3. 智能合约内签名的验证

ECDSA公钥密码学

为了方便读者理解和实战本文中的内容,本文将结合一个可以使用Typescript编写用于生产环境的noble-secp256k1库作为实战案例解析。你可以在这里找到源代码。当然,为了节省篇幅,本文不会对此库中的所有代码进行解析。

公钥生成

以下内容部分参考了比特币 P2TR 交易详解

在椭圆密码学中,许多不同种类的曲线都可以用于生成公钥。以太坊选择了与比特币相同的曲线类型,形式为y² = x³ + 7,被称为secp256k1。具体的图像如下图:

secp256k1 Img

在此图像上,我们可以选择一个点作为生成点G,使用陷门函数计算获得公钥。陷门函数特点是正向计算简单,我们可以快速从私钥求出公钥,而逆向计算难度巨大。

比特币与以太坊均选择了一种被称为点倍增的陷门函数。如下图为我们选择的生成点G
G point

我们画出过点G的切线与曲线交与一点,我们选择此点关于x轴的对称点作为2G点。下图展示了进行第一次点倍增后的结果2G:

2G point

连结G2G与曲线交与一点,我们选择与此点关于x轴对称的点作为3G。如下图:

3G point

依次类推,我们可以得到4G的图像如下:

4G point

显然上述操作是直觉上是无法逆向的,关于严格的数学证明,读者可以自行查阅相关论文。以上过程可以进行算法上的优化,读者可以自行阅读noble-secp256k1的开发者的写的关于加速secp256k1计算的博客

在此给出比特币规定的secp256k1G的数值:

G.x = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
G.y = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8

综上所述,当我们生成一个私钥d后,我可以通过计算P=dG获得公钥P,而以太坊账户就是选择公钥最后20 Bytes进行keccak-256计算得到的。

noble-secp256k1实现如下:

static fromPrivateKey(privateKey: PrivKey) {
   
    return Point.BASE.multiply(normalizePrivateKey(privateKey));
}

当然,上述代码中的multiply是经过优化的。Point.BASE即上文给出的G点。在代码中使用了bigint表示,定义如下:

Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240')
Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424')

当然,secp256k1也存在定义域,其最大值被记为n,任何有效的点都应在n之内。具体定义如下:

n: BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141')

我们也可以在go-ethereum找到以下定义:

var (
	secp256k1N, _  = new(big.Int).SetString("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16)
	secp256k1halfN = new(big.Int).Div(secp256k1N, big.NewInt(2))
)

签名

为了方便读者阅读,给出变量说明:

变量缩写 含义
d 私钥
G 生成点
n 曲线最大值

标准的ECDSA签名由两个整数rs构成,签名流程如下:

  1. 对待签数据进行哈希计算获得哈希值m
  2. 生产随机数k,并使用kG相乘获得点R
  3. 计算r = R.x mod n。如果r = 0则需要重新生产随机数k
  4. 计算s = (1/k * (m + dr) mod n。如果s = 0则需要重新生产随机数k

上述过程中进行mod n是为了确保计算出的数值在我们所规定的定义域内。

在以太坊中,为了避免签名字段被其他应用使用,对哈希值m计算进行特别规定,即使用Keccak256("\x19Ethereum Signed Message:\n32" + Keccak256(message))进行哈希计算,使用go实现如下:

func signHash(data []byte) []byte {
   
   msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
   return crypto.Keccak256([]byte(msg))
}

对其签名的具体的代码实现方式如下:

function kmdToSig(kBytes: Uint8Array, m: bigint, d: bigint): RecoveredSig | undefined {
   
  const k = bytesToNumber(kBytes);
  if (!isWithinCurveOrder(k)) return;
  // Important: all mod() calls in the function must be done over `n`
  const {
    n } = CURVE;
  const q = Point.BASE.multiply(k);
  // r = x mod n
  const r = mod(q.x, n);
  if (r === _0n) return;
  // s = (1/k * (m + dr) mod n
  const s = mod(invert(k, n) * mod(m + d * r, n), n);
  if (s === _0n) return;
  const sig = new Signature(r, s);
  const recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n);
  return {
    sig, recovery };
}

上述代码给出了签名必要的两个元素rs,通过这两个元素我们就可以得到一个完整的比特币签名。根据BIP66规定,比特币的签名组成如下:

 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]

我们可以通过上述代码给出的sig变量中的信息填充上述签名。

但在以太坊中,以太坊要求的签名格式如下:

[R][S][V]

其中增加了变量v,此值已在上文的代码中给出,为recovery值。但与尚未给出的通用recover不同,以太坊交易签名中的vCHAIN_ID * 2 + 35CHAIN_ID * 2 + 36,分别对应v=0v=1。此过程由EIP155规定,目的是为了防止签名层面的重放攻击。

CHAIN_ID即每一条区块链的专属ID,具体可参考ChianList

验证签名

验证签名需要对签名者的公钥进行恢复,具体的流程如下:

  1. 计算签名信息的哈希值m
  2. 计算点R = (x, y)。其中,当v=0时,x=r; 当v=1时,x=r+n
  3. 计算u1 = hs^-1 mod n,其中h为经过调整的哈希值,调整算法参考truncateHash
  4. 计算u2 = sr^-1 mod n
  5. 计算Q = u1 * G + u2 * RQ即签名者的公钥。

我们在此给出对应的实现代码:

static fromSignature(msgHash: Hex, signature: Sig, recovery: number): Point {
   
    msgHash = ensureBytes(msgHash);
    const h = truncateHash(msgHash);
    const {
    r, s } = normalizeSignature(signature);
    if (recovery !== 0 && recovery !== 1) {
   
        throw new Error('Cannot recover signature: invalid recovery bit');
    }
    if (h === _0n) throw new Error('Cannot recover signature: msgHash cannot be 0');
    const prefix = recovery & 1 ? '03' : '02';
    const R = Point.fromHex(prefix + numTo32bStr(r));
    const {
    n } = CURVE;
    const rinv = invert(r, n);
    // Q = u1⋅G + u2⋅R
    const u1 = mod(-h * rinv, n);
    const u2 = mod(s * rinv, n);
    const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2);
    if (!Q) throw new Error('Cannot recover signature: point at infinify');
    Q.assertValidity();
    return Q;
}

对于上述代码,基本逻辑与我们介绍的流程是相同的。但开发者为了优化代码使用了许多函数,这些函数大多包含位移、算法优化和增强安全性的内容,我们不在此深入研究。开发者将所有的代码都放在了index.ts中,读者可以仅下载index.ts,然后自行使用vscode阅读代码,请善用函数定义跳转功能F12

通过上述流程,我们可以获得信息签名者的公钥,进一步可以获得签名者的以太坊地址。

以太坊交易签名

go-ethereum中,我们可以查到以下关于交易签名的源代码

// SignTx signs the transaction using the given signer and private key.
func SignTx(tx *
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WongSSH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值