Skip to content

Carry the PSBT finalization-clearing findings upstream: one PR to Core, one to the BIPs #696

Closed

Description

@fametrano

Split off from #446, which found that BIP174, BIP371 and Bitcoin Core
disagree about which fields finalizing a PSBT input clears, and settled
what btclib does. This is the delivery half: the two pull requests, and
what each needs checked before it is proposed.

No btclib code changes here. #446 concluded "btclib stays as it is",
and the position is not only implemented but pinned:
_FINALIZED_KEEPS in btclib/psbt/psbt.py names the ten fields that
survive, and
tests/psbt/psbt_test.py::test_a_finalized_input_keeps_the_utxo_and_the_unknown_fields_only
walks every other field of PsbtIn for both a p2wsh and a p2tr
input, so a field added to the dataclass and left out of both lists fails
the suite. That test spells the list out rather than importing it, on the
stated grounds that a test importing _FINALIZED_KEEPS would agree with a
wrong list as readily as with a right one.

Re-verified today, before proposing anything

#446 pinned Core at c36ffd87 and bips at 01d2c841. Both claims still
hold at the current tips:

  • Core 4c045f1a0323 (2026-08-12), src/psbt.cpp:361: the
    sigdata.complete branch of PSBTInput::FromSignatureData clears
    partial_sigs, hd_keypaths, redeem_script and witness_script,
    and nothing else. Grepping the whole file for a clear() on any
    taproot, MuSig2 or preimage field, or a reset of the sighash type,
    returns nothing.
  • bips c38071c8c45a (2026-08-10), bip-0371.mediawiki: the
    description of PSBT_OUT_TAP_BIP32_DERIVATION = 0x07 still ends
    "Finalizers should remove this field after
    PSBT_IN_FINAL_SCRIPTWITNESS is constructed" — a finalizer clause on
    an output field, referring to an input field, with no indication of
    which input. Its two neighbours in the same table carry no such clause.

The pull request to Core

Clear the six BIP371 input fields (PSBT_IN_TAP_KEY_SIG through
PSBT_IN_TAP_MERKLE_ROOT, 0x13 to 0x18) in the sigdata.complete
branch, beside the four already there, with a functional test showing a
finalized taproot input carrying no derivation path.

Scope it to those six and no further: each is an explicit per-field
"should" in BIP371, which makes the change reviewable on its own, whereas
the sighash type and the four preimage maps Core also keeps are covered
only by BIP174's general sentence and belong to the BIP discussion below.

What to check before proposing, and to state in the PR as having been
checked: that nothing downstream of FinalizePSBT reads a taproot field,
FinalizeAndExtractPSBT and the wallet's own paths included.

The privacy asymmetry this closes is the one worth leading with: Core
clears hd_keypaths and leaves m_tap_bip32_paths, so a finalized
taproot input goes on publishing the master fingerprint and derivation
path of every key where a finalized ECDSA input publishes none. Core
#30294 asks for this
metadata to be strippable and
#35370 is building an
RPC eraser for it — both worth re-reading for their current state before
citing them, since either may have moved.

The pull request to the BIPs

Two changes, and the first is the one that matters:

  • Amend BIP174's sentence so a version 2 psbt survives it. "All
    other data except the UTXO and unknown fields ... should be cleared"
    was written when the unsigned transaction was a global field. BIP370
    puts the transaction's own data in the input map, where
    PSBT_IN_PREVIOUS_TXID and PSBT_IN_OUTPUT_INDEX are required and a
    psbt missing either is invalid; BIP370 never mentions the Finalizer, so
    nothing amends the sentence for the version it defines. A finalizer
    following BIP174 literally on a v2 psbt produces one that cannot be
    parsed back. _FINALIZED_KEEPS keeps those five fields precisely
    because of this, so btclib is already the worked example.
  • Drop the finalizer clause from PSBT_OUT_TAP_BIP32_DERIVATION, or
    say which input it means. It reads as a copy-paste from the input
    table, and no implementation clears output fields on finalization.

The precedent to follow is
bips#1396, which kept the
input UTXO through finalization to fix a conflict with BIP78: a conflict
between the sentence and a later document, fixed in the sentence. The
other two merged PRs that touched it —
#771 naming the finalized
fields and #2183 adding the
proprietary clause — went nowhere near these points.

Whether the sighash type and the four preimage maps should also survive
is the open question Core's behaviour raises. It belongs in this PR's
discussion rather than being decided here, and btclib clears them today
on the grounds that what they buy after finalization is nothing: the
preimage a hash-locked script needs is in the witness by then.

