PSBT: How a Computer With No Keys Builds a Transaction
The format that makes air-gapped wallets and multisig actually work. The six roles of BIP-174, what a signing device is required to verify, the attack that lies to offline devices about the fee, and how Taproot ended it.
When you hit send in a wallet app, one machine does everything. It picks which UTXOs to spend, builds the outputs, calculates the fee, signs with the private key, and pushes the result to the network. Because it all happens in one place, nothing has to be handed off along the way.
The moment you take self-custody seriously, that assumption breaks. The private key lives on a device that has never touched the internet, while the machine that knows which UTXOs you own is online. In a 2-of-3 multisig, the keys that need to sign are scattered across three locations to begin with.
Once the party that knows the transaction is separated from the party that can sign it, you need a container to carry an unfinished transaction between machines. That container is the PSBT.
The problem before there was a standard
PSBT stands for Partially Signed Bitcoin Transaction. It was standardized in BIP-174, submitted in 2017 by Bitcoin Core maintainer Ava Chow.
BIP-174 states two motivations. First, passing an unfinished transaction around to multiple signers was implementation dependent at the time, so people using different wallet software could not collaborate. The second matters more. Signing a transaction requires knowing the contents of the UTXOs being spent, but a hardware wallet or air-gapped signer cannot see the blockchain. BIP-174 frames the goal precisely: let offline signers sign without direct access to the UTXO set and without risk of being defrauded.
That last clause is the whole point. Handing an offline device something and saying "sign this" is easy. The hard part is letting that device verify for itself what it is signing.
What is inside the file
A PSBT is a byte structure, not a human-readable format. It opens with a five byte magic.
0x70 0x73 0x62 0x74 0xFF
p s b t separator
The first four bytes are ASCII for psbt. The trailing 0xFF is a deliberate safeguard. A normal transaction parser handed this file is guaranteed to fail, and a PSBT parser will likewise never mistake an ordinary transaction for a PSBT. The two formats are separated at the header so they cannot quietly blend.
After that come three layers of key-value maps.
<magic> <global-map> <input-map>* <output-map>*
The global map holds information about the transaction as a whole; the input and output maps hold data belonging to each individual input and output. Every map terminates with a 0x00 byte. Since no key can have a length of zero, encountering that byte unambiguously means the map has ended, which keeps parser implementations simple.
Values are distinguished by type number. A handful of the ones you actually meet are enough to see the shape.
| Field | What it holds |
|---|---|
PSBT_GLOBAL_UNSIGNED_TX | The transaction body with all signatures empty (v0) |
PSBT_IN_NON_WITNESS_UTXO | The entire previous transaction this input references |
PSBT_IN_WITNESS_UTXO | Only the single output this input references |
PSBT_IN_PARTIAL_SIG | One signature from one signer |
PSBT_IN_BIP32_DERIVATION | Which wallet this key belongs to and where |
PSBT_IN_WITNESS_SCRIPT | The script body, for things like multisig |
PSBT_OUT_BIP32_DERIVATION | The evidence that an output is your own change |
PSBT_IN_FINAL_SCRIPTWITNESS | Finished witness data |
The difference between PSBT_IN_NON_WITNESS_UTXO and PSBT_IN_WITNESS_UTXO looks like a mere size tradeoff. As we will see, a real theft vector hangs on that one choice.
Six roles
The real design in BIP-174 is not the format but the separation of roles. The specification splits everything that touches a PSBT into six of them. One program may play several, but what each role is permitted to do is strictly bounded.
The Creator makes an empty PSBT. It places an unsigned transaction inside and leaves the input and output fields empty.
The Updater fills in what it knows: the UTXO each input references, redeem scripts, witness scripts, BIP32 derivation paths. An online watch-only wallet usually plays this part.
The Signer only adds signatures. The constraints the spec places on it are the heart of the design. A Signer must use only the UTXO information contained in the PSBT, should not need any additional data source, and may only add data to a PSBT. If it cannot sign, it adds nothing and hands the PSBT back untouched.
The Combiner merges several copies of a PSBT into one. Strikingly, a Combiner does not need to understand scripts at all. It merges key-value pairs and drops duplicates.
The Input Finalizer decides whether the collected partial signatures are enough to pass validation and, if so, assembles the final scriptSig and scriptWitness, clearing the intermediate data used as raw material.
The Transaction Extractor pulls a fully network-serialized transaction out of a finalized PSBT.
The reason for slicing the roles this thin is clear. It minimizes what the most dangerous party, the one holding the private key, actually does. The Signer does not build the transaction, does not modify it, and does not finalize it. It adds exactly one signature to something it has verified for itself.
What actually happens in an air-gapped wallet
Cold storage operation is these roles executing in sequence.
- A watch-only wallet on the online computer builds the transaction. It knows only public keys, has no private key, and therefore cannot sign. It plays Creator and Updater.
- The unsigned PSBT is exported to a microSD card as a file or displayed on screen as a QR code. Binary files use the
.psbtextension; for QR transport it is encoded as a Base64 string. - The air-gapped device reads it and displays the destination address and amount. Once the user confirms, it attaches a signature. No USB, no wireless.
- The signed PSBT returns to the online computer by the same path.
- The online computer finalizes, extracts, and broadcasts.
Among hardware wallets, Coldcard using an SD card while SeedSigner or Jade use QR codes is only a difference in which physical medium crosses step three. What travels across it is the same PSBT.
Multisig adds one combining step. The same PSBT goes to three people, each attaches only their own signature and sends it back, and a Combiner merges them. The spec guarantees this property explicitly: combining the results of A and B working independently is functionally equivalent to running A and then B in sequence. Because signing order does not change the outcome, three people can sign simultaneously, in different cities, using different wallet software.
What a signing device is required to check
A device putting something on its screen and a user pressing approve does not make the operation safe. If the device does not verify the data handed to it, what it displays can itself be a lie. So BIP-174 pins down what a Signer must check.
- If the full previous transaction is provided, its hash must match the hash in the input's
prevout. - If only a witness UTXO is provided, no non-witness signature may be created.
- If a redeem script is provided, the
scriptPubKeymust correspond to it. - If a witness script is provided, the
scriptPubKeyor the redeem script must correspond to it. - If a sighash type is specified, the signer must check that it is acceptable, and must fail if it is not.
Change detection is part of the same problem. Pulling 1 BTC out of a 2-of-3 vault to send 0.1 leaves 0.9 coming back as change. But from the signing device's point of view, whether that 0.9 output is really yours or has been swapped for an attacker's address cannot be determined by looking at the address alone. The evidence is the PSBT_OUT_BIP32_DERIVATION field. The device follows that derivation path, derives the key itself, and if it arrives at a key it can produce, it confirms the output is its own change. Sign a PSBT missing that field without thinking and the device will mistake your change for someone else's without ever asking you about it.
Lying to an offline device about the fee
This is the most practically important part of understanding PSBTs.
The BIP-143 scheme used to sign SegWit inputs includes the amount of that input inside the signed message. That is what lets a signing device sign after receiving only the single output being spent rather than the entire previous transaction, and it is why PSBT_IN_WITNESS_UTXO exists. It is far smaller, which also makes it pleasant to push through a QR code.
The problem is that the signature commits only to its own input's amount. The amounts of the other inputs are not bound by it. In a transaction with multiple inputs, an attacker can state another input's amount as smaller than it really is, and the device has no way to verify the claim. The fee the device calculates and shows on screen is therefore wrong.
The attack completes if the user can be induced to sign the same transaction more than once. Feed a tampered amount for a different input each time and collect the signatures, and in the finished transaction the gap between total input value and total output value, which is the fee paid to miners, balloons to a number the user never saw. The coins do not go to the attacker, but they do leave the user.
This is why many wallets demand PSBT_IN_NON_WITNESS_UTXO, the entire previous transaction, even for SegWit inputs. Given the full transaction, the device can hash it itself and compare against what the input references, which makes the amount impossible to forge. BIP-174 explicitly permits a Signer to refuse to sign a segwit input when this data is absent. That is generally why a hardware wallet takes oddly long on a large transaction: it is ingesting and hashing every referenced previous transaction.
Taproot ended this at the source rather than routing around it. The BIP-341 signature message commits to the amounts and scriptPubKeys of all inputs, not just its own. BIP-341 records the reason in a one-line footnote: it eliminates the possibility of lying to offline signing devices about the fee of a transaction. When signing a Taproot input, a device can know the fee exactly without ever receiving a full previous transaction.
What v2 fixed: adding inputs later
BIP-174 v0 carries one structural limit. The unsigned transaction sitting in the global map freezes the list of inputs and outputs wholesale. Once a PSBT exists, no new input can be added to it.
That is fine alone and immediately blocking for collaborative transactions. CoinJoin, where several people each bring their own inputs to a single transaction, runs into it, and so does PayJoin, where the recipient slips in an input of their own to defeat transaction graph analysis.
PSBT v2, defined in BIP-370, removes the fixed transaction body and decomposes inputs and outputs into individual fields such as PSBT_IN_PREVIOUS_TXID, PSBT_IN_OUTPUT_INDEX, PSBT_OUT_AMOUNT, and PSBT_OUT_SCRIPT. A beneficial side effect is that everything about a given input now lives inside that input's map instead of being split across two places.
v2 also adds a role: the Constructor, which adds inputs and outputs to the empty PSBT the Creator made. It cannot do so at will. It must first check the PSBT_GLOBAL_TX_MODIFIABLE field, which signers update themselves as signatures land. If someone added a signature that does not use SIGHASH_ANYONECANPAY, the inputs-modifiable flag goes false; if they added one that does not use SIGHASH_NONE, the outputs-modifiable flag goes false. The format itself prevents changes that would invalidate existing signatures from even being attempted.
Separately, BIP-371 adds Taproot fields to both v0 and v2: PSBT_IN_TAP_KEY_SIG, PSBT_IN_TAP_SCRIPT_SIG, PSBT_IN_TAP_LEAF_SCRIPT, PSBT_IN_TAP_INTERNAL_KEY, PSBT_OUT_TAP_TREE and others. Schnorr signatures and script trees could not be expressed with the existing fields, so they had to be defined separately.
Try it yourself
Bitcoin Core ships an RPC for each role. You can walk through all of them on signet or testnet with no real coins at risk.
# Creator + Updater: build a funded PSBT
bitcoin-cli walletcreatefundedpsbt '[]' '[{"address":0.001}]'
# See what is inside, in human-readable form
bitcoin-cli decodepsbt "cHNidP8B..."
# Report what is missing and which role comes next
bitcoin-cli analyzepsbt "cHNidP8B..."
# Signer: attach a signature
bitcoin-cli walletprocesspsbt "cHNidP8B..."
# Combiner: merge what came back from several signers
bitcoin-cli combinepsbt '["cHNidP8B...","cHNidP8B..."]'
# Finalizer + Extractor: produce the network transaction
bitcoin-cli finalizepsbt "cHNidP8B..."
It is no coincidence that a Base64-encoded PSBT always begins with cHNidP8. That is the five byte magic 70 73 62 74 ff rendered in Base64. You can identify a PSBT by eye from the string alone.
analyzepsbt is especially useful. Because it tells you what this PSBT is still missing and which role is needed next, it is the reference point to return to when you lose track of the flow while shuttling between devices.
In closing
PSBT is plumbing rather than a headline feature. But without this plumbing, neither keeping private keys wholly detached from the internet nor having several people guard one vault with different wallets would be possible at all. Hardware wallets and multisig work in practice because this format sits underneath them.
And the format is more than a container for moving data. The specification dictates what a signing device must verify, and it decides which field carries the material needed for that verification. That is why both the existence of the fee-lying attack and the defense against it are recorded in its footnotes. Signing should not be the act of pressing an approve button; it should be the act of stamping a fact the device has confirmed for itself. PSBT is the format that makes that confirmation possible.