BIP-174: PSBT
Partially Signed Bitcoin Transactions. Standard format for multi-party transaction signing and hardware wallet integration.
| Type | Bitcoin Improvement Proposal |
| Number | bip-174 |
| Status | Final |
| Authors | Andrew Chow |
| Original | https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki |
| Requires |
BIP-174: Partially Signed Bitcoin Transactions (PSBT)
PSBT is a standard format for passing around unsigned or partially signed transactions. It enables hardware wallets, multi-signature setups, and airgapped signing.
Motivation
Before PSBT:
- No standard format for unsigned transactions
- Hardware wallets needed custom protocols
- Multi-party signing was fragmented
- Airgapped signing was complex
PSBT provides:
- Interoperability: Standard format across tools
- Modularity: Each party adds their data
- Security: Clear separation of concerns
- Extensibility: New fields can be added
Use Cases
| Scenario | How PSBT Helps |
|---|---|
| Hardware wallet | Create PSBT → sign on device → extract signed tx |
| Multi-sig | Create PSBT → each signer adds signature → combine |
| Airgapped computer | Create PSBT → transfer via QR/USB → sign offline |
| Watch-only wallet | Create PSBT → send to hot wallet for signing |
Structure
PSBT consists of:
- Global section: Unsigned transaction and global metadata
- Input sections: One per transaction input
- Output sections: One per transaction output
Binary Format
<magic: 0x70736274ff> // "psbt" + 0xff
<global>
<input 0>
<input 1>
...
<output 0>
<output 1>
...
Each section is a series of key-value pairs:
<key length><key><value length><value>
Section separator: 0x00
Key Types
Global Keys:
| Key | Value | Description |
|---|---|---|
| 0x00 | Transaction | Unsigned transaction |
| 0x01 | xpub | Extended public key |
| 0xfb | Version | PSBT version number |
Input Keys:
| Key | Value | Description |
|---|---|---|
| 0x00 | Non-witness UTXO | Full transaction for legacy inputs |
| 0x01 | Witness UTXO | Output being spent (SegWit) |
| 0x02 | Partial signature | Signature for specific pubkey |
| 0x03 | Sighash type | Signature hash type |
| 0x04 | Redeem script | P2SH redeem script |
| 0x05 | Witness script | P2WSH witness script |
| 0x06 | BIP-32 derivation | HD key path |
| 0x07 | Final scriptSig | Finalized scriptSig |
| 0x08 | Final witness | Finalized witness |
Output Keys:
| Key | Value | Description |
|---|---|---|
| 0x00 | Redeem script | P2SH redeem script |
| 0x01 | Witness script | P2WSH witness script |
| 0x02 | BIP-32 derivation | HD key path |
Roles
PSBT workflow involves distinct roles:
Creator
Creates initial PSBT with unsigned transaction.
const bitcoin = require('bitcoinjs-lib');
// Create unsigned transaction
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
psbt.addInput({
hash: 'abcd...', // txid
index: 0,
witnessUtxo: {
script: Buffer.from('0014...', 'hex'),
value: 100000
}
});
psbt.addOutput({
address: 'bc1q...',
value: 50000
});
// Export as base64
const psbtBase64 = psbt.toBase64();
Updater
Adds information needed for signing:
- UTXO data (witness or non-witness)
- Redeem/witness scripts
- BIP-32 derivation paths
- Sighash types
// Add derivation info
psbt.updateInput(0, {
bip32Derivation: [{
masterFingerprint: Buffer.from('deadbeef', 'hex'),
path: "m/84'/0'/0'/0/0",
pubkey: publicKey
}]
});
Signer
Adds partial signatures using available keys.
// Sign with private key
psbt.signInput(0, keyPair);
// Or sign all inputs the key can sign
psbt.signAllInputs(keyPair);
Combiner
Merges multiple PSBTs with partial signatures.
const psbt1 = bitcoin.Psbt.fromBase64(signer1Result);
const psbt2 = bitcoin.Psbt.fromBase64(signer2Result);
psbt1.combine(psbt2);
Finalizer
Creates final scriptSig/witness from partial signatures.
psbt.finalizeAllInputs();
Extractor
Extracts the final signed transaction.
const tx = psbt.extractTransaction();
const txHex = tx.toHex();
// Broadcast
await fetch('https://mempool.space/api/tx', {
method: 'POST',
body: txHex
});
Complete Workflow Example
Single Signer (Hardware Wallet)
const bitcoin = require('bitcoinjs-lib');
// 1. CREATOR: Watch-only wallet creates PSBT
const psbt = new bitcoin.Psbt();
psbt.addInput({
hash: txid,
index: vout,
witnessUtxo: { script, value }
});
psbt.addOutput({ address: recipient, value: amount });
psbt.addOutput({ address: changeAddress, value: change });
// 2. UPDATER: Add derivation paths
psbt.updateInput(0, {
bip32Derivation: [{
masterFingerprint,
path: "m/84'/0'/0'/0/5",
pubkey
}]
});
// 3. Export for hardware wallet
const psbtBase64 = psbt.toBase64();
// Send to hardware wallet via USB/QR
// 4. SIGNER: Hardware wallet signs and returns signed PSBT
const signedPsbtBase64 = await hardwareWallet.sign(psbtBase64);
// 5. FINALIZER: Finalize
const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64);
signedPsbt.finalizeAllInputs();
// 6. EXTRACTOR: Get raw transaction
const tx = signedPsbt.extractTransaction();
console.log(tx.toHex());
Multi-Signature (2-of-3)
// 1. Create PSBT with multisig input
const psbt = new bitcoin.Psbt();
psbt.addInput({
hash: txid,
index: 0,
witnessUtxo: { script: p2wshScript, value: 100000 },
witnessScript: multisigScript
});
psbt.addOutput({ address: recipient, value: 90000 });
// 2. Signer 1 signs
const psbt1 = bitcoin.Psbt.fromBase64(psbtBase64);
psbt1.signInput(0, signer1KeyPair);
const signed1 = psbt1.toBase64();
// 3. Signer 2 signs (can start from original or signed1)
const psbt2 = bitcoin.Psbt.fromBase64(psbtBase64);
psbt2.signInput(0, signer2KeyPair);
const signed2 = psbt2.toBase64();
// 4. Combine
const combined = bitcoin.Psbt.fromBase64(signed1);
combined.combine(bitcoin.Psbt.fromBase64(signed2));
// 5. Finalize (now has 2 of 3 signatures)
combined.finalizeAllInputs();
// 6. Extract and broadcast
const tx = combined.extractTransaction();
Encoding Formats
| Format | Use Case |
|---|---|
| Binary | File storage, efficient transfer |
| Base64 | Text transmission, APIs |
| Hex | Debugging, logging |
// Convert between formats
const psbt = new bitcoin.Psbt();
const base64 = psbt.toBase64();
const hex = psbt.toHex();
const buffer = psbt.toBuffer();
// Parse from any format
const fromBase64 = bitcoin.Psbt.fromBase64(base64);
const fromHex = bitcoin.Psbt.fromHex(hex);
const fromBuffer = bitcoin.Psbt.fromBuffer(buffer);
Validation
Before signing, validate PSBT contents:
function validatePsbt(psbt) {
// Check all inputs have UTXO data
for (let i = 0; i < psbt.inputCount; i++) {
const input = psbt.data.inputs[i];
if (!input.witnessUtxo && !input.nonWitnessUtxo) {
throw new Error(`Input ${i} missing UTXO data`);
}
}
// Verify output amounts are reasonable
const inputSum = psbt.data.inputs.reduce((sum, input) => {
return sum + (input.witnessUtxo?.value || 0);
}, 0);
const outputSum = psbt.txOutputs.reduce((sum, output) => {
return sum + output.value;
}, 0);
const fee = inputSum - outputSum;
if (fee < 0) {
throw new Error('Invalid: outputs exceed inputs');
}
if (fee > inputSum * 0.1) {
throw new Error('Warning: fee exceeds 10% of inputs');
}
return { inputSum, outputSum, fee };
}
CLI Tools
bitcoin-cli
# Create PSBT
bitcoin-cli createpsbt '[{"txid":"...","vout":0}]' '[{"bc1q...":0.001}]'
# Process (add UTXO info)
bitcoin-cli walletprocesspsbt "cHNidP8B..."
# Sign
bitcoin-cli walletprocesspsbt "cHNidP8B..." true
# Finalize
bitcoin-cli finalizepsbt "cHNidP8B..."
# Decode for inspection
bitcoin-cli decodepsbt "cHNidP8B..."
HWI (Hardware Wallet Interface)
# Enumerate devices
hwi enumerate
# Sign PSBT with hardware wallet
hwi -d <device_id> signtx "cHNidP8B..."
Security Considerations
- Verify UTXO data: Malicious PSBT could claim wrong amounts
- Check change addresses: Ensure change goes to your wallet
- Validate fee: Prevent excessive fees
- Inspect all outputs: Know where funds are going
- Use deterministic signing: Consistent signatures
PSBT Version 2 (BIP-370)
PSBT v2 adds:
- Transaction modifiability flags
- Output modifiers
- Better extensibility
Check version: psbt.data.globalMap.get(0xfb)
Machine-Readable Summary
{
"bip": 174,
"title": "Partially Signed Bitcoin Transactions",
"status": "final",
"magic_bytes": "0x70736274ff",
"roles": ["creator", "updater", "signer", "combiner", "finalizer", "extractor"],
"formats": ["binary", "base64", "hex"],
"use_cases": ["hardware_wallets", "multisig", "airgapped_signing"],
"libraries": {
"javascript": ["bitcoinjs-lib"],
"python": ["python-bitcoinlib", "bdk"],
"rust": ["rust-bitcoin", "bdk"]
}
}