跳至主要內容
進階

Multisig

多重簽名:m-of-n 閾值簽名的實作與應用

18 分鐘

概述

多重簽名(Multisig)允許創建需要多個私鑰授權才能花費的比特幣地址。 常見的配置如 2-of-3(三把鑰匙中的任意兩把)提供了安全性和容錯能力的平衡。 多簽是企業託管、共同帳戶和進階安全方案的基礎。

實作方式: 傳統多簽使用 P2SH 或 P2WSH 包裝 OP_CHECKMULTISIG。 Taproot 引入了基於 Schnorr 的 MuSig2,可實現更高效的多簽。

腳本類型

Bare Multisig (不推薦)

Bare Multisig (P2MS):
直接在 scriptPubKey 中放置多簽腳本

scriptPubKey:
OP_2 <PubKey1> <PubKey2> <PubKey3> OP_3 OP_CHECKMULTISIG

scriptSig:
OP_0 <Sig1> <Sig2>

問題:
- 公鑰直接暴露在輸出中
- 浪費區塊空間
- 不標準,大多數節點不轉發

限制:
- 最多 3 個公鑰 (標準規則)
- 共識規則允許最多 20 個公鑰

P2SH Multisig

P2SH Wrapped Multisig:

redeemScript (2-of-3):
OP_2 <PubKey1> <PubKey2> <PubKey3> OP_3 OP_CHECKMULTISIG

scriptPubKey (發送地址):
OP_HASH160 <HASH160(redeemScript)> OP_EQUAL

scriptSig (花費時):
OP_0 <Sig1> <Sig2> <redeemScript>

地址格式: 3xxx... (以 3 開頭)

優點:
- 發送方不需知道多簽細節
- 公鑰在花費前隱藏
- 標準交易格式

缺點:
- 花費時 redeemScript 暴露
- 手續費較高 (包含完整腳本)

P2WSH Multisig

Native SegWit Multisig (推薦):

witnessScript (2-of-3):
OP_2 <PubKey1> <PubKey2> <PubKey3> OP_3 OP_CHECKMULTISIG

scriptPubKey:
OP_0 <SHA256(witnessScript)>

witness:
OP_0 <Sig1> <Sig2> <witnessScript>

地址格式: bc1q... (較長的 bech32)

優點:
- SegWit 折扣 (見證數據 1/4 權重)
- 更低手續費
- 無簽名可塑性

範例地址:
bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3

P2TR Multisig (MuSig2)

Taproot 多簽 (最佳方案):

方法 1: Key Path (MuSig2)
- 聚合所有公鑰為單一公鑰
- 產生單一 64 byte 簽名
- 外部無法區分單簽/多簽

方法 2: Script Path
- 使用 Tapscript 中的 OP_CHECKSIGADD
- 支援任意 m-of-n 配置
- 僅在需要時暴露使用的分支

優點:
- 最高隱私 (key path)
- 最小交易大小
- 靈活的備用腳本

地址格式: bc1p...

TypeScript 實作

P2WSH 多簽地址

import { bech32 } from 'bech32';
import * as crypto from 'crypto';

interface MultisigConfig {
  m: number;           // 所需簽名數
  n: number;           // 公鑰總數
  pubkeys: Uint8Array[]; // 壓縮公鑰 (33 bytes each)
}

function createWitnessScript(config: MultisigConfig): Uint8Array {
  const { m, n, pubkeys } = config;

  if (pubkeys.length !== n) {
    throw new Error('公鑰數量不匹配');
  }
  if (m > n || m < 1 || n > 20) {
    throw new Error('無效的 m-of-n 配置');
  }

  // 排序公鑰 (BIP-67 標準)
  const sortedPubkeys = [...pubkeys].sort((a, b) => {
    for (let i = 0; i < 33; i++) {
      if (a[i] !== b[i]) return a[i] - b[i];
    }
    return 0;
  });

  // 構建 witnessScript
  const parts: number[] = [];

  // OP_m
  parts.push(0x50 + m); // OP_1 = 0x51, OP_2 = 0x52, etc.

  // 公鑰
  for (const pubkey of sortedPubkeys) {
    parts.push(33); // push 33 bytes
    parts.push(...pubkey);
  }

  // OP_n
  parts.push(0x50 + n);

  // OP_CHECKMULTISIG
  parts.push(0xae);

  return new Uint8Array(parts);
}

