非对称加密的应用场景之一是签名和验证。签名和验证是一种验证数据完整性和真实性的方法。在以太坊中,按照签名对象的不同,可以划分出两种类型:签名交易和签名消息。
以太坊中,可以根据 私钥 和 消息,通过 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 编码。下面是签名交易的步骤:
RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0)
RLP(nonce, gasPrice, gasLimit, to, value, data, v, r, s)
在开发过程中,不必逐步实现上面的签名过程,可以使用 ethers.js 提供的 signTransaction
方法,直接对交易数据进行编码及签名。
笔者在学习这部分内容时,认为签名交易的验证发生在以太坊客户端中,由客户端语言如 Golang、C++ 等实现,因此该过程不在本文讨论范围。
签名消息也称为 presigned message。EIP-191 和 EIP-712 制定了签名消息的规范,本文不会讨论这些规范,仅为说明下面签名消息的步骤:
同样的,ethers.js 提供了签名消息方法 signMessage
和 signTypedData
,这两种方法分别对应了规范中的两类格式。
验证签名的过程,即通过 签名 和 消息 计算出 公钥,进而验证 公钥 和 地址 是否一致。ethers.js 提供了计算公钥的方法 verifyMessage
和 verifyTypedData
,分别对应规范中的两类格式。在合约代码中,则通过 ecrecover
方法计算公钥。