跳至主要內容

Bitcoin Script 實戰教學(二):標準腳本類型詳解

3 分鐘閱讀
Bitcoin Script 實戰教學(二):標準腳本類型詳解

這是「Bitcoin Script 實戰教學」系列的第二篇。本篇將詳細解析各種標準腳本類型,了解它們的結構、用途和演進歷史。

系列文章導航:


一、腳本類型的歷史演進

1.1 為什麼會有不同的腳本類型?

比特幣的腳本系統並非一成不變。從 2009 年誕生至今,腳本類型經歷了多次重大演進,每一次演進都是為了解決前一代的問題或增加新的功能。

理解這個演進歷程很重要,因為它解釋了為什麼比特幣有這麼多種地址格式,以及為什麼某些看似複雜的設計是必要的。

1.2 演進時間線

2009年:P2PK(Pay to Public Key)

中本聰在最初的比特幣實現中使用的就是 P2PK。這是最直接的方式——公鑰直接寫在腳本中,花費時提供簽名即可。早期的 coinbase 交易(礦工獎勵)都使用這種格式。

2010年:P2PKH(Pay to Public Key Hash)

人們很快意識到直接暴露公鑰有安全隱患,於是引入了公鑰哈希。這種方式把公鑰隱藏起來,只有在花費時才需要揭露。同時,這也催生了「地址」的概念——以 1 開頭的熟悉格式就是從這時開始的。

2012年:P2SH(Pay to Script Hash)

BIP 16 提出了一個革命性的想法:把任意複雜的腳本用一個哈希值代替。這讓多重簽名等複雜功能變得實用,因為發送者不需要知道腳本的具體內容,只需要發送到一個以 3 開頭的地址。

2017年:SegWit(P2WPKH / P2WSH)

隔離見證是比特幣歷史上最重要的升級之一。它不僅解決了交易延展性問題,還提高了區塊容量效率。bc1q 開頭的地址就是 SegWit 地址。

2021年:Taproot(P2TR)

Taproot 引入了 Schnorr 簽名和 MAST(Merkelized Alternative Script Trees),讓複雜的智能合約在正常情況下看起來和普通交易一樣。bc1p 開頭的地址代表 Taproot。

1.3 地址格式一覽

不同腳本類型對應不同的地址格式:

腳本類型 主網地址前綴 測試網前綴 引入年份
P2PKH 1 m 或 n 2010
P2SH 3 2 2012
P2WPKH bc1q(42字元) tb1q 2017
P2WSH bc1q(62字元) tb1q 2017
P2TR bc1p tb1p 2021

注意 P2WPKH 和 P2WSH 都以 bc1q 開頭,但長度不同——P2WPKH 是 42 字元,P2WSH 是 62 字元。這是因為它們使用不同長度的哈希(20 字節 vs 32 字節)。


二、P2PK:最原始的支付方式

2.1 歷史背景

P2PK(Pay to Public Key)是比特幣最原始的支付腳本類型。中本聰在設計比特幣時,最自然的想法就是:用公鑰標識接收者,用對應私鑰的簽名證明所有權。

這種方式在密碼學上是正確的,但在實際使用中很快暴露出問題。

2.2 腳本結構

P2PK 的結構非常簡單:

鎖定腳本(ScriptPubKey)

1
<公鑰> OP_CHECKSIG

解鎖腳本(ScriptSig)

1
<簽名>

執行過程也很直觀:

  1. 把簽名推入堆疊
  2. 把公鑰推入堆疊
  3. OP_CHECKSIG 驗證簽名是否有效

如果簽名確實是用這個公鑰對應的私鑰簽署的,驗證就通過。

2.3 P2PK 的問題

問題一:公鑰提前暴露

在 P2PK 中,公鑰從 UTXO 創建的那一刻起就是公開的。這帶來兩個安全隱患:

首先是量子計算威脅。雖然目前的量子計算機還無法破解橢圓曲線密碼學,但理論上,擁有足夠強大量子計算機的攻擊者可以從公鑰推導出私鑰。如果你的公鑰已經暴露,你的比特幣就處於風險中。

其次是隱私問題。公鑰是長期身份標識,如果某人知道你的公鑰,就能追蹤你的所有交易。

問題二:空間效率低

未壓縮的公鑰有 65 字節,即使是壓縮公鑰也有 33 字節。這些數據存儲在區塊鏈上,佔用寶貴的空間。相比之下,公鑰的哈希只有 20 字節。

問題三:用戶體驗差

公鑰是一長串十六進制字符,不適合作為「地址」使用。人們很難記住、比對或手動輸入公鑰。

2.4 現代使用情況

今天,P2PK 幾乎不再被使用。但你仍然可以在早期區塊中看到它,特別是創世區塊和早期的 coinbase 交易。這些古老的比特幣如果要移動,其公鑰就會暴露——這也是為什麼中本聰的早期挖礦收益(如果他還有私鑰的話)被認為是「危險資產」。


三、P2PKH:地址的誕生

3.1 從公鑰到地址

P2PKH(Pay to Public Key Hash)解決了 P2PK 的主要問題。它的核心創新是:不直接使用公鑰,而是使用公鑰的哈希值。

這個哈希值稱為「公鑰哈希」(Public Key Hash),計算方式是:

1
公鑰哈希 = RIPEMD160(SHA256(公鑰))

這就是我們常說的 HASH160 操作。結果是一個 20 字節(160 位元)的哈希值。

3.2 地址的誕生

有了公鑰哈希,我們需要一種用戶友好的方式來表示它。這就是「地址」的由來:

1
地址 = Base58Check(版本前綴 + 公鑰哈希)

Base58Check 是一種編碼方式,它:

  • 使用 58 個字符(去掉了 0、O、I、l 等易混淆字符)
  • 包含校驗和(防止輸入錯誤)
  • 主網版本前綴是 0x00,產生以 1 開頭的地址
  • 測試網版本前綴是 0x6F,產生以 m 或 n 開頭的地址

這就是為什麼傳統比特幣地址都以「1」開頭。

3.3 腳本結構詳解

鎖定腳本(ScriptPubKey)

1
OP_DUP OP_HASH160 <公鑰哈希> OP_EQUALVERIFY OP_CHECKSIG

這個腳本的設計很巧妙,讓我們逐步解析:

  1. OP_DUP:複製堆疊頂端的值(花費者提供的公鑰)
  2. OP_HASH160:計算公鑰的哈希
  3. <公鑰哈希>:推入預期的哈希值
  4. OP_EQUALVERIFY:比較兩個哈希是否相等
  5. OP_CHECKSIG:驗證簽名

解鎖腳本(ScriptSig)

1
<簽名> <公鑰>

3.4 執行流程深入解析

假設 Alice 想要花費一筆發送給她的比特幣:

初始狀態:堆疊為空

第一步:處理解鎖腳本

推入 Alice 的簽名:

1
堆疊:[sig]

推入 Alice 的公鑰:

1
堆疊:[sig, pubkey]

第二步:執行鎖定腳本

OP_DUP - 複製公鑰:

1
堆疊:[sig, pubkey, pubkey]

OP_HASH160 - 計算頂端公鑰的哈希:

1
堆疊:[sig, pubkey, pubkey_hash]

推入預期的公鑰哈希:

1
堆疊:[sig, pubkey, pubkey_hash, expected_hash]

OP_EQUALVERIFY - 比較兩個哈希:

  • 如果相等:彈出這兩個值,繼續
  • 如果不等:腳本立即失敗
    1
    
    堆疊:[sig, pubkey]
    

OP_CHECKSIG - 驗證簽名:

1
堆疊:[TRUE]  (如果簽名有效)

3.5 P2PKH 的優勢

