PSBT

Partially Signed Bitcoin Transaction

Diagram showing an overview of the PSBT data format.

A PSBT (Partially Signed Bitcoin Transaction) is a data format for passing transactions around for signing.

This format essentially contains raw unsigned transaction data, along with extra data required to sign the inputs.

So if you export an unsigned transaction from your bitcoin wallet, it will most likely be in the PSBT format. Most modern wallets support the exporting and signing of PSBTs, for example:

Duplicate Tool
tool-688a2767a2dd9
Tool Icon

PSBT Decoder

Basic decoder for the PSBT data format.

PSBT Data

Examples

v0: P2WPKH
v0: Multisig
v2: P2WPKH
v2: Multisig
0 bytes
0 bytes
0 secs

Purpose

Why do we need PSBTs?

Diagram showing a PSBT being sent to a hardware device to be signed.

To sign a bitcoin transaction, you actually need some additional information that isn't included within the raw unsigned transaction data itself.

For example, if you want to create a signature to unlock a P2WPKH input, you need the following extra data:

This information is stored on the previous transaction that created the output (UTXO), but not on the current transaction spending it as an input.

Now, this isn't a problem if you're using a wallet with an Internet connection (i.e. a "hot wallet"), as this information can be retrieved from the blockchain by searching for the previous transaction that created the UTXO you're spending.

However, if you're using an offline wallet to sign your transaction (e.g. a hardware device or "cold wallet"), it will not have an up-to-date copy of the blockchain, so even though it has the private key to sign for the input, it does not have access to the extra information needed to actually create the signature.

Therefore, the PSBT format allows you to pass all the data required to sign the transaction on a separate wallet/device.

Benefits

Why was the PSBT format created?

Diagram showing a PSBT being used to unlock a multisig input. The PSBT is sent multiple devices for signing, before being combined in to a single PSBT and final signed transaction.

A PSBT provides a standard format for passing transactions around for signing.

Before PSBTs, wallets would implement their own format for exporting the extra data needed to create the signatures for a transaction. This worked, but it meant their unsigned transaction format was probably not compatible with other wallets and devices. Therefore, the person signing the PSBT would need to use the same software as the person who created it.

However, with PSBTs, an unsigned transaction can be exported and signed by different wallets and devices.

For example, you could construct a transaction that spends a multisig output (e.g. P2MS) requiring signatures from 3 different people to unlock it. By using the PSBT format, you can send each signer the unsigned transaction data, and each signer can use whichever wallet/device they prefer to add their signature, without having to worry about whether their software understands the unsigned transaction format.

These separate PSBTs can then be combined to create the final signed transaction.

So having a common format for unsigned (or partially signed) transactions enables compatibility between different bitcoin wallets and devices.

Structure

What does a PSBT look like?

The PSBT format is a series of key-value pairs contained within separate maps.

Diagram showing the data structure of a PSBT.

Header

A PSBT always starts with some header bytes to identify the data as a PSBT:

Maps

After the header, the PSBT contains three map types (in the following order):

The end of each map is signified by a 1-byte 00 separator.

The input map(s) and output map(s) must be in order corresponding to the inputs and outputs in the unsigned transaction. Each input and output must have its own map. If you do not need to include any key-value pairs for an input or output, just use the 00 separator to indicate the end of the map.

You need to extract the input count and output count of the unsigned transaction from the global map to determine the number of input map(s) and output map(s) in the PSBT.

Key-Value

Within each map is a series of key-value pairs.

Each key-value pair has the following structure:

These key-value pairs are repeated until the end of the map is reached.

Code

Here are some code simple snippets for decoding and encoding raw PSBTs.

