2026-10-07

Trying to Understand PSBT

#Bitcoin #PSBT
SeedSignerWhat does SeedSigner actually trust

I wrote about SeedSigner, but if I'm honest, I still don't understand PSBT very well. It made me wonder what exactly we send over the air gap. I'm also thinking about building my own DIY signer, so I wanted to understand it better. I worked through a small example and wrote down what I found.

What a PSBT is

A PSBT (Partially Signed Bitcoin Transaction) is a binary format for exchanging a transaction before all its signatures are in place, defined in BIP174. It was proposed in 2017 and added to Bitcoin Core as RPCs in version 0.17.

BIP174: Partially Signed Bitcoin Transaction FormatA format that carries, along with the transaction, what a signer needs to sign it. Status: Deployedgithub.com

BIP174's abstract describes a PSBT as a format that carries the information needed for signing and holds partial signatures until all signatures are in place. Since the required information travels with the transaction, a signer can work offline.

The motivation section describes two problems:

  • Each wallet had its own way to pass an unsigned transaction to several signers, so different wallet software could not exchange them.
  • Signing requires information about the UTXOs being spent, but air-gapped and hardware wallets cannot look up the UTXO set themselves.

With an air-gapped signer, the flow looks like the figure below: the wallet and signer exchange PSBT data as animated QR codes. Compatibility between them comes down to whether both can handle this PSBT format.

Wallet online holds no keys screen camera Broadcast Signer offline holds the keys camera seed phrase 1 2 3 4 5 6 7 8 9 10 11 12 → derive key, sign screen 1 Unsigned PSBT 4 QR frames 2 PSBT with signature 7 QR frames AIR GAP no cable, no radio 3 Once it is signed, Broadcast sends the transaction to the Bitcoin network
The signer's camera reads the unsigned PSBT the wallet shows on its screen; the wallet's camera then reads the signed PSBT back from the signer's screen. Both QR codes carry real data; only the signature is a dummy

How each role uses a PSBT

As the figure shows, the wallet and signer send different data to each other. BIP174 defines the processing of a PSBT in terms of roles. One piece of software can take on several roles, as many implementations do.

Role Figure participant What it does
Creator Wallet Builds the unsigned transaction and puts it in a PSBT, with empty input and output maps
Updater Wallet Adds what it knows: the UTXOs spent, scripts and BIP32 derivation paths
Signer Signing device Checks the contents and adds partial signatures for the inputs its keys can sign
Combiner Wallet (multi-signer flow) Merges several PSBTs for the same transaction into one
Input Finalizer Wallet Builds the scriptSig / scriptWitness of inputs that have all their signatures
Transaction Extractor Wallet Pulls a network-ready transaction out of a PSBT whose inputs are all complete

Bitcoin Core's doc/psbt.md lists the roles performed by each RPC. For example, walletcreatefundedpsbt performs Creator and Updater, walletprocesspsbt performs Updater, Signer and Finalizer, and finalizepsbt performs Finalizer and Extractor.

Bitcoin Core doc/psbt.mdPSBT Howto for Bitcoin Core: the workflow by role and the matching RPCsgithub.com

In the figure, the signer adds its signature, then the wallet completes and broadcasts the transaction. A Signer can add information but must not remove anything from the PSBT it received. The wallet and signer exchange the PSBT as UR text carried by QR codes.

From payment details to UR

A PSBT is binary, but in an air-gapped workflow we usually see it as a QR code. UR (Uniform Resources) is a format for carrying binary data over QR codes, defined by Blockchain Commons. It is not a BIP, but many air-gapped devices and wallets use it.

BCR-2020-005: Uniform ResourcesTurns binary into typed, URI-like text and splits it across several QR codesgithub.com

Here is how the payment information becomes a PSBT, then UR text and QR codes, from the sender's side.

