Merge pull request #10987 from spesmilo/fix_10986
What changed, and why it matters
This commit fixes a bug where Electrum failed to recognize certain already-signed Bitcoin transactions as complete. Specifically, for native SegWit inputs, Bitcoin Core can produce a finalized PSBT with a valid witness but an empty scriptSig. Electrum's 'is this transaction complete?' check previously required a non-empty scriptSig for such inputs, so it wrongly treated these valid, signed transactions as incomplete. The fix adds a rule: if a native SegWit input has a non-empty witness, treat it as complete. This could have led users to try re-signing or re-broadcasting, or to confusion about whether a transaction was ready.
Review related completeness and signing flows to ensure no other input types (e.g., P2SH-P2WPKH, P2TR) have similar detection gaps. Consider adding more PSBT cross-implementation test vectors. Users should upgrade to include this fix if they handle PSBTs from Bitcoin Core or other wallets.
Security signals we found
Logic error in transaction completeness detection
Native SegWit witness handling edge case
PSBT interoperability issue with Bitcoin Core
Potential user-facing misclassification of signed transactions
Evidence from the diff
In electrum/transaction.py, the is_complete() method now returns True when self.has_witness() and self.is_native_segwit() are both true. Previously, the method returned True for non-SegWit inputs with a script_sig, but native SegWit inputs (P2WPKH, P2WSH) can be finalized with an empty scriptSig and a populated witness. The added test case uses a real finalized P2WPKH PSBT produced by Bitcoin Core that has script_sig=None and a valid witness, asserting both input-level and transaction-level completeness. This is a correctness fix in transaction finalization detection.
Changed components
electrum/transaction.pyPartially Signed Bitcoin Transaction (PSBT) parsingTransaction signing/completeness logicInspect captured patch +11 / −0
### electrum/transaction.py
@@ -1942,6 +1942,8 @@ def is_complete(self) -> bool:
return True
if self.script_sig is not None and not self.is_segwit():
return True
+ if self.has_witness() and self.is_native_segwit():
+ return True
if desc := self.script_descriptor:
try:
desc.satisfy(allow_dummy=False, sigdata=self.sigs_ecdsa)
### tests/test_psbt.py
@@ -97,6 +97,15 @@ def test_valid_psbt__input_with_both_witness_utxo_and_nonwitness_utxo(self):
tx = tx_from_any(bytes.fromhex('70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a0100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000'))
self.assertEqual(1, len(tx.inputs()))
+ def test_valid_psbt__finalized_native_segwit_input_without_final_scriptsig(self):
+ # Case: PSBT with one P2WPKH input, finalized, empty scriptSig (Bitcoin Core does this). see #10986
+ raw_psbt = '70736274ff01005201000000010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297ac80100000001011f807c814a00000000160014460fc70f208bffa9abf3ae4abbd2f629d9cdcf5901086b024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0ee0000'
+ tx = tx_from_any(raw_psbt)
+ self.assertEqual(1, len(tx.inputs()))
+ self.assertIsNone(tx.inputs()[0].script_sig)
+ self.assertTrue(tx.inputs()[0].is_complete())
+ self.assertTrue(tx.is_complete())
+
class TestInvalidPSBT(ElectrumTestCase):
# test cases from BIP-0174Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.