跳至主要內容
高級

PSBT

部分簽名比特幣交易:多方協作簽名、硬體錢包整合和離線交易

22 分鐘

概述

PSBT(Partially Signed Bitcoin Transaction,部分簽名比特幣交易)是 BIP-174 定義的標準格式, 允許在多個參與者之間傳遞未完成簽名的交易。這對於硬體錢包、多簽錢包和複雜的簽名流程至關重要。

核心價值: PSBT 讓交易的「構建」和「簽名」完全分離。冷錢包可以離線簽名, 多簽參與者可以分別簽名,而無需暴露私鑰。

使用場景

  • 硬體錢包:構建交易 → 傳送到硬體錢包 → 簽名 → 廣播
  • 多簽錢包:參與者依序簽名同一個 PSBT
  • CoinJoin:多方構建共同交易
  • 離線簽名:氣隙電腦安全簽名
  • 交易協調:原子交換、閃電網路通道開啟

工作流程

PSBT 生命週期:

1. Creator (創建者)
   └─→ 創建未簽名交易,添加輸入輸出

2. Updater (更新者)
   └─→ 添加簽名所需的元數據(UTXO、腳本、派生路徑)

3. Signer (簽名者)
   └─→ 使用私鑰簽名各個輸入

4. Combiner (合併者)
   └─→ 合併來自不同簽名者的部分簽名

5. Finalizer (完成者)
   └─→ 構建最終的 scriptSig/witness

6. Extractor (提取者)
   └─→ 提取完整交易,準備廣播

PSBT 結構

全局字段

Key 說明 必須
PSBT_GLOBAL_UNSIGNED_TX 未簽名交易
PSBT_GLOBAL_XPUB 擴展公鑰(BIP-32)
PSBT_GLOBAL_TX_VERSION 交易版本
PSBT_GLOBAL_FALLBACK_LOCKTIME 備用 locktime
PSBT_GLOBAL_INPUT_COUNT 輸入數量
PSBT_GLOBAL_OUTPUT_COUNT 輸出數量

輸入字段

Key 說明
PSBT_IN_NON_WITNESS_UTXO 完整的前置交易(非 SegWit)
PSBT_IN_WITNESS_UTXO UTXO 的金額和 scriptPubKey
PSBT_IN_PARTIAL_SIG 部分簽名(pubkey → signature)
PSBT_IN_SIGHASH_TYPE 簽名雜湊類型
PSBT_IN_REDEEM_SCRIPT P2SH 的 redeemScript
PSBT_IN_WITNESS_SCRIPT P2WSH 的 witnessScript
PSBT_IN_BIP32_DERIVATION 公鑰的 BIP-32 派生路徑
PSBT_IN_FINAL_SCRIPTSIG 最終的 scriptSig
PSBT_IN_FINAL_SCRIPTWITNESS 最終的 witness
PSBT_IN_TAP_KEY_SIG Taproot key path 簽名
PSBT_IN_TAP_SCRIPT_SIG Taproot script path 簽名

輸出字段

Key 說明
PSBT_OUT_REDEEM_SCRIPT 輸出的 redeemScript
PSBT_OUT_WITNESS_SCRIPT 輸出的 witnessScript
PSBT_OUT_BIP32_DERIVATION 輸出地址的派生路徑
PSBT_OUT_TAP_INTERNAL_KEY Taproot 內部公鑰
PSBT_OUT_TAP_TREE Taproot 腳本樹

Bitcoin CLI 操作

創建 PSBT

# 創建未簽名 PSBT
bitcoin-cli createpsbt \
  '[{"txid":"abc...", "vout":0}]' \
  '[{"bc1q...": 0.001}, {"bc1q...": 0.0005}]'

# 從錢包創建(自動選擇輸入)
bitcoin-cli walletcreatefundedpsbt \
  '[]' \
  '[{"bc1q...": 0.001}]' \
  0 \
  '{"changeAddress": "bc1q..."}'

更新 PSBT

# 添加 UTXO 和派生信息
bitcoin-cli utxoupdatepsbt "cHNidP8..."

# 從錢包添加資訊
bitcoin-cli walletprocesspsbt "cHNidP8..." true

簽名 PSBT

# 用錢包簽名
bitcoin-cli walletprocesspsbt "cHNidP8..."

# 結果
{
  "psbt": "cHNidP8...",
  "complete": false  # 如果需要更多簽名
}

合併和完成

# 合併多個 PSBT
bitcoin-cli combinepsbt '["cHNidP8...", "cHNidP8..."]'

