第2章:Bitcoin Script 基础#
参考:
book/translations/zh-Hans/Chapter 2.md
代码示例:code/chapter02/
最后更新: 2025-12-06
Bitcoin 的真正创新不仅在于数字签名或去中心化共识,更在于其可编程货币系统。每笔 Bitcoin 交易本质上都是一个定义花费条件的计算机程序。本章探讨使 Taproot 成为可能的基础概念:UTXO 模型和 Bitcoin Script。
2.1 UTXO 模型:数字现金,而非数字银行#
在深入脚本之前,必须理解 Bitcoin 如何表示价值。与维护账户余额的传统银行系统不同,Bitcoin 使用**未花费交易输出(Unspent Transaction Output,UTXO)**模型——一个更像物理现金而非数字银行账户的系统。
现金与银行:心智模型#
传统银行(账户模型):
账户显示余额:$500
花费 $350 只需从余额中扣除
结果:账户余额更新为 $150
无需处理”找零”
Bitcoin UTXO 模型(现金模型):
没有”$500 余额”
而是拥有具体的”钞票”:一张 $200 和三张 $100
要花费 $350,必须提供价值 $400 的钞票($200 + $100 + $100)
收到 $50 找零作为新的”钞票”
结果:现在有一张 $100 和一张 $50
这种类似现金的行为是 Bitcoin 设计和安全模型的基础。
UTXO 模型实践#
让我们追踪 Alice 和 Bob 之间的简单交易:
初始状态:
Alice 拥有一个 10 BTC 的 UTXO
Bob 没有 bitcoin
Alice 向 Bob 发送 7 BTC:
交易输入:Alice 的 10 BTC UTXO(必须完全消费)
交易输出:
7 BTC 给 Bob(新 UTXO)
3 BTC 找零给 Alice(新 UTXO)
结果:原始的 10 BTC UTXO 被销毁,创建两个新 UTXO
UTXO 标识:每个 UTXO 由 transaction_id:output_index 唯一标识
Bob 的 UTXO:
TX123:0(7 BTC)Alice 的找零:
TX123:1(3 BTC)
UTXO 关键属性#
完全消费:UTXO 必须完全花费——不能部分花费。
原子创建:交易要么完全成功(所有输入被消费,所有输出被创建),要么完全失败。
找零处理:输入和输出金额之间的任何差异都成为交易费用,除非明确作为找零返回。
并行处理:由于每个 UTXO 只能花费一次,多个交易可以并行验证,无需复杂的状态管理。
2.2 Bitcoin Script 与 P2PKH 基础#
Bitcoin Script:可编程花费条件#
每个 UTXO 不仅包含金额——它还包含一个锁定脚本(locking script)(ScriptPubKey),定义可以花费它的条件。要花费一个 UTXO,必须提供一个解锁脚本(unlocking script)(ScriptSig),满足这些条件。
脚本架构#
Unlocking Script (ScriptSig) + Locking Script (ScriptPubKey) → Valid/Invalid
锁定脚本(ScriptPubKey):
附加到每个 UTXO 输出
定义花费条件
示例:”只有能够为公钥 X 提供有效签名的人才能花费”
解锁脚本(ScriptSig):
在花费 UTXO 时提供
包含满足锁定脚本所需的数据
示例:”这是我的签名和公钥”
验证过程:
组合解锁和锁定脚本
作为单个程序执行
如果最终结果为 TRUE,UTXO 可以被花费
基于栈的执行#
Bitcoin Script 使用基于栈的执行模型,类似于 Forth 或 PostScript 等编程语言。操作操作一个后进先出(Last-In-First-Out,LIFO)栈:
初始栈:空
│ (empty) │
└───────────────────────────────────────┘
PUSH 3
│ 3 │
└───────────────────────────────────────┘
PUSH 5
│ 5 │
│ 3 │
└───────────────────────────────────────┘
ADD 操作
│ 8 │
└───────────────────────────────────────┘
操作过程:
从栈中弹出两个数字:5(顶部)和 3 执行加法:5 + 3 = 8 将结果 8 推回栈顶
这个简单模型在保持可预测和安全的同时,支持复杂的花费条件。
P2PKH:基础脚本#
支付到公钥哈希(Pay-to-Public-Key-Hash,P2PKH)是 Bitcoin 最基础的脚本类型,也是理解更复杂脚本(如 Taproot 中使用的脚本)的基础。
P2PKH 锁定脚本
OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG
此脚本表示:”此 UTXO 可以由任何能够提供哈希到 pubkey_hash 的公钥以及来自相应私钥的有效签名的人花费。”
P2PKH 解锁脚本
<signature> <public_key>
花费者提供:
证明拥有私钥的数字签名
公钥本身(将被哈希和验证)
真实示例:Satoshi 向 Hal Finney#
让我们检查著名的第一笔 Bitcoin 交易:中本聪(Satoshi Nakamoto)向 Hal Finney 发送 10 BTC。
交易 ID:f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16
交易结构:
输入:Satoshi 的 coinbase UTXO(挖矿获得的 50 BTC)
输出:
10 BTC 给 Hal Finney
40 BTC 找零给 Satoshi
注意:此早期交易使用 P2PK(Pay-to-Public-Key)而非 P2PKH,直接在锁定脚本中嵌入公钥。现代 Bitcoin 使用 P2PKH 以获得更好的安全性和空间效率。
逐步 P2PKH 执行——Hal Finney 示例#
让我们使用真实交易追踪 P2PKH 脚本执行。考虑 Hal Finney 后来花费他的 10 BTC:
锁定脚本(来自 UTXO):
OP_DUP OP_HASH160 OP_PUSHBYTES_20 340cfcffe029e6935f4e4e5839a2ff5f29c7a571 OP_EQUALVERIFY OP_CHECKSIG
解锁脚本(由 Hal 提供):
OP_PUSHBYTES_71 30440220576497b7e6f9b553c0aba0d8929432550e092db9c130aae37b84b545e7f4a36c022066cb982ed80608372c139d7bb9af335423d5280350fe3e06bd510e695480914f01
OP_PUSHBYTES_33 02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
执行步骤:
推送签名到栈:
│ 30440220...914f01 (signature) │
└───────────────────────────────────────┘
推送公钥到栈:
│ 02898711...8519 (public_key) │
│ 30440220...914f01 (signature) │
└───────────────────────────────────────┘
OP_DUP:复制栈顶项(公钥):
│ 02898711...8519 (public_key) │
│ 02898711...8519 (public_key) │
│ 30440220...914f01 (signature) │
└───────────────────────────────────────┘
OP_HASH160:哈希栈顶项:
│ 340cfcff...7a571 (hash160_result) │
│ 02898711...8519 (public_key) │
│ 30440220...914f01 (signature) │
└───────────────────────────────────────┘
推送预期哈希:来自锁定脚本:
│ 340cfcff...7a571 (expected_hash) │
│ 340cfcff...7a571 (computed_hash) │
│ 02898711...8519 (public_key) │
│ 30440220...914f01 (signature) │
└───────────────────────────────────────┘
OP_EQUALVERIFY:比较栈顶两项,如果相等则移除两者:
│ 02898711...8519 (public_key) │
│ 30440220...914f01 (signature) │
└───────────────────────────────────────┘
(如果哈希不匹配,脚本失败)
OP_CHECKSIG:验证公钥和交易的签名:
│ 1 (TRUE) │
└───────────────────────────────────────┘
最终检查:如果栈顶非零,脚本成功。
P2PKH 安全属性#
哈希原像抗性:公钥在首次花费前保持隐藏,提供针对 ECDSA 潜在量子攻击的保护。
签名验证:密码学证明花费者控制与公钥哈希对应的私钥。
交易完整性:签名覆盖交易详情,防止签名后修改。
重放保护:签名特定于特定交易,不能重复使用。
2.3 实践实现:构建 P2PKH 交易#
构建真实的测试网 Legacy 到 SegWit 交易#
让我们逐步构建完整的 P2PKH 交易,解释每个组件,然后使用真实数据追踪脚本执行。
# 示例 1: 构建 P2PKH 交易
# 参考: code/chapter02/01_build_p2pkh_transaction.py
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis
from bitcoinutils.transactions import Transaction, TxInput, TxOutput
from bitcoinutils.keys import P2wpkhAddress, P2pkhAddress, PrivateKey
from bitcoinutils.script import Script
def main():
# Setup testnet environment
setup('testnet')
# Sender information - Legacy P2PKH
# NOTE: This is a TESTNET private key used for educational purposes only.
# The key is intentionally exposed here for reproducibility of the example.
# NEVER use this key or any exposed private key for real Bitcoin transactions.
private_key = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
public_key = private_key.get_public_key()
from_address_str = "myYHJtG3cyoRseuTwvViGHgP2efAvZkYa4"
from_address = P2pkhAddress(from_address_str)
# Receiver - SegWit address
to_address = P2wpkhAddress('tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4')
print(f"Sender Legacy Address: {from_address_str}")
print(f"Receiver SegWit Address: {to_address.to_string()}")
# Create transaction input (referencing previous UTXO)
txin = TxInput(
'34b90a15d0a9ec9ff3d7bed2536533c73278a9559391cb8c9778b7e7141806f7',
1 # vout index
)
# Calculate amounts
total_input = 0.00029606 # Input amount in BTC
amount_to_send = 0.00029400 # Amount to send
fee = total_input - amount_to_send # Transaction fee
# Create transaction output
txout = TxOutput(to_satoshis(amount_to_send), to_address.to_script_pub_key())
# Create unsigned transaction
tx = Transaction([txin], [txout])
print(f"Unsigned transaction: {tx.serialize()}")
# Get the P2PKH locking script for signing
p2pkh_script = from_address.to_script_pub_key()
# Sign the transaction input
signature = private_key.sign_input(tx, 0, p2pkh_script)
# Create the unlocking script: <signature> <public_key>
txin.script_sig = Script([signature, public_key.to_hex()])
# Get the signed transaction
signed_tx = tx.serialize()
print(f"Signed transaction: {signed_tx}")
print(f"Transaction size: {tx.get_size()} bytes")
if __name__ == "__main__":
main()
Sender Legacy Address: myYHJtG3cyoRseuTwvViGHgP2efAvZkYa4
Receiver SegWit Address: tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4
Unsigned transaction: 0200000001f7061814e7b778978ccb919355a97832c7336553d2bed7f39feca9d0150ab9340100000000fdffffff01d872000000000000160014c5b28d6bba91a2693a9b1876bcd3929323890fb200000000
Signed transaction: 0200000001f7061814e7b778978ccb919355a97832c7336553d2bed7f39feca9d0150ab934010000006a473044022016a36f9cf57cc24e0d3210491a6cc6f9235d8e5682092bead300b392bae41d800220163b8fdd0548d6ac543e064ca36027be80663fbf98277bf066f27bfa5ad1c3fa012102898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519fdffffff01d872000000000000160014c5b28d6bba91a2693a9b1876bcd3929323890fb200000000
Transaction size: 188 bytes
关键函数和组件说明#
设置和密钥管理:
setup('testnet'):为测试网操作配置库PrivateKey():从 WIF 格式创建私钥对象P2pkhAddress():从地址字符串创建 Legacy 地址对象P2wpkhAddress():创建 SegWit 地址对象
交易构建:
TxInput():通过交易 ID 和输出索引引用先前的 UTXOTxOutput():定义发送币的地址和金额Transaction():将输入和输出组合成完整交易to_satoshis():将 BTC 金额转换为 satoshi 单位(1 BTC = 100,000,000 satoshis)
脚本和签名操作:
to_script_pub_key():为地址生成锁定脚本sign_input():为特定输入创建密码学签名Script():从签名和公钥数据构建解锁脚本
真实数据分析和栈执行#
让我们分析交易执行的实际数据。当此代码运行时,它产生一个已广播到测试网的真实交易:
交易 ID:bf41b47481a9d1c99af0b62bb36bc864182312f39a3e1e06c8f6304ba8e58355
原始交易数据:
0100000001f7061814e7b778978ccb9193559a7832c733655302d2bef3d7f9eca9d0150ab9010000006a473044022055c309fe3f6099f4f881d0fd960923eb91aff0d8ef3501a2fc04dce99aca609d0220174b9aec4fc22f6f81b637bbafec9554e497ec2d9f3ca4992ee4209dd047443d012102898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519ffffffff01c8720000000000001600148c7bf2dd2b38b5ed13c3b24ceebf2e7ae30a47df00000000
让我们分解解锁脚本(ScriptSig)并追踪其执行:
解锁脚本(ScriptSig):
473044022055c309fe3f6099f4f881d0fd960923eb91aff0d8ef3501a2fc04dce99aca609d0220174b9aec4fc22f6f81b637bbafec9554e497ec2d9f3ca4992ee4209dd047443d012102898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
解析组件:
47:OP_PUSHBYTES_71(推送 71 字节——签名)304402...443d01:DER 编码的签名(71 字节)21:OP_PUSHBYTES_33(推送 33 字节——公钥)02898711...8519:压缩公钥(33 字节)
**锁定脚本(ScriptPubKey)**来自被花费的 UTXO:
76a914c5b28d6bba91a2693a9b1876bcd3929323890fb288ac
解析锁定脚本:
76:OP_DUPa9:OP_HASH16014:OP_PUSHBYTES_20(推送 20 字节)c5b28d6bba91a2693a9b1876bcd3929323890fb2:公钥哈希(20 字节)88:OP_EQUALVERIFYac:OP_CHECKSIG
栈执行追踪#
现在让我们逐步追踪完整的脚本执行:
初始状态:
│ (empty) │
└───────────────────────────────────────┘
脚本:
步骤 1 - 推送签名:
操作:PUSH 304402…443d01
│ 304402...443d01 (signature) │
└───────────────────────────────────────┘
步骤 2 - 推送公钥:
操作:PUSH 02898711…8519
│ 02898711...8519 (public_key) │
│ 304402...443d01 (signature) │
└───────────────────────────────────────┘
步骤 3 - OP_DUP:
操作:复制栈顶项
│ 02898711...8519 (public_key) │
│ 02898711...8519 (public_key) │
│ 304402...443d01 (signature) │
└───────────────────────────────────────┘
步骤 4 - OP_HASH160:
操作:Hash160(栈顶项) 计算:hash160(02898711…8519) = c5b28d6bba91a2693a9b1876bcd3929323890fb2
│ c5b28d6b...890fb2 (computed_hash) │
│ 02898711...8519 (public_key) │
│ 304402...443d01 (signature) │
└───────────────────────────────────────┘
步骤 5 - 推送预期哈希:
操作:PUSH c5b28d6bba91a2693a9b1876bcd3929323890fb2
│ c5b28d6b...890fb2 (expected_hash) │
│ c5b28d6b...890fb2 (computed_hash) │
│ 02898711...8519 (public_key) │
│ 304402...443d01 (signature) │
└───────────────────────────────────────┘
步骤 6 - OP_EQUALVERIFY:
操作:比较栈顶两项,如果相等则移除两者 验证:c5b28d6b… == c5b28d6b… ✓(匹配!)
│ 02898711...8519 (public_key) │
│ 304402...443d01 (signature) │
└───────────────────────────────────────┘
步骤 7 - OP_CHECKSIG:
操作:验证公钥和交易的签名 输入:
公钥:02898711…8519 签名:304402…443d01 交易数据:(用于签名的序列化交易)
验证:ECDSA 验证 ✓(有效签名!)
│ 1 (TRUE) │
└───────────────────────────────────────┘
最终状态:
│ 1 (TRUE) │
└───────────────────────────────────────┘
结果:成功(栈顶非零值)
交易广播结果#
此交易已成功广播到 Bitcoin 测试网,可在以下位置查看: https://mempool.space/testnet/tx/bf41b47481a9d1c99af0b62bb36bc864182312f39a3e1e06c8f6304ba8e58355
关键观察:
输入引用交易
34b90a15...1806f7索引 1 处的 UTXO输出将 29,400 satoshis 发送到 SegWit 地址
交易费用为 206 satoshis(29,606 - 29,400)
签名验证证明拥有私钥,而不暴露私钥
从 P2PKH 到高级脚本#
P2PKH 为理解 Bitcoin 的可编程货币系统提供了基础,但这只是开始。相同的原则——基于栈的执行、密码学验证和条件逻辑——支持更复杂的脚本,我们将在后续章节中探讨:
P2SH(Pay-to-Script-Hash):
在保持地址简短的同时支持复杂的花费条件
将脚本复杂性从区块链转移到花费者
包装 SegWit 和多重签名方案的基础
P2WPKH(Pay-to-Witness-Public-Key-Hash):
SegWit 的 P2PKH 等价物,效率更高
将签名数据与交易数据分离
减少交易可延展性并支持 Lightning Network
P2TR(Pay-to-Taproot):
Bitcoin 脚本演化的顶点
支持看起来像简单支付的复杂智能合约
将 Schnorr 签名与 Merkle 树结合,实现最大灵活性
每次演化都保持向后兼容,同时添加新功能。理解 P2PKH 的栈执行模型至关重要,因为 Taproot 使用相同的基础方法,只是使用更复杂的密码学原语和脚本结构。
在下一章中,我们将深入探讨这些地址类型,检查它们的脚本结构,并理解每个改进如何建立在从 P2PKH 学到的经验之上。
章节总结#
本章建立了使 Taproot 成为可能的基础概念:
UTXO 模型:Bitcoin 将价值表示为离散的、可花费的输出,而非账户余额。每个 UTXO 必须完全消费,创建一个类似现金的系统,支持并行交易验证并消除复杂的状态管理。
脚本系统:每个 UTXO 通过锁定脚本(ScriptPubKey)包含可编程的花费条件。要花费一个 UTXO,必须提供一个解锁脚本(ScriptSig),当一起执行时满足这些条件。
栈执行:Bitcoin Script 使用简单的基于栈的模型处理条件,操作操作一个后进先出栈以验证花费授权。
P2PKH 实现:基础脚本类型通过七步过程演示签名验证和公钥验证:提供签名和公钥、复制、哈希、比较和签名验证。
实践开发:使用 bitcoinutils 等工具,开发者可以构建、签名和广播 P2PKH 交易,同时理解底层的密码学操作和栈执行。
理解这些概念至关重要,因为 Taproot 建立在这些概念之上,使用相同的基于栈的执行模型,同时引入新的密码学原语和脚本结构。从简单的 P2PKH 脚本到 Taproot 复杂花费条件的旅程,说明了 Bitcoin 从基本数字现金到复杂金融应用平台的演化——同时保持了使 Bitcoin 独特的安全性和简洁性。