比特币中的签名数据是通过私钥对交易数据进行 ECDSA
或 Schnorr
签名生成的, 签名数据被放置在交易的 scriptSig
或 witness
字段中。目的是为了在脚本执行时, 通过 OP_CHECKSIG
等操作码的检查, 以确认是否可以花费该 UTXO
。
交易的签名针对的是特定的交易输入, 多个交易输入需要多个签名。
未签名交易
签名前需要确认交易的输入和输出:
-
交易输入
txid
- 要花费的UTXO
来自于哪个交易IDvout
- 要花费的UTXO
来自于交易输出的哪个索引号
-
交易输出
amount
- 输出的聪的数量scriptPubKey
- 锁定脚本。根据接收地址生成的锁定脚本。
未签名交易构造器
签名
比特币中签名指的是计算出交易中 ScriptSig
或 Witness
字段的签名数据, 用于验证交易的合法性。
不同的交易输入的锁定脚本类型具有不同的签名过程, 但总体上都需要经历下面的步骤:
- 确认签名哈希类型
- 构造未签名数据
- 数据签名
- 签名数据编码
- 解锁脚本
签名交易
1. 确认签名哈希类型
签名哈希类型 SigHash
, 用于指定签名的数据。
SIGHASH_DEFAULT
- 值为0x00
, 用于P2TR
默认值, 等同于SIGHASH_ALL
, 签名所有输入和输出。SIGHASH_ALL
- 值为0x01
, 默认值, 签名所有输入和输出, 意味着交易的输入和输出有任何变动, 签名将失效。SIGHASH_NONE
- 值为0x02
, 签名所有输入, 但不签名任何输出, 除当前要签名的输入外, 其他交易输入的sequence
设置为0, 意味着生成的签名可以用于相同输入但有不同输出的交易。SIGHASH_SINGLE
- 值为0x03
, 签名所有输入, 但只签名对应索引的输出。交易输出的数量最多等于要签名的输入的索引号加 1, 例如签名的输入索引是 3, 则输出数量最多为 4。同时在输出中, 除了对应索引的输出外, 其他输出的金额设置为最大值, 锁定脚本为空。最后除当前要签名的输入外, 其他交易输入的sequence
设置为0SIGHASH_ANYONECANPAY
- 值为0x80
, 与其他类型组合使用:SIGHASH_ALL | SIGHASH_ANYONECANPAY
- 值为0x81
, 签名当前输入和所有输出SIGHASH_NONE | SIGHASH_ANYONECANPAY
- 值为0x82
, 签名当前输入, 但不签名输出SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
- 值为0x83
, 签名当前输入, 但只签名对应索引的输出
2. 构造未签名数据
对不同的类型的 UTXO
锁, 有不同的构造过程。
非隔离见证锁
非隔离见证锁有 P2PKH
、P2SH
、P2MS
。
当要签名的交易输入的 UTXO
锁是非隔离见证锁时, 则需要将 scriptSig
字段临时替换为 UTXO
的 scriptPubKey
字段, 其他交易输入的 scriptSig
字段保持为空。
例如有如下交易
要签名的第 0 个交易输入的 UTXO
锁是 P2PKH
类型,需将第 0 个交易输入scriptSig
字段替换为 scriptPubKey
字段, 当签名哈希类型是 SIGHASH_ALL
时, 得到签名数据是:
当 sigHash
不同时, 需要对交易数据进行不同的处理:
隔离见证V0
隔离见证V0锁有 P2WPKH
、P2WSH
、P2SH-P2WSH
。
BIP143 定义了隔离见证交易的签名规则, 如果要签名的交易输入的 UTXO
锁是隔离见证V0类型 则首先需要分别计算下列数据:
-
hashPrevouts
-sigHash
不包含SIGHASH_ANYONECANPAY
时存在,hashPrevouts
等于所有交易输入的txid
和vout
拼接后进行Hash256
计算 -
hashSequence
-sigHash
等于SIGHASH_ALL
时存在,hashSequence
等于所有交易输入的sequence
拼接后进行Hash256
计算 -
hashOutputs
sigHash
不包含SIGHASH_SINGLE
和SIGHASH_NONE
时,hashOutputs
等于所有交易输出的amount
、scriptPubKeySize
和scriptPubKey
拼接后进行Hash256
计算sigHash
等于SIGHASH_SINGLE
且要签名的交易输入索引小于输出数量时,hashOutputs
等于要签名的交易输入索引对应的交易输出的amount
、scriptPubKeySize
和scriptPubKey
拼接后进行Hash256
计算
其次依次拼接:
- 交易的版本号
version
hashPrevouts
hashSequence
- 要签名的交易输入的
txid
- 要签名的交易输入的
vout
scriptCode
的字节大小scriptCode
: 如果是P2WPKH
锁, 则提取出公钥哈希转换为P2PKH
锁定脚本作为scriptCode
; 如果是P2WSH
锁, 则提取出redeemScript
作为scriptCode
- 花费的
UTXO
的金额 - 要签名的交易输入的
sequence
hashOutputs
- 交易的
lockTime
具体见代码 signature.ts
隔离见证V1
隔离见证V1锁有 P2TR
。
BIP341 定义了 Taproot
签名规则, 如果要签名的交易输入的 UTXO
锁是 P2TR
, 则首先需要分别计算下列数据:
hashPrevouts
-sigHash
不包含SIGHASH_ANYONECANPAY
时存在,hashPrevouts
等于所有交易输入的txid
和vout
拼接后进行sha256
计算hashAmounts
-sigHash
不包含SIGHASH_ANYONECANPAY
时存在,hashAmounts
等于所有交易输入的Amount
拼接后进行sha256
计算hashScriptPubKeys
-sigHash
不包含SIGHASH_ANYONECANPAY
时存在,hashScriptPubKeys
等于所有交易输入的scriptPubKeySize
和scriptPubKey
拼接后进行sha256
计算hashSequences
-sigHash
不包含SIGHASH_ANYONECANPAY
时存在,hashSequences
等于所有交易输入的sequence
拼接后进行sha256
计算hashOutputs
sigHash
不包含SIGHASH_SINGLE
和SIGHASH_NONE
时,hashOutputs
等于所有交易输出的amount
、scriptPubKeySize
和scriptPubKey
拼接后进行sha256
计算sigHash
等于SIGHASH_SINGLE
且要签名的交易输入索引小于输出数量时,hashOutputs
等于要签名的交易输入索引对应的交易输出的amount
、scriptPubKeySize
和scriptPubKey
拼接后进行Hash256
计算
spendType
- 花费方式- 0 - 秘钥路径花费, 且没有附加数据
annex
- 1 - 秘钥路径花费, 且有附加数据
annex
- 2 - 脚本路径花费, 且没有附加数据
annex
- 3 - 脚本路径花费, 且有附加数据
annex
- 0 - 秘钥路径花费, 且没有附加数据
其次依次拼接:
- 交易的版本号
version
- 交易的
lockTime
hashPrevouts
hashAmounts
hashScriptPubKeys
hashSequences
hashOutputs
spendType
- 当
sigHash
包含SIGHASH_ANYONECANPAY
时, 继续拼接- 签名的交易输入的
txid
- 签名的交易输入的
vout
- 签名的交易输入的
Amount
- 签名的交易输入的
scriptPubKeySize
- 签名的交易输入的
scriptPubKey
- 签名的交易输入的
sequence
- 签名的交易输入的
- 当
sigHash
包含SIGHASH_ANYONECANPAY
时只拼接签名的交易输入的索引
具体见代码 signature.ts
3. 数据签名
P2TR
使用 Schnorr
签名, 其他类型的锁定脚本使用 ECDSA
签名。
ECDSA 签名
对于非 P2TR
类型的锁定脚本使用 ECDSA
签名。签名前需要在上一步构造的未签名数据末尾添加4字节小端序签名哈希类型, 并进行 Hash256
运算。将运算结果作为 ECDSA
签名的消息。
ECDSA
签名结果是一个 64
字节的数据, 前 32
字节是 r
, 后 32
字节是 s
。
验证时通过公钥和签名数据计算得到的 r
是否与签名数据中的 r
相同, 若相同则签名有效。
因此实际的交易中, 凡是用到 ECDSA
签名的脚本, 都需要提供公钥。
例如:
P2PK
将公钥存储在锁定脚本中, 解锁脚本只需要包含签名数据P2PKH
和P2WPKH
将公钥和签名数据都放在解锁脚本中P2MS
将多签用到的公钥都放在了锁定脚本中
Schnorr 签名
P2TR
使用 Schnorr
签名。签名前需要在上一步构造的未签名数据前添加 1 字节签名哈希类型。并计算 tapKeyHash
作为 Schnorr
签名的消息。
Schnorr
签名的私钥不是原始私钥, 而是 Taproot
私钥。
Taproot 私钥
Taproot
私钥, 也称 Tweaked Private Key
。等于原始私钥加上 tweak
值。
最后使用 tweakSigner
对 tapKeyHash
签名得到签名数据
4. 签名数据编码
ECDSA 签名
ECDSA
签名数据需要进行 DER
编码, 生成最终的签名数据。
假设 ECDSA
签名数据是:
- 在
r
和s
前添加类型字节0x02
(表示整数) 和长度字节0x20
- 组合
r
和s
后并在前添加整体长度字节0x44
和 标识符字节0x30
- 末尾添加签名使用的哈希类型
最终的签名数据大小为 71 字节
Schnorr 签名
Schnorr
签名数据不需要进行 DER
编码, 当 SigHash
不是 SIGHASH_DEFAULT
时, 需要在签名数据后添加 1 字节的SigHash
。
所以 Schnorr
签名数据是大小通常是 64 或 65 字节
5. 解锁脚本
根据要花费的 UTXO
的锁定脚本类型, 将签名数据以及其他数据联合生成解锁脚本。
例如:
P2PKH
要花费的 UTXO
是 P2PKH
类型, 则解锁脚本格式为:
并放入对应交易输入的 scriptSig
字段。
P2WPKH
要花费的 UTXO
是 P2WPKH
类型, 则将签名数据和公钥放入对应交易输入索引的 witness
字段。
P2TR
要花费的 UTXO
是 P2TR
类型且是秘钥路径, 则将签名数据直接放入对应交易输入索引 witness
字段。
如果是脚本路径花费, 则除了签名数据外, 还需要使用的脚本和控制块一并放入 witness
字段。