比特币以 UTXO 的形式“存储”在全网账本中,被放置在其上的加密难题(锁定脚本)锁定,只有(要)提供正确的解锁脚本解决或满足这个加密难题或条件,才(就)可以用于支付。
结合丰富的操作码,锁定脚本和解锁脚本的形式拥有广泛的可能性。当锁定脚本为OP_ADD 7 OP_EQUAL
时,5 2
和4 3
都是正确解锁脚本。当蔡明使用比特币收款时,她需要提供一个收款模板(锁定脚本),以确保这些比特币只有自己才能花费。很明显,上述这类锁定脚本与现实世界人的身份毫无关联,虽然蔡明可以想方设法将锁定脚本搞的足够复杂,但这种方式并不通用,更没有安全保障,无法彻底杜绝其他人也能提供正确的解锁脚本。
非对称加密中的公钥可以作为身份标识,签名可以作为身份认证和授权的手段。为了做到这一点,蔡明需要在锁定脚本里关联自己的公钥,并限制只有提供了正确的数字签名才能花费这个 UTXO。数学原理可以保证,只要蔡明的私钥没有丢失或泄露就没有其他人能提供正确的签名。
上述这类交易被统称为 P2PKH 交易(P2PK 的演进版),他们的锁定脚本和解锁脚本格式固定,能方便各类钱包集成。比特币网络中的绝大多数交易都是(郭达付款给蔡明)这样的形式,本文将以 P2PKH 交易为例,详细介绍交易签名的细节。
序列化 ECDSA 签名
之前的文章介绍了如何创建 ECDSA 签名。在把 $(r, s)$ 放入解锁脚本前,需要先将其序列化成 DER 格式,结构如下。
字节长度 | 内容 |
---|---|
1 | 格式头 0x30 |
1 | 紧跟其后的所有数据的总长度 |
1 | 整数标志 0x02 |
1 | R 的长度 |
变长 | 整数 r 按大端模式序列化后的字节流 R。当流的起始字节不小于 0x80 时,还需要在流的开头添加 0x00 |
1 | 整数标志 0x02 |
1 | S 的长度 |
变长 | 整数 s 按大端模式序列化后的字节流 S。当流的起始字节不小于 0x80 时,还需要在流的开头添加 0x00 |
根据定义不难写出代码。请注意序列化时对 S 的处理,因为 ECDSA 签名的“对称性”,为了避免“混乱”,会强制 DER 中的 S 不大于
0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0 |
即 curve.n // 2
。
from binascii import hexlify |
我们写一个简单的例子来测试。
if __name__ == '__main__': |
运行结果为
b'3045022100fd5647a062d42cdde975ad4796cefd6b5613e731c08e0fb6907f757a60f44b020220350fee392713423ebfcd8026ea29cc95917d823392f07cd6c80f46712650388e' |
准备工作
在继续之前,我们需要先实现一些基础方法,限于篇幅,请直接参考完整代码。
- crypto.py,包含常用的哈希算法和 Base58Check 编解码方法等
- meta.py,包含
int_to_varint
、address_to_public_key_hash
、build_locking_script
等常用方法,这些内容在之前的“学习笔记”系列文章中都有过介绍
顺便封装一下交易的输入和输出。
from collections import namedtuple |
交易摘要
验证 ECDSA 签名是否有效,需要三个参数:
- 消息
- ECC 公钥
- ECDSA 签名
如果你还记得 P2PKH 的定义,你会发现,不论是解锁脚本还是锁定脚本,都没有明确被签名的消息是什么。
[签名] [公钥] OP_DUP OP_HASH160 [公钥哈希] OP_EQUALVERIFY OP_CHECKSIG |
请注意,解锁脚本中的签名,其实由两部分构成。
字节长度 | 内容 |
---|---|
1 | 紧跟其后的所有数据的总长度 |
变长 | 序列化后的 ECDSA 签名 |
1 | SIGHASH |
之前的文章提到过,交易中被签名的消息,是交易本身。更准确的说,是通过 SIGHASH 标记区分的、交易中特定的数据子集。
交易本身在签名和验签时是已知的,也就是说,虽然脚本中没有直接存储消息的内容,但存储了能间接推算出消息内容的 SIGHASH。这个“推算出的消息内容”,叫交易的摘要。
我们需要实现一个方法,根据交易和 SIGHASH 来计算交易的摘要。
SIGHASH 有 6 不同的类型:
- SIGHASH_ALL
- SIGHASH_NONE
- SIGHASH_SINGLE
- SIGHASH_ALL | ANYONECANPAY
- SIGHASH_NONE | ANYONECANPAY
- SIGHASH_SINGLE | ANYONECANPAY
全网几乎所有的交易都使用 SIGHASH_ALL,这是最简单的一种类型,我们将以此为例。其他类型的 SIGHASH 本文暂不涉及,你可以通过文章 SIGHASH flags 和 BIP-143 探索。
请注意,SIGHASH_ALL 会对所有的交易输入签名,也就是说,对应交易摘要的个数,与交易输入的个数相同。
VERSION = 0x01.to_bytes(4, 'little') |
实验
让我们在之前的例子上继续。私钥
0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 |
锁定了 3 个 UTXO。
priv_key = 0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 |
验证已签名的交易
事先使用其他钱包 App,消耗inputs[0]
,向地址1JDZRGf5fPjGTpqLNwjHFFZnagcZbwDsxw
支付 800 聪,对应的交易是
4674da699de44c9c5d182870207ba89e5ccf395e5101dab6b0900bbf2f3b16cb |
基于此场景,让我们开始第一个实验:验证这个已签名交易中的 ECDSA 签名。
公钥已知,为了验签,还需要利用这个交易
- 计算出交易摘要,得到要签名的消息
- 反序列化 DER 签名,得到 $(r, s)$
开始吧。
- 构造输入和输出
tx_inputs = inputs[0:1] |
- 根据交易的输入和输出计算交易摘要(使用 SIGHASH_ALL)
tx_digest = transaction_digest(tx_inputs, tx_outputs)[0] |
- 反序列化签名,得到 $(r, s)$
通过区块链浏览器,查询序列化后的交易数据。
图中标注的部分,是序列化后的 ECDSA 签名,通过之前实现的方法反序列化。
serialized_sig = unhexlify('304402207e2c6eb8c4b20e251a71c580373a2836e209c50726e5f8b0f4f59f8af00eee1a022019ae1690e2eb4455add6ca5b86695d65d3261d914bc1d7abb40b188c7f46c9a5') |
- 验签
print(verify_signature(pub_key, tx_digest, sig)) |
运行结果为
True |
验签成功。
创建交易并签名
第二个小实验,我们用自己实现的代码,创建交易并对其签名。如果交易广播后比特币网络能正常接受,那么说明我们的代码是正确的。
这个交易会将inputs[1]
和inputs[2]
作为输入,向地址18CgRLx9hFZqDZv75J5kED7ANnDriwvpi1
支付 1700 聪。
开始吧。
- 构造输入和输出,并计算交易摘要
tx_inputs = inputs[1:] |
- 对每个交易摘要签名,并且构造对应的解锁脚本
serialized_pub_key = serialize_public_key(pub_key) |
- 根据输入(已签名)和输出构造完整的交易
序列化后的交易格式在“学习笔记”系列文章中有过详细介绍,这里也列出来方便你对应代码。
字节长度 | 内容 |
---|---|
4 | 交易结构的版本 |
1~9 VarInt | 交易包含几个输入,非零正整数 |
变长 | 输入数组 |
1~9 VarInt | 交易包含几个输出,非零正整数 |
变长 | 输出数组 |
4 | nLockTime |
def serialize_transaction(tx_ins: list, tx_outs: list, lock_time: bytes = LOCK_TIME) -> bytes: |
将序列化后的交易数据打印出来,广播时会用到。同时计算交易的哈希。
raw = serialize_transaction(tx_inputs, tx_outputs) |
- 验证
代码的运行结果为
b'463043022053b1f5a28a011c60614401eeef88e49c676a098ce36d95ded1b42667f40efa37021f4de6703f8c74b0ce5dad617c00d1fb99580beb7972bf681e7215911c3648de412102e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789' |
其中,第一行和第三行是两个交易输入的解锁脚本,倒数第二行是序列化后的交易,最后一行是这个交易的哈希。
我在 WhatsOnChain 上,正常广播了这个交易。请注意下图标注的部分,浏览器解析后的解锁脚本,跟我们的计算结果是相同的。
至此,实验成功。
完整代码
参考
- BIP-62
- BIP-66
- Programming Bitcoin by Jimmy Song,Chapter 4. Serialization
- Money Button Documentation,Signatures
- BIP-143
- Transaction malleability