0 Fill in the send form Sparrow Wallet, BlueWallet, ... SEND To bc1qzyg3zyg3…zyg3h8ffkz Amount 1.00000000 BTC Fee sat/vB Create PSBT The wallet adds on its own Picks the coins to spend from its UTXOs it already knows their amounts and scripts Points to the key that will sign master fingerprint and path, from the xpub 1 What goes in CREATOR / UPDATER unsigned transaction inputs, outputs, locktime witness UTXO coin amount and script BIP32 derivation pubkey, fingerprint, path 2 Put into a PSBT 187 byte magic 5 global map unsigned tx · 86 byte input map UTXO + derivation · 95 byte output 1 3 Wrap in a CBOR byte string 189 byte · CRC32 61e6e143 58 bb PSBT 187 byte 4 Split into 48-byte pieces 4 fragments fragment 1 0–47 fragment 2 48–95 fragment 3 96–143 fragment 4 144–188 + 0 ×3 fragment 1 0–47 fragment 2 48–95 fragment 3 96–143 fragment 4 144–188 + 0 ×3 5 Turn fragment 1 into a part (CBOR array) 5 Turn fragment 2 into a part (CBOR array) 5 Turn fragment 3 into a part (CBOR array) 5 Turn fragment 4 into a part (CBOR array) 60 byte 85 array 04 seqLen 18 bd msgLen 189 1a 61 e6 e1 43 checksum 58 30 48 byte 01 seq fragment 1 02 seq fragment 2 03 seq fragment 3 04 seq fragment 4 6 Bytewords: 2 letters per byte + the part CRC32 (4 bytes) = 128 letters 85 LP 01 AD 04 AA 18 CS bd RY 1a CY 61 HS e6 VA e1 VY 43 FX 58 HD 30 DY 58 HD bb RK 85 LP 02 AO 04 AA 18 CS bd RY 1a CY 61 HS e6 VA e1 VY 43 FX 58 HD 30 DY 00 AE 00 AE 85 LP 03 AX 04 AA 18 CS bd RY 1a CY 61 HS e6 VA e1 VY 43 FX 58 HD 30 DY 00 AE e1 VY 85 LP 04 AA 04 AA 18 CS bd RY 1a CY 61 HS e6 VA e1 VY 43 FX 58 HD 30 DY 0b BD 07 AT … 7 Into a UR, then a QR code 147 chars · alphanumeric mode UR:CRYPTO-PSBT/1-4/LPADAACSRYCYHSV AVYFXHDDYHDRKJOJKIDJYZMADAEGMAOAEA EAEADAEAEAEAEAEAEAEAEAEAEAEAEAEAEA EAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEA EAEBEGMHPHP UR:CRYPTO-PSBT/2-4/LPAOAACSRYCYHSV AVYFXHDDYAEAEAEAEZMZMZMZMADAEVYYKA HAEAEAEAECMAEBBBYBYBYBYBYBYBYBYBYB YBYBYBYBYBYBYBYBYBYBYAEAEAEAEAEADA DCTCEBGEMEC UR:CRYPTO-PSBT/3-4/LPAXAACSRYCYHSV AVYFXHDDYAEVYYKAHAEAEAEAECMAEBBCPC PCPCPCPCPCPCPCPCPCPCPCPCPCPCPCPCPC PCPCPAMAOKKRNIYKBYTUORKPSGONBIDMDT OLTMWGOHPBG UR:CRYPTO-PSBT/4-4/LPAAAACSRYCYHSV AVYFXHDDYBDATAONDZTUYDPTODETAHKWZL YHPCMYACHMKCSTEGTQDFHGHAEAELAAEAEA ELAAEAEAELAAEAEAEAEATAEAEAEAEAEAEA EAEWPOTNYGR type · seq-seqLen · Bytewords (part + CRC32)
The send form input and the UTXO and derivation path the wallet adds become a PSBT, which is wrapped in CBOR, split into four parts and written as Bytewords text inside QR codes. The figure highlights each fragment in turn and switches the part, Bytewords, UR and QR built from it. All values match the demo

