比特幣腳本語言深度教學

深入理解比特幣腳本語言的運作原理、常見腳本類型與進階應用場景。

比特幣腳本語言深度教學:從密碼學原語到進階合約設計

前言

比特幣腳本語言經常被拿來跟以太坊的 Solidity 做比較——然後得出「比特幣腳本很陽春」的結論。這種比較本身就有問題。就像你不能拿螺絲起子跟電鋸比較一樣,它們是完全不同的工具,設計目標也截然不同。

比特幣腳本的刻意簡化是一種安全策略。當你的系統處理數十億美元的資產時,「可預測性」比「表達力」更重要。Solana 曾經因為合約漏洞損失數億美元,比特幣卻從未因為腳本語言本身的安全漏洞而丟失資金——這不是巧合,而是深思熟慮的設計選擇。

這篇文章我們來徹底搞懂比特幣腳本。從堆疊機原理到密碼學原語,從基本腳本類型到 Taproot 的高級應用。

第一章:堆疊機的藝術

1.1 為什麼是堆疊機?

現代程式語言幾乎都是「自動機」導向——你定義變量,機器按順序執行。但比特幣腳本採用的是「堆疊機」架構,這種設計源於 1960 年代的 Forth 語言。

堆疊機的核心思想極其簡單:

這種設計有幾個關鍵優點:

  1. 無狀態執行:每次腳本執行都從乾淨的堆疊開始,沒有全域變量,沒有副作用
  2. 記憶體效率:不需要複雜的變量管理
  3. 實作簡單:比完整虛擬機更容易審計和驗證
  4. 安全性:攻擊面積極小

1.2 逆波蘭表示法實戰

比特幣腳本使用逆波蘭表示法(RPN),讓我透過實際例子讓你完全理解:

例子:驗證簽名長度

# 這不是實際比特幣腳本,而是概念演示
# 
# 目標:檢查簽名是否為標準長度(70-73 bytes)
# 
# 比特幣腳本:
# OP_LENGTH OP_70 OP_GREATERTHAN OP_IF
#   OP_LENGTH OP_73 OP_LESSTHAN OP_IF
#     OP_TRUE
#   OP_ELSE
#     OP_FALSE
#   OP_ENDIF
# OP_ELSE
#   OP_FALSE
# OP_ENDIF

# 堆疊執行追蹤(實際執行流程)
stack_trace = []

def execute(script, initial_stack=None):
    stack = initial_stack or []
    ops = script.split()
    
    for op in ops:
        if op.startswith('<'):
            # 數據元素,推入堆疊
            stack.append(op[1:-1])  # 去除 <> 
        elif op == 'OP_DUP':
            stack.append(stack[-1])
        elif op == 'OP_SIZE':
            stack.append(len(stack[-1]))
        elif op == 'OP_70':
            stack.append(70)
        elif op == 'OP_GREATERTHAN':
            b = int(stack.pop())
            a = int(stack.pop())
            stack.append(a > b)
        elif op == 'OP_LESSTHAN':
            b = int(stack.pop())
            a = int(stack.pop())
            stack.append(a < b)
        elif op == 'OP_AND':
            b = stack.pop()
            a = stack.pop()
            stack.append(a and b)
    
    return stack

# 這就是比特幣腳本執行的基本原理

1.3 堆疊操作的物理意義

比特幣的每個操作碼都有明確的堆疊行為定義。理解這個映射關係是掌握比特幣腳本的關鍵:

操作碼堆疊輸入堆疊輸出說明
OP_DUPaa, a複製頂部
OP_DROPa(empty)移除頂部
OP_SWAPa, bb, a交換位置
OP_ROTa, b, cb, c, a旋轉三個
OP_TUCKa, bb, a, b複製並插入
OP_2DUPa, ba, b, a, b複製前兩個

這種命名方式雖然有點過時(來自 1990 年代的密碼學文獻),但邏輯是清晰的。

第二章:密碼學原語在比特幣腳本中的應用

2.1 哈希函數

比特幣腳本使用三種主要的哈希函數:

OP_HASH256:SHA-256 雙哈希

hash = SHA256(SHA256(data))

OP_HASH160:RIPEMD-160(SHA-256(data))