# 完成 PSBT
bitcoin-cli finalizepsbt "cHNidP8..."

# 結果
{
  "hex": "0200000001...",  # 可廣播的交易
  "complete": true
}

# 廣播
bitcoin-cli sendrawtransaction "0200000001..."

分析 PSBT

# 解碼 PSBT
bitcoin-cli decodepsbt "cHNidP8..."

# 分析狀態
bitcoin-cli analyzepsbt "cHNidP8..."

# 結果
{
  "inputs": [
    {
      "has_utxo": true,
      "is_final": false,
      "next": "signer",
      "missing": {
        "signatures": ["02abc..."]
      }
    }
  ],
  "estimated_vsize": 141,
  "estimated_feerate": 0.00001000,
  "fee": 0.00000141,
  "next": "signer"
}

TypeScript 實作

使用 bitcoinjs-lib

import * as bitcoin from 'bitcoinjs-lib';
import { ECPairFactory } from 'ecpair';
import * as ecc from 'tiny-secp256k1';

const ECPair = ECPairFactory(ecc);

// 創建 PSBT
function createPsbt(
  inputs: Array<{
    txid: string;
    vout: number;
    witnessUtxo: { script: Buffer; value: number };
  }>,
  outputs: Array<{ address: string; value: number }>
): bitcoin.Psbt {
  const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

  // 添加輸入
  for (const input of inputs) {
    psbt.addInput({
      hash: input.txid,
      index: input.vout,
      witnessUtxo: input.witnessUtxo,
    });
  }

  // 添加輸出
  for (const output of outputs) {
    psbt.addOutput({
      address: output.address,
      value: output.value,
    });
  }

  return psbt;
}

// 簽名 PSBT
function signPsbt(psbt: bitcoin.Psbt, privateKey: Buffer): bitcoin.Psbt {
  const keyPair = ECPair.fromPrivateKey(privateKey);

  // 簽名所有可以簽名的輸入
  for (let i = 0; i < psbt.inputCount; i++) {
    try {
      psbt.signInput(i, keyPair);
    } catch (e) {
      // 此輸入不需要這個私鑰
    }
  }

  return psbt;
}

// 完成和提取
function finalizePsbt(psbt: bitcoin.Psbt): string {
  psbt.finalizeAllInputs();
  return psbt.extractTransaction().toHex();
}

多簽 PSBT 流程

// 2-of-3 多簽範例
async function multisigPsbtWorkflow() {
  // 創建者:構建 PSBT
  const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

  // 添加多簽輸入
  const redeemScript = bitcoin.script.compile([
    bitcoin.opcodes.OP_2,
    pubKey1,
    pubKey2,
    pubKey3,
    bitcoin.opcodes.OP_3,
    bitcoin.opcodes.OP_CHECKMULTISIG,
  ]);

  psbt.addInput({
    hash: 'txid...',
    index: 0,
    witnessUtxo: {
      script: bitcoin.payments.p2wsh({
        redeem: { output: redeemScript },
      }).output!,
      value: 100000,
    },
    witnessScript: redeemScript,
  });

  psbt.addOutput({
    address: 'bc1q...',
    value: 99000,
  });

  // 簽名者 1
  const psbtBase64 = psbt.toBase64();
  const psbt1 = bitcoin.Psbt.fromBase64(psbtBase64);
  psbt1.signInput(0, keyPair1);

  // 簽名者 2
  const psbt2 = bitcoin.Psbt.fromBase64(psbt1.toBase64());
  psbt2.signInput(0, keyPair2);

  // 合併(如果分開簽名)
  // psbt.combine(psbt1, psbt2);

  // 完成
  psbt2.finalizeAllInputs();
  const txHex = psbt2.extractTransaction().toHex();

  return txHex;
}

Taproot PSBT

import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371';

function createTaprootPsbt(
  internalPubKey: Buffer,
  privateKey: Buffer
): bitcoin.Psbt {
  const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

  // Taproot 輸出 (P2TR)
  const { output, address } = bitcoin.payments.p2tr({
    internalPubkey: toXOnly(internalPubKey),
    network: bitcoin.networks.bitcoin,
  });

  // 添加 Taproot 輸入
  psbt.addInput({
    hash: 'txid...',
    index: 0,
    witnessUtxo: {
      script: output!,
      value: 100000,
    },
    tapInternalKey: toXOnly(internalPubKey),
  });

  psbt.addOutput({
    address: 'bc1p...',
    value: 99000,
  });

  // 簽名(使用 Schnorr)
  const keyPair = ECPair.fromPrivateKey(privateKey);
  psbt.signInput(0, keyPair);

  psbt.finalizeAllInputs();
  return psbt;
}