The sender builds the data in this order:

  1. Enter the payment in the wallet. Specify the recipient, amount and fee. The wallet selects the coins to spend and provides the derivation path for the signing key.
  2. The wallet creates a PSBT. The Creator puts an unsigned transaction reflecting the payment in the global map. The Updater adds the spent coin's information (witness UTXO) and the key's derivation path to the input map.
  3. Serialize the PSBT as bytes. The PSBT in this example is 187 bytes. Before UR encoding, the data is binary.
  4. Wrap it in CBOR. The PSBT bytes go into a CBOR byte string, and a CRC32 checksum is computed for the whole message. The leading 58 bb means a byte string of 187 bytes.
  5. Split it to fit QR codes. Here, the data is split into 48-byte pieces. Each piece is wrapped in a CBOR array called a part, which records its sequence number, the total number of pieces, the original length and the overall checksum.
  6. Encode it as Bytewords. Each byte in a part is replaced with two letters, and the part's own CRC32 is appended. The result uses uppercase letters, digits and /:-, which fit QR alphanumeric mode.
  7. Turn the UR text into a QR code. UR:CRYPTO-PSBT/1-4/ identifies the type as crypto-psbt and marks this as the first of four frames.

The PSBT as bytes

The flow makes sense, but what is actually in the binary data? Here, I look at the bytes of the recovered PSBT.

This article covers BIP174 version 0, which stores the unsigned transaction in the global map.

BIP370 version 2 does not contain an unsigned transaction. Instead, it stores the transaction's parts in separate maps so inputs and outputs can be added later.

BIP370: PSBT Version 2Drops PSBT_GLOBAL_UNSIGNED_TX and keeps the parts of the transaction in each mapgithub.com

The bytes are laid out 16 per row.

After the magic, a PSBT contains maps made of key length → key → value length → value records. A key length of 00 marks the end of a map. Lengths use CompactSize; every length in this example is below 253, so each takes one byte.

The unsigned transaction inside the global map

In PSBT v0, the value of global key 00 is the unsigned transaction. Its bytes break down further according to the transaction format.

Field Bytes Meaning
version 02 00 00 00 Transaction format version (little-endian)
input count 01 One input
prevout 00 × 32 · 00 00 00 00 ID of the previous transaction (dummy) and output index 0 within it
scriptSig length 00 Empty, since it is unsigned
sequence ff ff ff ff The input's sequence value
output count 01 One output
amount 00 e1 f5 05 00 00 00 00 100,000,000 sat (1 BTC), little-endian
scriptPubKey 16 · 00 14 · 11 × 20 22 bytes. A dummy script shaped like P2WPKH
locktime 00 00 00 00 No locktime

The recipient and amount are recorded in the unsigned transaction. A signer reads this transaction to display who gets how much.

Input and output map contents

The input map holds signing information that is not part of the transaction itself. This example has two keys.

The value of key type 01 witness UTXO consists of an 8-byte amount, a script length and the script. Here it contains 1 BTC and a P2WPKH script: the output that the unsigned transaction's prevout points to, included for the signature calculation.

Key type 06 BIP32 derivation has a public key in the key and a 4-byte fingerprint plus a derivation path in the value. Here the path is m/84'/0'/0'/0/7. It is not a private key; it tells the device which of its keys to use for signing.

The parser treats derivations with a matching fingerprint as candidates, then checks whether the derived key is valid for P2WPKH or a specific Taproot key path.

In PSBT v0, the unsigned transaction in the global map determines how many input and output maps follow. The output map in this example is empty and contains only the terminating 00. The output amount and scriptPubKey are in the unsigned transaction, so no additional data is needed in the output map to read the payment details.

The signer walks it back and signs

The signer receives the QR codes and restores the PSBT in reverse order:

  1. Read the UR text from each QR code, decode Bytewords back into bytes and check each part's CRC32.
  2. Use the frame numbers to assemble the fragments, verify the overall checksum and unwrap the CBOR.
  3. Extract the original PSBT bytes and read the payment details, spent coin and key derivation path. The frames can arrive in any order.
1 The signer's camera reads the QR 147 chars each · 4 frames UR:CRYPTO-PSBT/1-4/LPADAACSRYCYHSV AVYFXHDDYHDRKJOJKIDJYZMADAEGMAOAEA EAEADAEAEAEAEAEAEAEAEAEAEAEAEAEAEA EAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEA EAEBEGMHPHP UR:CRYPTO-PSBT/2-4/LPAOAACSRYCYHSV AVYFXHDDYAEAEAEAEZMZMZMZMADAEVYYKA HAEAEAEAECMAEBBBYBYBYBYBYBYBYBYBYB YBYBYBYBYBYBYBYBYBYBYAEAEAEAEAEADA DCTCEBGEMEC UR:CRYPTO-PSBT/3-4/LPAXAACSRYCYHSV AVYFXHDDYAEVYYKAHAEAEAEAECMAEBBCPC PCPCPCPCPCPCPCPCPCPCPCPCPCPCPCPCPC PCPCPAMAOKKRNIYKBYTUORKPSGONBIDMDT OLTMWGOHPBG UR:CRYPTO-PSBT/4-4/LPAAAACSRYCYHSV AVYFXHDDYBDATAONDZTUYDPTODETAHKWZL YHPCMYACHMKCSTEGTQDFHGHAEAELAAEAEA ELAAEAEAELAAEAEAEAEATAEAEAEAEAEAEA EAEWPOTNYGR 2 Bytewords back to bytes the trailing CRC32 rejects a damaged QR LP 85 AD 01 AA 04 CS 18 RY bd CY 1a HS 61 VA e6 VY e1 FX 43 HD 58 DY 30 HD 58 RK bb LP 85 AO 02 AA 04 CS 18 RY bd CY 1a HS 61 VA e6 VY e1 FX 43 HD 58 DY 30 AE 00 AE 00 LP 85 AX 03 AA 04 CS 18 RY bd CY 1a HS 61 VA e6 VY e1 FX 43 HD 58 DY 30 AE 00 VY e1 LP 85 AA 04 AA 04 CS 18 RY bd CY 1a HS 61 VA e6 VY e1 FX 43 HD 58 DY 30 BD 0b AT 07 … 3 Order fragments by the part seq 1 / 4 received 3 Order fragments by the part seq 2 / 4 received 3 Order fragments by the part seq 3 / 4 received 3 Order fragments by the part seq 4 / 4 received fragment 1 0–47 fragment 2 48–95 fragment 3 96–143 fragment 4 144–188 fragment 1 0–47 fragment 1 0–47 fragment 2 48–95 fragment 1 0–47 fragment 2 48–95 fragment 3 96–143 fragment 1 0–47 fragment 2 48–95 fragment 3 96–143 fragment 4 144–188 4 Join and unwrap the CBOR check the message CRC32 61e6e143 58 bb PSBT 187 byte 5 Parse the PSBT parser_parse magic 5 global map unsigned tx · 86 byte input map UTXO + derivation · 95 byte output 1 unsigned transaction recipient, amount witness UTXO coin amount and script BIP32 derivation fingerprint, path 6 Check, hash, derive Check on screen To bc1qzyg3…zyg3h8ffkz Amount 1.00000000 BTC Fee inputs total − outputs total Press OK sighash (BIP143) The unsigned tx plus the amount and script of the coin spent, hashed into 32 bytes. The signature covers this value It commits to the amount, so a faked amount fails to verify Derive the key seed phrase (typed on the device) 1 2 3 4 5 6 7 8 9 10 11 12 BIP39 → seed → BIP32 m/84'/0'/0'/0/7 match fingerprint and script private key 7 Sign with ECDSA (secp256k1) Sign the sighash with the private key, verify before releasing it libsecp256k1 (Bitcoin Core's library) · DER 70 bytes + sighash type 01 = 71 bytes 8 Add a partial signature 294 byte magic 5 global map unchanged · 86 bytes input map unchanged partial sig 02 + pubkey → signature · 107 bytes out 1 9 Into a UR, shown as QR codes 139 chars each · 7 frames UR:CRYPTO-PSBT/1-7/LPADATCFADDTCYG HGESFWDHDDNHKADDSJOJKIDJYZMADAEGMA OAEAEAEADAEAEAEAEAEAEAEAEAEAEAEAEA EAEAEAEAEAEAEAEAEAEAEAEAEAEAEWFTEP TDW UR:CRYPTO-PSBT/2-7/LPAOATCFADDTCYG HGESFWDHDDNAEAEAEAEAEAEAEAEAEAEZMZ MZMZMADAEVYYKAHAEAEAEAECMAEBBBYBYB YBYBYBYBYBYBYBYBYBYBYBYBYBYBYECONM UJE UR:CRYPTO-PSBT/3-7/LPAXATCFADDTCYG HGESFWDHDDNBYBYBYAEAEAEAEAEADADCTA EVYYKAHAEAEAEAECMAEBBCPCPCPCPCPCPC PCPCPCPCPCPCPCPCPCPCPCPCPCPCPRDRKK OFY UR:CRYPTO-PSBT/4-7/LPAAATCFADDTCYG HGESFWDHDDNAMAOKKRNIYKBYTUORKPSGON BIDMDTOLTBDATAONDZTUYDPTODETAHKWZL YHPCMYACHMKCSTEGTQDFHGHAEAELAKOHPO EHD UR:CRYPTO-PSBT/5-7/LPAHATCFADDTCYG HGESFWDHDDNAEAEAELAAEAEAELAAEAEAEA EATAEAEAECPAOAOKKRNIYKBYTUORKPSGON BIDMDTOLTBDATAONDZTUYDPTODETAKTLBC TPT UR:CRYPTO-PSBT/6-7/LPAMATCFADDTCYG HGESFWDHDDNHKWZLYHPCMYACHMKFLDYFYA OCXEOEOEOEOEOEOEOEOEOEOEOEOEOEOEOE OEOEOEOEOEOEOEOEOEOEOEOEOEOEOLAHDK GJE UR:CRYPTO-PSBT/7-7/LPATATCFADDTCYG HGESFWDHDDNEOEOAOCXFYFYFYFYFYFYFYF YFYFYFYFYFYFYFYFYFYFYFYFYFYFYFYFYF YFYFYFYFYFYFYFYADAEAEAEAEAEAEWTZTH YDR
The signer turns the QR codes back into a PSBT and parses it, asks the user to confirm it on screen, signs the sighash with ECDSA using a key derived from the seed phrase, adds the signature to the input map and returns it as QR codes. Only the signature is a dummy

The signer checks four things before returning the PSBT:

  1. Review the payment. Display the recipient, amount and fee for the user to confirm before signing. The fee is not in the PSBT; the device calculates it by subtracting the total outputs from the total spent coins.
  2. Calculate the sighash. For SegWit v0, BIP143 hashes the unsigned transaction together with the amount and script of the spent coin. The signature is made over this 32-byte hash. If the PSBT gives a false amount, a signature based on it will not be accepted by the network.
  3. Derive the private key. The key does not come from the PSBT. The device turns the seed phrase into a seed with BIP39, then follows the derivation path in the PSBT (m/84'/0'/0'/0/7) with BIP32. It also checks that the master fingerprint belongs to the device and that the derived public key matches the spent coin's script.
  4. Sign and add the signature. The signer uses the sighash and private key to create an ECDSA signature on secp256k1. It verifies the signature, then adds it to the input map as a partial signature (PSBT_IN_PARTIAL_SIG, key 02 + public key). In this example, the PSBT grows from 187 to 294 bytes and returns to the wallet across seven QR frames.

Trying it with my own signing module

I have been developing jitsu-in, a signing module for my own DIY signer. The main target is microcontrollers such as the Raspberry Pi Pico 2, but it is built on WebAssembly and can run anywhere with a WASM runtime.

The demo below uses parser.wasm to read a PSBT restored from QR codes. It shows the animated QR reception progress, the recovered PSBT hex and the plan used by the signing device. You can also point a real camera at a PSBT UR and scan it.

This parser.wasm supports PSBT v0, up to 32,768 bytes, with up to 16 inputs and 16 outputs. It supports URs split across up to 1,024 frames. PSBT v2 is not supported yet.

When the fourth QR frame arrives, the 187-byte PSBT is restored and parser.wasm builds a plan. The plan lists the payment details, input amount and key derivation path shown above.