View Source Bitcoinex.PSBT (bitcoinex v0.3.0)

Support for Partially Signed Bitcoin Transactions (PSBT).

The format consists of key-value maps. Each map consists of a sequence of key-value records, terminated by a 0x00 byte.

Reference: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki

Only PSBT version 0 (BIP-174) is supported; version 2 (BIP-370) and the BIP-371 taproot fields are not.

Known limitations (deliberate):

  • Public keys in partial_sig and bip32_derivation records must be 33-byte compressed SEC keys: decoded keys are stored as Secp256k1.Point structs, which re-serialize compressed, so legacy uncompressed (65-byte) keys — permitted by BIP-174 — are rejected with {:error, :uncompressed_public_key} rather than silently re-encoded to a different key.
  • Serialization always emits records in ascending key-type order with unknown records last (as Bitcoin Core does). decode |> encode_b64 is byte-identical for canonically-ordered PSBTs; a PSBT whose records arrive in a different order decodes to the same struct but re-encodes canonically.
  • redeem_script, witness_script, and final_scriptsig values must parse as Script: they are stored as Bitcoinex.Script structs, so a value whose bytes are not valid script (e.g. a truncated push) is rejected with {:error, :invalid_script}. BIP-174 treats these fields as opaque byte strings, so this is stricter than Bitcoin Core — which accepts (almost certainly unspendable) unparseable scripts.

Link to this section Summary

Functions

Adds a field to the PSBT's global map (the BIP-174 Updater role). See Bitcoinex.PSBT.Global.add_field/3 for the accepted fields.

Adds a field to the input map at index. See Bitcoinex.PSBT.In.add_field/3 for the accepted fields.

Adds a field to the output map at index. See Bitcoinex.PSBT.Out.add_field/3 for the accepted fields.

Combines two PSBTs into one (the BIP-174 Combiner role).

Decodes a base64 encoded string into a PSBT.

Encodes a PSBT as base64.

Extracts the fully-signed network transaction from a finalized PSBT (the BIP-174 Transaction Extractor role). Returns {:error, :not_finalized} unless every input is finalized.

Finalizes every input that can be finalized (the BIP-174 Input Finalizer role), leaving the rest untouched (best-effort, matching Bitcoin Core). For each finalizable input it builds the final_scriptsig and/or final_scriptwitness from the collected signatures and scripts and removes the now-redundant fields.

Returns true if every input has been finalized. A PSBT with no inputs at all (or a hand-built one with inputs: nil) is not considered finalized — there is nothing extractable in it.

Decodes a binary-encoded PSBT file.

Builds a PSBT from an unsigned transaction (the BIP-174 Creator role).

to_file writes a PSBT to file as binary.

Returns the txid of the PSBT's global unsigned transaction.

Link to this section Types

@type t() :: %Bitcoinex.PSBT{
  global: Bitcoinex.PSBT.Global.t(),
  inputs: [Bitcoinex.PSBT.In.t()],
  outputs: [Bitcoinex.PSBT.Out.t()]
}

Link to this section Functions

Link to this function

add_global_field(psbt, field, tx)

View Source
@spec add_global_field(t(), atom(), any()) :: {:ok, t()} | {:error, atom()}

Adds a field to the PSBT's global map (the BIP-174 Updater role). See Bitcoinex.PSBT.Global.add_field/3 for the accepted fields.

Link to this function

add_input_field(psbt, index, field, utxo)

View Source
@spec add_input_field(t(), non_neg_integer(), atom(), any()) ::
  {:ok, t()} | {:error, atom()}

Adds a field to the input map at index. See Bitcoinex.PSBT.In.add_field/3 for the accepted fields.

Link to this function

add_output_field(psbt, index, field, value)

View Source
@spec add_output_field(t(), non_neg_integer(), atom(), any()) ::
  {:ok, t()} | {:error, atom()}

Adds a field to the output map at index. See Bitcoinex.PSBT.Out.add_field/3 for the accepted fields.

@spec combine(t(), t()) :: {:ok, t()} | {:error, atom()}