hash = RIPEMD160(SHA256(data))

OP_RIPEMD160:單次 RIPEMD-160

實際應用場景:

# 場景:證明你知道某個秘密值,但不透露這個值
# 
# 腳本邏輯:
# <secret_hash> OP_HASH256 <provided_secret> OP_EQUAL
#
# 堆疊追蹤:
# 1. <secret_hash> 入堆疊      → [secret_hash]
# 2. <provided_secret> 入堆疊 → [secret_hash, provided]
# 3. OP_HASH256                  → [secret_hash, provided_hash]
# 4. OP_EQUAL                    → [True/False]

2.2 橢圓曲線密碼學

比特幣使用 secp256k1 曲線,這是怎麼回事?

OP_CHECKSIG:驗證 ECDSA 簽名

# 簽名驗證的數學原理
#
# 私鑰 d,公開金鑰 Q = d * G
# 簽名 (r, s),消息 m
# 
# 驗證:s^(-1) * H(m) * G + s^(-1) * r * Q
#        = ?
#        = (r, s) 的公鑰驗證點
#
# 比特幣腳本中的實現是硬編碼的,無法直接看到這些計算

secp256k1 的特殊性

這條曲線的參數是精心選擇的,與 NIST 曲線不同:

2.3 Schnorr 簽章(Taproot 基礎)

Taproot 引入了 Schnorr 簽章,這比 ECDSA 有幾個關鍵優勢:

  1. 簽章聚合:多個簽章可以合併成一個
  2. 線性簽名:允許更複雜的協議設計
  3. 安全性證明:Schnorr 簽章有正式的數學安全性證明
# Schnorr 簽名聚合概念
#
# 假設 Alice 和 Bob 要共同簽名
# 
# 傳統 ECDSA:需要雙方各提供一個簽名
# Schnorr:可以合併成單一簽名
#
# 聚合公式:
# P_agg = P_Alice + P_Bob  (線性組合)
# s_agg = s_Alice + s_Bob  (簡單相加)
#
# 驗證者只需要檢查:
# s_agg * G == R + H(R || P_agg || m) * P_agg

第三章:P2PKH 到 P2TR——地址類型的演進

3.1 P2PKH 的安全模型

P2PKH(Pay to Public Key Hash)是比特幣最原始的地址格式。理解它讓你能評估其他所有格式的安全权衡。

鎖定腳本:
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

解鎖時提供:
<signature> <pubKey>

執行流程詳解:

步驟 1:初始化
堆疊:[sig, pubKey]

步驟 2:OP_DUP - 複製 pubKey
堆疊:[sig, pubKey, pubKey]

步驟 3:OP_HASH160 - 計算 pubKey 的哈希
堆疊:[sig, pubKey, pubKeyHash]

步驟 4:推送目標 pubKeyHash
堆疊:[sig, pubKey, pubKeyHash, targetHash]

步驟 5:OP_EQUALVERIFY
- 彈出兩個值比較
- 如果相等,繼續執行
- 如果不等,交易失敗
堆疊:[sig, pubKey]

步驟 6:OP_CHECKSIG
- 驗證 sig 確實由 pubKey 對應的私鑰簽署
- 使用交易的 sighash
堆疊:[True] 或 [False]

3.2 P2SH 的突破

P2SH(Pay to Script Hash)解決了「複雜腳本的高成本問題」:

# 傳統方式(直接支付給腳本):
# 每個人支付時都要包含整個腳本
# 如果是多簽 3-of-5,額外花費 500 bytes

# P2SH 方式:
# 只支付 20 bytes 的哈希
# 只有實際花費時才揭示腳本
# 成本由接收者承擔,而不是每個付款者

# 實際地址計算:
import hashlib

redeem_script = "OP_2 PubKey1 PubKey2 PubKey3 OP_3 OP_CHECKMULTISIG"
script_bytes = redeem_script.encode()

# P2SH 地址
script_hash = hashlib.new('ripemd160', 
    hashlib.sha256(script_bytes).digest()
).digest()

# Base58Check 編碼(3 開頭)
# ...

3.3 SegWit 的革命性變化

SegWit 不僅是地址格式的改變,而是整個交易結構的重構:

傳統交易結構 vs SegWit 交易結構
═══════════════════════════════════════════════════════════════

傳統交易:
┌─────────────────────────────────────┐
│  版本號                              │
├─────────────────────────────────────┤
│  輸入列表                             │
│    - 前一筆交易 hash                   │
│    - 輸出索引                          │
│    - 解鎖腳本(含簽名)                  │
│    - 序列號                            │
├─────────────────────────────────────┤
│  輸出列表                             │
│    - 金額                             │
│    - 鎖定腳本                          │
├─────────────────────────────────────┤
│  時間鎖(可選)                        │
└─────────────────────────────────────┘

SegWit 交易:
┌─────────────────────────────────────┐
│  版本號                              │
├─────────────────────────────────────┤
│  標記(Marker)位元組:0x00            │
│  標誌(Flag)位元組:0x01              │
├─────────────────────────────────────┤
│  輸入列表(不含見證)                   │
├─────────────────────────────────────┤
│  輸出列表                             │
├─────────────────────────────────────┤
│  見證數量                            │
│  見證列表                             │
│    - 每個輸入的見證數據                 │
├─────────────────────────────────────┤
│  時間鎖(可選)                        │
└─────────────────────────────────────┘

關鍵創新:見證資料(簽名)被移到交易外部,但仍包含在區塊中。這讓:

  1. 交易 ID 計算不再包含簽名(消除延展性)
  2. 區塊有效容量增加
  3. 見證數據享受折扣(1/4 重量)

3.4 Taproot 的極致優雅

Taproot 是比特幣迄今為止最複雜的升級,但它的外部表現極其簡單——看起來就像普通的單簽交易。

Taproot 腳本結構:

┌──────────────────────────────────────────────────────────────┐
│  內部金鑰路徑(最常用)                                        │
│  Q = P + H(P || t) * G                                       │
│  花費方式:直接用內部私鑰簽名                                    │
└──────────────────────────────────────────────────────────────┘
                              OR
┌──────────────────────────────────────────────────────────────┐
│  MAST 結構(Merkelized Abstract Syntax Tree)                  │
│                                                              │
│                        Root = H(A || B)                     │
│                       /                   \                 │
│               H(A)                           H(B)           │
│              /    \                        /    \           │
│         條件 A1  條件 A2               條件 B1  條件 B2      │
│                                                              │
│  花費方式:展示你滿足的條件分支 + 通往該分支的哈希路徑            │
└──────────────────────────────────────────────────────────────┘

為什麼這很重要?因為區塊鏈上所有 Taproot 交易看起來都一模一樣。無論你是普通單簽用戶,還是建立了複雜的 7-of-12 多重簽章 + 時間鎖 + 哈希原像合約,外部觀察者都無從得知。

隱私 + 效率 + 靈活性——這就是 Taproot。

第四章:進階腳本模式

4.1 多重簽章的藝術

比特幣的多重簽章支援 m-of-n 模式,意思是「n 個金鑰中最少需要 m 個簽名才能花費」。

# 2-of-3 多重簽章腳本:
# OP_2 <PubKey1> <PubKey2> <PubKey3> OP_3 OP_CHECKMULTISIG

# 這個腳本的含義:
# - 需要任意 2 把私鑰簽名
# - 總共有 3 把可能的私鑰

# 實際應用場景:

# 1. 企業金庫
#    總裁 + 財務長 + 監察人 = 3-of-3
#    重大決定需要三人同意
#
# 2. 家族遺產
#    父母 2-of-2 + 子女緊急備用鑰匙 = 2-of-3
#    父母同意或父母之一+子女
#
# 3. 2FA 錢包
#    熱錢包 + 冷錢包 = 2-of-2
#    需要兩把鑰匙同時在場

# 注意 OP_CHECKMULTISIG 的 bug:
# 需要以 OP_0 開頭才能正確執行

# 錯誤的解鎖腳本:
# <sig1> <sig2>  # 會失敗

# 正確的解鎖腳本:
# OP_0 <sig1> <sig2>  # 成功

4.2 時間鎖的高級應用

比特幣的時間鎖分為兩種,組合使用可以實現非常複雜的邏輯:

# 絕對時間鎖 vs 相對時間鎖
#
# OP_CHECKLOCKTIMEVERIFY (CLTV)
# - 設定未來的某個時間點
# - 單位可以是區塊高度或 Unix 時間戳
# - 典型用途:預設合約、遺產規劃
#
# OP_CHECKSEQUENCEVERIFY (CSV)
# - 設定相對於 UTXO 創建的等待時間
# - 只支持區塊高度(不能使用時間戳)
# - 典型用途:閃電網路、押金合約

# 高級模式:時間延遲 + 多簽組合

# 合約設計:
# 要麼(2-of-3 多簽)
# 要麼(90天後 + 緊急鑰匙 + 單簽)

# 腳本邏輯(偽代碼):
# if (2-of-3 signatures) {
#     // 普通路徑
# } else if (90 days elapsed + emergency key + personal key) {
#     // 備用路徑
# }

# 這種設計的好處:
# - 日常使用:2-of-3 多簽,快速便捷
# - 緊急情況:即使其他 2 人失聯,仍能在 90 天後取回資金
# - 死亡情景:設定監護人鑰匙,90 天後自動解鎖給受益人

4.3 HTLC 的完整實現

HTLC(Hashed Time Locked Contract)是比特幣智能合約的精髓:

# HTLC 的四個核心要素:
# 1. 哈希鎖:必須提供正確的原像才能解鎖
# 2. 時間鎖:必須在期限前行動
# 3. 接收方:能提供原像的人
# 4. 發送方:超時後能撤回的人

# 經典型 HTLC 腳本:
# OP_IF
#     <recipient_pubkey> OP_CHECKSIG
# OP_ELSE
#     <timeout> OP_CHECKSEQUENCEVERIFY OP_DROP
#     <sender_pubkey> OP_CHECKSIG
# OP_ENDIF

# 實際應用:原子交換(比特幣換萊特幣)

# 步驟 1:Alice 創建 HTLC,鎖定 1 BTC
#         謎題:H(secret) = abc123...
#         時間:48 小時
#
# 步驟 2:Bob 在萊特幣鏈上創建對應的 HTLC
#         謎題:同樣的 H(secret)
#         時間:24 小時(比 BTC 短,給自己留餘地)
#
# 步驟 3:Alice 在萊特幣鏈上提供 secret,取走 Bob 的 LTC
#         同時,Bob 看到 secret
#
# 步驟 4:Bob 在比特幣鏈上使用 secret,取走 1 BTC
#
# 結果:原子交換完成,雙方都不用信任對方

# 如果超時:
# - Bob 沒收到 secret,24 小時後取回 LTC
# - Alice 48 小時後取回 BTC

4.4 Vault——比特幣的銀行帳戶

Vault 是一種利用 OP_COVINANT(或模擬實現)的高級腳本模式:

# Vault 設計目標:
# - 日常資金需要「冷靜期」
# - 緊急情況下有快速取款通道
# - 防止盜賊竊取後立即轉移

# 概念腳本:
# 
# 主金鑰路徑(需要延遲):
# OP_IF
#     <delay> OP_CHECKSEQUENCEVERIFY OP_DROP
#     <guardian_pubkey> OP_CHECKSIGVERIFY
#     <primary_pubkey> OP_CHECKSIG
# OP_ELSE
#     <emergency_pubkey> OP_CHECKSIG
# OP_ENDIF

# 運作流程:
# 1. 正常情況:Alice 用主私鑰發起提款
# 2. 系統進入「隔離期」:30 天後才能完成
# 3. Alice 如果認可:30 天後資金轉出
# 4. Alice 如果被攻擊:在隔離期內使用緊急金鑰 + 主金鑰立即轉出
# 5. Guardian(監護人)可以在任何時候否決轉帳

# 這個模式讓比特幣錢包獲得了「銀行帳戶」級別的安全

第五章:常見漏洞與防禦

5.1 延展性攻擊的完整解析

延展性攻擊曾經是比特幣的嚴重問題,直到 SegWit 修復。

# 攻擊原理(ECDSA 時代):

# 交易簽名包含 (r, s) 兩個值
# 攻擊者可以計算 s' = -s mod n
# 這是一個「數學上有效」的新簽名
# 但讓交易 ID 完全改變