How this gets filed

Both drafts go on this issue before they go upstream, and every citation
is re-derived against the tip at the moment of submission rather than
against the shas above: a report has already been closed upstream for
citing a blob id as though it were a commit.

Activity

  1. fametrano commented on Aug 14, 2026

    @fametrano
    MemberAuthor

    Re-verified 2026-08-14, at today's tips

    Both claims still hold, and the two drafts below are ready to send.

    • Core 11090c8b (2026-08-13), src/psbt.cpp:361
      PSBTInput::FromSignatureData: the sigdata.complete branch (363-376)
      clears partial_sigs, hd_keypaths, redeem_script, witness_script
      and nothing else. The only other clear() calls in the file are at 182
      and 183 (a different path) and 773/778/785 (a local to_drop).
    • bips 60f5b33b (2026-08-13), bip-0371.mediawiki: the six input
      fields carry "Finalizers should remove this field after
      PSBT_IN_FINAL_SCRIPTWITNESS is constructed" (L51, 61, 71, 81, 91,
      101), and so does the output field PSBT_OUT_TAP_BIP32_DERIVATION
      (L145). Its two neighbours in the output table,
      PSBT_OUT_TAP_INTERNAL_KEY and PSBT_OUT_TAP_TREE, do not.
    • bips bip-0174.mediawiki:523 is the sentence, unchanged.
    • The two Core references are both still open:
      issue #30294 (about
      walletprocesspsbt emitting output bip32_derivs even with
      bip32derivs=false) and
      PR #35370 ("rpc: add
      key-origin modes to PSBT processing RPCs", last touched 2026-07-11). No
      open Core PR clears taproot fields at finalization, so this does not
      duplicate anything.
    • The three bips precedents are as cited:
      #1396 merged 2025-01-14,
      #771 merged 2020-06-12,
      #2183 merged 2026-05-30.
    • bitcoin/bips has issues disabled, so the BIP half can only be a
      pull request. Noting it because it removes the "file an issue first to
      ask" option that the Core half still has.

    PR 1, to bitcoin/bitcoin

    Title: psbt: clear BIP371 taproot fields when an input is finalized

    Body:

    PSBTInput::FromSignatureData clears partial_sigs, hd_keypaths,
    redeem_script and witness_script once sigdata.complete, and leaves
    every BIP371 taproot field in place. BIP371 asks for the opposite: each
    of the six input fields PSBT_IN_TAP_KEY_SIG (0x13) through
    PSBT_IN_TAP_MERKLE_ROOT (0x18) is documented "Finalizers should remove
    this field after PSBT_IN_FINAL_SCRIPTWITNESS is constructed".

    The visible effect is a privacy asymmetry between input types: a
    finalized ECDSA input publishes no derivation path, while a finalized
    taproot input still carries PSBT_IN_TAP_BIP32_DERIVATION — the master
    key fingerprint and derivation path of every key involved — to whoever
    the psbt is handed to next.

    This clears the six fields beside the four already cleared, and adds a
    functional test showing a finalized taproot input carrying no derivation
    path.

    Scope: the six BIP371 input fields only. The sighash type and the four
    preimage maps Core also keeps are covered only by BIP174's general
    sentence, whose wording is itself under discussion; they are deliberately
    left alone here.

    Implementation: in the sigdata.complete branch of
    PSBTInput::FromSignatureData (src/psbt.cpp:363), beside the four
    existing clear() calls, reset m_tap_key_sig, m_tap_script_sigs,
    m_tap_scripts, m_tap_bip32_paths, m_tap_internal_key and
    m_tap_merkle_root (the last two are XOnlyPubKey and uint256, so
    assignment of a default rather than clear()). Names read from
    src/psbt.h:306-311.

    What to check before proposing, and to state in the PR as checked: that
    nothing downstream of FinalizePSBT reads a taproot field —
    FinalizeAndExtractPSBT, the wallet's signing paths, and
    PSBTInput::FillSignatureData, which returns early on
    sigdata.complete (src/psbt.cpp:307) and therefore cannot be the
    consumer, but the RPC layer that serializes a finalized psbt back to the
    user can be.

    Test: test/functional/rpc_psbt.py or wallet_taproot.py — finalize a
    taproot input and assert the decoded psbt input has no
    taproot_bip32_derivs, taproot_internal_key, taproot_key_path_sig.

    PR 2, to bitcoin/bips

    Title: bip-0174, bip-0371: fix the finalizer clearing rules

    Two independent commits, so a reviewer can take one and refuse the other.

    Commit 1 — BIP174: a v2 psbt must survive finalization. L523's "All
    other data except the UTXO and unknown fields ... should be cleared" was
    written when the unsigned transaction was a global field. BIP370 moves the
    transaction's own data into the input map, where PSBT_IN_PREVIOUS_TXID
    and PSBT_IN_OUTPUT_INDEX are required and a psbt missing either is
    invalid; BIP370 never mentions the Finalizer, so nothing amends the
    sentence for the version it defines. A finalizer following BIP174 literally
    on a v2 psbt produces one that cannot be parsed back. The edit adds the
    version 2 required fields to the exception list, in the shape
    #1396 used for the UTXO.
    btclib's _FINALIZED_KEEPS is the worked example and can be cited.

    Commit 2 — BIP371: drop the finalizer clause from
    PSBT_OUT_TAP_BIP32_DERIVATION
    (L145), or say which input it means. It
    refers to PSBT_IN_FINAL_SCRIPTWITNESS from an output field, names no
    input, and no implementation clears output fields on finalization.

    Open question to raise in the PR discussion rather than decide in the
    diff
    : whether the sighash type and the four preimage maps should also
    survive, since Core keeps them today. btclib clears them, on the grounds
    that a hash-locked script's preimage is in the witness by the time the
    input is final.

    Order: Core first. If it merges, the BIP PR can say "implemented in
    Core as of <sha>", which is the argument the BIP editors respond to.

    Verified against btclib d7c4b3d5.

  2. fametrano commented on Sep 7, 2026

    @fametrano
    MemberAuthor

    2026-09-07: the Core half is withdrawn, measured

    The premise of "PR 1, to bitcoin/bitcoin" is wrong. PSBTInput::Serialize (src/psbt.h:371 at master 19b0ff2fa0) writes partial signatures, sighash, scripts, keypaths, preimages, the six taproot fields and the MuSig2 fields only when the input has no final scriptSig and no final scriptWitness; the block runs to line 506. A finalized input leaves Core with the UTXO, the final scripts, the PSBTv2 input fields, proprietary and unknown fields, and nothing else. The gate dates from c6c3d42a7d (2019-02-11).

    Verified in three ways on a worktree at 19b0ff2fa0:

    • the six clears added to the sigdata.complete branch of FromSignatureData, plus a wallet_taproot.py check that a finalized taproot input has no taproot field: green;
    • the same check with master's psbt.cpp: also green, so the change is unobservable;
    • a direct probe (walletprocesspsbt(finalize=False) → finalizepsbt(extract=False) → decodepsbt) on a taproot key path input: after finalization the input has final_scriptwitness, previous_txid, previous_vout, sequence, witness_utxo only.

    So there is no privacy asymmetry on the wire, Core already drops the sighash type and the preimages too, and Core and btclib keep the same set of fields. No pull request to Core. The three btclib passages that claim the opposite are #1841.

    Two smaller corrections to the drafts above: bips#1396 touches only bip-0078, not the BIP174 sentence, which is unchanged since f515da0 (2018-07-05) apart from the proprietary clause added in a4668bb (#2183); and BIP370, not BIP174, is where the v2 rule belongs, since BIP370 already amends every other role for v2 and has no Input Finalizer section.

    "PR 2, to bitcoin/bips" stands, reshaped: a commit adding an Input Finalizer section to BIP370 that keeps the five v2 input fields, citing Core's serializer as the deployed behaviour, and the BIP371 commit dropping the clause from PSBT_OUT_TAP_BIP32_DERIVATION. Branch under local review now; link to follow.

  3. fametrano commented on Sep 7, 2026

    @fametrano
    MemberAuthor

    Filed: bitcoin/bips#2277

    bitcoin/bips#2277 — branch fametrano:psbt-finalizer-clearing, two commits over master 09e2103: 4901287 adds an Input Finalizer section to BIP 370 keeping the five PSBTv2 input fields, f5034ac drops the finalizer clause from PSBT_OUT_TAP_BIP32_DERIVATION in BIP 371. +6/−1. Local review, two rounds, cleared the content; typos, link-format-chk.sh and diffcheck.sh pass.

    Closing: the Core half is withdrawn (comment above), and nothing in btclib depends on the BIP outcome. _FINALIZED_KEEPS already keeps the five fields, and test_a_finalized_input_keeps_the_utxo_and_the_unknown_fields_only pins it. The three passages that misstate Core's behaviour are #1841.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstream-reportA report owed to an upstream project

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions