跳至主要內容
進階

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:可以快速生成區塊測試時間鎖邏輯

相關資源

已複製連結
已複製到剪貼簿