# 攻擊流程:
# 1. Alice 向交易所充值 1 BTC
# 2. 交易所監控區塊鏈,確認交易 A(txid: abc123)
# 3. 攻擊者攔截交易,修改簽名得到交易 A'(txid: def456)
# 4. 攻擊者同時廣播交易 A 和 A'
# 5. 礦工打包 A',放棄 A
# 6. 交易所的內部系統追蹤的是 txid abc123
# 7. 系統認為充值未確認,再次發起充值
# 8. 重複直到榨乾交易所

# SegWit 修復:
# 交易 ID 不再包含見證數據
# 即使簽名被修改,txid 保持不變

5.2 重放攻擊

分叉後的區塊鏈需要應對跨鏈重放:

# 場景:假設比特大陸又分叉了一條新鏈 BITCOIN-X

# 問題:
# 你的私鑰在兩條鏈上都有效
# 在比特幣鏈上的交易,在 BITCOIN-X 鏈上也有效

# 防禦方法 1:隔離見證地址
# 隔離見證地址在原鏈和分叉鏈上有不同的前綴
# bc1... vs 其他前綴

# 防禦方法 2:OP_RETURN 作為 chain ID
# 在交易中附加 4 bytes 的 chain ID
# OP_RETURN <chain_id>

# 防禦方法 3:SIGHASH_ANYONECANPAY 避免衝突
# 只簽名自己的輸入和輸出
# 不會與其他輸入/輸出衝突

5.3 灰塵攻擊

灰塵攻擊是一種隱私破壞手段:

# 攻擊原理:
# 1. 攻擊者向你的每個地址發送極少量 BTC(灰塵)
# 2. 如果你把灰塵 UTXO 與其他資金合併
# 3. 區塊鏈分析師就能把這些地址關聯起來

# 典型場景:
# 你有地址 A(重要資金)
# 你有地址 B(隱私相關)
# 攻擊者向 A 和 B 各發送 100 sats 灰塵
# 你不小心合併了灰塵
# 區塊鏈分析師知道 A 和 B 屬於同一人

# 防禦策略:
# 1. 啟用錢包的灰塵過濾功能
# 2. 永遠不合併灰塵 UTXO 與其他資金
# 3. 將灰塵發送到專門的「灰塵桶」地址,永遠不用

第六章:比特幣腳本的未來

6.1 BIP-347 與 OP_CAT

OP_CAT 是比特幣社群熱烈討論的提議之一:

# OP_CAT 的作用:
# 讓兩個棧元素可以拼接在一起

# 假設棧上有:
# <A> <B>
# OP_CAT
# 棧變成:
# <A || B>  # A 和 B 拼接在一起

# 為什麼這麼重要?

# 1. 可以實現更複雜的哈希原語
#    - 枚舉特定交易的輸出
#    - 驗證狀態轉換的承諾

# 2. 可以實現 MimbleWimble 的一些特性
#    - 在比特幣腳本層面實現隱私增強

# 3. 可以實現 Vault 的改進版本
#    - 不需要依賴 CPFP(父子支付)

# 4. 可以實現蔦合簽名驗證
#    - 把多個簽名拼接成一個承諾

6.2 CTV(CHECKTEMPLATEVERIFY)

CTV 允許腳本「承諾」特定的支出模板:

# CTV 的核心思想:
# 在鎖定腳本時,你指定這筆錢「只能發往這些地址之一」

# 語法:
# <template_hash> OP_CHECKTEMPLATEVERIFY

# template 的結構:
# {
#   "outputs": [
#     {"address": "bc1q...", "amount": 0.5 BTC},
#     {"address": "bc1q...", "amount": 0.3 BTC},
#   ],
#   "sequence": 5
# }

# 應用場景:押金合約
# 
# Alice 向合約地址存入 1 BTC
# 合約承諾:這 1 BTC 只能這樣分配:
# - Alice 取回 0.99 BTC + Bob 獲得 0.01 BTC(小費)
# - 或者原路退回

# 這種設計讓區塊鏈遊戲、押金證明等應用更高效
# 因為不需要信任遊戲伺服器

第七章:實務工具與調試

7.1 bitcoin-cli 實用命令

