比特币交易中的签名

比特币以 UTXO 的形式“存储”在全网账本中,被放置在其上的加密难题(锁定脚本)锁定,只有(要)提供正确的解锁脚本解决或满足这个加密难题或条件,才(就)可以用于支付。

结合丰富的操作码,锁定脚本和解锁脚本的形式拥有广泛的可能性。当锁定脚本为OP_ADD 7 OP_EQUAL时,5 24 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


def serialize_signature(signature: tuple) -> bytes:
"""Serialize ECDSA signature (r, s) to bitcoin strict DER format."""
r, s = signature
# BIP-62, BIP-66
# Enforce low s value in signature
# Using (curve.n - s) if s > 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0
# https://en.bitcoin.it/wiki/Transaction_malleability
if s > curve.n // 2:
s = curve.n - s
# r
r_bytes = r.to_bytes(32, byteorder='big').lstrip(b'\x00')
if r_bytes[0] & 0x80:
r_bytes = b'\x00' + r_bytes
serialized = bytes([2, len(r_bytes)]) + r_bytes
# s
s_bytes = s.to_bytes(32, byteorder='big').lstrip(b'\x00')
if s_bytes[0] & 0x80:
s_bytes = b'\x00' + s_bytes
serialized += bytes([2, len(s_bytes)]) + s_bytes
return bytes([0x30, len(serialized)]) + serialized


def deserialize_signature(serialized: bytes) -> tuple:
"""Deserialize ECDSA bitcoin DER formatted signature to (r, s)"""
try:
assert serialized[0] == 0x30
assert int(serialized[1]) == len(serialized) - 2
# r
assert serialized[2] == 0x02
r_len = int(serialized[3])
r = int.from_bytes(serialized[4: 4 + r_len], byteorder='big')
# s
assert serialized[4 + r_len] == 0x02
s_len = int(serialized[5 + r_len])
s = int.from_bytes(serialized[-s_len:], byteorder='big')
return r, s
except Exception:
raise ValueError(f'Invalid DER encoded {hexlify(serialized)}.')

我们写一个简单的例子来测试。

if __name__ == '__main__':
sig = (114587593887127314608220924841831336233967095853165151956820984900193959037698, 24000727837347392504013031837120627225728348681623127776947626422811445180558)
serialized_sig = serialize_signature(sig)
print(hexlify(serialized_sig))
decoded_sig = deserialize_signature(serialized_sig)
print(decoded_sig == sig)

运行结果为

b'3045022100fd5647a062d42cdde975ad4796cefd6b5613e731c08e0fb6907f757a60f44b020220350fee392713423ebfcd8026ea29cc95917d823392f07cd6c80f46712650388e'
True

准备工作

在继续之前,我们需要先实现一些基础方法,限于篇幅,请直接参考完整代码。

  • crypto.py,包含常用的哈希算法和 Base58Check 编解码方法等
  • meta.py,包含int_to_varintaddress_to_public_key_hashbuild_locking_script等常用方法,这些内容在之前的“学习笔记”系列文章中都有过介绍

顺便封装一下交易的输入和输出。

from collections import namedtuple
from binascii import unhexlify

SEQUENCE = 0xffffffff.to_bytes(4, byteorder='little')


class TxIn:
def __init__(self, satoshi: int, txid: str, index: int, locking_script: str, sequence: bytes = SEQUENCE) -> None:
self.satoshi = satoshi.to_bytes(8, byteorder='little')
self.txid = unhexlify(txid)[::-1]
self.index = index.to_bytes(4, byteorder='little')
self.locking_script = unhexlify(locking_script)
self.locking_script_len = int_to_varint(len(self.locking_script))
self.unlocking_script = b''
self.unlocking_script_len = b''
self.sequence = sequence


TxOut = namedtuple('TxOut', 'address satoshi')

交易摘要

验证 ECDSA 签名是否有效,需要三个参数:

  • 消息
  • ECC 公钥
  • ECDSA 签名

如果你还记得 P2PKH 的定义,你会发现,不论是解锁脚本还是锁定脚本,都没有明确签名对应的消息是什么。

[签名] [公钥] OP_DUP OP_HASH160 [公钥哈希] OP_EQUALVERIFY OP_CHECKSIG

请注意,解锁脚本中的签名,其实由两部分构成。

字节长度 内容
1 紧跟其后的所有数据的总长度
变长 序列化后的 ECDSA 签名
1 SIGHASH

之前的文章提到过,交易中签名的消息,是交易本身。更准确的说,是通过 SIGHASH 标记区分的、交易中特定的数据子集

交易本身在签名和验签时是已知的,也就是说,虽然脚本中没有直接存储消息的内容,但存储了能间接推算出消息内容的 SIGHASH。这个“推算出的消息内容”,叫交易的摘要,也叫原像(PreImage)。

我们需要实现一个方法,根据交易和 SIGHASH 来计算交易的摘要。

