钱包要从一个合成 Bitcoin 输出拿出 7000 sat,准备付给别人 5000 sat;假设剩余 2000 sat 作为手续费。有人说“把交易哈希签一下就行”。但交易序列化有见证数据、有上一笔交易的输出金额,还有多种 sighash 标志:究竟是哪些字节进签名,签名以后为什么节点还要查这笔输出有没有被别人先花?

本章只取一种确定的条件:Bitcoin 隔离见证 v0 的原生 P2WPKH、SIGHASH_ALL。它足以把输入、旧输出、收款方以及消费状态分开;legacy 旧式 sighash 与 Taproot 的 BIP 341 采用其他规则,不可直接复制这段算法。

拥有私钥与有权花费某个输出是两道判断

Bitcoin 使用 UTXO:上一次交易的某个输出含金额及锁定脚本;本次交易输入引用它的 outpoint(前交易 ID 和输出索引),并提交能满足锁定条件的数据。对本例 P2WPKH 而言,旧输出的见证程序为 OP_0 <20字节HASH160(压缩公钥)>,这笔花费的 witness 提交签名及压缩公钥;节点把公钥哈希与旧输出预期值核对,再按指定签名算法检查签名。

1
2
3
4
5
6
7
8
9
10
已存在 UTXO(本次仅教学模型):
outpoint = 上一交易标识 + 输出编号 0
amount = 7000 sat
scriptPubKey = OP_0 <HASH160(付款人公钥)>
│
本次输入:引用 outpoint,提交签名与公钥
↓
本次输出:5000 sat → 收款者的见证程序;差额 2000 sat(示意)

节点还必须检查:被引用输出确实存在且未花费、金额算术、脚本和整笔交易规则

持有这把私钥可对两个相冲突的花费意图都分别产生合法签名;同一 outpoint 在共同有效账本里仍只能按规则花费一次。哪一笔进入有效历史是节点与共识判断,不是 ECDSA 本身在两条签名间选赢家。

BIP 143:签的是规定预映像的双 SHA-256

对 v0 P2WPKH 的 SIGHASH_ALL,BIP 143 固定了待签预映像的字段及顺序。下面的 hashPrevouts、hashSequence、hashOutputs 各自是对指定序列化集合做双 SHA256,并非对 Markdown 中显示的字面单词求哈希。多个输入/输出时按顺序包含各自的字段,不能只拿当前一个输入去算 hashPrevouts。

1
2
3
4
5
6
7
8
9
10
11
12
13
preimage = nVersion
|| hashPrevouts 全部输入的 outpoint
|| hashSequence 全部输入的 nSequence
|| signing_input.outpoint
|| scriptCode 本例 P2WPKH 对应的 P2PKH 脚本编码
|| previous_output.value 旧输出金额,8 字节小端
|| signing_input.nSequence
|| hashOutputs SIGHASH_ALL 下所有输出的序列化
|| nLockTime
|| nHashType 4 字节小端的 01 00 00 00

digest = SHA256(SHA256(preimage))
signature = ECDSA_sign(私钥, digest)

正确公钥可验证签名对这些字节有效。把新输出脚本中的收款地址换掉,就改变了 hashOutputs;把被花费旧输出的金额从 7000 改为 7100,则 previous_output.value 改变。已有签名都不能直接沿用。用旧输出的金额计算摘要尤其关键:签名者需要知道正在花费哪个输出、它是多少钱,而不是只看这次交易新输出打算付多少。脚本码长度、紧凑长度编码、字段端序及压缩公钥表示若有一处错,节点不会按人类的“同一笔”来替两套不同字节补齐含义。

SIGHASH_ALL 的承诺范围与 SIGHASH_NONE、SIGHASH_SINGLE、ANYONECANPAY 不同,后者是否承诺全部输入和输出不能按本图推断。本章代码只处理长度小于 253 的单字节 CompactSize 等限定子集;它不是可安全解析任意交易的 Bitcoin 实现。

真签名、真规范向量与没有运行的节点

examples/cryptography/27_bip143_sighash.py 先用 BIP 143 原文公布的 Native P2WPKH 双输入向量对照摘要 c37af31116d1b27caf68aae9e3ac82f1477929014d5b917657d0eb49478cb670,不用其中公开测试私钥。另以一次性随机私钥和真实 secp256k1 ECDSA 签署一笔合成单输入交易的 BIP 143 摘要;改收款程序/旧金额或用错公钥都会验不过。脚本分别计算不带见证的 txid 与带见证的 wtxid,二者不同并不意味着任何一个已经被全网接受。

先按仓库内 examples/cryptography/README.md 完成环境准备;以下命令从仓库根目录执行。

1
2
python examples/cryptography/27_bip143_sighash.py
python -m unittest discover -s examples/cryptography -p 'test_27*.py' -v

2026-10-06 UTC 两条命令退出码 0,3 项测试通过。模型里的第一次消费会从 Python 字典删掉 outpoint,再次对原来真实有效的签名字节验签仍为真,但字典里已无这个 UTXO,规则检查失败。这是有意区分“签名有效”和“可花费”的教学模型,不是 Bitcoin Core 的 sendrawtransaction 返回值。实验 outpoint 完全虚构,不存在任何 regtest 区块上。Bitcoin Core 官方发行包在当前环境下载失败,因此未运行真实节点、没有真实钱包、没有生成区块、也没有可广播交易;这部分如实标为 NOT_RUN,不能靠上述模型替代。

对贯穿的“钱包经 HTTPS RPC 提交测试交易”来说,HTTPS 可以防止在这一跳通信里被窃听/篡改、帮助钱包确认 RPC 服务端;BIP 143 签名表达的是对指定交易字节的支出授权。即使两者通过,RPC 返回 txid 不等于交易符合 UTXO 规则,也不等于区块包含、确认或现实订单已经交付。要验收链上结果,需有冻结节点版本、可信输出、真实状态变化和冲突交易负例。

两道带答案的练习

画图题: 画出 BIP 143 SIGHASH_ALL 输入的旧输出 outpoint、旧输出金额、当前输入的 nSequence、新输出脚本以及用于验签的公钥。若同一旧输出已经被另一笔有效交易花过,而签名仍针对原始字节验真,哪个判断应先拒绝本交易?

可核对答案: 钱包须取出旧输出的金额和脚本条件、全部 outpoint/sequence、所有新输出;摘要还含版本/锁定时间/哈希类型。验签用被引用输出要求的 HASH160 所对应公钥。即便摘要与签名仍匹配,UTXO 状态查询显示 outpoint 已花费时,节点应拒绝本次花费;真实哪个区块包含先前交易须由可信账本确认,不能由单个签名判断。

实验变更题: 把 27_bip143_sighash.py 原来 output_bytes(5000, recipient_script) 的 5000 改为 5100,仅在负例重新计算待验摘要时使用新输出,原签名不重签;另一遍只把 previous_amount 从 7000 改为 7100。两次原签名分别在哪个字段失效?直接改代码中的全局初始输出然后重新签,哪项断言未必会失败?

可核对答案: 第一种变更改变 hashOutputs;第二种改变旧输出金额,都使原签名不再匹配新的 digest。若更改正例的初始输出并一起重算 digest 和签名,正例仍可能通过,不能把“重新签对新输出也有效”误解为旧签名未绑定收款方。并且新的输入输出金额是否满足节点规则须另核对。

资料与导航