These code snippets do not implement all the steps required for processing PSBTs (that's your job), nor do they detect any errors, but they should be helpful for getting the hang of deconstructing and building PSBTs for debugging and testing.

Decode

# ----
# PSBT
# ----

# example psbt in base64
base64 = "cHNidP8BAHcCAAAAAfdOPcmRewY2v88bQwUxucGsxSuXH0uEkKSNE80NzyiB
AAAAAAD9////AjU+AAAAAAAAGXapFD8wj0I2S+BXFbGa4wd7u8o36zxniKy2
WQAAAAAAABl2qRRjuoLSTMaSnO/tORKfAZa2RonLgYisf8kNAAABAL8CAAAA
AV4xZeLXJkrfKXk+fL6JBWhcMD/l0EPa78AWP1Pr4UpyAAAAAGpHMEQCIBS7
ZFI/kRlbC4oeDirf3uVy1EWPiNqSHhcwmkkiSwQ9AiAitN+1CQUHNCeY9dsc
ruF1U+B4Y+XtR5wXd83/YNy8ugEhAqAoKT0yVpnlt6sXX1Qz6eC2kO49L16M
A34ipmgeGZtl/f///wHNmAAAAAAAABl2qRSFdGbzr4xiIfOuLmEpSa4n414z
LYisf8kNACIGAxWaFXJLMzktDDpTbQAFCIllf+xM8nxjA86XolMXHogOGJwO
R5IsAACAAAAAgAAAAIAAAAAAAAAAAAAiAgKh3fjioSvCMeCiWW2YHs/jG7yf
7Ld4HzMyz/wF8fcKehicDkeSLAAAgAAAAIAAAACAAQAAAAAAAAAAAA=="

# convert base64 to hex string
require 'base64'
hex = Base64.decode64(base64).unpack("H*")[0]

# tip: if you want to decode from hex instead of base64, uncomment the line below and set the hex string there
# hex = '70736274ff0100770200000001f74e3dc9917b0636bfcf1b430531b9c1acc52b971f4b8490a48d13cd0dcf28810000000000fdffffff02353e0000000000001976a9143f308f42364be05715b19ae3077bbbca37eb3c6788acb6590000000000001976a91463ba82d24cc6929cefed39129f0196b64689cb8188ac7fc90d00000100bf02000000015e3165e2d7264adf29793e7cbe8905685c303fe5d043daefc0163f53ebe14a72000000006a473044022014bb64523f91195b0b8a1e0e2adfdee572d4458f88da921e17309a49224b043d022022b4dfb5090507342798f5db1caee17553e07863e5ed479c1777cdff60dcbcba012102a028293d325699e5b7ab175f5433e9e0b690ee3d2f5e8c037e22a6681e199b65fdffffff01cd980000000000001976a914857466f3af8c6221f3ae2e612949ae27e35e332d88ac7fc90d00220603159a15724b33392d0c3a536d00050889657fec4cf27c6303ce97a253171e880e189c0e47922c0000800000008000000080000000000000000000220202a1ddf8e2a12bc231e0a2596d981ecfe31bbc9fecb7781f3332cffc05f1f70a7a189c0e47922c000080000000800000008001000000000000000000'

# show the PSBT in hex format
puts hex
puts

# ---------
# Functions
# ---------
# Utility functions to help with decoding

def read_compact_size(buffer)
  # read the first byte
  byte = buffer.read(1)

  # convert byte to integer value
  int = byte.unpack("C")[0] # C = integer [8 bit] (unsigned)

  # get the value held by the compact size field  
  if (int <= 0xFC)
    value = int # this byte
  elsif (int == 0xFD)
    value = buffer.read(2).unpack("v")[0] # next 2 bytes
  elsif (int == 0xFE)
    value = buffer.read(4).unpack("V")[0] # next 4 bytes
  elsif (int == 0xFF)
    value = buffer.read(8).unpack("Q")[0] # next 8 bytes
  end

  # return value
  return value
end

# ---------
# Key Types
# ---------
# An array of all the key types, useful for displaying the name of the key type

types = {
  'global' => {
    0 => 'PSBT_GLOBAL_UNSIGNED_TX',
    1 => 'PSBT_GLOBAL_XPUB',
    2 => 'PSBT_GLOBAL_TX_VERSION',
    3 => 'PSBT_GLOBAL_FALLBACK_LOCKTIME',
    4 => 'PSBT_GLOBAL_INPUT_COUNT',
    5 => 'PSBT_GLOBAL_OUTPUT_COUNT',
    6 => 'PSBT_GLOBAL_TX_MODIFIABLE',
    7 => 'PSBT_GLOBAL_SP_ECDH_SHARE',
    8 => 'PSBT_GLOBAL_SP_DLEQ',
    251 => 'PSBT_GLOBAL_VERSION',
    252 => 'PSBT_GLOBAL_PROPRIETARY'
  },
  'input' => {
    0 => 'PSBT_IN_NON_WITNESS_UTXO',
    1 => 'PSBT_IN_WITNESS_UTXO',
    2 => 'PSBT_IN_PARTIAL_SIG',
    3 => 'PSBT_IN_SIGHASH_TYPE',
    4 => 'PSBT_IN_REDEEM_SCRIPT',
    5 => 'PSBT_IN_WITNESS_SCRIPT',
    6 => 'PSBT_IN_BIP32_DERIVATION',
    7 => 'PSBT_IN_FINAL_SCRIPTSIG',
    8 => 'PSBT_IN_FINAL_SCRIPTWITNESS',
    9 => 'PSBT_IN_POR_COMMITMENT',
    10 => 'PSBT_IN_RIPEMD160',
    11 => 'PSBT_IN_SHA256',
    12 => 'PSBT_IN_HASH160',
    13 => 'PSBT_IN_HASH256',
    14 => 'PSBT_IN_PREVIOUS_TXID',
    15 => 'PSBT_IN_OUTPUT_INDEX',
    16 => 'PSBT_IN_SEQUENCE',
    17 => 'PSBT_IN_REQUIRED_TIME_LOCKTIME',
    18 => 'PSBT_IN_REQUIRED_HEIGHT_LOCKTIME',
    19 => 'PSBT_IN_TAP_KEY_SIG',
    20 => 'PSBT_IN_TAP_SCRIPT_SIG',
    21 => 'PSBT_IN_TAP_LEAF_SCRIPT',
    22 => 'PSBT_IN_TAP_BIP32_DERIVATION',
    23 => 'PSBT_IN_TAP_INTERNAL_KEY',
    24 => 'PSBT_IN_TAP_MERKLE_ROOT',
    26 => 'PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS',
    27 => 'PSBT_IN_MUSIG2_PUB_NONCE',
    28 => 'PSBT_IN_MUSIG2_PARTIAL_SIG',
    29 => 'PSBT_IN_SP_ECDH_SHARE',
    30 => 'PSBT_IN_SP_DLEQ',
    252 => 'PSBT_IN_PROPRIETARY'
  },
  'output' => {
    0 => 'PSBT_OUT_REDEEM_SCRIPT',
    1 => 'WPSBT_OUT_WITNESS_SCRIPT',
    2 => 'PSBT_OUT_BIP32_DERIVATION',
    3 => 'PSBT_OUT_AMOUNT',
    4 => 'PSBT_OUT_SCRIPT',
    5 => 'PSBT_OUT_TAP_INTERNAL_KEY',
    6 => 'PSBT_OUT_TAP_TREE',
    7 => 'PSBT_OUT_TAP_BIP32_DERIVATION',
    8 => 'PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS',
    9 => 'PSBT_OUT_SP_V0_INFO',
    10 => 'PSBT_OUT_SP_V0_LABEL',
    53 => 'PSBT_OUT_DNSSEC_PROOF',
    252 => 'PSBT_OUT_PROPRIETARY'
  }
}

# ------
# Decode
# ------
# Decode the PSBT

# these variables will be updated when reading through the global map, and are required to be able to read the following input map and output map
inputcount = 0
outputcount = 0

# convert psbt hex string to bytes
bytes = [hex].pack("H*")

# use StringIO to read through the bytes as we go
require "stringio"
buffer = StringIO.new(bytes)

# Header
magic_bytes = buffer.read(4)
separator   = buffer.read(1)

# Global Map
puts "GLOBAL MAP:"
puts
while true do

  # keylen
  keylen = read_compact_size(buffer)

  # if keylen is 0x00, we have hit the separator byte and are at the end of the global map
  if (keylen == 0)
    break
  end

  # use keylen to read keytype and keydata
  keytype_and_keydata = StringIO.new(buffer.read(keylen)) # create another StringIO from the buffer so we can read bytes from it

  # read the keytype
  keytype = read_compact_size(keytype_and_keydata)

  # read the keydata, which is the all the bytes left after the keytype
  keydata = keytype_and_keydata.read

  # valuelen
  valuelen = read_compact_size(buffer)

  # valuedata
  valuedata = buffer.read(valuelen)

  # if keytype = 0x00 (TX), extract the inputcount and outputcount from the raw transaction data given
  if (keytype == 0)

    # convert raw transaction data to StringIO so we can read bytes from it
    tx = StringIO.new(valuedata)
    version = tx.read(4) # ignore

    # input count field
    inputcount = read_compact_size(tx)
    
    # skip past the inputs to get to the outputcount field
    inputcount.times do
      txid_vout = tx.read(36)
      scriptsig_size = read_compact_size(tx)
      scriptsig = tx.read(scriptsig_size)
      sequence = tx.read(4)
    end

    # output count field
    outputcount = read_compact_size(tx)
  end

  # if keytype = 0x04, the inputcount is explicitly provided
  if (keytype == 4)
    inputcount = read_compact_size(StringIO.new(valuedata)) # convert to StringIO to read compact size field
  end

  # if keytype = 0x05, the outputcount is explicitly provided
  if (keytype == 5)
    outputcount = read_compact_size(StringIO.new(valuedata)) # convert to StringIO to read compact size field
  end

  # show keypair data
  puts "  key:   #{keytype} (#{types['global'][keytype]}) #{keydata.unpack('H*')[0]}"
  puts "  value: #{valuedata.unpack('H*')[0]}"
  puts
end


# Input Map(s)
inputcount.times do |i|
  # run through each input
  puts "INPUT #{i} MAP:"
  puts

  # get the keypairs for each input
  while true do

    # keylen
    keylen = read_compact_size(buffer)

    # if keylen is 0x00, we have hit the separator byte and are at the end of the keypairs for this input
    if (keylen == 0)
      break
    end

    # use keylen to read keytype and keydata
    keytype_and_keydata = StringIO.new(buffer.read(keylen)) # create another StringIO from the buffer so we can read bytes from it

    # read the keytype
    keytype = read_compact_size(keytype_and_keydata)

    # read the keydata, which is the all the bytes left after the keytype
    keydata = keytype_and_keydata.read

    # valuelen
    valuelen = read_compact_size(buffer)

    # valuedata
    valuedata = buffer.read(valuelen)

    # show keypair data
    puts "  key:   #{keytype} (#{types['input'][keytype]}) #{keydata.unpack('H*')[0]}"
    puts "  value: #{valuedata.unpack('H*')[0]}"
    puts
  end
end

# Output Map(s)
outputcount.times do |i|
  # run through each output
  puts "OUTPUT #{i} MAP:"
  puts

  # get the keypairs for each output
  while true do

    # keylen
    keylen = read_compact_size(buffer)

    # if keylen is 0x00, we have hit the separator byte and are at the end of the keypairs for this output
    if (keylen == 0)
      break
    end

    # use keylen to read keytype and keydata
    keytype_and_keydata = StringIO.new(buffer.read(keylen)) # create another StringIO from the buffer so we can read bytes from it

    # read the keytype
    keytype = read_compact_size(keytype_and_keydata)

    # read the keydata, which is the all the bytes left after the keytype
    keydata = keytype_and_keydata.read

    # valuelen
    valuelen = read_compact_size(buffer)

    # valuedata
    valuedata = buffer.read(valuelen)

    # show keypair data
    puts "  key:   #{keytype} (#{types['output'][keytype]}) #{keydata.unpack('H*')[0]}"
    puts "  value: #{valuedata.unpack('H*')[0]}"
    puts
  end
end

Encode

# ---------
# Constants
# ---------

# Define constants for global key types
PSBT_GLOBAL_UNSIGNED_TX = 0x00
PSBT_GLOBAL_XPUB = 0x01
PSBT_GLOBAL_TX_VERSION = 0x02
PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03
PSBT_GLOBAL_INPUT_COUNT = 0x04
PSBT_GLOBAL_OUTPUT_COUNT = 0x05
PSBT_GLOBAL_TX_MODIFIABLE = 0x06
PSBT_GLOBAL_SP_ECDH_SHARE = 0x07
PSBT_GLOBAL_SP_DLEQ = 0x08
PSBT_GLOBAL_VERSION = 0xFB
PSBT_GLOBAL_PROPRIETARY = 0xFC

# Define constants for input key types
PSBT_IN_NON_WITNESS_UTXO = 0x00
PSBT_IN_WITNESS_UTXO = 0x01
PSBT_IN_PARTIAL_SIG = 0x02
PSBT_IN_SIGHASH_TYPE = 0x03
PSBT_IN_REDEEM_SCRIPT = 0x04
PSBT_IN_WITNESS_SCRIPT = 0x05
PSBT_IN_BIP32_DERIVATION = 0x06
PSBT_IN_FINAL_SCRIPTSIG = 0x07
PSBT_IN_FINAL_SCRIPTWITNESS = 0x08
PSBT_IN_POR_COMMITMENT = 0x09
PSBT_IN_RIPEMD160 = 0x0A
PSBT_IN_SHA256 = 0x0B
PSBT_IN_HASH160 = 0x0C
PSBT_IN_HASH256 = 0x0D
PSBT_IN_PREVIOUS_TXID = 0x0E
PSBT_IN_OUTPUT_INDEX = 0x0F
PSBT_IN_SEQUENCE = 0x10
PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12
PSBT_IN_TAP_KEY_SIG = 0x13
PSBT_IN_TAP_SCRIPT_SIG = 0x14
PSBT_IN_TAP_LEAF_SCRIPT = 0x15
PSBT_IN_TAP_BIP32_DERIVATION = 0x16
PSBT_IN_TAP_INTERNAL_KEY = 0x17
PSBT_IN_TAP_MERKLE_ROOT = 0x18
PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1A
PSBT_IN_MUSIG2_PUB_NONCE = 0x1B
PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1C
PSBT_IN_SP_ECDH_SHARE = 0x1D
PSBT_IN_SP_DLEQ = 0x1E
PSBT_IN_PROPRIETARY = 0xFC

# Define constants for output key types
PSBT_OUT_REDEEM_SCRIPT = 0x00
PSBT_OUT_WITNESS_SCRIPT = 0x01
PSBT_OUT_BIP32_DERIVATION = 0x02
PSBT_OUT_AMOUNT = 0x03
PSBT_OUT_SCRIPT = 0x04
PSBT_OUT_TAP_INTERNAL_KEY = 0x05
PSBT_OUT_TAP_TREE = 0x06
PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08
PSBT_OUT_SP_V0_INFO = 0x09
PSBT_OUT_SP_V0_LABEL = 0x0A
PSBT_OUT_DNSSEC_PROOF = 0x35
PSBT_OUT_PROPRIETARY = 0xFC

# ---------
# Functions
# ---------
# Utility functions to help with decoding

# Convert an integer to a compact size field
def compact_size(i)
  if (                     i <= 252)                  then compactsize =        [i].pack("C").unpack("H*")[0]
  elsif (i > 252        && i <= 65535)                then compactsize = 'fd' + [i].pack("S<").unpack("H*")[0]
  elsif (i > 65535      && i <= 4294967295)           then compactsize = 'fe' + [i].pack("L<").unpack("H*")[0]
  elsif (i > 4294967295 && i <= 18446744073709551615) then compactsize = 'ff' + [i].pack("Q<").unpack("H*")[0]
  end

  return compactsize
end

# ----
# PSBT
# ----
# Construct an array of maps and key-value pairs to convert to a hex PSBT

psbt = {
  global: {
    0 => [
      {
        key: {type: PSBT_GLOBAL_UNSIGNED_TX, data: ''},
        value: '0200000001f74e3dc9917b0636bfcf1b430531b9c1acc52b971f4b8490a48d13cd0dcf28810000000000fdffffff02353e0000000000001976a9143f308f42364be05715b19ae3077bbbca37eb3c6788acb6590000000000001976a91463ba82d24cc6929cefed39129f0196b64689cb8188ac7fc90d00', # 1 input, 1 output
      },
    ]
  },
  inputs: {
    0 => [
      {
        key: {type: PSBT_IN_NON_WITNESS_UTXO, data: ''},
        value: '02000000015e3165e2d7264adf29793e7cbe8905685c303fe5d043daefc0163f53ebe14a72000000006a473044022014bb64523f91195b0b8a1e0e2adfdee572d4458f88da921e17309a49224b043d022022b4dfb5090507342798f5db1caee17553e07863e5ed479c1777cdff60dcbcba012102a028293d325699e5b7ab175f5433e9e0b690ee3d2f5e8c037e22a6681e199b65fdffffff01cd980000000000001976a914857466f3af8c6221f3ae2e612949ae27e35e332d88ac7fc90d00',
      },
      {
        key: {type: PSBT_IN_BIP32_DERIVATION, data: '03159a15724b33392d0c3a536d00050889657fec4cf27c6303ce97a253171e880e'},
        value: '9c0e47922c00008000000080000000800000000000000000',
      },
    ],
  },
  outputs: {
    0 => [
      {
        key: {type: PSBT_OUT_BIP32_DERIVATION, data: '02a1ddf8e2a12bc231e0a2596d981ecfe31bbc9fecb7781f3332cffc05f1f70a7a'},
        value: '9c0e47922c00008000000080000000800100000000000000',
      },
    ],
    1 => [], # use empty array if an input/output map is expected but no key pairs are needed for it
  },
}

# ------
# Encode
# ------

# buffer
hex = ""

# header
hex += "70736274ff" # magic bytes + separator

# run through each map section (global, input, output)
psbt.each do |mapname, maplist|
  
  # run through the individual maps in each section (one for global, can be multiple input and output maps)
  maplist.each do |map_index, map|
  
  # run through all the keypairs in a map
  map.each do |keypair|
    
    # extract the fields from the array
    keytype = keypair[:key][:type] # integer (will need to convert this to a compact size field)
    keydata = keypair[:key][:data] # bytes
    valuedata = keypair[:value] # bytes
    
    # encode key
    keytype = compact_size(keytype)
    keydata = keydata
    keylen = compact_size((keytype.length / 2) + (keydata.length / 2)) # length of keytype+keydata in bytes
    # note: working with hex _strings_, so need to divide their length by 2 to get their length in _bytes_
    # note: ideally you should be working with the data in raw bytes rather than hex strings
    
    # encode value
    valuelen = compact_size(valuedata.length / 2) # length of valuedata in bytes
    valuedata = valuedata
    
    # add key+value to buffer
    hex += keylen + keytype + keydata + valuelen + valuedata
  end # end of keypairs
  
  # add separator at end of each map
  hex += '00'

  end # end of map
end

# ------
# Result
# ------

# show original data array
require 'json'
puts JSON.pretty_generate(psbt) # encode to json to pretty print the original array
puts

# show PSBT in hex
puts "hex:"
puts hex
puts

# show PSBT in base64
require 'base64'
base64 = Base64.encode64([hex].pack('H*')) # need to convert hex string to raw bytes before converting to base64
puts "base64:"
puts base64

The only tricky part about decoding PSBTs is making sure you grab the correct number of inputs and outputs from the global map (via the PSBT_GLOBAL_UNSIGNED_TX, or the PSBT_GLOBAL_INPUT_COUNT and PSBT_GLOBAL_OUTPUT_COUNT keys). This is needed so that you can correctly determine whether the upcoming maps are either input or output maps.

Make sure you interpret the keytype as a compact size field instead of a single byte. It's usually only 1-byte, but it's technically a compact size field that can expand to include more key types in the future. So you want to account for that.

Key Types

Each map within a PSBT uses a variety of key types to store data.

These key types are represented by integers, and indicate the type of data stored in the key data and value data fields for the key-value pair.

Below is a list of all the different key types available for each map, along with descriptions of the data stored.

The keys within each map do not need to be in numerical order. However, it's useful to put them in numerical order if you can to help compare the structure of PSBTs as they get updated during usage.

The key type integers are specific to a map type. Different maps may use the same integers, but they represent different key types depending on which map you're in. Therefore, you cannot determine the exact key type from the integer alone, as you also need to know the map you're using it in.

The descriptions below are taken directly from BIP 174

Global Map (11)

Input Map (31)

Output Map (13)

Usage

How do you use PSBTs?

A PSBT gets processed through multiple steps before being converted into the final signed transaction.

You'd think that it would just be a 2-step process, like this:

  1. Create — Create the PSBT and add all the extra data needed for signing.
  2. Sign — Sign the inputs using the extra data and return the signed transaction.

However, these two steps can be broken down in to further steps (or "roles"), where each role has a specific job for updating the state of the PSBT:

  1. Creator — Create the initial PSBT with the unsigned transaction data.
    • Constructor — An additional role for v2 PSBTs, used for adding inputs and outputs to the unsigned transaction data.
  2. Updater — Add the extra data required to sign the inputs.
  3. Signer — Sign the inputs using the extra data and add the raw signatures to the PSBT.
  4. Input Finalizer — Use the raw signatures to construct the complete unlocking code for the inputs.
  5. Transaction Extractor — Convert the PSBT into the raw signed transaction data ready to broadcast to the network.

There is also a helpful Combiner role, which can be used at any intermediate step:

The above steps do not have to be executed in a single sequence. For example, you could Update/Sign/Finalize one input for a PSBT, then send it to someone else who can go back and Update/Sign/Finalize another input.

Most of the time a single wallet/entity will perform multiple roles at once. For example, when using a hardware device to sign a v0 PSBT, the watch-only wallet will be the Creator/Updater, and the hardware device will be the Signer/Input Finalizer/Transaction Extractor (the Combiner role is not needed).

Nonetheless, each individual role is specialized, and these roles allow you to split the work between multiple wallets/entities for maximum flexibility when processing PSBTs.

Below is a list of the roles for both version 0 and version 2 PSBTs. There are slight variations, but the overall process is generally the same.

Version 0

Diagram showing an overview of the individual roles/steps involved in processing a v0 PSBT.

1. Creator

A Creator creates a new PSBT.

It constructs the initial unsigned transaction data, and stores it in the following key:

This unsigned transaction data should be a serialized transaction with empty ScriptSig and Witness fields.

  • The input map(s) and output map(s) must be empty.
  • PSBT_GLOBAL_VERSION can optionally be set to 0. However, it is 0 by default if not included.

2. Updater

An Updater adds the extra data needed to create signatures for the unsigned transaction data.

This almost always involves including the required data from the previous transaction(s) for each input via the following keys:

Optionally, the Updater can also specify a SIGHASH type for each input:

If an input is a P2SH or P2WSH, it should also include the custom script that was used (i.e. Redeem Script for a P2SH, or Witness Script for a P2WSH) to create the Script Hash placed in the ScriptPubKey on the output.

These original custom scripts are required to be able to unlock P2SH and P2WSH inputs.

These custom scripts should have been stored by the wallet that created the address for receiving payments.

Lastly, an Updater can also add useful information about the public keys (and derivation paths) used for the inputs and outputs.

The above keys help wallets to identify which inputs they can sign for, as well as which outputs are used for change. A wallet can identify both of these things without these keys, but including them helps a wallet to identify them faster.

Hardware wallets may rely on these keys to identify the inputs they can sign for, as they may not have the processing power to do so without them.

In summary, an Updater should add any extra data to the PSBT that it has access to. The main goal of an Updater is to add all the relevant data to the input maps so that a Signer has everything they need to sign the inputs.

A PSBT can be passed between multiple Updaters to get all the extra data needed for signing (e.g. if a single Updater does not have access to all the information required).

3. Signer

A Signer uses the data already provided in the PSBT to create signatures for the inputs.

The resulting signature is placed within the following key for each input map:

If an input requires multiple signatures (e.g. a P2MS style locking script), the input maps will end up with multiple PSBT_IN_PARTIAL_SIG keys.

The PSBT_IN_PARTIAL_SIG key data field contains the public key for the corresponding signature. This prevents duplicate keys if there are multiple signatures in a single input map.

Signers should sign as many inputs in the PSBT as they can, assuming they have the private keys to sign for them.

A PSBT can be passed between multiple Signers if multiple signatures are required from different people, with each adding PSBT_IN_PARTIAL_SIG keys to the inputs as they go.

  • A Signer should check the transaction to make sure they agree with it before adding their signature.
  • A Signer can use the PSBT_IN_BIP32_DERIVATION keys to detect which inputs to sign, and the PSBT_OUT_BIP32_DERIVATION keys to help detect the change outputs.
  • Detecting change outputs can be useful when displaying the transaction details to the wallet user before signing.
Checks

A Signer should perform checks before signing an input.

These checks depend on the data provided in the input map:

4. Input Finalizer

An Input Finalizer takes the signatures from the PSBT_IN_PARTIAL_SIG keys (added by the Signer(s)) and uses them to construct the complete unlocking code for each input.

The resulting unlocking scripts are placed within either of the following keys:

The key you use depends on whether the input is unlocked via the ScriptSig field (e.g. P2PK, P2PKH, P2SH) or the Witness field (e.g. P2WPKH, P2WSH, P2TR).

All other keys (except for PSBT_IN_NON_WITNESS_UTXO and PSBT_IN_WITNESS_UTXO) should be cleared from the input map after the unlocking code has been added.

The UTXO keys are kept so that the Transaction Extractor can verify that the final transaction is correct.

If an Input Finalizer does not yet have enough data to create the code to unlock an input (e.g. not enough signatures to unlock a multisig), the input map should not be updated.

5. Transaction Extractor

A Transaction Extractor converts the PSBT into the final signed transaction.

It takes the data from the PSBT_IN_FINAL_SCRIPTSIG and PSBT_IN_FINAL_SCRIPTWITNESS keys for each input, and inserts it into the raw unsigned transaction data in PSBT_GLOBAL_UNSIGNED_TX.

The result is a serialized transaction that is ready to be broadcast to the network.

Every input map must have a PSBT_IN_FINAL_SCRIPTSIG or PSBT_IN_FINAL_SCRIPTWITNESS to be able to construct the final signed transaction.

Combiner

A Combiner merges multiple PSBTs into a single PSBT.

The resulting PSBT must contain all of the key-value pairs from each of the individual PSBTs. Any duplicates must be removed.

If there are key-value pairs with the same key but different values, the Combiner can choose which key-value pair to keep. However, the Combiner can refuse to combine the PSBTs if there are likely to be inconsistencies or conflicts in the resulting PSBT.

PSBTs should only be combined if they are for the same unsigned transaction. The unique identifier for a v0 PSBT is the unsigned transaction data in PSBT_GLOBAL_UNSIGNED_TX.

Version 2

A v2 PSBT splits up the unsigned transaction data and stores in separate keys across all three maps.

This design change makes it easier to add and remove inputs from the PSBT before signing.

To accommodate for this change, a new role has been added (Constructor), and some of the existing roles have been modified slightly. But there's not a massive difference overall, and the general process for working with PSBTs remains the same.

Diagram showing an overview of the individual roles/steps involved in processing a v2 PSBT.

1. Creator

A v2 Creator creates a new PSBT.

However, in v2, the Creator does not include the unsigned transaction data just yet. Instead, it simply adds the basic settings for the PSBT in the global map.

Firstly, the PSBT version number must be set to 2 to indicate that you're using PSBT v2:

Next, set the version number for the transaction, and set an optional fallback value for the transaction's locktime:

Lastly, the modifiable field should be set to allow for inputs and outputs to be added to the PSBT:

However, this last field can be omitted if the Creator is also a Constructor.

2. Constructor

A v2 Constructor adds the inputs and outputs to be included in the unsigned transaction data.

For each new input, the following keys must be added to the input's map:

These keys allow you to reference the outputs you want to use as inputs to the transaction.

In addition, each input can specify a locktime (if needed):

You can add one, both, or neither.

If you're adding an input with just one of these locktime key types, you must check that it does not conflict with the locktime key types on the other input maps. For example, if one input already has PSBT_IN_REQUIRED_TIME_LOCKTIME only, you cannot add an input with PSBT_IN_REQUIRED_HEIGHT_LOCKTIME only (and vice versa).

Furthermore, if the PSBT has already been signed, you cannot add an input that specifies a locktime that will change the entire transaction's final locktime value.

See the Locktime Calculation section in the Transaction Extractor role for details.

For each new output, the following keys must be added to the output's map:

After adding a new input or output to the PSBT, the count fields in the global map must be incremented:

Lastly, if no more inputs or outputs should be added to the transaction, this should be declared by updating or removing the modifiable key in the global map:

If the PSBT_GLOBAL_TX_MODIFIABLE key has the Has SIGHASH_SINGLE flag (bit 2) set to True, you must iterate through the inputs and find the inputs which have signatures that use SIGHASH_SINGLE. The same number of inputs and outputs must be added before those inputs and their corresponding outputs.

3. Updater

A v2 Updater works the same as a v0 Updater.

However, it can also set the sequence number for each input:

If this key is not set, the sequence number defaults to 0xFFFFFFFF.

4. Signer

A v2 Signer is the same as a v0 Signer.

However, after signing an input, a v2 Signer should update the modifiable key to reflect the state of the PSBT:

This field must be updated based on the SIGHASH type the Signer has added for the input:

5. Input Finalizer

A v2 Input Finalizer works the same as a v0 Input Finalizer.

However, after adding the relevant PSBT_IN_FINAL_SCRIPTSIG or PSBT_IN_FINAL_SCRIPTWITNESS, the following v2 keys should not be removed from the input maps (if present):

These keys are needed by the Transaction Extractor to construct the final signed transaction.

6. Transaction Extractor

A v2 Transaction Extractor converts the PSBT into a raw signed transaction that can be broadcast to the network.

However, in v2 this step is a little more involved as you first need to build the unsigned transaction data from the individual keys added to the PSBT by the v2 Creator and v2 Constructor.

You will also need to calculate the locktime field for the transaction by looking at the relevant locktime keys across the global map and input map(s).

Lastly, the PSBT_IN_FINAL_SCRIPTSIG and PSBT_IN_FINAL_SCRIPTWITNESS fields (added by the Input Finalizer) are merged in to the unsigned transaction data to form the final signed transaction.

In v0, the complete unsigned transaction data is already prepared in the PSBT_GLOBAL_UNSIGNED_TX. In v2, the unsigned transaction data needs to be built from the data stored in the individual keys across the global map, input map(s), and output map(s).

Locktime Calculation

To calculate the locktime field for the transaction, you need to run through each input map to check the individual locktime values that may have been set for each input.

The basic algorithm is as follows:

  1. If one or more inputs has a specified locktime value, use the highest locktime value present across the inputs.*
    1. If no input has a specified locktime value, use the fallback value in the global map (PSBT_GLOBAL_FALLBACK_LOCKTIME).
      1. If a fallback locktime is not set, use the default locktime value of 0 (i.e. no locktime).

*For each input there are actually two different locktime key types that can be specified:

  1. PSBT_IN_REQUIRED_TIME_LOCKTIME — Specifies the desired transaction locktime in seconds (Unix Time).
  2. PSBT_IN_REQUIRED_HEIGHT_LOCKTIME — Specifies the desired transaction locktime as a block height.

To determine which locktime type to use, you need to check which type(s) are supported across all of the inputs:

If both types are supported across all inputs (i.e. you have a choice between the two), you must use the highest value PSBT_IN_REQUIRED_HEIGHT_LOCKTIME. This is because it takes precedence over PSBT_IN_REQUIRED_TIME_LOCKTIME (even if it has a higher value overall).

However, if there is a conflict where one or more input maps support one locktime type only, and one or more inputs support the other locktime type only, the transaction locktime field cannot be calculated.

Ideally a Constructor will not add an input with a conflicting locktime key. But a Transaction Extractor still needs to check for conflicts when determining the final value for the transaction's locktime field.

Combiner

A v2 Combiner works the same as a v0 Combiner.

Format

What data format do PSBTs use?

There are two "official" formats for PSBTs:

  1. Binary (File) — Used for passing a PSBT around as a file (using the file extension .psbt).
  2. Base64 (Text) — Used for copying/pasting, or storing a PSBT as text.

So if you export a PSBT from a wallet, you should either get a binary file or a base64 string. Either is fine, and they both represent the same underlying data, so you can convert directly between both formats:

# convert binary file to base64 string
base64 -w0 file.psbt
# note: -w0 prevents line breaks, which can cause problems when decoding

# convert base64 string to binary file
echo "base64psbtgoeshere" | base64 -d > file.psbt

But sometimes it's easier to inspect PSBTs in hex format, so it may be useful to convert a PSBT from a binary/base64 to hex format if needed:

# convert binary file to hex string (without formatting)
xxd -p file.psbt | tr -d '\n' && echo ''

# convert base64 string to hex string (without formatting)
echo "base64psbtgoeshere" | base64 -d | xxd -p | tr -d '\n' && echo ''

Hex Format. I've displayed PSBT data in hex format throughout this page, as that's generally easier to use when testing/debugging. But it's not technically a format that PSBTs should be passed around in, and most wallets won't support exporting/importing a PSBT in hex format.

History

When were PSBTs first introduced?

The original idea of having a standard format for exporting unsigned transactions was proposed by Pieter Wuille (who also came up with the name "Partially Signed Bitcoin Transactions"). The actual PSBT format was then designed and implemented by Ava Chow.

The first version (PSBTv0) was introduced in 2017:

The second version (PSBTv2) was introduced in 2021:

This second version uses the same PSBT data structure as before, but introduces new key types and modifies some roles to allow for inputs and outputs to be added to the unsigned transaction data before signing. So it's basically a slightly more flexible version compared to PSBTv0.

Bitcoin Core does not yet offer functionality to support PSBTv2, but it is currently supported by other third-party wallets such as Sparrow Wallet and Coldcard.

There is no PSBTv1. The first version was called v0, and seeing as this was commonly referred to as the "first version", the second version was called v2 instead of v1 to help prevent confusion (except for the confusion about why there's no "v1", of course).

Summary

A PBST is just a transaction that includes all the extra data needed to sign it.

Most modern wallets understand the PSBT format, so if you're creating some software that can export/import unsigned transactions, you're going to want to get the hang of handling PSBTs.

They may seem intimidating at first due to all the available key types, but you don't need to understand all of them to take advantage of PSBTs — you only need the ones that are relevant to your software's needs. The rest are just there if you need them.

So if you're getting started with PSBTs, start by figuring out how to decode and encode them. From there, you can just use the keys you need for signing the specific input types that your wallet needs to handle.

Most wallets only implement the functionality to handle a subset of all the possible key types. For example, to process a v0 PSBT for signing a simple input type like P2PKH, you only need to work with the following key types:

So as always, just start with the basic functionality and go from there. You can figure out how to use the other key types as you go.

Have fun.

Resources

Thanks