PSBT 驗證

interface PsbtValidation {
  isValid: boolean;
  errors: string[];
  warnings: string[];
  signedInputs: number;
  totalInputs: number;
  estimatedFee: number;
}

function validatePsbt(psbtBase64: string): PsbtValidation {
  const errors: string[] = [];
  const warnings: string[] = [];

  try {
    const psbt = bitcoin.Psbt.fromBase64(psbtBase64);
    let signedInputs = 0;
    let totalValue = 0;
    let outputValue = 0;

    // 檢查輸入
    for (let i = 0; i < psbt.inputCount; i++) {
      const input = psbt.data.inputs[i];

      if (!input.witnessUtxo && !input.nonWitnessUtxo) {
        errors.push(`Input ${i}: Missing UTXO data`);
      }

      if (input.witnessUtxo) {
        totalValue += input.witnessUtxo.value;
      }

      if (input.partialSig && input.partialSig.length > 0) {
        signedInputs++;
      }

      if (input.finalScriptSig || input.finalScriptWitness) {
        signedInputs++;
      }
    }

    // 檢查輸出
    for (const output of psbt.txOutputs) {
      outputValue += output.value;
    }

    // 計算手續費
    const estimatedFee = totalValue - outputValue;

    if (estimatedFee < 0) {
      errors.push('Outputs exceed inputs (negative fee)');
    }

    if (estimatedFee > totalValue * 0.1) {
      warnings.push('Fee seems unusually high (> 10% of inputs)');
    }

    return {
      isValid: errors.length === 0,
      errors,
      warnings,
      signedInputs,
      totalInputs: psbt.inputCount,
      estimatedFee: Math.max(0, estimatedFee),
    };
  } catch (e) {
    return {
      isValid: false,
      errors: [`Invalid PSBT: ${e}`],
      warnings: [],
      signedInputs: 0,
      totalInputs: 0,
      estimatedFee: 0,
    };
  }
}

硬體錢包整合

工作流程

1. 軟體錢包創建 PSBT
   - 選擇 UTXO
   - 構建交易
   - 添加派生路徑資訊

2. 傳輸到硬體錢包
   - Base64 編碼
   - QR 碼、USB、SD 卡

3. 硬體錢包驗證和簽名
   - 顯示交易詳情
   - 用戶確認
   - 離線簽名

4. 返回簽名後的 PSBT
   - 包含部分簽名
   - 可能需要更多簽名

5. 軟體錢包完成和廣播
   - 合併簽名
   - 最終化
   - 廣播交易

派生路徑

// 添加 BIP-32 派生資訊
function addDerivationInfo(
  psbt: bitcoin.Psbt,
  inputIndex: number,
  pubKey: Buffer,
  masterFingerprint: Buffer,
  path: string
) {
  const bip32Derivation = [{
    pubkey: pubKey,
    masterFingerprint,
    path,  // 例如 "m/84'/0'/0'/0/0"
  }];

  psbt.updateInput(inputIndex, { bip32Derivation });
}

編碼格式

Base64

// 標準 PSBT Base64
const base64 = psbt.toBase64();
// "cHNidP8BAH0CAAAAAbi..."

const decoded = bitcoin.Psbt.fromBase64(base64);

Hex

// 十六進位格式
const hex = psbt.toHex();
// "70736274ff01..."

const decoded = bitcoin.Psbt.fromHex(hex);

Magic Bytes

PSBT 開頭始終是:
0x70736274ff = "psbt" + 0xff

這讓工具可以識別 PSBT 格式

PSBT v2 (BIP-370)

PSBT v2 引入了改進,主要用於多方協議:

  • Constructor 角色:分離交易構建和更新
  • 輸入/輸出計數:明確的計數字段
  • Modifiable 標誌:指示 PSBT 是否可以添加輸入/輸出
  • 更好的 Locktime 處理:支援相對和絕對時間鎖

最佳實踐

  • 驗證輸入:確保 UTXO 存在且金額正確
  • 檢查手續費:防止意外的高額手續費
  • 包含派生路徑:幫助硬體錢包找到正確的密鑰
  • 不重複簽名:檢查輸入是否已簽名
  • 保護 PSBT:未簽名的 PSBT 不要洩露

相關資源

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