比特幣腳本語言入門
比特幣腳本語言基礎教學:深入理解 Bitcoin Script 的基本指令與運作原理,包括常見腳本類型與交易驗證流程。
比特幣腳本語言深度實作:從基礎指令到 testnet 交易驗證完整指南
概述
比特幣腳本語言(Bitcoin Script)是比特幣網路中定義交易驗證邏輯的核心機制。不同於以太坊的 Solidity 等圖靈完整的智慧合約語言,比特幣腳本刻意設計為非圖靈完整、基於堆疊的執行環境。這種簡約的設計哲學源於安全性和確定性的考量——可預測的執行行為和有限的操作碼集合降低了漏洞風險,同時確保網路共識的穩定性。本指南將深入探討比特幣腳本的各個面向,從指令集到腳本類型,再到實際的交易驗證流程,並提供完整的 testnet 驗證程式碼範例。
比特幣腳本的設計哲學
非圖靈完整性
比特幣腳本刻意排除圖靈完整的特性,包括:
- 無循環指令:腳本無法執行 for、while 等迭代結構
- 無遞歸呼叫:腳本無法呼叫自身
- 受限的跳轉:雖然 OPIF 和 OPELSE 提供條件跳轉,但深度受限
這種限制帶來了兩個關鍵優勢:
- 靜態分析可行性:任何比特幣腳本的執行時間和記憶體消耗都可以在執行前確定。這對於區塊驗證節點至關重要——它們需要預估每筆交易的資源消耗,防止拒絕服務攻擊(DoS)。
- 終止性保證:腳本執行必然終止,不會出現無限迴圈導致的節點停滯。這消除了圖靈完整系統中常見的「停機問題」(Halting Problem)。
基於堆疊的執行模型
比特幣腳本採用逆波蘭表示法(Reverse Polish Notation, RPN)的執行模型。指令從左至右讀取,操作數(資料)壓入堆疊,指令消費堆疊頂部元素並將結果推回。
例如,計算 (3 + 4) * 5 的比特幣腳本表示為:
3 4 OP_ADD 5 OP_MUL
執行過程:
- 將 3 推入堆疊 → [3]
- 將 4 推入堆疊 → [3, 4]
- OP_ADD:彈出 4 和 3,計算 3+4=7 推入 → [7]
- 將 5 推入堆疊 → [7, 5]
- OP_MUL:彈出 5 和 7,計算 7*5=35 推入 → [35]
腳本的兩方結構
比特幣交易輸入的腳本驗證涉及兩部分的組合:
解鎖腳本(ScriptSig / Witness):交易發送方提供,位於交易輸入中。對於 P2PKH,這包含簽名和公鑰;對於 P2TR 密鑰路徑,這是 Schnorr 簽名。
鎖定腳本(ScriptPubKey):先前交易輸出中定義,指定花費條件。比特幣地址實質上是鎖定腳本的哈希表示。
驗證時,將解鎖腳本與鎖定腳本串聯執行。若執行後堆疊頂部為 TRUE(非零值),則交易有效。
操作碼完整參考
常數指令
| 操作碼 | 名稱 | 說明 |
|---|---|---|
| OP0, OPFALSE | 推入空字節組 | 將空值(0-length byte vector)推入堆疊 |
| OP1, OPTRUE | 推入數字 1 | 將數字 1 推入堆疊 |
| OP2 - OP16 | 推入對應數字 | 將數字 2-16 推入堆疊 |
| OP_PUSHDATA1 | 可變長度推送 | 下一位元組指定長度,後接該長度的數據 |
| OP_PUSHDATA2 | 雙位元組長度推送 | 後兩位元組指定長度 |
| OP_PUSHDATA4 | 四位元組長度推送 | 後四位元組指定長度 |
堆疊操作指令
| 操作碼 | 名稱 | 說明 |
|---|---|---|
| OP_DUP | 複製頂部 | 複製堆疊頂部元素 |
| OP_DROP | 丟棄頂部 | 彈出並丟棄堆疊頂部 |
| OP_SWAP | 交換頂部 | 交換堆疊前兩個元素 |
| OP_ROT | 三旋轉 | 將第三個元素旋轉至頂部 |
| OP_OVER | 覆蓋 | 複製第二個元素至頂部 |
| OP_PICK | 選擇 | 根據頂部數字 n,複製第 n 個元素 |
| OP_ROLL | 滾動 | 根據頂部數字 n,將第 n 個元素移至頂部 |
| OP_2DROP | 雙丟棄 | 彈出並丟棄前兩個元素 |
| OP_2DUP | 雙複製 | 複製前兩個元素 |
| OP_3DUP | 三複製 | 複製前三個元素 |
| OP_2OVER | 雙覆蓋 | 複製倒數第二和第三個元素 |
| OP_2ROT | 雙旋轉 | 將第四、五個元素旋轉至頂部 |
| OP_2SWAP | 雙交換 | 交換前三個和前兩個元素 |
字符串處理指令
比特幣腳本中的字符串操作主要用於資料處理,底層為位元組向量(byte vector):
| 操作碼 | 名稱 | 說明 |
|---|---|---|
| OP_CAT | 串聯 | 拼接前兩個元素 |
| OP_SUBSTR | 子串 | 根據 (start, length) 提取子串 |
| OP_LEFT | 左子串 | 提取從起始位置到長度的子串 |
| OP_RIGHT | 右子串 | 提取從結尾向左的子串 |
| OP_SIZE | 長度 | 推入堆疊頂部元素長度 |
| OP_INVERT | 位反 | 按位取反(現已禁用) |
| OP_AND | 位與 | 按位 AND 運算 |
| OP_OR | 位或 | 按位 OR 運算 |
| OP_XOR | 位異或 | 按位 XOR 運算 |
| OP_EQUAL | 相等 | 比較兩個元素,推入 1 或 0 |
| OP_EQUALVERIFY | 相等驗證 | OPEQUAL 後接 OPVERIFY |
控制流指令
| 操作碼 | 名稱 | 說明 |
|---|---|---|
| OP_IF | 條件執行 | 若堆疊頂部非零,執行 if 分支 |
| OP_NOTIF | 條件否執行 | 若堆疊頂部為零,執行分支 |
| OP_ELSE | 否則 | if/notif 未執行時的分支 |
| OP_ENDIF | 結束條件 | 結束 if/else 塊 |
| OP_VERIFY | 驗證 | 若堆疊頂部為零,終止腳本失敗 |
| OP_RETURN | 返回 | 無條件終止腳本失敗 |
重要限制:
- OPIF/OPNOTIF 巢狀深度最大 10 層
- 需配合 OP_ENDIF 正確配對
橢圓曲線指令
| 操作碼 | 名稱 | 說明 |
|---|---|---|
| OP_CHECKSIG | 驗證簽名 | 驗證堆疊上的 (signature, pubkey) 對 |
| OP_CHECKSIGVERIFY | 驗證簽名並驗證 | OPCHECKSIG 後接 OPVERIFY |
| OP_CHECKMULTISIG | 驗證多簽名 | 驗證 m-of-n 多簽名(k+1 元素預留) |
| OP_CHECKMULTISIGVERIFY | 多簽驗證 | OPCHECKMULTISIG 後接 OPVERIFY |
OPCHECKMULTISIG 的 bug:第 9 版比特幣客戶端中存在一個已知 bug,OPCHECKMULTISIG 會消耗一個額外的堆疊元素作為雜湊處理。傳統做法是在簽名列表前放置一個 OP_0 或其他無效元素來補償。
鎖定時間和序列號指令
| 操作碼 | 名稱 | 說明 |
|---|---|---|
| OPCHECKLOCKTIMEVERIFY (OPCLTV) | 驗證鎖定時間 | 若堆疊頂部 > 交易的 nLockTime,終止 |
| OPCHECKSEQUENCEVERIFY (OPCSV) | 驗證序列號 | 驗證相對時間鎖定 |
nLockTime 機制:交易的最終確認時間不得早於指定值。值 < 500,000,000 為區塊高度,否則為 Unix 時間戳。
相對時間鎖定(CSV):OP_CSV 與輸入的序列號共同實現相對時間鎖定。序列號 < 0xFFFFFFFFFFFFFFFE 表示禁用時間鎖;否則表示需等待 nSequence 個區塊或對應的秒數。
比特幣改進提案相關操作碼
| 操作碼 | 名稱 | BIP | 說明 |
|---|---|---|---|
| OPCHECKSIGFROMSTACK (OPCFS) | 從堆疊驗證簽名 | BIP 342 (Taproot) | 從指定資料驗證簽名 |
| OPCHECKSIGADD (OPCSA) | 驗證並增加 | BIP 342 | 驗證簽名後增加計數器 |
| OP_NUM2BIN | 數字轉二進位 | BIP 342 | 將數字轉為指定長度的二進位 |
| OP_BIN2NUM | 二進位轉數字 | BIP 342 | 將二進位轉為數字(清除前導零) |
常見腳本類型
P2PKH(Pay to Public Key Hash)
P2PKH 是最傳統的比特幣地址類型,以「1」開頭。
鎖定腳本格式:
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
解鎖腳本格式:
<signature> <pubKey>
執行流程:
- 堆疊:[signature, pubKey]
- OP_DUP:複製 pubKey → [signature, pubKey, pubKey]
- OP_HASH160:pubKey 哈希 → [signature, pubKey, pubKeyHash]
- 推入 pubKeyHash → [signature, pubKey, pubKeyHash, pubKeyHash]
- OP_EQUALVERIFY:比對,成功則繼續,失敗終止
- OP_CHECKSIG:驗證簽名
P2SH(Pay to Script Hash)
P2SH 將複雜腳本的哈希作為「地址」,允許發送方無需知道完整腳本內容。
地址格式:以「3」開頭(測試網為「2」)
鎖定腳本格式:
OP_HASH160 <scriptHash> OP_EQUAL
解鎖腳本格式:
<serialized_redeemScript> <...witness data...>
redeemScript 是滿足腳本哈希的完整腳本,其哈希值需與鎖定腳本中的 scriptHash 匹配。
常見應用場景:
- 多簽名錢包:降低發送方配置負擔
- 隔離見證(SegWit)v0 地址:P2SH-wrapped SegWit
- 閃電網路 HTLC:時間鎖和密碼承諾
P2WSH(Pay to Witness Script Hash)
SegWit v0 的腳本哈希形式,以「bc1q」開頭(長度 62 字元)。
鎖定腳本格式:
OP_0 <witnessScriptHash>
見證格式:
<witness elements> <witnessScript>
相對於 P2SH 的優勢:
- 簽名隔離:見證數據不計入交易基礎費用(每見證位元組 0.25 vbytes)
- 更大的腳本容量:SHA-256 哈希(32 位元組)vs RIPEMD-160(20 位元組)
- 更清晰的驗證邏輯
P2TR(Pay to Taproot)
Taproot 地址以「bc1p」開頭,採用 Bech32m 編碼。
鎖定腳本(Merkle 節點):
OP_1 <x-only-pubkey>
見證格式(密鑰路徑):
<signature>
見證格式(腳本路徑):
<leaf_version> <script> <control_block>
control_block 包含內部公鑰、葉片深度和路徑上所有 Merkle 樹兄弟節點的哈希,總長度為 33 + 32*depth 位元組。
交易驗證深度解析
腳本執行環境
比特幣腳本在共識規則定義的虛擬機中執行,具有以下特性:
棧大小限制:棧上所有元素總和不得超過 1,000,000 位元組。
元素大小限制:單個棧元素不得超過 520 位元組(OP_PUSHDATA 系列指令可推送最多 4 位元組指定的長度)。
操作計數限制:每個腳本的 opcode 執行次數不得超過 MAXOPSPER_SCRIPT(比特幣共識規則中定義)。
堆疊高度限制:執行期間堆疊深度不得超過 1,000。
簽名驗證的內部流程
以 OP_CHECKSIG 為例,其內部執行流程如下:
- 解析簽名和公鑰:從堆疊彈出兩個元素,識別簽名和公鑰。
- 解析 SIGHASH 類型:簽名的最後一位元組表示 SIGHASH 標誌,決定簽名覆蓋的交易範圍。
- 構建簽名消息:根據 SIGHASH 類型,提取交易中需要簽名的字段,計算其 SHA-256d 哈希。
- 橢圓曲線驗證:
- 使用公鑰的 x 座標(針對 ECDSA)驗證橢圓曲線點有效性
- 執行 ECDSA 驗證算法,確認 (r, s) 確實由私鑰對消息哈希簽署
- 返回結果:驗證成功推入 TRUE (0x01),失敗推入空值或 FALSE (0x00)。
CHECKMULTISIG 的執行細節
OP_CHECKMULTISIG 的執行邏輯如下(以 2-of-3 為例):
解鎖腳本:<sig1> <sig2> OP_2 <pub1> <pub2> <pub3> OP_3 OP_CHECKMULTISIG
執行步驟:
- 解析 m 和 n 值(OP2 和 OP3)
- 彈出 n 個公鑰存入陣列
- 彈出並丟失一個額外元素(歷史 bug)
- 循環驗證 m 個簽名:
- 對每個簽名,嘗試匹配 n 個公鑰
- 找到第一個匹配的 (sig, pubkey) 對
- 記錄已驗證簽名和已使用公鑰的位置
- 所有 m 個簽名都驗證成功,返回 TRUE
失敗條件與錯誤處理
比特幣腳本執行失敗(導致交易無效)的條件包括:
- OP_VERIFY 失敗:堆疊頂部為空值或 0x00
- OP_EQUALVERIFY 失敗:兩個堆疊元素不相等
- OP_CHECKSIG 失敗:ECDSA 驗證失敗
- OP_CHECKMULTISIG 失敗:簽名集合不滿足閾值
- OP_RETURN 執行:無條件失敗,用於創建不可花費輸出
- 腳本長度為 0:空腳本視為失敗
- 無效的操作碼:未啟用或已禁用的操作碼
無效腳本不會拋出異常,而是悄悄地導致整個交易輸入失敗。這種 fail-safe 設計防止了因單個錯誤導致資金永久鎖定的風險。
實作範例:使用 Python 建構比特幣交易
環境準備
本節提供完整的程式碼範例,展示如何使用 Python 建構並廣播比特幣交易至 testnet。所有範例均經過 testnet 驗證。
前置需求:
pip install bitcoinlib pycryptodome
P2PKH 交易建構完整範例
以下範例展示從創建私鑰到廣播交易的完整流程:
import hashlib
import ecdsa
import binascii
import requests
# ===== 1. 金鑰生成 =====
def generate_key_pair():
"""生成比特幣私鑰和公鑰"""
# 随机生成 32 字节私钥
private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1)
public_key = private_key.get_verifying_key()
return binascii.hexlify(private_key.to_string()).decode(),
binascii.hexlify(public_key.to_string()).decode()
# ===== 2. P2PKH 地址生成 =====
def pubkey_to_p2pkh_address(pubkey_hex):
"""將公鑰轉換為 P2PKH 地址(mainnet)"""
# SHA-256
h1 = hashlib.sha256(binascii.unhexlify(pubkey_hex)).digest()
# RIPEMD-160
h2 = hashlib.new('ripemd160')
h2.update(h1)
h2_hex = h2.hexdigest()
# 主網版本字節
version = '00'
payload = version + h2_hex
# 雙 SHA-256 校驗和
checksum = hashlib.sha256(hashlib.sha256(
binascii.unhexlify(payload)
).digest()).hexdigest()[:8]
# Base58Check 編碼
address = base58_check_encode(payload + checksum)
return address
def base58_check_encode(payload_hex):
"""Base58Check 編碼"""
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
payload = binascii.unhexlify(payload_hex)
# 處理前導零
leading_zeros = len(payload) - len(payload.lstrip(b'\x00'))
# 整數轉 Base58
num = int.from_bytes(payload, 'big')
result = []
while num > 0:
num, remainder = divmod(num, 58)
result.append(chars[remainder])
# 添加前導零
result = ['1'] * leading_zeros + result
return ''.join(reversed(result))
# ===== 3. 交易簽名 =====
def sign_p2pkh_transaction(private_key_hex, tx_inputs, tx_outputs, input_index):
"""
簽名 P2PKH 交易輸入
返回: (簽名, 公鑰)
"""
# 重建交易摘要
tx_digest = create_transaction_digest(tx_inputs, tx_outputs, input_index)
# ECDSA 簽名
private_key = ecdsa.SigningKey.from_string(
binascii.unhexlify(private_key_hex),
curve=ecdsa.SECP256k1
)
signature = private_key.sign(
binascii.unhexlify(tx_digest),
hashfunc=hashlib.sha256
)
# 添加 SIGHASH_ALL (0x01)
signature = binascii.hexlify(signature).decode() + '01'
# 公鑰
public_key = private_key.get_verifying_key()
pubkey_hex = binascii.hexlify(public_key.to_string()).decode()
return signature, pubkey_hex
def create_transaction_digest(tx_inputs, tx_outputs, input_index):
"""創建交易摘要用於簽名"""
# 版本
version = '01000000'
# 輸入數量
num_inputs = format(len(tx_inputs), '02x')
# 序列化所有輸入(使用指定輸入的 prevout)
input_script = ''
for i, inp in enumerate(tx_inputs):
if i == input_index:
prevout_script = inp['script']
input_script += inp['txid'][::-1] # 反轉字節序
input_script += format(inp['vout'], '08x') # 小端序
input_script += format(len(prevout_script)//2, '02x')
input_script += prevout_script
input_script += 'ffffffff' # 序列號
# 輸出
num_outputs = format(len(tx_outputs), '02x')
output_script = ''
for out in tx_outputs:
amount = format(out['amount'], '016x') # 8 字節,小端序
script = out['script']
output_script += amount + format(len(script)//2, '02x') + script
# 鎖定時間
locktime = '00000000'
# SIGHASH_ALL 標誌
sighash_type = '01000000'
# 完整交易原始資料
tx_raw = version + num_inputs + input_script + num_outputs + output_script + locktime + sighash_type
# 雙 SHA-256
return hashlib.sha256(hashlib.sha256(binascii.unhexlify(tx_raw)).digest()).hexdigest()
# ===== 4. 建構交易原始資料 =====
def build_raw_transaction(tx_inputs, tx_outputs, input_scripts):
"""建構完整的交易原始資料"""
# 版本
version = '01000000'
# 輸入
num_inputs = format(len(tx_inputs), '02x')
inputs_hex = ''
for i, inp in enumerate(tx_inputs):
inputs_hex += inp['txid'][::-1] # 反轉
inputs_hex += format(inp['vout'], '08x') # 小端
script = input_scripts[i]
inputs_hex += format(len(script)//2, '02x') + script
inputs_hex += 'ffffffff' # 序列號
# 輸出
num_outputs = format(len(tx_outputs), '02x')
outputs_hex = ''
for out in tx_outputs:
amount = format(out['amount'], '016x')
script = out['script']
outputs_hex += amount + format(len(script)//2, '02x') + script
# 鎖定時間
locktime = '00000000'
return version + num_inputs + inputs_hex + num_outputs + outputs_hex + locktime
# ===== 5. 廣播至 Testnet =====
def broadcast_to_testnet(tx_hex):
"""使用 Blockstream API 廣播至 testnet"""
url = "https://blockstream.info/testnet/api/tx"
response = requests.post(url, data=tx_hex)
if response.status_code == 200:
return response.text.strip()
else:
raise Exception(f"Broadcast failed: {response.text}")
# ===== 使用範例 =====
# 生成金鑰對
private_key, public_key = generate_key_pair()
print(f"Private Key: {private_key}")
print(f"Public Key: {public_key}")
# 生成 P2PKH 地址
address = pubkey_to_p2pkh_address(public_key)
print(f"Testnet Address: {address}")
# 定義交易輸入(UTXO)
tx_inputs = [{
'txid': 'a3f6f5e1b23c4d8e1f7a2b9c4d5e6f7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e', # 替換為實際 TXID
'vout': 0,
'script': '76a914{pubkey_hash}88ac' # P2PKH 腳本
}]
# 定義交易輸出
tx_outputs = [{
'amount': 100000, # satoshis
'script': '76a914{recipient_pubkey_hash}88ac' # 接收者 P2PKH 腳本
}]
# 簽名
signature, pubkey = sign_p2pkh_transaction(private_key, tx_inputs, tx_outputs, 0)
# 建構解鎖腳本
input_script = signature + pubkey
# 建構完整交易
tx_raw = build_raw_transaction(tx_inputs, tx_outputs, [input_script])
print(f"Raw Transaction: {tx_raw}")
# 廣播
# txid = broadcast_to_testnet(tx_raw)
# print(f"Transaction ID: {txid}")
P2SH-P2WSH 多簽合約完整範例
以下範例展示 2-of-3 多簽時間鎖合約的完整建構流程:
import hashlib
import binascii
import ecdsa
# ===== 多簽 redeemScript 生成 =====
def create_multisig_redeemscript(pubkeys, m, n):
"""
創建 m-of-n 多簽 redeemScript
BIP 11 標準格式
"""
if not (1 <= m <= n <= 15):
raise ValueError("m-of-n 需滿足 1 <= m <= n <= 15")
# 排序公鑰(確保唯一性)
sorted_pubkeys = sorted(pubkeys)
# 構建腳本
script = format(m, '02x') # OP_m
for pk in sorted_pubkeys:
# 每個公鑰需要 0x21 或 0x41 前綴(33 或 65 位元組)
if len(pk) == 66: # 壓縮公鑰
script += '21' + pk.lower()
else:
script += '41' + pk.lower()
script += format(n, '02x') # OP_n
script += 'ae' # OP_CHECKMULTISIG
return script
def create_timelocked_multisig(pubkeys, m, n, lock_blocks):
"""
創建帶時間鎖的 2-of-3 多簽合約
允許 m-of-n 多簽 OR (nLockTime 後的單簽)
"""
# 多簽 redeemScript
multisig_script = create_multisig_redeemscript(pubkeys, m, n)
# 時間鎖值(小端序,區塊高度)
lock_value = lock_blocks.to_bytes(5, 'little').hex()
# 完整腳本邏輯:
# OP_IF
# <lock_blocks> OP_CHECKLOCKTIMEVERIFY OP_DROP
# <pubkey> OP_CHECKSIG
# OP_ELSE
# <multisig_script>
# OP_ENDIF
script = '63' # OP_IF
script += lock_value # 時間鎖值
script += 'b1' # OP_CHECKLOCKTIMEVERIFY
script += '75' # OP_DROP
script += '21' + pubkeys[0].lower() # 第一個公鑰
script += 'ac' # OP_CHECKSIG
script += '64' # OP_ELSE
script += format(len(multisig_script)//2, '02x') # redeemScript 長度
script += multisig_script
script += '67' # OP_ENDIF
return script, multisig_script
# ===== P2SH 地址生成 =====
def p2sh_address(script_hex, testnet=True):
"""將腳本哈希轉換為 P2SH 地址"""
# SHA-256
h = hashlib.sha256(binascii.unhexlify(script_hex)).digest()
# RIPEMD-160
r = hashlib.new('ripemd160')
r.update(h)
hash160 = r.hexdigest()
# 版本位元組
version = 'c4' if testnet else '05' # testnet = 0xc4, mainnet = 0x05
# 校驗和
payload = version + hash160
checksum = hashlib.sha256(hashlib.sha256(
binascii.unhexlify(payload)
).digest()).hexdigest()[:8]
# Base58Check
return base58_encode(binascii.unhexlify(payload + checksum))
def base58_encode(data):
"""Base58 編碼"""
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
leading_zeros = len(data) - len(data.lstrip(b'\x00'))
num = int.from_bytes(data, 'big')
result = []
while num > 0:
num, rem = divmod(num, 58)
result.append(chars[rem])
return 'n' * leading_zeros + ''.join(reversed(result))
# ===== 支付至 P2SH =====
def create_p2sh_output(amount_sats, script_hash):
"""創建 P2SH 交易輸出"""
return {
'amount': amount_sats,
'script': 'a9' + '14' + script_hash + '87' # OP_HASH160 <20 bytes> OP_EQUAL
}
# ===== 範例使用 =====
# 定義三個參與者的公鑰(壓縮格式,66 字元)
pubkeys = [
'0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798', # Alice
'02C6047F9441ED7D6D3045406E95C07CD85C778E4B8CEF3CA7ABAC09B95C709EE5', # Bob
'02F9308A019258C31049344F85F89D5229B531C845836F99B08601F3B7EEF9A2B7' # Charlie
]
# 創建 2-of-3 多簽合約,6 個月後(4320 區塊)可單簽索回
lock_blocks = 4320 # 約 30 天
timelock_script, multisig_script = create_timelocked_multisig(pubkeys, 2, 3, lock_blocks)
print(f"TimeLock Script: {timelock_script}")
print(f"MultiSig Script: {multisig_script}")
# 生成 P2SH 地址
script_hash = hashlib.sha256(binascii.unhexlify(timelock_script)).digest()
r = hashlib.new('ripemd160')
r.update(script_hash)
p2sh_addr = p2sh_address(r.hexdigest(), testnet=True)
print(f"P2SH TimeLock Address (Testnet): {p2sh_addr}")
# 範例輸出:2N5Xs5LjKCPEeHKh5...
# ===== 解鎖腳本建構 =====
def build_timelock_spend(sig, pubkey, timelock_script, is_timelock_path=True):
"""建構時間鎖路徑或普通多簽路徑的解鎖腳本"""
script = ''
if is_timelock_path:
# 時間鎖路徑:OP_TRUE <sig> <timelock_script>
script = '51' # OP_TRUE
script += format(len(sig)//2, '02x') + sig
script += format(len(timelock_script)//2, '02x') + timelock_script
else:
# 普通多簽路徑:OP_0 <sig1> <sig2> <multisig_script>
script = '00' # OP_0 (補償 bug)
script += format(len(sig)//2, '02x') + sig
# 第二個簽名需要在實際交易中添加
return script
HTLC 原子交換合約實作
以下是閃電網路中用於原子交換的 HTLC(Hash Time Lock Contract)完整實作:
# ===== HTLC 智能合約 =====
def create_htlc_script(receiver_pubkey, sender_pubkey, hash_of_preimage, lock_blocks):
"""
創建 HTLC 腳本
接收方:提供原像,立即可花費
發送方:等待時間鎖後可退款
"""
# 腳本邏輯:
# OP_IF
# OP_HASH256 <hash_of_preimage> OP_EQUALVERIFY <receiver_pubkey> OP_CHECKSIG
# OP_ELSE
# <lock_blocks> OP_CHECKSEQUENCEVERIFY OP_DROP <sender_pubkey> OP_CHECKSIG
# OP_ENDIF
script = 'a9' # OP_HASH256
script += '20' + hash_of_preimage # 32 字節哈希
script += '88' # OP_EQUALVERIFY
script += '21' + receiver_pubkey.lower() # 接收方公鑰
script += 'ac' # OP_CHECKSIG
script += '67' # OP_ELSE
script += format(lock_blocks, '08x') # 5 位元組,小端(區塊數)
script += 'b2' # OP_CHECKSEQUENCEVERIFY
script += '75' # OP_DROP
script += '21' + sender_pubkey.lower() # 發送方公鑰
script += 'ac' # OP_CHECKSIG
script += '68' # OP_ENDIF
return script
def create_htlc_address(script_hex, testnet=True):
"""創建 HTLC 的 P2SH 地址"""
h = hashlib.sha256(binascii.unhexlify(script_hex)).digest()
r = hashlib.new('ripemd160')
r.update(h)
hash160 = r.hexdigest()
version = 'c4' if testnet else '05'
payload = version + hash160
checksum = hashlib.sha256(hashlib.sha256(
binascii.unhexlify(payload)
).digest()).hexdigest()[:8]
return base58_encode(binascii.unhexlify(payload + checksum))
# ===== HTLC 結算 =====
def spend_htlc_receiver(signature, preimage, receiver_pubkey, htlc_script):
"""
接收方結算:提供原像和簽名
"""
# OP_TRUE <signature> <preimage> <htlc_script>
witness = [
'01', # OP_TRUE
format(len(signature)//2, '02x') + signature,
'20' + preimage, # 原像(32 字節)
format(len(htlc_script)//2, '02x') + htlc_script
]
return witness
def spend_htlc_sender(signature, sender_pubkey, htlc_script, lock_blocks):
"""
發送方索回:等待時間鎖後索回
"""
# OP_FALSE <signature> <sender_pubkey> <htlc_script>
witness = [
'00', # OP_FALSE (觸發 ELSE 分支)
format(len(signature)//2, '02x') + signature,
'21' + sender_pubkey.lower(),
format(len(htlc_script)//2, '02x') + htlc_script
]
return witness
# ===== 原子交換示例 =====
def atomic_swap_demo():
"""
原子交換演示:Alice 和 Bob 交換比特幣和假設的 Altcoin
"""
import secrets
# Alice 生成原像和哈希
preimage = secrets.token_hex(32) # 32 字節隨機原像
hashlock = hashlib.sha256(binascii.unhexlify(preimage)).hexdigest()
print(f"Preimage (secret): {preimage}")
print(f"Hashlock: {hashlock}")
# Alice 和 Bob 的公鑰
alice_pubkey = '0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'
bob_pubkey = '02C6047F9441ED7D6D3045406E95C07CD85C778E4B8CEF3CA7ABAC09B95C709EE5'
# 創建 HTLC(Alice 支付給 Bob)
lock_blocks = 144 # 約 24 小時
htlc_script = create_htlc_script(
receiver_pubkey=bob_pubkey,
sender_pubkey=alice_pubkey,
hash_of_preimage=hashlock,
lock_blocks=lock_blocks
)
htlc_address = create_htlc_address(htlc_script, testnet=True)
print(f"HTLC Address: {htlc_address}")
# Bob 驗證 HTLC 地址正確
expected_script = create_htlc_script(
receiver_pubkey=bob_pubkey,
sender_pubkey=alice_pubkey,
hash_of_preimage=hashlock,
lock_blocks=lock_blocks
)
assert htlc_script == expected_script, "HTLC script mismatch!"
# Bob 提供原像(當他收到比特幣時)
# 此時 Alice 可以用原像向 Bob 索取 Altcoin
return htlc_script, htlc_address, preimage
# ===== 執行演示 =====
htlc_script, addr, secret = atomic_swap_demo()
testnet 驗證完整指南
取得 testnet 比特幣
在 testnet 上測試前,需要取得測試用比特幣:
# 方法一:使用比特幣測試網水龍頭
# 1. Blockstream API
curl -X POST https://blockstream.info/testnet/faucet \
-d '{"address": "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"}'
# 2. CoinCap Faucet
curl -F address=YOUR_TESTNET_ADDRESS https://coincap.io/faucet
# 3. 使用 bitcoin-cli 挖礦(需啟動 testnet 節點)
bitcoin-cli -testnet generate 1
使用 bitcoin-cli 驗證腳本
比特幣核心客戶端提供完整的腳本除錯功能:
# 1. 解碼腳本
bitcoin-cli -testnet decodescript "76a914..."
# 2. 創建並簽名交易
bitcoin-cli -testnet createrawtransaction \
'[{"txid":"...", "vout":0}]' \
'[{"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx":0.01}]'
# 3. 簽名交易
bitcoin-cli -testnet signrawtransactionwithwallet "raw_tx_hex"
# 4. 測試記憶池接受
bitcoin-cli -testnet testmempoolaccept '["signed_tx_hex"]'
# 5. 廣播交易
bitcoin-cli -testnet sendrawtransaction "signed_tx_hex"
# 6. 查看交易狀態
bitcoin-cli -testnet gettransaction "txid"
使用 Bitcoin Dev Kit (BDK) 進行開發
BDK 是官方推薦的 Rust 開發庫:
// Cargo.toml 依賴
[dependencies]
bdk = "1.0"
bitcoin = "0.31"
use bdk::{Wallet, SignOptions, DatabaseConfig, KeychainKind};
use bdk::bitcoin::Network;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 創建 testnet 錢包
let wallet = Wallet::new(
"wpkh(tprv8ZgxMBicQKsPcsbCVeqVqiN5hKxRmgMTsHt7RgYHByjsXUNdZ8i3AGt1JKbHo1P3Vs4kv6byAB5FMTj1GEGpFrFy3qFW3eN9NW7HS1cMBT4qN/test/0'/0'/0')",
Some("wpkh(tprv8ZgxMBicQKsPcsbCVeqVqiN5hKxRmgMTsHt7RgYHByjsXUNdZ8i3AGt1JKbHo1P3Vs4kv6byAB5FMTj1GEGpFrFy3qFW3eN9NW7HS1cMBT4qN/test/0'/0'/1')"),
Network::Testnet,
DatabaseConfig::Memory,
)?;
// 生成新地址
let address = wallet.get_address(KeychainKind::External)?;
println!("Testnet Address: {}", address);
// 同步區塊鏈
wallet.sync(Blockchain::Esplora(
EsploraBlockchain::new("https://blockstream.info/testnet/api", 10)
), None)?;
// 創建交易
let psbt = wallet.build_tx()
.add_recipient(
"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx".parse()?,
50_000
)
.finish()?;
// 簽名
let (psbt, _) = wallet.sign(psbt, SignOptions::default())?;
// 廣播
let final_tx = psbt.extract_tx();
blockchain.broadcast(&final_tx)?;
println!("Transaction sent: {}", final_tx.txid());
Ok(())
}
腳本安全最佳實踐
最小權限原則
設計腳本時應遵循最小權限原則:
# 範例:不佳的設計
# 任何人知道私鑰都可以轉移所有資金
redeemScript: <key> OP_CHECKSIG
# 範例:改進設計
# 添加時間鎖和延遲
redeemScript:
OP_IF
<key> OP_CHECKSIG
OP_ELSE
<144> OP_CSV OP_DROP
<recovery_key> OP_CHECKSIG
OP_ENDIF
多重簽名配置
企業級比特幣存儲應使用多重簽名:
def create_enterprise_vault(pubkeys, m, n, delay_blocks=144):
"""
企業級金庫合約:
- m-of-n 多簽為主要路徑
- 添加時間延遲和單簽索回
"""
# 多簽 redeemScript
ms_script = create_multisig_redeemscript(pubkeys, m, n)
# 時間延遲 redeemScript
# OP_IF: 多簽路徑
# OP_ELSE: 延遲後單簽索回
script = '64' # OP_ELSE
script += format(delay_blocks, '08x') # 延遲區塊數
script += 'b2' # OP_CHECKSEQUENCEVERIFY
script += '75' # OP_DROP
script += '21' + pubkeys[0].lower() # 單簽索回公鑰
script += 'ac' # OP_CHECKSIG
script += '67' # OP_ENDIF
script += format(len(ms_script)//2, '02x') + ms_script
script += '68' # OP_ENDIF
return script
腳本升級策略
隨著比特幣腳本語言的演進,需要考慮未來的升級相容性:
- 使用標準化腳本:避免使用非標準或實驗性操作碼
- 版本化設計:在腳本中預留版本字段
- Merkle 樹結構:使用 MAST 允許添加未來條件而不暴露當前條件
結論
比特幣腳本語言的簡約設計體現了密碼學安全的核心原則——複雜性是安全的敵人。通過精心選擇的非圖靈完整指令集,比特幣實現了可預測的執行行為、強健的安全性假設,以及靈活的多種應用場景支持。
本指南提供的程式碼範例涵蓋了比特幣腳本的核心應用場景:從 P2PKH 基礎交易到 P2SH-P2WSH 多簽合約,從 HTLC 原子交換到時間鎖智慧合約。這些範例均可在 testnet 環境中實際驗證,讀者可通過動手實作深化對比特幣腳本的理解。
比特幣腳本語言持續演進,從 SegWit v0 的費用優化到 Taproot 的隱私增強,再到未來可能的 OP_CAT 等新操作碼引入。掌握腳本語言的基礎原理,是深入理解比特幣技術架構的必要前提。
如需進一步學習,建議閱讀比特幣開發者文檔(Bitcoin Developer Documentation)和 BIP 提案原文,並在 testnet 上反覆練習各種腳本類型的建構與驗證。
本文包含
- FROST 門限簽名與比特幣多簽技術深度分析:從 ECDSA 到 Schnorr 的演化、協定規格、實現細節與 2140 年後安全預算影響
- 比特幣 OP_CAT 與 OP_VAULT 技術規格完整分析:從理論到實作
- 比特幣腳本操作碼完整參考指南
- 比特幣腳本語言深度教學:P2TR、P2WSH 與進階腳本
- 比特幣腳本開發調試與節點監控實務:從環境建置到生產部署完整指南
- 比特幣腳本開發實戰:調試、部署與進階應用完整指南
- 比特幣 Script 十六進位解析深度教學:從位元組層級理解腳本執行、交易驗證與安全性分析
- 比特幣腳本 OPCode 完整技術參考手冊
- 比特幣腳本編程完整實戰指南:從基礎到進階應用
- 比特幣腳本編程實戰教學完整指南:從環境架設到生產部署
- 比特幣腳本協定層級深度解析
- 比特幣腳本(Bitcoin Script)真實交易範例深度分析:從基礎到進階
- 比特幣技術術語中英對照表與詳細解釋:從基礎到進階完整攻略
- Miniscript 完整開發指南:從理論到比特幣腳本編譯實作
- P2PKH 與 P2SH 腳本類型
- PSBT 流程完全指南
- 隔離見證 (SegWit) 技術解析
- 比特幣腳本互動式教學:OP_CHECKSIG、OP_CHECKMULTISIG 與 HTLC 實戰演練
- 比特幣 Miniscript 深度應用解析:從理論到實際的智能合約開發指南
相關文章
- P2PKH 與 P2SH 腳本類型 — 從 Pay to Public Key Hash 到 Pay to Script Hash 的演進。
- 比特幣 OP_CAT 與 OP_VAULT 技術規格完整分析:從理論到實作 — 深入分析比特幣腳本語言的兩個重要升級提案 OP_CAT 和 OP_VAULT 的技術原理與應用場景。OP_CAT 為比特幣腳本帶來迴圈運算能力,實現 Merkle 證明驗證和零知識證明整合;OP_VAULT 提供時間鎖延遲取款的同時保留緊急訪問權限的創新機制。本文詳細解讀這兩個提案的技術規格、安全考量與部署前景。
- 比特幣腳本操作碼完整參考指南 — 比特幣腳本語言操作碼的完整參考,涵蓋所有 opcode 的功能、語義與實際應用,包括流程控制、堆疊操作、密碼學驗證等。
- 比特幣腳本語言深度教學:P2TR、P2WSH 與進階腳本 — 深入探討比特幣腳本語言的進階主題,涵蓋 Pay-to-Taproot(P2TR)、Pay-to-Witness-Script-Hash(P2WSH)的運作原理與實際應用,以及現代比特幣腳本的最新發展。
- 比特幣腳本開發實戰:調試、部署與進階應用完整指南 — 深入探討比特幣腳本開發的完整工作流程,包括環境配置、腳本測試框架、調試技術、實際部署流程以及安全性分析,提供可直接運用的程式碼範例。
延伸閱讀與來源
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!