比特幣腳本語言入門

比特幣腳本語言基礎教學:深入理解 Bitcoin Script 的基本指令與運作原理,包括常見腳本類型與交易驗證流程。

比特幣腳本語言深度實作:從基礎指令到 testnet 交易驗證完整指南

概述

比特幣腳本語言(Bitcoin Script)是比特幣網路中定義交易驗證邏輯的核心機制。不同於以太坊的 Solidity 等圖靈完整的智慧合約語言,比特幣腳本刻意設計為非圖靈完整、基於堆疊的執行環境。這種簡約的設計哲學源於安全性和確定性的考量——可預測的執行行為和有限的操作碼集合降低了漏洞風險,同時確保網路共識的穩定性。本指南將深入探討比特幣腳本的各個面向,從指令集到腳本類型,再到實際的交易驗證流程,並提供完整的 testnet 驗證程式碼範例。

比特幣腳本的設計哲學

非圖靈完整性

比特幣腳本刻意排除圖靈完整的特性,包括:

這種限制帶來了兩個關鍵優勢:

  1. 靜態分析可行性:任何比特幣腳本的執行時間和記憶體消耗都可以在執行前確定。這對於區塊驗證節點至關重要——它們需要預估每筆交易的資源消耗,防止拒絕服務攻擊(DoS)。
  1. 終止性保證:腳本執行必然終止,不會出現無限迴圈導致的節點停滯。這消除了圖靈完整系統中常見的「停機問題」(Halting Problem)。

基於堆疊的執行模型

比特幣腳本採用逆波蘭表示法(Reverse Polish Notation, RPN)的執行模型。指令從左至右讀取,操作數(資料)壓入堆疊,指令消費堆疊頂部元素並將結果推回。

例如,計算 (3 + 4) * 5 的比特幣腳本表示為:

3 4 OP_ADD 5 OP_MUL

執行過程:

  1. 將 3 推入堆疊 → [3]
  2. 將 4 推入堆疊 → [3, 4]
  3. OP_ADD:彈出 4 和 3,計算 3+4=7 推入 → [7]
  4. 將 5 推入堆疊 → [7, 5]
  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返回無條件終止腳本失敗

重要限制

橢圓曲線指令

操作碼名稱說明
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>

執行流程

  1. 堆疊:[signature, pubKey]
  2. OP_DUP:複製 pubKey → [signature, pubKey, pubKey]
  3. OP_HASH160:pubKey 哈希 → [signature, pubKey, pubKeyHash]
  4. 推入 pubKeyHash → [signature, pubKey, pubKeyHash, pubKeyHash]
  5. OP_EQUALVERIFY:比對,成功則繼續,失敗終止
  6. OP_CHECKSIG:驗證簽名

P2SH(Pay to Script Hash)

P2SH 將複雜腳本的哈希作為「地址」,允許發送方無需知道完整腳本內容。

地址格式:以「3」開頭(測試網為「2」)

鎖定腳本格式

OP_HASH160 <scriptHash> OP_EQUAL

解鎖腳本格式

<serialized_redeemScript> <...witness data...>

redeemScript 是滿足腳本哈希的完整腳本,其哈希值需與鎖定腳本中的 scriptHash 匹配。

常見應用場景

P2WSH(Pay to Witness Script Hash)

SegWit v0 的腳本哈希形式,以「bc1q」開頭(長度 62 字元)。

鎖定腳本格式

OP_0 <witnessScriptHash>

見證格式

<witness elements> <witnessScript>

相對於 P2SH 的優勢

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 為例,其內部執行流程如下:

  1. 解析簽名和公鑰:從堆疊彈出兩個元素,識別簽名和公鑰。
  1. 解析 SIGHASH 類型:簽名的最後一位元組表示 SIGHASH 標誌,決定簽名覆蓋的交易範圍。
  1. 構建簽名消息:根據 SIGHASH 類型,提取交易中需要簽名的字段,計算其 SHA-256d 哈希。
  1. 橢圓曲線驗證
  1. 返回結果:驗證成功推入 TRUE (0x01),失敗推入空值或 FALSE (0x00)。

CHECKMULTISIG 的執行細節

OP_CHECKMULTISIG 的執行邏輯如下(以 2-of-3 為例):

解鎖腳本:<sig1> <sig2> OP_2 <pub1> <pub2> <pub3> OP_3 OP_CHECKMULTISIG

執行步驟:

  1. 解析 m 和 n 值(OP2 和 OP3)
  2. 彈出 n 個公鑰存入陣列
  3. 彈出並丟失一個額外元素(歷史 bug)
  4. 循環驗證 m 個簽名:
  1. 所有 m 個簽名都驗證成功,返回 TRUE

失敗條件與錯誤處理

比特幣腳本執行失敗(導致交易無效)的條件包括:

無效腳本不會拋出異常,而是悄悄地導致整個交易輸入失敗。這種 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

腳本升級策略

隨著比特幣腳本語言的演進,需要考慮未來的升級相容性:

  1. 使用標準化腳本:避免使用非標準或實驗性操作碼
  2. 版本化設計:在腳本中預留版本字段
  3. Merkle 樹結構:使用 MAST 允許添加未來條件而不暴露當前條件

結論

比特幣腳本語言的簡約設計體現了密碼學安全的核心原則——複雜性是安全的敵人。通過精心選擇的非圖靈完整指令集,比特幣實現了可預測的執行行為、強健的安全性假設,以及靈活的多種應用場景支持。

本指南提供的程式碼範例涵蓋了比特幣腳本的核心應用場景:從 P2PKH 基礎交易到 P2SH-P2WSH 多簽合約,從 HTLC 原子交換到時間鎖智慧合約。這些範例均可在 testnet 環境中實際驗證,讀者可通過動手實作深化對比特幣腳本的理解。

比特幣腳本語言持續演進,從 SegWit v0 的費用優化到 Taproot 的隱私增強,再到未來可能的 OP_CAT 等新操作碼引入。掌握腳本語言的基礎原理,是深入理解比特幣技術架構的必要前提。

如需進一步學習,建議閱讀比特幣開發者文檔(Bitcoin Developer Documentation)和 BIP 提案原文,並在 testnet 上反覆練習各種腳本類型的建構與驗證。

本文包含

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!