Combines two PSBTs into one (the BIP-174 Combiner role).

Both PSBTs must describe the same global unsigned transaction — compared by full serialization, byte for byte — otherwise {:error, :mismatched_tx} is returned; a PSBT lacking an unsigned transaction altogether yields {:error, :missing_unsigned_tx}, and PSBTs whose input or output map counts do not match yield {:error, :map_count_mismatch} rather than silently dropping maps. Each map is merged field by field: singleton fields must agree where both are set, and repeatable fields are unioned by key. A record present in both maps under the same key but with a different value yields {:error, :conflicting_field}.

The union keeps the first PSBT's records in their existing order and appends records new from the second (Bitcoin Core's merge semantics), so combine/2 is idempotent (combine(a, a) == {:ok, a}) and commutative up to record order for non-conflicting inputs.

@spec decode(String.t()) :: {:ok, t()} | {:error, term()}

Decodes a base64 encoded string into a PSBT.

@spec encode_b64(t()) :: {:ok, String.t()} | {:error, :missing_unsigned_tx}

Encodes a PSBT as base64.

Returns {:ok, base64}, or {:error, :missing_unsigned_tx} for a PSBT without a global unsigned transaction (see to_file/2).

@spec extract_tx(t()) :: {:ok, Bitcoinex.Transaction.t()} | {:error, :not_finalized}

Extracts the fully-signed network transaction from a finalized PSBT (the BIP-174 Transaction Extractor role). Returns {:error, :not_finalized} unless every input is finalized.

@spec finalize(t()) :: t()

Finalizes every input that can be finalized (the BIP-174 Input Finalizer role), leaving the rest untouched (best-effort, matching Bitcoin Core). For each finalizable input it builds the final_scriptsig and/or final_scriptwitness from the collected signatures and scripts and removes the now-redundant fields.

Supported spend types: p2pkh, p2wpkh, p2sh-p2wpkh, bare/p2sh/p2wsh multisig, and p2sh-p2wsh. Anything else — bare p2pk, p2sh-wrapped p2pkh, arbitrary scripts, taproot — is left unfinalized (not an error), so an untouched input can mean "unsupported spend type" as well as "missing signatures". A non-witness spend type additionally requires a non_witness_utxo whose txid matches the input's outpoint; a witness_utxo alone cannot be verified and is never trusted to finalize a non-witness input (BIP-174 Signer checks).

Deliberate divergence from BIP-174: when an input specifies a sighash_type, the BIP says the finalizer "must fail to sign" if any partial_sig carries a different flag; this implementation instead treats such signatures as ineligible and finalizes if enough matching ones remain, so a stray signature contributed for an unrelated key (e.g. picked up in a combine/2) does not block an otherwise-complete input. Every signature actually placed in a final_scriptsig/final_scriptwitness always carries the required flag.

@spec finalized?(t()) :: boolean()

Returns true if every input has been finalized. A PSBT with no inputs at all (or a hand-built one with inputs: nil) is not considered finalized — there is nothing extractable in it.

@spec from_file(String.t()) :: {:ok, t()} | {:error, term()}

Decodes a binary-encoded PSBT file.

@spec from_tx(Bitcoinex.Transaction.t()) :: {:ok, t()} | {:error, atom()}

Builds a PSBT from an unsigned transaction (the BIP-174 Creator role).

The transaction must be unsigned: every input's scriptSig must be empty and it must carry no witnesses. Returns one empty input map per transaction input and one empty output map per transaction output.

Link to this function

to_file(packet, filename)

View Source
@spec to_file(t(), String.t()) :: :ok | {:error, File.posix() | :missing_unsigned_tx}

to_file writes a PSBT to file as binary.

Returns {:error, :missing_unsigned_tx} for a PSBT without a global unsigned transaction: it is mandatory in v0, so serializing without one would emit a PSBT this module's own decoder rejects.

@spec txid(t()) :: {:ok, String.t()} | {:error, :no_unsigned_tx}

Returns the txid of the PSBT's global unsigned transaction.