進階
Timelock
時間鎖機制:CLTV 和 CSV 實現基於時間的花費條件
15 分鐘
概述
時間鎖(Timelock)是比特幣的重要功能,允許創建只能在特定時間後才能花費的交易。 這是閃電網路、原子交換和許多智能合約的基礎構建塊。
兩種類型: 絕對時間鎖(CLTV)使用區塊高度或 Unix 時間戳, 相對時間鎖(CSV)使用相對於輸入確認後的區塊數。
時間鎖類型
概覽比較
時間鎖分類:
| 交易級別 | 腳本級別 |
|---|---|
nLockTime(絕對時間) | OP_CHECKLOCKTIMEVERIFY(CLTV, 絕對時間) |
nSequence(相對時間) | OP_CHECKSEQUENCEVERIFY(CSV, 相對時間) |
時間單位:
- 區塊高度: 值 < 500,000,000
- Unix 時間戳: 值 >= 500,000,000
nLockTime (交易級別)
基本概念
nLockTime 欄位:
- 位置: 交易結構的最後 4 字節
- 作用: 指定交易最早可被打包的時間
規則:
- nLockTime = 0: 立即有效
- nLockTime < 500,000,000: 區塊高度
- nLockTime >= 500,000,000: Unix 時間戳
限制:
- 只有當所有輸入的 nSequence < 0xFFFFFFFF 時才生效
- 如果所有 nSequence = 0xFFFFFFFF,nLockTime 被忽略 使用範例
interface Transaction {
version: number;
inputs: TxInput[];
outputs: TxOutput[];
locktime: number; // nLockTime
}
// 區塊高度鎖定
function createHeightLockedTx(targetHeight: number): Transaction {
return {
version: 2,
inputs: [{
txid: '...',
vout: 0,
scriptSig: Buffer.alloc(0),
sequence: 0xFFFFFFFE, // 必須小於 0xFFFFFFFF
}],
outputs: [{
value: 100000n,
scriptPubKey: Buffer.from('...'),
}],
locktime: targetHeight, // 例如 800000
};
}
// Unix 時間戳鎖定
function createTimeLockedTx(timestamp: number): Transaction {
// timestamp 必須 >= 500000000
return {
version: 2,
inputs: [{
txid: '...',
vout: 0,
scriptSig: Buffer.alloc(0),
sequence: 0xFFFFFFFE,
}],
outputs: [{
value: 100000n,
scriptPubKey: Buffer.from('...'),
}],
locktime: timestamp, // 例如 1704067200 (2024-01-01)
};
} OP_CHECKLOCKTIMEVERIFY (CLTV)
BIP-65 介紹
CLTV (BIP-65):
- 操作碼: 0xB1
- 啟用區塊: 388,381 (2015年12月)
- 作用: 腳本級別的絕對時間鎖
執行邏輯:
1. 從堆疊頂部讀取鎖定時間
2. 與交易的 nLockTime 比較
3. 如果 nLockTime < 腳本中的值,交易無效
條件:
- 堆疊頂部值 < 0: 失敗
- 堆疊頂部值類型與 nLockTime 不同: 失敗
- nLockTime < 堆疊頂部值: 失敗
- nSequence = 0xFFFFFFFF: 失敗 腳本範例
基本 CLTV 腳本:
<locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP <pubkey> OP_CHECKSIG
執行流程:
1. <locktime>: 推入鎖定時間
2. OP_CLTV: 驗證 nLockTime >= locktime
3. OP_DROP: 移除堆疊上的 locktime
4. <pubkey>: 推入公鑰
5. OP_CHECKSIG: 驗證簽名
花費條件:
- 必須提供有效簽名
- 交易的 nLockTime 必須 >= 腳本中的 locktime
- 交易的 nSequence 必須 < 0xFFFFFFFF TypeScript 實作
// 創建 CLTV 鎖定腳本
function createCLTVScript(
locktime: number,
pubkey: Uint8Array
): Uint8Array {
const script: number[] = [];
// 編碼 locktime
script.push(...encodeLocktime(locktime));
// OP_CHECKLOCKTIMEVERIFY
script.push(0xb1);
// OP_DROP
script.push(0x75);
// 公鑰
script.push(0x21); // push 33 bytes
script.push(...pubkey);
// OP_CHECKSIG
script.push(0xac);
return new Uint8Array(script);
}
function encodeLocktime(value: number): number[] {
if (value === 0) return [0x00];
if (value >= 1 && value <= 16) return [0x50 + value];
// 編碼為最小字節數
const bytes: number[] = [];
let n = value;
while (n > 0) {
bytes.push(n & 0xff);
n >>= 8;
}
// 處理負數標誌位
if (bytes[bytes.length - 1] & 0x80) {
bytes.push(0x00);
}
return [bytes.length, ...bytes];
}
// 創建花費 CLTV 輸出的交易
function spendCLTVOutput(
utxo: UTXO,
locktime: number,
destination: Uint8Array,
signature: Uint8Array
): Transaction {
return {
version: 2,
inputs: [{
txid: utxo.txid,
vout: utxo.vout,
scriptSig: Buffer.from([
signature.length, ...signature,
]),
sequence: 0xFFFFFFFE, // 啟用 nLockTime
}],
outputs: [{
value: utxo.value - 1000n, // 扣除手續費
scriptPubKey: destination,
}],
locktime: locktime, // 必須 >= 腳本中的 locktime
};
} OP_CHECKSEQUENCEVERIFY (CSV)
BIP-112 介紹
CSV (BIP-112):
- 操作碼: 0xB2
- 啟用區塊: 419,328 (2016年7月)
- 作用: 腳本級別的相對時間鎖
nSequence 欄位解析 (BIP-68):
┌────────────────────────────────────────────────────────┐
│ bit 31 │ bit 22 │ bits 21-16 │ bits 15-0 │
├────────┼────────┼────────────┼────────────────────────┤
│ 禁用位 │ 類型位 │ 保留 │ 時間值 │
│ 1=禁用 │ 0=區塊 │ │ 區塊數或512秒單位 │
│ │ 1=時間 │ │ │
└────────────────────────────────────────────────────────┘
時間計算:
- 區塊模式: 值 = 區塊數 (最大 65535)
- 時間模式: 值 × 512 秒 (最大約 1 年) nSequence 編碼
// CSV 常量
const SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31; // 0x80000000
const SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22; // 0x00400000
const SEQUENCE_LOCKTIME_MASK = 0x0000FFFF;
// 創建區塊相對時間鎖
function blocksToSequence(blocks: number): number {
if (blocks < 0 || blocks > 0xFFFF) {
throw new Error('Blocks must be 0-65535');
}
return blocks;
}
// 創建時間相對時間鎖 (512秒為單位)
function secondsToSequence(seconds: number): number {
const units = Math.ceil(seconds / 512);
if (units < 0 || units > 0xFFFF) {
throw new Error('Time must be 0-33553920 seconds');
}
return SEQUENCE_LOCKTIME_TYPE_FLAG | units;
}
// 解析 nSequence
function parseSequence(sequence: number): {
disabled: boolean;
isTime: boolean;
value: number;
} {
const disabled = (sequence & SEQUENCE_LOCKTIME_DISABLE_FLAG) !== 0;
const isTime = (sequence & SEQUENCE_LOCKTIME_TYPE_FLAG) !== 0;
const value = sequence & SEQUENCE_LOCKTIME_MASK;
return { disabled, isTime, value };
} CSV 腳本範例
基本 CSV 腳本:
<sequence> OP_CHECKSEQUENCEVERIFY OP_DROP <pubkey> OP_CHECKSIG
HTLC 風格腳本 (閃電網路):
OP_IF
# 成功路徑: hashlock + 接收方簽名
OP_HASH160 <payment_hash> OP_EQUALVERIFY
<receiver_pubkey> OP_CHECKSIG
OP_ELSE
# 超時路徑: timelock + 發送方簽名
<timeout_sequence> OP_CHECKSEQUENCEVERIFY OP_DROP
<sender_pubkey> OP_CHECKSIG
OP_ENDIF TypeScript 實作
// 創建 CSV 鎖定腳本
function createCSVScript(
sequence: number,
pubkey: Uint8Array
): Uint8Array {
const script: number[] = [];
// 編碼 sequence
script.push(...encodeSequence(sequence));
// OP_CHECKSEQUENCEVERIFY
script.push(0xb2);
// OP_DROP
script.push(0x75);
// 公鑰
script.push(0x21);
script.push(...pubkey);
// OP_CHECKSIG
script.push(0xac);
return new Uint8Array(script);
}
function encodeSequence(value: number): number[] {
// 與 locktime 編碼相同
return encodeLocktime(value & 0x00FFFFFF);
}
// 創建 HTLC 腳本
function createHTLCScript(
paymentHash: Uint8Array,
receiverPubkey: Uint8Array,
senderPubkey: Uint8Array,
timeoutBlocks: number
): Uint8Array {
const script: number[] = [];
// OP_IF
script.push(0x63);
// 成功路徑: hashlock
script.push(0xa9); // OP_HASH160
script.push(0x14); // push 20 bytes
script.push(...paymentHash);
script.push(0x88); // OP_EQUALVERIFY
script.push(0x21);
script.push(...receiverPubkey);
script.push(0xac); // OP_CHECKSIG
// OP_ELSE
script.push(0x67);
// 超時路徑: timelock
script.push(...encodeSequence(timeoutBlocks));
script.push(0xb2); // OP_CSV
script.push(0x75); // OP_DROP
script.push(0x21);
script.push(...senderPubkey);
script.push(0xac); // OP_CHECKSIG
// OP_ENDIF
script.push(0x68);
return new Uint8Array(script);
} 應用場景
閃電網路承諾交易
承諾交易結構:
本地輸出 (to_local):
OP_IF
# 撤銷路徑 (對方持有撤銷密鑰)
<revocation_pubkey>
OP_ELSE
# 延遲路徑 (自己的資金)
<to_self_delay> OP_CSV OP_DROP
<local_delayed_pubkey>
OP_ENDIF
OP_CHECKSIG
遠端輸出 (to_remote):
# 立即可用 (無延遲)
<remote_pubkey> OP_CHECKSIG
HTLC 輸出:
# 更複雜的 CSV + hashlock 組合 保險庫 (Vault)
// 簡單保險庫: 熱錢包可發起,但需等待或冷錢包撤銷
function createVaultScript(
hotKey: Uint8Array,
coldKey: Uint8Array,
delayBlocks: number
): Uint8Array {
const script: number[] = [];
// OP_IF - 冷錢包立即撤銷
script.push(0x63);
script.push(0x21);
script.push(...coldKey);
script.push(0xac);
// OP_ELSE - 熱錢包延遲提取
script.push(0x67);
script.push(...encodeSequence(delayBlocks));
script.push(0xb2); // OP_CSV
script.push(0x75); // OP_DROP
script.push(0x21);
script.push(...hotKey);
script.push(0xac);
// OP_ENDIF
script.push(0x68);
return new Uint8Array(script);
}
// 使用範例: 144 區塊 (~1天) 延遲
const vault = createVaultScript(hotPubkey, coldPubkey, 144); 遺產規劃
// 遺產合約: 本人隨時可用,繼承人需等待
function createInheritanceScript(
ownerKey: Uint8Array,
heirKey: Uint8Array,
waitBlocks: number // 例如 52560 (~1年)
): Uint8Array {
const script: number[] = [];
// OP_IF - 所有者路徑
script.push(0x63);
script.push(0x21);
script.push(...ownerKey);
script.push(0xac);
// OP_ELSE - 繼承人路徑 (需等待)
script.push(0x67);
script.push(...encodeSequence(waitBlocks));
script.push(0xb2); // OP_CSV
script.push(0x75); // OP_DROP
script.push(0x21);
script.push(...heirKey);
script.push(0xac);
// OP_ENDIF
script.push(0x68);
return new Uint8Array(script);
} CLTV vs CSV 比較
| 特性 | CLTV | CSV |
|---|---|---|
| 時間類型 | 絕對時間 | 相對時間 |
| 參考點 | 區塊高度/Unix 時間 | 輸入確認後 |
| 最大延遲 | 無限制 | ~1年 (65535 區塊) |
| BIP | BIP-65 | BIP-68, BIP-112 |
| 操作碼 | 0xB1 | 0xB2 |
| 用途 | 固定到期日 | 通道協議 |
Median Time Past (MTP)
時間戳驗證:
比特幣使用 Median Time Past 而非當前時間:
- 取最近 11 個區塊的時間戳
- 使用中位數 (第 6 個)
- 防止礦工操縱時間
MTP 特性:
- 永不倒退
- 比真實時間慢約 1 小時
- 提供穩定的時間參考
範例:
區塊時間戳: [100, 102, 101, 103, 105, 104, 106, 108, 107, 109, 110]
排序後: [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110]
MTP = 105 (第 6 個) 最佳實踐
- 區塊高度優於時間戳:區塊高度更可預測,時間戳可能有約 2 小時的不確定性
- 考慮 MTP 延遲:使用時間戳時,考慮 MTP 比真實時間慢
- CSV 用於通道:閃電網路等協議應使用 CSV 而非 CLTV
- 合理的延遲值:太短無法提供安全性,太長影響用戶體驗
- 測試在 regtest:可以快速生成區塊測試時間鎖邏輯
相關資源
已複製連結