高級
Bitcoin Script
比特幣腳本語言深入解析:操作碼、標準腳本類型和智能合約基礎
25 分鐘
概述
Bitcoin Script 是一種基於堆疊的腳本語言,用於定義交易輸出的花費條件。 它不是圖靈完備的,這是刻意設計以避免無限循環和複雜的安全問題。 每個比特幣交易都包含腳本,決定誰可以花費這些資金。
設計哲學: Script 故意保持簡單和受限。沒有循環、沒有狀態、執行時間可預測。 這讓每個節點都能快速驗證交易,確保網路安全。
堆疊機器
Script 使用兩個堆疊執行:主堆疊和替代堆疊。 操作碼從腳本中依序讀取,對堆疊進行操作。 執行結束時,如果主堆疊頂部是非零值(true),腳本成功。
執行流程
腳本: OP_2 OP_3 OP_ADD OP_5 OP_EQUAL
執行步驟:
1. OP_2 → 堆疊: [2]
2. OP_3 → 堆疊: [2, 3]
3. OP_ADD → 堆疊: [5] (2+3=5)
4. OP_5 → 堆疊: [5, 5]
5. OP_EQUAL → 堆疊: [1] (5==5, true)
結果: 成功 (堆疊頂部為 true) 常用操作碼
常數操作
| 操作碼 | Hex | 說明 |
|---|---|---|
OP_0 / OP_FALSE | 0x00 | 推入空字節數組(false) |
OP_1 - OP_16 | 0x51-0x60 | 推入數字 1-16 |
OP_1NEGATE | 0x4f | 推入 -1 |
OP_PUSHDATA1/2/4 | 0x4c-0x4e | 推入指定長度的數據 |
堆疊操作
| 操作碼 | 說明 | 範例 |
|---|---|---|
OP_DUP | 複製堆疊頂部元素 | [a] → [a, a] |
OP_DROP | 移除堆疊頂部元素 | [a, b] → [a] |
OP_SWAP | 交換頂部兩元素 | [a, b] → [b, a] |
OP_PICK | 複製第 n 個元素到頂部 | [a, b, c, 2] → [a, b, c, a] |
OP_ROLL | 移動第 n 個元素到頂部 | [a, b, c, 2] → [b, c, a] |
加密操作
| 操作碼 | 說明 |
|---|---|
OP_SHA256 | SHA-256 雜湊 |
OP_HASH160 | RIPEMD160(SHA256(x)) |
OP_HASH256 | SHA256(SHA256(x)) |
OP_CHECKSIG | 驗證簽名 |
OP_CHECKMULTISIG | 驗證多重簽名 |
流程控制
| 操作碼 | 說明 |
|---|---|
OP_IF / OP_NOTIF | 條件執行 |
OP_ELSE | 條件分支 |
OP_ENDIF | 結束條件 |
OP_VERIFY | 如果頂部不為 true 則失敗 |
OP_RETURN | 標記輸出為不可花費 |
標準腳本類型
P2PKH (Pay to Public Key Hash)
最傳統的比特幣地址格式(以 1 開頭)。
scriptPubKey: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
scriptSig: <signature> <pubKey>
驗證流程:
1. 推入 signature
2. 推入 pubKey
3. OP_DUP: 複製 pubKey
4. OP_HASH160: 對 pubKey 做 HASH160
5. 推入 pubKeyHash
6. OP_EQUALVERIFY: 確認雜湊匹配
7. OP_CHECKSIG: 驗證簽名 P2SH (Pay to Script Hash)
允許複雜腳本,地址以 3 開頭。花費時提供完整腳本。
scriptPubKey: OP_HASH160 <scriptHash> OP_EQUAL
scriptSig: <sig1> ... <sigN> <redeemScript>
範例 - 2-of-3 多簽:
redeemScript: OP_2 <pubKey1> <pubKey2> <pubKey3> OP_3 OP_CHECKMULTISIG P2WPKH (SegWit v0)
原生 SegWit 地址(以 bc1q 開頭),簽名移到 witness。
scriptPubKey: OP_0 <20-byte-pubKeyHash>
witness: <signature> <pubKey>
優點:
- 手續費更低(witness 數據折扣)
- 解決交易延展性問題
- 更簡潔的腳本 P2WSH (SegWit Script)
SegWit 版本的 P2SH,支援更複雜的腳本。
scriptPubKey: OP_0 <32-byte-scriptHash>
witness: <witness items> <witnessScript>
scriptHash = SHA256(witnessScript) P2TR (Taproot)
最新的地址格式(以 bc1p 開頭),支援 Schnorr 簽名和 MAST。
scriptPubKey: OP_1 <32-byte-tweaked-pubKey>
兩種花費路徑:
1. Key Path: 直接用調整後的私鑰簽名
2. Script Path: 揭露特定的腳本分支
優點:
- 隱私性更好(所有輸出看起來相同)
- 支援複雜條件但只揭露使用的分支
- Schnorr 簽名更高效 時間鎖
絕對時間鎖
OP_CHECKLOCKTIMEVERIFY (CLTV)
- 交易必須在指定時間/區塊後才能被包含
- 數值 < 500000000: 區塊高度
- 數值 >= 500000000: Unix 時間戳
範例: 2024年後才能花費
<1704067200> OP_CLTV OP_DROP <pubKey> OP_CHECKSIG 相對時間鎖
OP_CHECKSEQUENCEVERIFY (CSV)
- 相對於輸入 UTXO 被確認的時間
- 用於通道協議和時間條件合約
範例: 確認後 144 區塊(約 1 天)才能花費
<144> OP_CSV OP_DROP <pubKey> OP_CHECKSIG 多重簽名
M-of-N 多簽結構:
OP_M <pubKey1> <pubKey2> ... <pubKeyN> OP_N OP_CHECKMULTISIG
範例 - 2-of-3 多簽:
scriptPubKey:
OP_2
<pubKey1>
<pubKey2>
<pubKey3>
OP_3
OP_CHECKMULTISIG
scriptSig:
OP_0 // 修復 off-by-one bug
<sig1>
<sig2> 歷史 Bug: OP_CHECKMULTISIG 有一個 off-by-one bug,會從堆疊多彈出一個元素。 因此 scriptSig 開頭需要
OP_0。這個 bug 已成為共識規則的一部分。
HTLC (雜湊時間鎖合約)
閃電網路的核心構建塊:
OP_IF
// Hash Path: 知道 preimage 可以立即花費
OP_SHA256 <hash> OP_EQUALVERIFY
<receiverPubKey> OP_CHECKSIG
OP_ELSE
// Timeout Path: 超時後發送方可以取回
<timeout> OP_CLTV OP_DROP
<senderPubKey> OP_CHECKSIG
OP_ENDIF
使用方式:
- Hash Path: <signature> <preimage> OP_TRUE
- Timeout Path: <signature> OP_FALSE Tapscript (SegWit v1)
Taproot 引入的改進腳本:
主要變化
- Schnorr 簽名:使用
OP_CHECKSIG驗證 64 字節 Schnorr 簽名 - OP_CHECKSIGADD:替代
OP_CHECKMULTISIG,更靈活的多簽 - OP_SUCCESS:保留操作碼,用於未來升級
- 簽名雜湊改進:更安全的 sighash 計算
新的多簽方式
傳統 2-of-3:
OP_2 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG
Tapscript 2-of-3:
<pk1> OP_CHECKSIG
<pk2> OP_CHECKSIGADD
<pk3> OP_CHECKSIGADD
OP_2 OP_NUMEQUAL
優點: 可以驗證任意組合,不限於連續簽名 TypeScript 實作
解析腳本
const OP_CODES: Record<number, string> = {
0x00: 'OP_0',
0x51: 'OP_1',
0x52: 'OP_2',
0x53: 'OP_3',
0x76: 'OP_DUP',
0x87: 'OP_EQUAL',
0x88: 'OP_EQUALVERIFY',
0xa9: 'OP_HASH160',
0xac: 'OP_CHECKSIG',
0xae: 'OP_CHECKMULTISIG',
};
function parseScript(script: Buffer): string[] {
const result: string[] = [];
let i = 0;
while (i < script.length) {
const opcode = script[i];
if (opcode >= 0x01 && opcode <= 0x4b) {
// Push data directly
const data = script.slice(i + 1, i + 1 + opcode);
result.push(data.toString('hex'));
i += 1 + opcode;
} else if (OP_CODES[opcode]) {
result.push(OP_CODES[opcode]);
i++;
} else {
result.push(`OP_UNKNOWN_${opcode.toString(16)}`);
i++;
}
}
return result;
}
// 範例
const p2pkh = Buffer.from(
'76a914' + '89abcdefabbaabbaabbaabbaabbaabbaabbaabba' + '88ac',
'hex'
);
console.log(parseScript(p2pkh));
// ['OP_DUP', 'OP_HASH160', '89abcdef...', 'OP_EQUALVERIFY', 'OP_CHECKSIG'] 建立 P2PKH 腳本
import * as bitcoin from 'bitcoinjs-lib';
import { hash160 } from 'bitcoinjs-lib/src/crypto';
function createP2PKH(pubKey: Buffer): Buffer {
const pubKeyHash = hash160(pubKey);
return bitcoin.script.compile([
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
pubKeyHash,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_CHECKSIG,
]);
}
// 建立 P2SH 多簽
function createMultisigP2SH(
m: number,
pubKeys: Buffer[]
): { address: string; redeemScript: Buffer } {
const redeemScript = bitcoin.script.compile([
bitcoin.opcodes.OP_0 + m, // OP_M
...pubKeys,
bitcoin.opcodes.OP_0 + pubKeys.length, // OP_N
bitcoin.opcodes.OP_CHECKMULTISIG,
]);
const scriptHash = hash160(redeemScript);
const outputScript = bitcoin.script.compile([
bitcoin.opcodes.OP_HASH160,
scriptHash,
bitcoin.opcodes.OP_EQUAL,
]);
const address = bitcoin.address.fromOutputScript(
outputScript,
bitcoin.networks.bitcoin
);
return { address, redeemScript };
} 建立 HTLC
import * as crypto from 'crypto';
interface HTLCParams {
receiverPubKey: Buffer;
senderPubKey: Buffer;
paymentHash: Buffer;
timeout: number;
}
function createHTLC(params: HTLCParams): Buffer {
return bitcoin.script.compile([
bitcoin.opcodes.OP_IF,
bitcoin.opcodes.OP_SHA256,
params.paymentHash,
bitcoin.opcodes.OP_EQUALVERIFY,
params.receiverPubKey,
bitcoin.opcodes.OP_CHECKSIG,
bitcoin.opcodes.OP_ELSE,
bitcoin.script.number.encode(params.timeout),
bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
bitcoin.opcodes.OP_DROP,
params.senderPubKey,
bitcoin.opcodes.OP_CHECKSIG,
bitcoin.opcodes.OP_ENDIF,
]);
}
// 使用範例
const preimage = crypto.randomBytes(32);
const paymentHash = crypto.createHash('sha256').update(preimage).digest();
const htlc = createHTLC({
receiverPubKey: Buffer.from('02...', 'hex'),
senderPubKey: Buffer.from('03...', 'hex'),
paymentHash,
timeout: 800000, // 區塊高度
}); 安全考量
- 腳本大小限制:標準腳本最大 10,000 字節
- 操作數限制:最多 201 個操作
- 堆疊大小:每個元素最大 520 字節
- 簽名檢查限制:每個區塊最多 80,000 次
- 禁用操作碼:
OP_CAT、OP_MUL等已禁用
相關資源
已複製連結