更好的安全性

公鑰只在花費時才暴露。在花費之前,即使量子計算機出現,也無法從地址(公鑰哈希)推導出公鑰,更無法推導出私鑰。

當然,一旦花費過,公鑰就暴露了。這就是為什麼安全最佳實踐建議「不要重複使用地址」——每次收款使用新地址,可以保持未花費資金的安全性。

更短的地址

公鑰哈希只有 20 字節,比 33 字節的壓縮公鑰更短。加上 Base58Check 編碼後,地址長度約 34 個字符,相對容易處理。

錯誤檢測

Base58Check 編碼包含 4 字節的校驗和。如果用戶輸入地址時打錯了字符,校驗和幾乎肯定會對不上,錢包軟件會提醒用戶。這大大降低了因輸入錯誤而損失資金的風險。

3.6 P2PKH 的局限

儘管 P2PKH 是一大進步,但它仍有局限:

只支援單一簽名

P2PKH 只能表示「需要某個特定私鑰的簽名」。如果你想實現多重簽名(如「3 個人中的 2 個必須簽名」),就需要更複雜的腳本,而這些腳本會讓地址變得又長又複雜。

發送者的負擔

如果接收者想使用複雜的花費條件,發送者需要知道完整的腳本。這不僅麻煩,還暴露了接收者的安全策略。

這些問題催生了 P2SH 的出現。


四、P2SH:腳本哈希革命

4.1 核心創新

P2SH(Pay to Script Hash)是 2012 年通過 BIP 16 引入的。它的核心思想非常優雅:

不是把腳本放在輸出中,而是放腳本的哈希。

這意味著,無論你的花費條件多複雜,發送者只需要發送到一個短短的地址。複雜的腳本由接收者保管,只在花費時才需要揭露。

4.2 運作原理

假設 Alice 想設置一個 2-of-3 多重簽名地址。傳統上,她需要把整個多簽腳本告訴所有要給她付款的人。使用 P2SH,流程變成:

  1. Alice 創建她的多簽腳本(稱為「贖回腳本」,Redeem Script)
  2. Alice 計算這個腳本的哈希
  3. Alice 把這個哈希轉換成地址(以 3 開頭)
  4. 付款人只需發送到這個地址,不需要知道背後的腳本

當 Alice 要花費這筆錢時,她需要提供:

  • 原始的贖回腳本
  • 滿足贖回腳本的數據(如簽名)

網路節點會驗證:

  1. 提供的腳本哈希確實等於地址中的哈希
  2. 提供的數據確實滿足腳本的條件

4.3 腳本結構

鎖定腳本(ScriptPubKey)

1
OP_HASH160 <腳本哈希> OP_EQUAL

這個結構非常簡潔!只有 23 字節,無論背後的贖回腳本多複雜。

解鎖腳本(ScriptSig)

1
<數據...> <贖回腳本>

解鎖腳本包含兩部分:

  1. 滿足贖回腳本所需的數據(如簽名)
  2. 贖回腳本本身

4.4 驗證流程

P2SH 的驗證分兩階段:

第一階段:驗證腳本哈希

  1. 取出解鎖腳本的最後一個元素(贖回腳本)
  2. 計算它的 HASH160
  3. 與鎖定腳本中的哈希比較
  4. 如果相等,進入第二階段

第二階段:執行贖回腳本

  1. 把解鎖腳本中的其他數據作為輸入
  2. 執行贖回腳本
  3. 如果執行成功(堆疊頂部為 TRUE),整個驗證通過

這種兩階段設計是 P2SH 的精髓。它保證了:

  • 只有知道原始腳本的人才能花費資金
  • 發送者不需要了解腳本細節

4.5 實際例子:2-of-3 多簽

讓我們看一個具體的例子。假設三個人(Alice、Bob、Charlie)想創建一個需要三人中任意兩人簽名的共同賬戶。

贖回腳本

1
OP_2 <Alice公鑰> <Bob公鑰> <Charlie公鑰> OP_3 OP_CHECKMULTISIG

計算地址

  1. 對贖回腳本進行 HASH160
  2. 加上 P2SH 版本前綴(0x05)
  3. 進行 Base58Check 編碼
  4. 得到以「3」開頭的地址

花費時(假設 Alice 和 Bob 簽名):

1
解鎖腳本:OP_0 <Alice簽名> <Bob簽名> <贖回腳本>

注意開頭的 OP_0,這是 OP_CHECKMULTISIG 的一個歷史 bug(多消耗一個堆疊元素),必須提供一個虛擬值。

4.6 P2SH 的限制

贖回腳本大小限制

為了防止濫用,贖回腳本有 520 字節的大小限制。這限制了可以使用的腳本複雜度。例如,標準的多重簽名最多支持 15-of-15,更多的參與者就需要使用更高級的技術。

腳本必須揭露

當你花費 P2SH 輸出時,完整的贖回腳本會被記錄在區塊鏈上。這意味著:

  • 你的安全策略最終會公開
  • 如果贖回腳本很大,交易費用會更高

簽名放在錯誤的位置

在 P2SH 中,簽名是解鎖腳本的一部分,包含在交易的「可修改」區域。這導致了「交易延展性」問題——第三方可以在不改變交易有效性的情況下修改交易 ID。這個問題在 SegWit 中得到解決。


五、SegWit 簡介:P2WPKH 和 P2WSH

5.1 為什麼需要 SegWit?

2017 年激活的隔離見證(Segregated Witness,SegWit)是比特幣最重要的升級之一。它解決了幾個關鍵問題:

交易延展性

在 SegWit 之前,交易 ID(txid)是基於整個交易計算的,包括簽名。由於簽名有多種有效的編碼方式,第三方可以修改簽名的編碼而不影響其有效性,從而改變 txid。這對閃電網路等依賴 txid 的協議是致命的。

SegWit 把簽名(見證數據)移到交易結構之外,交易 ID 不再包含簽名,從根本上解決了延展性問題。

區塊容量

SegWit 引入了「區塊重量」的概念,見證數據的重量只有普通數據的 1/4。這有效地增加了區塊容量,讓更多交易可以被打包。

腳本版本控制

SegWit 引入了「見證版本」的概念,讓未來的升級更容易實現。Taproot 就是作為見證版本 1 實現的。

5.2 P2WPKH:原生 SegWit 單簽

P2WPKH(Pay to Witness Public Key Hash)是 P2PKH 的 SegWit 版本。

鎖定腳本(ScriptPubKey)

1
OP_0 <20字節公鑰哈希>

是的,就這麼簡單!OP_0 表示見證版本 0,後面跟著 20 字節的公鑰哈希。

解鎖腳本(ScriptSig)

1
(空)

在 SegWit 中,ScriptSig 是空的。所有的解鎖數據都放在新的「見證」(Witness)區域:

見證數據(Witness)

1
<簽名> <公鑰>

這種設計實現了真正的「隔離見證」——簽名數據被隔離到單獨的區域,不影響交易 ID 的計算。

5.3 P2WSH:原生 SegWit 腳本哈希

P2WSH(Pay to Witness Script Hash)是 P2SH 的 SegWit 版本,用於複雜腳本。

鎖定腳本(ScriptPubKey)

1
OP_0 <32字節腳本哈希>

注意哈希長度是 32 字節(使用 SHA256),比 P2SH 的 20 字節更長。這提供了更強的安全性,防止生日攻擊。

見證數據(Witness)

1
<數據...> <見證腳本>

5.4 地址格式:Bech32

SegWit 引入了新的地址格式 Bech32(BIP 173),以 bc1 開頭:

  • bc1q:見證版本 0(P2WPKH 和 P2WSH)
  • bc1p:見證版本 1(Taproot,使用 Bech32m)

