PSBT: SignPSBTInput rejects valid 65-byte taproot script signatures when PSBT_IN_SIGHASH_TYPE is absent #36507

issue GracedEternalKingCabbageMan opened this issue on October 13, 2026
  1. GracedEternalKingCabbageMan commented at 12:29 AM on October 13, 2026: none

    Title: PSBT: SignPSBTInput rejects valid 65-byte taproot script signatures when PSBT_IN_SIGHASH_TYPE is absent

    Summary

    SignPSBTInput() (src/psbt.cpp, master 60afa0b8d71f, lines 690–712) defaults sighash to SIGHASH_DEFAULT for taproot inputs when neither PSBT_IN_SIGHASH_TYPE nor the sighash_type option is set (line 690). The "check all existing signatures" block then requires every existing PSBT_IN_TAP_SCRIPT_SIG to be exactly 64 bytes:

    if (sighash == SIGHASH_DEFAULT) {
        if (!input.m_tap_key_sig.empty() && input.m_tap_key_sig.size() != 64) {
            return util::Unexpected{PSBTError::SIGHASH_MISMATCH};
        }
        for (const auto& [_, sig] : input.m_tap_script_sigs) {
            if (sig.size() != 64) return util::Unexpected{PSBTError::SIGHASH_MISMATCH};
        }
    }
    

    Per BIP-371, PSBT_IN_TAP_SCRIPT_SIG carries "the 64 or 65 byte Schnorr signature", and per BIP-341 the signature is self-describing: a 64-byte signature implies hash_type SIGHASH_DEFAULT (0x00), while a 65-byte signature carries an explicit trailing hash_type (which must not be 0x00). A 65-byte signature with e.g. 0x01 (SIGHASH_ALL) is valid for an input that carries no PSBT_IN_SIGHASH_TYPE. BIP-174 requires finalizers to fail on sighash mismatch only when the field is present ("The sighash type ... finalizers must fail to finalize inputs which have signatures that do not match the specified sighash type") — when the field is absent, no sighash has been "specified" for the input. Consensus likewise places no per-input uniformity requirement: BIP-341 validation is per-signature, and the valid hash_type set is {0x00, 0x01–0x03, 0x81–0x83}. Mixed sighash types within one input are consensus-valid.

    Impact

    A PSBT containing a 65-byte taproot script-path signature and no per-input sighash field can no longer be signed further or finalized by Core:

    • FinalizePSBT fails: it calls SignPSBTInput with options.sighash_type = input.sighash_type (= std::nullopt, psbt.cpp:811), so sighash resolves to SIGHASH_DEFAULT and the 65-byte signature trips SIGHASH_MISMATCH.
    • walletprocesspsbt fails for any subsequent signer: adding a signature to an input that already holds a 65-byte script sig returns SIGHASH_MISMATCH before signing even starts.

    So Core cannot finish a PSBT for a transaction that the network itself accepts.

    Real-world occurrence

    Hit in production with a taproot-miniscript 3-of-4 (multi_a) wallet whose cosigners used different default sighash types (one signer produced a 65-byte SIGHASH_ALL script signature, others 64-byte SIGHASH_DEFAULT). The resulting transaction was broadcast and confirmed on mainnet:

    txid 4652e1c186845b2007c13bd97b85721128092cce17f376bb3dda44acf497150b (block 970679), input 0 witness: [sig(65B, trailing 0x01), sig(64B), sig(64B), <empty>, script(138B), control(65B)] — a multi_a(3 of 4) script-path spend with one empty key slot.

    But the PSBT could not be finalized by software linking Core's PSBT code (Nunchuk's vendored Core, pinned at af78d36512, same check at their psbt.cpp:462-469); downstream report: https://github.com/nunchuk-io/nunchuk-desktop/issues/137

    Origin of the check

    Introduced by 15ce1bd73f in #31622 ("psbt: Enforce sighash type of signatures matches psbt"), motivated by MuSig2 (all signers must use the same sighash) and CLI-vs-field consistency. The PR discussion doesn't appear to consider 65-byte script-path signatures in the no-sighash-field case; the application of the strict check to m_tap_script_sigs under an assumed SIGHASH_DEFAULT looks like an unintended side effect.

    Suggested fix

    In the sighash == SIGHASH_DEFAULT branch, treat 65-byte signatures as self-describing: accept them when the trailing byte is a valid non-DEFAULT sighash type (0x01–0x03, 0x81–0x83), and reject only encodings that can never be valid (length not in {64, 65}, or a 65-byte sig whose trailing byte is 0x00 or out of range). The same applies to m_tap_key_sig in that branch (a key-path signature with explicit SIGHASH_ALL is likewise 65 bytes and currently rejected). The else branch (caller or field explicitly requested a non-default sighash) should stay strict, per BIP-174.

    Sketch (covers m_tap_script_sigs; m_tap_key_sig needs the analogous treatment):

    for (const auto& [_, sig] : input.m_tap_script_sigs) {
        if (sig.size() == 64) continue;
        if (sig.size() == 65 && sig.back() != SIGHASH_DEFAULT &&
            (sig.back() & ~SIGHASH_ANYONECANPAY) <= SIGHASH_SINGLE) continue;
        return util::Unexpected{PSBTError::SIGHASH_MISMATCH};
    }
    

    Happy to turn this into a PR with a functional test (mixed-sighash taproot miniscript input) if the approach is endorsed.

  2. GracedEternalKingCabbageMan commented at 1:14 AM on October 14, 2026: none

    Additional spec support for the "field absent" case, from BIP-174 itself:

    • The Signer role section ("Data Signers Check For") states: "If a sighash type is not provided, the signer should sign using SIGHASH_ALL, but may use any sighash type they wish."
    • The Input Finalizer role section states the must-fail rule conditionally: "If the input has a PSBT_IN_SIGHASH_TYPE field, the Input Finalizer must fail to finalize that input if any signature does not match the specified sighash type."

    So the BIP explicitly (a) permits signatures with any sighash type when the field is absent, and (b) scopes the finalizer's must-fail obligation to inputs where the field is present. The current SignPSBTInput check goes beyond both when the field is absent by enforcing the locally assumed default (SIGHASH_DEFAULT for taproot) against self-describing 65-byte signatures.

    (For taproot specifically, BIP-341 makes the signature itself authoritative: 64 bytes = SIGHASH_DEFAULT, 65 bytes = explicit trailing hash_type; both forms are valid in the same input, as the confirmed mainnet transaction in the OP demonstrates.)


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-30 09:40 UTC