“与其感慨路难行,不如马上出发。”
以太坊中的数字签名和验证过程

非对称加密的应用场景之一是签名和验证。签名和验证是一种验证数据完整性和真实性的方法。在以太坊中,按照签名对象的不同,可以划分出两种类型:签名交易和签名消息。

概述

以太坊中,可以根据 私钥消息,通过 ECDSA 算法生成 签名。签名结果 r s v 可以拼接为一个 65 字节的序列,进而编码为长度 130 的十六进制数据(不包括前缀 0x)。私钥通常在链下由用户保管,因此签名的过程一般发生在链下。

验证过程会根据 签名消息,计算出 公钥,亦即消息发送方的地址。通过比对 公钥地址,可以验证签名的真实性。对于签名交易,验证过程通常发生在将交易打包到区块之前;对于签名消息,验证过程可能发生在链下,也可能发生在合约方法执行过程中。

下面给出将 签名 分割为 r s v 的主要代码。

// solidity 方法,需要使用内联汇编语法
assembly {
    r := mload(add(signature, 0x20))
    s := mload(add(signature, 0x40))
    v := byte(0, mload(add(signature, 0x60)))
}
// javascript 方法
const r = signature.slice(0, 66); // 前32字节(包括前缀 0x,共66个字符)
const s = '0x' + signature.slice(66, 130); // 中间32字节(64个字符,另外拼接前缀 0x)
const v = '0x' + signature.slice(130, 132); // 最后1字节(2个字符,另外拼接前缀 0x)

签名交易

以太坊为减少网络传输和存储开销,实现了一种 RLP(Recursive Length Prefix) 编码方法。在对交易进行签名的过程中,会对数据进行 RLP 编码。下面是签名交易的步骤:

  1. 对交易数据进行 RLP 编码 RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0)
  2. 对 RLP 编码结果进行 Keccak256 哈希计算
  3. 使用私钥对哈希结果进行 ECDSA 签名
  4. 将交易数据和签名结果进行 RLP 编码 RLP(nonce, gasPrice, gasLimit, to, value, data, v, r, s)

在开发过程中,不必逐步实现上面的签名过程,可以使用 ethers.js 提供的 signTransaction 方法,直接对交易数据进行编码及签名。

笔者在学习这部分内容时,认为签名交易的验证发生在以太坊客户端中,由客户端语言如 Golang、C++ 等实现,因此该过程不在本文讨论范围。

签名消息

签名消息也称为 presigned message。EIP-191EIP-712 制定了签名消息的规范,本文不会讨论这些规范,仅为说明下面签名消息的步骤:

  1. 按照规范对消息进行格式化
  2. 对格式化后的消息进行 Keccak256 哈希计算
  3. 使用私钥对哈希结果进行 ECDSA 签名

同样的,ethers.js 提供了签名消息方法 signMessagesignTypedData,这两种方法分别对应了规范中的两类格式。

验证签名的过程,即通过 签名消息 计算出 公钥,进而验证 公钥地址 是否一致。ethers.js 提供了计算公钥的方法 verifyMessageverifyTypedData,分别对应规范中的两类格式。在合约代码中,则通过 ecrecover 方法计算公钥。