function createP2WSHAddress(
  witnessScript: Uint8Array,
  network: 'mainnet' | 'testnet' = 'mainnet'
): string {
  // SHA256 of witnessScript
  const scriptHash = crypto.createHash('sha256')
    .update(witnessScript)
    .digest();

  const prefix = network === 'mainnet' ? 'bc' : 'tb';
  const version = 0;

  const words = bech32.toWords(scriptHash);
  words.unshift(version);

  return bech32.encode(prefix, words);
}

// 使用範例
const pubkeys = [
  hexToBytes('02...'), // Alice's pubkey
  hexToBytes('03...'), // Bob's pubkey
  hexToBytes('02...'), // Carol's pubkey
];

const witnessScript = createWitnessScript({
  m: 2,
  n: 3,
  pubkeys
});

const address = createP2WSHAddress(witnessScript);
console.log('2-of-3 地址:', address);

創建多簽交易

interface MultisigInput {
  txid: string;
  vout: number;
  value: bigint;
  witnessScript: Uint8Array;
  sequence?: number;
}

interface Output {
  address: string;
  value: bigint;
}

function createMultisigTx(
  inputs: MultisigInput[],
  outputs: Output[],
  locktime: number = 0
): Uint8Array {
  const parts: Uint8Array[] = [];

  // Version (2 for SegWit)
  parts.push(new Uint8Array([0x02, 0x00, 0x00, 0x00]));

  // Marker and flag
  parts.push(new Uint8Array([0x00, 0x01]));

  // Input count
  parts.push(encodeVarInt(inputs.length));

  // Inputs
  for (const input of inputs) {
    // Previous output
    parts.push(hexToBytes(input.txid).reverse());
    parts.push(uint32LE(input.vout));
    // Empty scriptSig for SegWit
    parts.push(new Uint8Array([0x00]));
    // Sequence
    parts.push(uint32LE(input.sequence ?? 0xffffffff));
  }

  // Output count
  parts.push(encodeVarInt(outputs.length));

  // Outputs
  for (const output of outputs) {
    parts.push(uint64LE(output.value));
    const scriptPubKey = addressToScriptPubKey(output.address);
    parts.push(encodeVarInt(scriptPubKey.length));
    parts.push(scriptPubKey);
  }

  // Witness (placeholder - will be filled with signatures)
  for (const input of inputs) {
    // Witness item count (m signatures + 1 empty + 1 witnessScript)
    const m = input.witnessScript[0] - 0x50;
    parts.push(encodeVarInt(m + 2));

    // OP_0 (dummy for CHECKMULTISIG bug)
    parts.push(new Uint8Array([0x00]));

    // Placeholder for signatures
    for (let i = 0; i < m; i++) {
      parts.push(new Uint8Array([0x00])); // empty sig placeholder
    }

    // WitnessScript
    parts.push(encodeVarInt(input.witnessScript.length));
    parts.push(input.witnessScript);
  }

  // Locktime
  parts.push(uint32LE(locktime));

  return concatBytes(parts);
}

多簽簽名流程

// BIP-143 SegWit 簽名雜湊
function signatureHashSegwit(
  tx: Uint8Array,
  inputIndex: number,
  witnessScript: Uint8Array,
  value: bigint,
  sigHashType: number = 0x01 // SIGHASH_ALL
): Uint8Array {
  // ... BIP-143 實作 (參見 sighash.astro)
}

// 創建單個簽名
function signMultisigInput(
  tx: Uint8Array,
  inputIndex: number,
  input: MultisigInput,
  privateKey: Uint8Array,
  sigHashType: number = 0x01
): Uint8Array {
  // 計算簽名雜湊
  const sigHash = signatureHashSegwit(
    tx,
    inputIndex,
    input.witnessScript,
    input.value,
    sigHashType
  );

  // ECDSA 簽名
  const signature = secp256k1.sign(sigHash, privateKey);

  // DER 編碼 + sigHashType
  return concatBytes([
    signature.toDERRawBytes(),
    new Uint8Array([sigHashType])
  ]);
}

// 組合多個簽名
function combineSignatures(
  tx: Uint8Array,
  inputIndex: number,
  input: MultisigInput,
  signatures: Uint8Array[]
): Uint8Array {
  // 解析交易並找到 witness 位置
  const { m, pubkeys } = parseWitnessScript(input.witnessScript);

  if (signatures.length < m) {
    throw new Error(`需要 ${m} 個簽名,只有 ${signatures.length} 個`);
  }

  // 按公鑰順序排列簽名
  const orderedSigs = orderSignaturesByPubkey(
    signatures,
    pubkeys,
    tx,
    inputIndex,
    input
  );

  // 構建 witness
  const witnessItems: Uint8Array[] = [];

  // OP_0 (CHECKMULTISIG bug workaround)
  witnessItems.push(new Uint8Array([]));

  // m 個簽名
  for (let i = 0; i < m; i++) {
    witnessItems.push(orderedSigs[i]);
  }

  // witnessScript
  witnessItems.push(input.witnessScript);

  return serializeWitness(witnessItems);
}

