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_sigandbip32_derivationrecords must be 33-byte compressed SEC keys: decoded keys are stored asSecp256k1.Pointstructs, 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_b64is 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, andfinal_scriptsigvalues must parse as Script: they are stored asBitcoinex.Scriptstructs, 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
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).
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.
Decodes a base64 encoded string into a PSBT.
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.
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.
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.
@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.
@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.
Returns the txid of the PSBT's global unsigned transaction.