rpc: detect invalid signatures in analyzepsbt #34765

pull overcookedpanda wants to merge 1 commits into bitcoin:master from overcookedpanda:fix-analyzepsbt-invalid-sig changing 2 files +29 −0
  1. overcookedpanda commented at 8:54 pm on March 6, 2026: none

    Summary

    • Detect invalid signatures in analyzepsbt by checking for the case where SignPSBTInput returns incomplete, no missing_* fields are populated, yet signature data exists — meaning the signature is present but cryptographically invalid
    • Report a clear error ("PSBT is not valid. Input N has an invalid signature") instead of the confusing "next": "updater" fallback
    • Add functional test covering the exact scenario from #33320 (corrupted taproot key path signature)

    Problem

    When a PSBT input has an invalid taproot_key_path_sig (e.g. a script path sig mistakenly placed in the key path field by external software like HWI), analyzepsbt returns "next": "updater" with no error message.

    This happens because SignTaproot in ProduceSignature finds the existing signature data and returns solved = true, but VerifyScript (with the real MutableTransactionSignatureChecker) fails the cryptographic check, setting sigdata.complete = false. Back in AnalyzePSBT, the missing_* fields
    are all empty — taproot signing never populates them — so the code falls through to the UPDATER fallback.

    Approach

    Rather than adding a separate signature verification step, this relies on the verification that already happens inside ProduceSignatureVerifyScript. When AnalyzePSBT calls SignPSBTInput with real PrecomputedTransactionData, the signature is verified against the actual transaction via libsecp256k1. If that fails but nothing is missing, the signature must be invalid.

    The check also covers m_tap_script_sigs and partial_sigs for completeness, though the primary motivation is the taproot key path case from #33320.

    Note: a previous attempt at this fix (#33360) was closed because it used DUMMY_SIGNATURE_CREATOR/DUMMY_CHECKER which accept any non-empty signature, and its hand-crafted test PSBT was malformed.

    Test plan

    • python3 test/functional/rpc_psbt.py — includes new test for corrupted taproot key path sig
    • build/bin/test_bitcoin -t psbt_wallet_tests — no regressions in C++ unit tests

    Fixes #33320

  2. DrahtBot added the label RPC/REST/ZMQ on Mar 6, 2026
  3. DrahtBot commented at 8:55 pm on March 6, 2026: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK Bortlesboat

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

  4. Bortlesboat commented at 10:16 pm on March 7, 2026: none

    Concept ACK

    This addresses a real gap — currently analyzepsbt reports missing signatures even when signatures exist but are invalid, which is confusing.

    The detection logic is sound: if UTXO info is present, nothing is reported as missing (no missing pubkeys, redeem script, witness script, or sigs), yet signature data exists (tap_key_sig, tap_script_sigs, or partial_sigs) and signing still couldn’t complete — the existing signatures must be invalid.

    The test approach of XOR-corrupting a valid taproot key path signature is a good way to exercise this without needing a custom signer.

    One thing I verified: the condition also covers legacy (non-taproot) invalid signatures via the !input.partial_sigs.empty() check, so this isn’t taproot-only despite the test focusing on taproot.

  5. Bortlesboat commented at 7:14 pm on March 8, 2026: none
    Concept ACK 27af927367a6. Ran rpc_psbt.py locally, passes. The detection logic makes sense for the taproot case — one question inline about legacy inputs.
  6. in src/node/psbt.cpp:78 in 27af927367 outdated
    73+                // complete, the existing signature(s) must be invalid.
    74+                if (input_analysis.has_utxo &&
    75+                    outdata.missing_pubkeys.empty() && outdata.missing_redeem_script.IsNull() &&
    76+                    outdata.missing_witness_script.IsNull() && outdata.missing_sigs.empty() &&
    77+                    (!input.m_tap_key_sig.empty() || !input.m_tap_script_sigs.empty() || !input.partial_sigs.empty())) {
    78+                    result.SetInvalid(strprintf("PSBT is not valid. Input %u has an invalid signature", i));
    


    Bortlesboat commented at 7:14 pm on March 8, 2026:
    For fully-signed legacy inputs where SignPSBTInput fails due to an invalid sig in scriptSig, would partial_sigs still be populated here? If not, this condition would miss that case since none of the three sig fields would be non-empty.

    overcookedpanda commented at 11:40 pm on March 8, 2026:

    Good question. I traced through both cases:

    Unfinalized legacy inputs (signatures in partial_sigs)

    This is covered correctly. When SignPSBTInput is called with DUMMY_SIGNING_PROVIDER, FillSignatureData copies partial_sigs into sigdata.signatures (since final_script_sig is empty). ProduceSignatureSignStepCreateSig finds the existing signature in sigdata.signatures and uses it, so solved = true. But then VerifyScript fails (invalid sig), so sigdata.complete = false and ProduceSignature returns false.

    Critically, none of the missing_* fields get populated (pubkey was found, sig was found), so all the empty()/IsNull() checks pass and !input.partial_sigs.empty() is true → invalid signature detected.

    Finalized legacy inputs (partial_sigs cleared)

    You’re right that this case was missed, but not for the reason you might expect. The issue is that the code path never reaches the check at all:

    1. FillSignatureData sees final_script_sig is non-empty → sets sigdata.complete = true → returns early
    2. ProduceSignature short-circuits on its first line (if (sigdata.complete) return true)
    3. SignPSBTInput returns OK
    4. complete = true → we skip the if (!complete) block entirely → fall through to next = FINALIZER

    This is misleading for an input that’s already finalized but has a broken signature.

    Fix

    I’ve added an early check for this case. Inside the !PSBTInputSignedAndVerified block, before calling SignPSBTInput:

    0// If the input has final scripts (i.e. it has been finalized) but
    1// verification failed, the existing signature(s) must be invalid.
    2if (input_analysis.has_utxo && PSBTInputSigned(input)) {
    3    result.SetInvalid(strprintf("PSBT is not valid. Input %u has an invalid signature", i));
    4    return result;
    5}
    

    The logic: if we’re inside !PSBTInputSignedAndVerified (verification failed), has_utxo is true (so we had UTXO info to verify against), and PSBTInputSigned is true (the input has final_script_sig or final_script_witness), then the only explanation is an invalid signature.

    This is safe because GetInputUTXO and PSBTInputSignedAndVerified use identical UTXO-finding logic, if has_utxo is true, the VerifyScript call is the only thing that can fail.

    Added a functional test that creates a finalized P2WPKH PSBT, XOR-corrupts the signature bytes inside PSBT_IN_FINAL_SCRIPTWITNESS, and verifies analyzepsbt detects it.

  7. overcookedpanda commented at 11:41 pm on March 8, 2026: none

    Code changes:

    1. src/node/psbt.cpp - Added early detection of finalized inputs with invalid signatures (before the SignPSBTInput call that would short-circuit)
    2. test/functional/rpc_psbt.py - Added import for PSBT_IN_FINAL_SCRIPTWITNESS and a new test that creates a finalized P2WPKH PSBT, corrupts the witness signature bytes, and asserts analyzepsbt returns the expected error

    Test result: Full rpc_psbt.py suite passes in WSL, including the new test.

  8. rpc: detect invalid signatures in analyzepsbt
    When a PSBT input contains a cryptographically invalid signature (e.g. a
    script path sig placed in the key path field by external software),
    analyzepsbt returns "next": "updater" with no error. This is because
    ProduceSignature's VerifyScript fails but the taproot signing path never
    populates the missing_* fields, so AnalyzePSBT falls through to the
    updater fallback.
    
    Detect this by checking for the case where signing is incomplete, nothing
    is reported as missing, yet signature data exists in the input. This
    means the signature is present but invalid.
    
    Add a functional test that creates a valid signed taproot PSBT, corrupts
    the key path signature, and verifies analyzepsbt reports the error.
    
    Fixes #33320
    1f4ea8d0cb
  9. overcookedpanda force-pushed on Mar 8, 2026
  10. overcookedpanda requested review from Bortlesboat on Mar 8, 2026

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-03-09 12:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me