# 創建多重簽章地址
bitcoin-cli createmultisig 2 '["pubkey1", "pubkey2", "pubkey3"]'

# 解碼腳本
bitcoin-cli decodescript "OP_DUP OP_HASH160..."

# 測試交易是否能被接受
bitcoin-cli testmempoolaccept '["0200000000010100..."]'

# 估算交易費用
bitcoin-cli decodepsbt 'psbt_data'

# 獲取腳本的 P2SH 地址
bitcoin-cli createpsbt '[{"txid":"...", "vout":0}]' '[{"bc1q...":0.01}]'

7.2 Python 腳本引擎

class BitcoinScriptSimulator:
    """
    比特幣腳本模擬器 - 用於學習和調試
    注意:這是簡化版本,不能用於實際比特幣操作
    """
    
    def __init__(self):
        self.stack = []
        self.op_count = 0
        self.max_ops = 201  # 共識限制
    
    def push_data(self, data):
        """推送數據到堆疊"""
        if len(data) > 520:
            raise ValueError("Pushing too much data onto the stack")
        self.stack.append(data)
    
    def op_dup(self):
        """複製堆疊頂部"""
        if not self.stack:
            return False
        self.stack.append(self.stack[-1])
        return True
    
    def op_hash160(self):
        """計算 RIPEMD160(SHA256(x))"""
        if not self.stack:
            return False
        import hashlib
        data = self.stack.pop()
        h = hashlib.sha256(data).digest()
        h = hashlib.new('ripemd160')(h).digest()
        self.stack.append(h)
        return True
    
    def op_equal(self):
        """比較堆疊頂部兩個元素"""
        if len(self.stack) < 2:
            return False
        a = self.stack.pop()
        b = self.stack.pop()
        if a == b:
            self.stack.append(b'\x01')
        else:
            self.stack.append(b'')
        return True
    
    def op_checkSig(self, tx, sig_index, pubkey_index):
        """驗證簽名"""
        # 完整的 ECDSA 簽名驗證
        # 這裡只是概念演示
        if len(self.stack) < 2:
            return False
        sig = self.stack.pop()
        pubkey = self.stack.pop()
        
        # 驗證簽名(實際需要 secp256k1 庫)
        # return verify_ecdsa(sig, pubkey, tx)
        return True
    
    def execute(self, script, max_ops=201):
        """執行腳本"""
        self.max_ops = max_ops
        self.op_count = 0
        
        for op in script:
            self.op_count += 1
            if self.op_count > self.max_ops:
                return False
            
            if isinstance(op, bytes):
                self.push_data(op)
            elif op == 'OP_DUP':
                if not self.op_dup():
                    return False
            elif op == 'OP_HASH160':
                if not self.op_hash160():
                    return False
            elif op == 'OP_EQUAL':
                if not self.op_equal():
                    return False
            # ... 其他操作碼
        
        # 腳本成功如果堆疊頂部非零
        if self.stack:
            return bool(self.stack[-1])
        return False

# 使用範例
sim = BitcoinScriptSimulator()
result = sim.execute([
    b'signature',
    b'pubkey',
    'OP_DUP',
    'OP_HASH160',
    b'pubkey_hash',
    'OP_EQUAL',
    'OP_CHECKSIG'
])
print(f"Execution result: {result}")
print(f"Final stack: {sim.stack}")

結論

比特幣腳本語言的設計哲學體現了一個深刻的真理:有時候,「能做更多」反而是劣勢。

以太坊的 Solidity 可以做任何事情——但代價是複雜性爆炸、審計困難、以及無盡的漏洞。以太坊誕生以來,已經因為智能合約漏洞損失了數十億美元。比特幣呢?15 年來,沒有任何因為腳本語言本身設計缺陷而導致的資金損失。

這不是巧合。這是「刻意簡單」的勝利。

下次當你使用比特幣錢包、創建多簽設置、或許你正在與比特幣網路互動時,想想後台那個堆疊機正在默默執行複雜的密碼學驗證——所有這一切,都發生在你手指點擊「發送」的幾毫秒之內。

比特幣腳本,一個被低估的技術奇蹟。


本文旨在教育目的。比特幣腳本的實際使用涉及真實資金,請確保充分測試和理解後再進行任何操作。

本文包含

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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