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:
FinalizePSBTfails: it callsSignPSBTInputwithoptions.sighash_type = input.sighash_type(=std::nullopt, psbt.cpp:811), sosighashresolves toSIGHASH_DEFAULTand the 65-byte signature tripsSIGHASH_MISMATCH.walletprocesspsbtfails for any subsequent signer: adding a signature to an input that already holds a 65-byte script sig returnsSIGHASH_MISMATCHbefore 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.