Bech32 相比 Base58Check 有幾個優勢:

  • 全部小寫,減少混淆
  • 更好的錯誤檢測能力
  • 更適合二維碼(字符集更緊湊)

5.5 向後兼容:P2SH 包裝的 SegWit

為了讓舊錢包也能發送到 SegWit 地址,比特幣支持把 SegWit 腳本「包裝」在 P2SH 裡:

P2SH-P2WPKH:以 3 開頭,內部是 SegWit P2SH-P2WSH:以 3 開頭,內部是 SegWit 腳本哈希

這種包裝方式讓不支援 SegWit 的錢包也能發送資金到 SegWit 地址。不過,現在大多數錢包都支援原生 SegWit 地址,這種包裝方式正在逐漸被淘汰。


六、如何選擇腳本類型?

6.1 現代最佳實踐

對於 2024 年的用戶和開發者,以下是建議的選擇順序:

首選:P2TR(Taproot)

如果你的錢包和對方的錢包都支持,優先使用 Taproot 地址(bc1p…)。它提供最好的隱私性和最低的費用。

次選:P2WPKH(原生 SegWit)

如果不支持 Taproot,使用原生 SegWit 地址(bc1q…)。它是目前最廣泛支持的高效格式。

向後兼容:P2SH-P2WPKH

如果對方錢包很舊,無法發送到 bc1 地址,使用 P2SH 包裝的 SegWit(3 開頭)。

盡量避免:P2PKH

除非有特殊原因,不建議使用舊的 P2PKH 地址(1 開頭)。它們費用更高,隱私性更差。

6.2 費用比較

不同腳本類型的交易費用差異顯著(以典型的 1 輸入 2 輸出交易為例):

類型 大約大小 相對費用
P2PKH 226 vbytes 100%
P2SH-P2WPKH 167 vbytes 74%
P2WPKH 141 vbytes 62%
P2TR 111 vbytes 49%

使用 Taproot 可以節省約一半的交易費用!


七、實作練習

練習 1:識別地址類型

看以下地址,判斷它們的類型:

1
2
3
4
1. 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
2. 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy
3. bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
4. bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr

練習 2:理解腳本執行

給定以下 P2PKH 腳本:

鎖定腳本:OP_DUP OP_HASH160 <0x89abcdef...> OP_EQUALVERIFY OP_CHECKSIG

問題:

  1. 解鎖腳本需要包含什麼?
  2. 如果提供了錯誤的公鑰(哈希不匹配),腳本會在哪一步失敗?
  3. 如果公鑰正確但簽名無效,腳本會在哪一步失敗?

練習 3:計算地址

給定公鑰(十六進制):

1
02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737

計算它的 P2PKH 地址。(提示:需要計算 HASH160,然後進行 Base58Check 編碼)


八、總結

本篇重點

  1. 演進歷程:從 P2PK 到 P2PKH 到 P2SH 到 SegWit,每一步都解決了前一代的問題。

  2. P2PKH 的創新:引入公鑰哈希和地址概念,提高了安全性和用戶體驗。

  3. P2SH 的革命:讓複雜腳本變得實用,發送者不需要了解腳本細節。

  4. SegWit 的突破:解決交易延展性,提高區塊效率,為未來升級鋪路。

  5. 現代選擇:優先使用 Taproot,其次是原生 SegWit,盡量避免舊格式。

下一篇預告

下一篇將深入探討 SegWit 和 Taproot 的技術細節:

  • SegWit 的完整交易結構
  • 見證數據的組織方式
  • Schnorr 簽名的優勢
  • Taproot 的 MAST 機制
  • 密鑰路徑與腳本路徑

參考資源

BIP 文檔

延伸閱讀

// 分享

CP

Cypherpunks Taiwan

密碼學使自由和隱私再次偉大。Cryptography makes freedom and privacy great again.

// 留言