payjoin: exclude 0-conf inputs from receiver candidates (#3389)
What changed, and why it matters
This change stops Payjoin transactions from using unconfirmed (0-confirmation) coins as inputs when the wallet is acting as the receiver in a Payjoin swap. Before the fix, the receiver could accidentally propose or sign a Payjoin that relied on coins that had not yet been mined into a block. Such coins can disappear, be double-spent, or get replaced, which could cause the Payjoin to fail, be invalid, or expose the receiver to loss or confusion. The fix adds an optional 'confirmed only' filter and turns it on for Payjoin receiver candidate inputs.
Treat this as a hardening/defensive fix. Review whether 0-conf inputs were ever actually selected in live Payjoin sessions and whether any related transaction failures or double-spend attempts occurred. No immediate emergency response is indicated, but ensure the change is included in the next release and consider adding tests that verify 0-conf UTXOs are excluded from Payjoin receiver candidates.
Security signals we found
0-conf input exclusion in receiver-side Payjoin candidate selection
Payjoin receiver input selection now requires at least one confirmation
Potential risk of building a Payjoin on unconfirmed/replaceable inputs reduced
Evidence from the diff
The commit modifies BitcoinWallet.getUtxoWithPrivateKeys() to accept a confirmedOnly parameter. When true, the filter requires (e.confirmations ?? 0) > 0 in addition to the existing isSending && !e.isFrozen checks. PayjoinManager now calls getUtxoWithPrivateKeys(confirmedOnly: true) when handling PayjoinReceiverRequestTypes.getCandidateInputs. This excludes 0-conf UTXOs from the receiver’s candidate input set in a Payjoin protocol flow.
Changed components
cw_bitcoin/lib/bitcoin_wallet.dartcw_bitcoin/lib/payjoin/manager.dartPayjoin receiver candidate input selectionInspect captured patch +4 / −4
diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index a2b2f798..51f48571 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -551,8 +551,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return tx;
}
- List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
- .where((e) => (e.isSending && !e.isFrozen))
+ List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) => unspentCoins
+ .where((e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList();
diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart
index 437fed3f..4fa2e2a6 100644
--- a/cw_bitcoin/lib/payjoin/manager.dart
+++ b/cw_bitcoin/lib/payjoin/manager.dart
@@ -297,10 +297,10 @@ class PayjoinManager {
break;
case PayjoinReceiverRequestTypes.getCandidateInputs:
- utxos = _wallet.getUtxoWithPrivateKeys();
+ utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
if (utxos.isEmpty) {
await _wallet.updateAllUnspents();
- utxos = _wallet.getUtxoWithPrivateKeys();
+ utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
}
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
Why this scored 48/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.