跳至主要內容
高級

Miniscript

結構化的 Bitcoin Script 子集:可組合、可分析的智能合約語言

20 分鐘

概述

Miniscript 是一種結構化的 Bitcoin Script 表示方式, 它允許軟件自動分析腳本的花費條件、計算見證大小、 並安全地組合多個條件。由 Pieter Wuille、Andrew Poelstra 等人開發。

關鍵特性: Miniscript 讓錢包可以處理任意複雜的花費條件,而無需為每種條件編寫特定代碼。 Bitcoin Core 從 v26.0 開始原生支持 Miniscript。

設計動機

傳統 Bitcoin Script 的問題

問題 1: 難以分析
給定一個腳本,很難回答:
- 誰可以花費這筆錢?
- 需要哪些簽名?
- 見證數據有多大?

問題 2: 難以組合
想要「A AND (B OR C)」這樣的條件:
- 需要手工編寫每種組合
- 容易出錯
- 無法自動化

問題 3: 安全性不明確
- 腳本是否符合共識規則?
- 腳本是否符合標準規則?
- 是否存在可塑性問題?

Miniscript 解決方案:
- 定義嚴格的語法規則
- 保證可分析性
- 保證可組合性
- 保證安全性

架構層次

三層架構:

┌─────────────────────────────────────┐
│         Policy Language             │  人類可讀
│  and(pk(A), or(pk(B), after(100))) │
└───────────────┬─────────────────────┘
                │ 編譯
                ▼
┌─────────────────────────────────────┐
│           Miniscript                │  結構化表示
│  and_v(v:pk(A),or_d(pk(B),older... │
└───────────────┬─────────────────────┘
                │ 轉換
                ▼
┌─────────────────────────────────────┐
│          Bitcoin Script             │  實際執行
│  <A> OP_CHECKSIGVERIFY <B> OP_CHE.. │
└─────────────────────────────────────┘

Policy 語言

Policy 是人類可讀的花費條件描述:

基本構件:

pk(KEY)          - 需要 KEY 的簽名
after(N)         - 區塊高度達到 N
older(N)         - 相對時間鎖 N 個區塊
sha256(H)        - 需要雜湊原像
hash256(H)       - 需要雙 SHA256 原像
ripemd160(H)     - 需要 RIPEMD160 原像
hash160(H)       - 需要 HASH160 原像

組合操作:

and(X, Y)        - X 和 Y 都必須滿足
or(X, Y)         - X 或 Y 至少一個滿足
thresh(k, X, Y, Z...)  - 至少 k 個條件滿足

範例:

# 2-of-3 多簽
thresh(2, pk(A), pk(B), pk(C))

# A 立即可花費,或 B 在 100 區塊後可花費
or(pk(A), and(pk(B), after(100)))

# 閃電網路 HTLC
or(
  and(pk(A), sha256(H)),
  and(pk(B), older(144))
)

Miniscript 語法

類型系統

表達式類型 (B, V, K, W):

B (Base):
- 消耗 0 或多個堆疊元素
- 推入 0 或 1 (nonzero = success)
- 範例: pk, thresh

V (Verify):
- 消耗 0 或多個堆疊元素
- 不推入任何東西(失敗則中止腳本)
- 範例: v:pk (pk + VERIFY)

K (Key):
- 消耗 0 或多個堆疊元素
- 推入公鑰
- 範例: pk_k

W (Wrapped):
- 用於表示已經被包裝的表達式
- 保證堆疊不變

修飾符:
a:X   - ALT (OP_TOALTSTACK ... OP_FROMALTSTACK)
s:X   - SWAP (OP_SWAP)
c:X   - CHECKSIG (X 必須是 K)
d:X   - DUP IF
v:X   - VERIFY
j:X   - SIZE 0NOTEQUAL IF
n:X   - 0NOTEQUAL
l:X   - IF 0 ELSE X ENDIF
u:X   - IF X ELSE 0 ENDIF

基本表達式

pk_k(KEY)        → <KEY>                             [K]
pk_h(KEY)        → OP_DUP OP_HASH160 <H> OP_EQUALVERIFY [K]
pk(KEY)          → 等同於 c:pk_k(KEY)                   [B]

older(N)         → <N> OP_CHECKSEQUENCEVERIFY          [B]
after(N)         → <N> OP_CHECKLOCKTIMEVERIFY          [B]

sha256(H)        → OP_SIZE <32> OP_EQUALVERIFY
                   OP_SHA256 <H> OP_EQUAL              [B]

組合表達式:

and_v(X, Y)      → X Y                    (X 是 V, Y 是 B/K/V)
and_b(X, Y)      → X Y OP_BOOLAND         (X, Y 都是 B)
and_n(X, Y)      → X OP_NOTIF 0 OP_ELSE Y OP_ENDIF

or_b(X, Z)       → X Z OP_BOOLOR          (X, Z 都是 B)
or_c(X, Z)       → X OP_NOTIF Z OP_ENDIF  (X 是 B, Z 是 V)
or_d(X, Z)       → X OP_IFDUP OP_NOTIF Z OP_ENDIF
or_i(X, Z)       → OP_IF X OP_ELSE Z OP_ENDIF

thresh(k, X...)  → X1 X2 OP_ADD X3 OP_ADD ... <k> OP_EQUAL

multi(k, K1...)  → <k> <K1> ... <Kn> <n> OP_CHECKMULTISIG

編譯過程

從 Policy 到 Miniscript

// Policy 語法樹
type Policy =
  | { type: 'pk'; key: string }
  | { type: 'after'; blocks: number }
  | { type: 'older'; blocks: number }
  | { type: 'sha256'; hash: string }
  | { type: 'and'; left: Policy; right: Policy }
  | { type: 'or'; left: Policy; right: Policy; weights?: [number, number] }
  | { type: 'thresh'; k: number; subs: Policy[] };

// Miniscript 語法樹
type Miniscript =
  | { type: 'pk_k'; key: string }
  | { type: 'pk_h'; key: string }
  | { type: 'older'; n: number }
  | { type: 'after'; n: number }
  | { type: 'sha256'; hash: string }
  | { type: 'and_v'; left: Miniscript; right: Miniscript }
  | { type: 'and_b'; left: Miniscript; right: Miniscript }
  | { type: 'or_b'; left: Miniscript; right: Miniscript }
  | { type: 'or_d'; left: Miniscript; right: Miniscript }
  | { type: 'or_i'; left: Miniscript; right: Miniscript }
  | { type: 'thresh'; k: number; subs: Miniscript[] }
  | { type: 'multi'; k: number; keys: string[] }
  | { type: 'wrapper'; w: string; sub: Miniscript };

// 編譯器選擇最優結構
function compilePolicy(policy: Policy): Miniscript {
  switch (policy.type) {
    case 'pk':
      return { type: 'wrapper', w: 'c', sub: { type: 'pk_k', key: policy.key } };

    case 'and': {
      // 選擇 and_v 或 and_b 取決於子表達式類型
      const left = compilePolicy(policy.left);
      const right = compilePolicy(policy.right);
      return optimizeAnd(left, right);
    }

    case 'or': {
      // 根據概率權重選擇最優 or 結構
      const [wL, wR] = policy.weights || [1, 1];
      const left = compilePolicy(policy.left);
      const right = compilePolicy(policy.right);
      return optimizeOr(left, right, wL, wR);
    }

    case 'thresh': {
      const subs = policy.subs.map(compilePolicy);
      if (policy.k === policy.subs.length) {
        // 全部都要滿足 = 連續的 and
        return chainAnd(subs);
      }
      return { type: 'thresh', k: policy.k, subs };
    }

    // ... 其他情況
  }
}

function optimizeOr(
  left: Miniscript,
  right: Miniscript,
  wL: number,
  wR: number
): Miniscript {
  // or_d: 左分支更可能 (左分支放前面)
  // or_i: 需要選擇分支 (用 OP_IF)
  // or_b: 兩個都要執行然後 BOOLOR

  if (wL >= wR) {
    return { type: 'or_d', left, right };
  }
  return { type: 'or_d', left: right, right: left };
}

從 Miniscript 到 Script

function miniscriptToScript(ms: Miniscript): Uint8Array {
  const ops: number[] = [];

  function compile(node: Miniscript): void {
    switch (node.type) {
      case 'pk_k':
        ops.push(0x21); // push 33 bytes
        ops.push(...hexToBytes(node.key));
        break;

      case 'wrapper':
        switch (node.w) {
          case 'c': // CHECKSIG
            compile(node.sub);
            ops.push(0xac); // OP_CHECKSIG
            break;
          case 'v': // VERIFY
            compile(node.sub);
            ops.push(0x69); // OP_VERIFY
            break;
          case 'd': // DUP IF ... ENDIF
            ops.push(0x76); // OP_DUP
            ops.push(0x63); // OP_IF
            compile(node.sub);
            ops.push(0x68); // OP_ENDIF
            break;
          case 's': // SWAP
            ops.push(0x7c); // OP_SWAP
            compile(node.sub);
            break;
        }
        break;

      case 'and_v':
        compile(node.left);
        compile(node.right);
        break;

      case 'and_b':
        compile(node.left);
        compile(node.right);
        ops.push(0x9a); // OP_BOOLAND
        break;

      case 'or_d':
        compile(node.left);
        ops.push(0x73); // OP_IFDUP
        ops.push(0x64); // OP_NOTIF
        compile(node.right);
        ops.push(0x68); // OP_ENDIF
        break;

      case 'or_i':
        ops.push(0x63); // OP_IF
        compile(node.left);
        ops.push(0x67); // OP_ELSE
        compile(node.right);
        ops.push(0x68); // OP_ENDIF
        break;

      case 'older':
        ops.push(...encodeNumber(node.n));
        ops.push(0xb2); // OP_CHECKSEQUENCEVERIFY
        break;

      case 'after':
        ops.push(...encodeNumber(node.n));
        ops.push(0xb1); // OP_CHECKLOCKTIMEVERIFY
        break;

      case 'multi':
        ops.push(...encodeNumber(node.k));
        for (const key of node.keys) {
          ops.push(0x21);
          ops.push(...hexToBytes(key));
        }
        ops.push(...encodeNumber(node.keys.length));
        ops.push(0xae); // OP_CHECKMULTISIG
        break;
    }
  }

  compile(ms);
  return new Uint8Array(ops);
}

腳本分析

花費條件分析

interface SpendingCondition {
  keys: string[];         // 需要的簽名
  timelocks: number[];    // 時間鎖條件
  hashlocks: string[];    // 雜湊鎖條件
}

function analyzeSpendingPaths(
  ms: Miniscript
): SpendingCondition[] {
  const paths: SpendingCondition[] = [];

  function analyze(
    node: Miniscript,
    current: SpendingCondition
  ): void {
    switch (node.type) {
      case 'pk_k':
        current.keys.push(node.key);
        break;

      case 'wrapper':
        analyze(node.sub, current);
        break;

      case 'and_v':
      case 'and_b':
        analyze(node.left, current);
        analyze(node.right, current);
        break;

      case 'or_d':
      case 'or_i':
      case 'or_b':
        // 分叉:兩條路徑
        const pathL = { ...current, keys: [...current.keys] };
        const pathR = { ...current, keys: [...current.keys] };
        analyze(node.left, pathL);
        analyze(node.right, pathR);
        paths.push(pathL);
        paths.push(pathR);
        return;

      case 'older':
      case 'after':
        current.timelocks.push(node.n);
        break;

      case 'sha256':
        current.hashlocks.push(node.hash);
        break;

      case 'multi':
        // k-of-n:只需要 k 個簽名
        // 這裡簡化處理
        current.keys.push(...node.keys.slice(0, node.k));
        break;
    }

    paths.push(current);
  }

  analyze(ms, { keys: [], timelocks: [], hashlocks: [] });
  return paths;
}

見證大小計算

interface WitnessSize {
  max: number;      // 最大見證大小
  min: number;      // 最小見證大小
  expected: number; // 預期見證大小(基於概率)
}

function calculateWitnessSize(ms: Miniscript): WitnessSize {
  switch (ms.type) {
    case 'pk_k':
      // 簽名 = 72 bytes (DER) 或 64 bytes (Schnorr)
      return { max: 73, min: 71, expected: 72 };

    case 'wrapper':
      const sub = calculateWitnessSize(ms.sub);
      switch (ms.w) {
        case 'c':
          return sub; // CHECKSIG 不改變見證大小
        case 's':
          return sub; // SWAP 也不改變
        case 'd':
          // DUP IF:成功時不需額外,失敗時需要 0
          return {
            max: sub.max,
            min: 1, // 空推入
            expected: sub.expected,
          };
      }
      return sub;

    case 'and_v':
    case 'and_b':
      const left = calculateWitnessSize(ms.left);
      const right = calculateWitnessSize(ms.right);
      return {
        max: left.max + right.max,
        min: left.min + right.min,
        expected: left.expected + right.expected,
      };

    case 'or_d':
    case 'or_i': {
      const l = calculateWitnessSize(ms.left);
      const r = calculateWitnessSize(ms.right);
      // or_i 需要額外的選擇位
      const extra = ms.type === 'or_i' ? 1 : 0;
      return {
        max: Math.max(l.max, r.max) + extra,
        min: Math.min(l.min, r.min) + extra,
        expected: (l.expected + r.expected) / 2 + extra,
      };
    }

    case 'multi':
      const sigSize = 72;
      return {
        max: ms.k * sigSize + 1, // +1 for dummy (bug compatibility)
        min: ms.k * sigSize + 1,
        expected: ms.k * sigSize + 1,
      };

    case 'sha256':
      return { max: 32, min: 32, expected: 32 }; // preimage

    default:
      return { max: 0, min: 0, expected: 0 };
  }
}

與 Descriptors 集成

Miniscript 描述符格式:

# P2WSH Miniscript
wsh(and_v(v:pk(A),or_d(pk(B),older(100))))

# P2SH-P2WSH Miniscript
sh(wsh(multi(2,A,B,C)))

# Taproot Script Path
tr(INTERNAL_KEY,{and_v(v:pk(A),older(100)),pk(B)})

Bitcoin Core 命令:

# 從描述符獲取地址
bitcoin-cli deriveaddresses "wsh(and_v(v:pk([...]),pk([...])))"

# 導入 Miniscript 錢包
bitcoin-cli importdescriptors '[{
  "desc": "wsh(and_v(v:pk(...),or_d(pk(...),older(100))))#checksum",
  "timestamp": "now"
}]'

# 分析描述符
bitcoin-cli getdescriptorinfo "wsh(multi(2,A,B,C))"

Taproot Miniscript

Tapscript 變體:

主要區別:
- 使用 OP_CHECKSIGADD 替代 OP_CHECKMULTISIG
- 簽名是 Schnorr 而非 ECDSA
- 移除 201 操作碼限制

multi_a(k, K1, K2, ..., Kn):
→ K1 OP_CHECKSIG K2 OP_CHECKSIGADD ... <k> OP_NUMEQUAL

範例:

# Taproot 2-of-3
tr(INTERNAL,{multi_a(2,A,B,C)})

# 帶時間鎖的恢復路徑
tr(MAIN,{
  pk(MAIN),
  {and_v(v:pk(BACKUP),older(52560)),pk(HEIR)}
})

實際範例

保險庫合約

// 保險庫:熱錢包可以發起,但需要等待或冷錢包批准
const vaultPolicy = `
or(
  99@and(pk(HOT_KEY), older(144)),
  1@pk(COLD_KEY)
)
`;

// 編譯結果
const vaultMiniscript = `
or_d(
  and_v(v:pk(HOT_KEY), older(144)),
  pk(COLD_KEY)
)
`;

// 花費路徑分析:
// 1. HOT_KEY 簽名 + 等待 144 區塊 (約 1 天)
// 2. COLD_KEY 立即簽名 (緊急撤銷)

遺產規劃

// 遺產:本人控制,或繼承人+律師,或繼承人等待2年
const inheritancePolicy = `
or(
  pk(OWNER),
  or(
    and(pk(HEIR), pk(LAWYER)),
    and(pk(HEIR), after(105120))
  )
)
`;

// 105120 blocks ≈ 2 years

function createInheritanceDescriptor(
  owner: string,
  heir: string,
  lawyer: string
): string {
  return `wsh(or_d(
    c:pk_k(${owner}),
    or_i(
      and_v(v:pk(${heir}),c:pk_k(${lawyer})),
      and_v(v:pk(${heir}),after(105120))
    )
  ))`;
}

多簽 + 時間鎖降級

// 開始需要 3-of-4,隨時間降級到 2-of-4,最終 1-of-4
const degradingMultisig = `
or(
  99@thresh(3, pk(A), pk(B), pk(C), pk(D)),
  or(
    9@and(thresh(2, pk(A), pk(B), pk(C), pk(D)), older(4320)),
    1@and(thresh(1, pk(A), pk(B), pk(C), pk(D)), older(8640))
  )
)
`;

// 4320 blocks ≈ 1 month
// 8640 blocks ≈ 2 months

// 這解決了密鑰丟失問題:
// - 正常情況:需要 3 個簽名
// - 1 個月後:只需 2 個簽名
// - 2 個月後:只需 1 個簽名

工具與資源

# 在線編譯器
https://bitcoin.sipa.be/miniscript/

# Rust 實現
cargo add miniscript

# JavaScript 實現
npm install @aspect/miniscript

# Bitcoin Core (v26.0+)
bitcoin-cli getdescriptorinfo "wsh(and_v(v:pk(...),pk(...)))"

最佳實踐

  • 使用概率權重:為 or 分支設置權重以優化常見路徑
  • 驗證腳本大小:確保腳本不超過共識限制
  • 測試所有路徑:驗證每條花費路徑都能正確執行
  • 使用描述符:Miniscript 與 Descriptors 結合使用
  • 考慮 Taproot:複雜條件考慮使用 Taproot Script Path

相關資源

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