// 解析 witnessScript
function parseWitnessScript(script: Uint8Array): {
  m: number;
  n: number;
  pubkeys: Uint8Array[];
} {
  const m = script[0] - 0x50;
  const pubkeys: Uint8Array[] = [];

  let offset = 1;
  while (script[offset] === 33) {
    offset++;
    pubkeys.push(script.slice(offset, offset + 33));
    offset += 33;
  }

  const n = script[offset] - 0x50;

  return { m, n, pubkeys };
}

PSBT 多簽流程

PSBT (BIP-174) 多簽工作流程:

1. 協調者創建 PSBT
   ┌─────────────────────────────────────┐
   │ Coordinator                          │
   │ - 創建未簽名交易                      │
   │ - 包含所有 UTXO 信息                  │
   │ - 包含 witnessScript                 │
   └─────────────────────────────────────┘
                  │
                  ▼ PSBT (base64)

2. 分發給簽名者
   ┌──────┐    ┌──────┐    ┌──────┐
   │Signer│    │Signer│    │Signer│
   │  A   │    │  B   │    │  C   │
   └──────┘    └──────┘    └──────┘
       │           │           │
       ▼           ▼           ▼
   簽名 A       簽名 B       簽名 C

3. 收集簽名
   ┌─────────────────────────────────────┐
   │ Combiner                             │
   │ - 合併所有部分簽名的 PSBT             │
   │ - 驗證簽名有效性                      │
   └─────────────────────────────────────┘
                  │
                  ▼

4. 最終化並廣播
   ┌─────────────────────────────────────┐
   │ Finalizer                            │
   │ - 構建最終 witness                    │
   │ - 驗證交易有效                        │
   │ - 廣播到網路                          │
   └─────────────────────────────────────┘

PSBT 多簽實作

interface PSBTInput {
  previousTxid: string;
  previousVout: number;
  witnessUtxo?: {
    value: bigint;
    scriptPubKey: Uint8Array;
  };
  witnessScript?: Uint8Array;
  partialSignatures?: Map<Uint8Array, Uint8Array>;
  sigHashType?: number;
}

class MultisigPSBT {
  private inputs: PSBTInput[] = [];
  private outputs: Output[] = [];

  // 簽名者添加簽名
  sign(inputIndex: number, privateKey: Uint8Array): void {
    const input = this.inputs[inputIndex];
    if (!input.witnessScript || !input.witnessUtxo) {
      throw new Error('Missing witnessScript or witnessUtxo');
    }

    // 計算簽名雜湊
    const sigHash = this.computeSigHash(inputIndex);

    // 創建簽名
    const pubkey = secp256k1.getPublicKey(privateKey, true);
    const signature = secp256k1.sign(sigHash, privateKey);
    const sigWithType = concatBytes([
      signature.toDERRawBytes(),
      new Uint8Array([input.sigHashType ?? 0x01])
    ]);

    // 添加部分簽名
    if (!input.partialSignatures) {
      input.partialSignatures = new Map();
    }
    input.partialSignatures.set(pubkey, sigWithType);
  }

  // 合併多個 PSBT
  static combine(psbts: MultisigPSBT[]): MultisigPSBT {
    const result = new MultisigPSBT();
    result.inputs = psbts[0].inputs.map(input => ({ ...input }));
    result.outputs = [...psbts[0].outputs];

    // 合併所有簽名
    for (let i = 1; i < psbts.length; i++) {
      for (let j = 0; j < result.inputs.length; j++) {
        const otherSigs = psbts[i].inputs[j].partialSignatures;
        if (otherSigs) {
          if (!result.inputs[j].partialSignatures) {
            result.inputs[j].partialSignatures = new Map();
          }
          for (const [pubkey, sig] of otherSigs) {
            result.inputs[j].partialSignatures!.set(pubkey, sig);
          }
        }
      }
    }

    return result;
  }

  // 最終化 PSBT
  finalize(): Uint8Array {
    for (const input of this.inputs) {
      const { m, pubkeys } = parseWitnessScript(input.witnessScript!);

      if (!input.partialSignatures ||
          input.partialSignatures.size < m) {
        throw new Error(`不足的簽名: 需要 ${m} 個`);
      }

      // 按公鑰順序選擇簽名
      const orderedSigs: Uint8Array[] = [];
      for (const pubkey of pubkeys) {
        const sig = input.partialSignatures.get(pubkey);
        if (sig && orderedSigs.length < m) {
          orderedSigs.push(sig);
        }
      }

      // 構建 witness
      input.finalWitness = buildWitness(orderedSigs, input.witnessScript!);
    }

    return this.extractTransaction();
  }
}

function buildWitness(
  signatures: Uint8Array[],
  witnessScript: Uint8Array
): Uint8Array[][] {
  return [
    [new Uint8Array([])], // OP_0
    ...signatures.map(s => [s]),
    [witnessScript]
  ];
}

Taproot 多簽

OP_CHECKSIGADD

Tapscript 多簽 (BIP-342):

新操作碼 OP_CHECKSIGADD:
- 取代 OP_CHECKMULTISIG
- 更高效且可批量驗證

傳統多簽腳本 (2-of-3):
OP_2 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG
問題: O(n×m) 簽名驗證

Tapscript 多簽腳本:
<pk1> OP_CHECKSIG
<pk2> OP_CHECKSIGADD
<pk3> OP_CHECKSIGADD
OP_2 OP_NUMEQUAL

OP_CHECKSIGADD 行為:
1. 彈出 n (累計數), pk, sig
2. 如果 sig 非空且有效: 返回 n+1
3. 如果 sig 為空: 返回 n
4. 如果 sig 非空但無效: 失敗

優點:
- 每個簽名只驗證一次
- 支持批量驗證
- 空簽名表示「跳過此公鑰」

FROST 門檻簽名

FROST (Flexible Round-Optimized Schnorr Threshold):

優於傳統多簽:
- t-of-n 門檻,無需 n 個公鑰
- 產生單一聚合公鑰和簽名
- 隱私: 無法區分單簽/門檻簽

流程概覽:
1. 密鑰生成 (DKG)
   - 每個參與者生成份額
   - 聯合計算聚合公鑰

2. 簽名 (兩輪)
   - 第一輪: 承諾 nonces
   - 第二輪: 產生部分簽名
   - 聚合為最終簽名

特點:
- 任意 t 個參與者可簽名
- 不需要信任的協調者
- 可與 Taproot key path 結合

安全最佳實踐

多簽安全注意事項:

1. 密鑰管理
   - 每個簽名者獨立管理私鑰
   - 考慮硬體錢包
   - 地理分散存儲

2. 備份
   - 備份所有公鑰 (恢復地址需要)
   - 備份 redeemScript/witnessScript
   - 考慮使用 Output Descriptors

3. 配置選擇
   ┌────────────────────────────────────────┐
   │ 配置     │ 適用場景                    │
   ├────────────────────────────────────────┤
   │ 2-of-2   │ 雙方共同控制                │
   │ 2-of-3   │ 個人安全 (主要+備份+託管)   │
   │ 3-of-5   │ 企業 (多批准人)             │
   │ 7-of-11  │ DAO/協議金庫                │
   └────────────────────────────────────────┘

4. 驗證
   - 在簽名前驗證地址正確
   - 每個簽名者獨立驗證交易內容
   - 檢查找零地址屬於自己

5. 時間鎖備份
   - 考慮加入時間鎖緊急恢復路徑
   - 防止密鑰永久丟失

應用場景

多簽使用案例:

1. 企業資金管理
   - CFO + CEO + 董事 (2-of-3)
   - 防止單人挪用

2. 交易所冷錢包
   - 多部門批准
   - 多地理位置密鑰

3. 繼承規劃
   - 家庭成員 + 律師 + 時間鎖
   - 確保資產可繼承

4. 託管服務
   - 買方 + 賣方 + 仲裁方 (2-of-3)
   - 爭議解決機制

5. 個人安全
   - 主設備 + 硬體錢包 + 備份
   - 單設備丟失不致損失資金

6. 多簽名保險庫
   - 即時: 2-of-3
   - 延遲: 1-of-3 (時間鎖)
   - 恢復: 緊急密鑰

方案比較

特性 P2SH 多簽 P2WSH 多簽 Taproot MuSig2
地址格式 3xxx... bc1q... bc1p...
隱私 花費時暴露 花費時暴露 完全隱藏
簽名大小 ~73B × m ~73B × m 64B
手續費 最高 中等 最低
互動輪數 1 1 2-3
支援狀態 通用 通用 需新錢包

相關資源

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