SIGHASH 有 6 不同的类型:

  • SIGHASH_ALL
  • SIGHASH_NONE
  • SIGHASH_SINGLE
  • SIGHASH_ALL | ANYONECANPAY
  • SIGHASH_NONE | ANYONECANPAY
  • SIGHASH_SINGLE | ANYONECANPAY

全网几乎所有的交易都使用 SIGHASH_ALL,这是最简单的一种类型,我们将以此为例。其他类型的 SIGHASH 本文暂不涉及,你可以通过文章 SIGHASH flagsBIP-143 探索。

请注意,SIGHASH_ALL 会对所有的交易输入签名,也就是说,对应交易摘要的个数,与交易输入的个数相同。

VERSION = 0x01.to_bytes(4, 'little')
LOCK_TIME = 0x00.to_bytes(4, byteorder='little')

SH_ALL = 0x01
SH_FORKID = 0x40
SIGHASH_ALL = SH_ALL | SH_FORKID


def serialize_outputs(outputs: list) -> bytes:
"""Serialize outputs [(address, satoshi), (address, satoshi), ...] to format ((LEN || locking_script) || (LEN || locking_script) || ...)"""
output_bytes = b''
for output in outputs:
output_bytes += output.satoshi.to_bytes(8, byteorder='little') + build_locking_script(address_to_public_key_hash(output.address))
return output_bytes


def transaction_digest(tx_ins: list, tx_outs: list, lock_time: bytes = LOCK_TIME, sighash: int = SIGHASH_ALL) -> list:
"""Returns the digest of unsigned transaction according to SIGHASH"""
# BIP-143 https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
# 1. nVersion of the transaction (4-byte little endian)
# 2. hashPrevouts (32-byte hash)
# 3. hashSequence (32-byte hash)
# 4. outpoint (32-byte hash + 4-byte little endian)
# 5. scriptCode of the input (serialized as scripts inside CTxOuts)
# 6. value of the output spent by this input (8-byte little endian)
# 7. nSequence of the input (4-byte little endian)
# 8. hashOutputs (32-byte hash)
# 9. nLocktime of the transaction (4-byte little endian)
# 10. sighash type of the signature (4-byte little endian)
if sighash == SIGHASH_ALL:
hash_prevouts = double_sha256(b''.join([tx_in.txid + tx_in.index for tx_in in tx_ins]))
hash_sequence = double_sha256(b''.join([tx_in.sequence for tx_in in tx_ins]))
hash_outputs = double_sha256(serialize_outputs(tx_outs))
digests = []
for tx_in in tx_ins:
digests.append(
VERSION +
hash_prevouts + hash_sequence +
tx_in.txid + tx_in.index + tx_in.locking_script_len + tx_in.locking_script + tx_in.satoshi + tx_in.sequence +
hash_outputs +
lock_time +
sighash.to_bytes(4, byteorder='little')
)
return digests
raise ValueError(f'Unsupported SIGHASH value {sighash}')

实验

让我们在之前的例子上继续。私钥

0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62

锁定了 3 个 UTXO。

priv_key = 0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62
pub_key = scalar_multiply(priv_key, curve.g)

inputs = [
TxIn(satoshi=1000, txid='d2bc57099dd434a5adb51f7de38cc9b8565fb208090d9b5ea7a6b4778e1fdd48', index=1, locking_script='76a9146a176cd51593e00542b8e1958b7da2be97452d0588ac'),
TxIn(satoshi=1000, txid='d2bc57099dd434a5adb51f7de38cc9b8565fb208090d9b5ea7a6b4778e1fdd48', index=2, locking_script='76a9146a176cd51593e00542b8e1958b7da2be97452d0588ac'),
TxIn(satoshi=1000, txid='fcc1a53e8bb01dbc094e86cb86f195219022c26e0c03d6f18ea17c3a3ba3c1e4', index=0, locking_script='76a9146a176cd51593e00542b8e1958b7da2be97452d0588ac'),
]

验证已签名的交易

事先使用其他钱包 App,消耗inputs[0],向地址1JDZRGf5fPjGTpqLNwjHFFZnagcZbwDsxw支付 800 聪,对应的交易是

4674da699de44c9c5d182870207ba89e5ccf395e5101dab6b0900bbf2f3b16cb

基于此场景,让我们开始第一个实验:验证这个已签名交易中的 ECDSA 签名。

公钥已知,为了验签,还需要利用这个交易

  • 计算出交易摘要,得到要签名的消息
  • 反序列化 DER 签名,得到 $(r, s)$

开始吧。

  1. 构造输入和输出
tx_inputs = inputs[0:1]
tx_outputs = [TxOut(address='1JDZRGf5fPjGTpqLNwjHFFZnagcZbwDsxw', satoshi=800)]
  1. 根据交易的输入和输出计算交易摘要(使用 SIGHASH_ALL)
tx_digest = transaction_digest(tx_inputs, tx_outputs)[0]
  1. 反序列化签名,得到 $(r, s)$

通过区块链浏览器,查询序列化后的交易数据。

图中标注的部分,是序列化后的 ECDSA 签名,通过之前实现的方法反序列化。

serialized_sig = unhexlify('304402207e2c6eb8c4b20e251a71c580373a2836e209c50726e5f8b0f4f59f8af00eee1a022019ae1690e2eb4455add6ca5b86695d65d3261d914bc1d7abb40b188c7f46c9a5')
sig = deserialize_signature(serialized_sig)
  1. 验签
print(verify_signature(pub_key, tx_digest, sig))

运行结果为

True

验签成功。

创建交易并签名

第二个小实验,我们用自己实现的代码,创建交易并对其签名。如果交易广播后比特币网络能正常接受,那么说明我们的代码是正确的。

这个交易会将inputs[1]inputs[2]作为输入,向地址18CgRLx9hFZqDZv75J5kED7ANnDriwvpi1支付 1700 聪。

开始吧。

  1. 构造输入和输出,并计算交易摘要
tx_inputs = inputs[1:]
tx_outputs = [TxOut(address='18CgRLx9hFZqDZv75J5kED7ANnDriwvpi1', satoshi=1700)]
tx_digests = transaction_digest(tx_inputs, tx_outputs)
  1. 对每个交易摘要签名,并且构造对应的解锁脚本
serialized_pub_key = serialize_public_key(pub_key)
for i in range(len(tx_digests)):
tx_digest = tx_digests[i]
sig = sign(priv_key, tx_digest)
serialized_sig = serialize_signature(sig)
# unlocking_script = LEN + der + sighash + LEN + public_key
tx_inputs[i].unlocking_script = bytes([len(serialized_sig) + 1]) + serialized_sig + bytes([SIGHASH_ALL, len(serialized_pub_key)]) + serialized_pub_key
print(hexlify(tx_inputs[i].unlocking_script))
tx_inputs[i].unlocking_script_len = int_to_varint(len(tx_inputs[i].unlocking_script))
print(hexlify(tx_inputs[i].unlocking_script_len))
  1. 根据输入(已签名)和输出构造完整的交易

序列化后的交易格式在“学习笔记”系列文章中有过详细介绍,这里也列出来方便你对应代码。

字节长度 内容
4 交易结构的版本
1~9 VarInt 交易包含几个输入,非零正整数
变长 输入数组
1~9 VarInt 交易包含几个输出,非零正整数
变长 输出数组
4 nLockTime
def serialize_transaction(tx_ins: list, tx_outs: list, lock_time: bytes = LOCK_TIME) -> bytes:
"""Serialize signed transaction"""
# version
raw_transaction = VERSION
# inputs
raw_transaction += int_to_varint(len(tx_ins))
for tx_in in tx_ins:
raw_transaction += tx_in.txid + tx_in.index + tx_in.unlocking_script_len + tx_in.unlocking_script + tx_in.sequence
# outputs
raw_transaction += int_to_varint(len(tx_outs)) + serialize_outputs(tx_outs)
# lock_time
raw_transaction += lock_time
return raw_transaction

将序列化后的交易数据打印出来,广播时会用到。同时计算交易的哈希。

raw = serialize_transaction(tx_inputs, tx_outputs)
print(hexlify(raw))
tx_id = double_sha256(raw)[::-1]
print(hexlify(tx_id))
  1. 验证

代码的运行结果为

b'463043022053b1f5a28a011c60614401eeef88e49c676a098ce36d95ded1b42667f40efa37021f4de6703f8c74b0ce5dad617c00d1fb99580beb7972bf681e7215911c3648de412102e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789'
b'69'
b'483045022100b9f293781ae1e269591df779dbadb41b9971d325d7b8f83d883fb55f2cb3ff7602202fe1e822628d85b0f52966602d0e153be411980d54884fa48a41d6fc32b4e9f5412102e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789'
b'6b'
b'010000000248dd1f8e77b4a6a75e9b0d0908b25f56b8c98ce37d1fb5ada534d49d0957bcd20200000069463043022053b1f5a28a011c60614401eeef88e49c676a098ce36d95ded1b42667f40efa37021f4de6703f8c74b0ce5dad617c00d1fb99580beb7972bf681e7215911c3648de412102e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789ffffffffe4c1a33b3a7ca18ef1d6030c6ec222902195f186cb864e09bc1db08b3ea5c1fc000000006b483045022100b9f293781ae1e269591df779dbadb41b9971d325d7b8f83d883fb55f2cb3ff7602202fe1e822628d85b0f52966602d0e153be411980d54884fa48a41d6fc32b4e9f5412102e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789ffffffff01a4060000000000001976a9144efe5cabaa9b56976d0c2a6171eb6af7f1ece36388ac00000000'
b'c04bbd007ad3987f9b2ea8534175b5e436e43d64471bf32139b5851adf9f477e'

其中,第一行和第三行是两个交易输入的解锁脚本,倒数第二行是序列化后的交易,最后一行是这个交易的哈希。

我在 WhatsOnChain 上,正常广播了这个交易。请注意下图标注的部分,浏览器解析后的解锁脚本,跟我们的计算结果是相同的。

至此,实验成功。

完整代码

sign_transaction.py

参考