高級
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 不要洩露
相關資源
已複製連結