第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

  1. 交易输入:Alice 的 10 BTC UTXO(必须完全消费)

  2. 交易输出

    • 7 BTC 给 Bob(新 UTXO)

    • 3 BTC 找零给 Alice(新 UTXO)

  3. 结果:原始的 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。

交易 IDf4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16

交易结构

  • 输入: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

执行步骤

  1. 推送签名到栈

    
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. 推送公钥到栈

    
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. OP_DUP:复制栈顶项(公钥):

    
│ 02898711...8519 (public_key)          │
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. OP_HASH160:哈希栈顶项:

    
│ 340cfcff...7a571 (hash160_result)     │
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. 推送预期哈希:来自锁定脚本:

    
│ 340cfcff...7a571 (expected_hash)      │
│ 340cfcff...7a571 (computed_hash)      │
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. OP_EQUALVERIFY:比较栈顶两项,如果相等则移除两者:

    
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
(如果哈希不匹配,脚本失败)
  1. OP_CHECKSIG:验证公钥和交易的签名:


│ 1 (TRUE)                              │
└───────────────────────────────────────┘
  1. 最终检查:如果栈顶非零,脚本成功。

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 和输出索引引用先前的 UTXO

  • TxOutput():定义发送币的地址和金额

  • Transaction():将输入和输出组合成完整交易

  • to_satoshis():将 BTC 金额转换为 satoshi 单位(1 BTC = 100,000,000 satoshis)

脚本和签名操作

  • to_script_pub_key():为地址生成锁定脚本

  • sign_input():为特定输入创建密码学签名

  • Script():从签名和公钥数据构建解锁脚本

真实数据分析和栈执行#

让我们分析交易执行的实际数据。当此代码运行时,它产生一个已广播到测试网的真实交易:

交易 IDbf41b47481a9d1c99af0b62bb36bc864182312f39a3e1e06c8f6304ba8e58355

原始交易数据

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_DUP

  • a9:OP_HASH160

  • 14:OP_PUSHBYTES_20(推送 20 字节)

  • c5b28d6bba91a2693a9b1876bcd3929323890fb2:公钥哈希(20 字节)

  • 88:OP_EQUALVERIFY

  • ac:OP_CHECKSIG

栈执行追踪#

现在让我们逐步追踪完整的脚本执行:

初始状态

│ (empty)                               │
└───────────────────────────────────────┘

脚本: OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG

步骤 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 独特的安全性和简洁性。