Skip to content

Commit eea508e

Browse files
authored
Fix try_preserving_privacy returning Empty when candidates exist (#1629)
2 parents 146092b + f165562 commit eea508e

1 file changed

Lines changed: 55 additions & 11 deletions

File tree

  • payjoin/src/core/receive/common

payjoin/src/core/receive/common/mod.rs

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,8 @@ impl WantsInputs {
208208
&self,
209209
candidate_inputs: impl IntoIterator<Item = InputPair>,
210210
) -> Result<InputPair, CoinSelectionError> {
211-
let mut candidate_inputs = candidate_inputs.into_iter().peekable();
212-
213-
self.avoid_uih(&mut candidate_inputs)
214-
.or_else(|_| self.select_first_candidate(&mut candidate_inputs))
211+
let candidates: Vec<_> = candidate_inputs.into_iter().collect();
212+
self.avoid_uih(&candidates).or_else(|_| self.select_first_candidate(&candidates))
215213
}
216214

217215
/// Returns the candidate input which avoids the UIH2 defined in [Unnecessary Input
@@ -226,7 +224,7 @@ impl WantsInputs {
226224
/// Errors if the transaction does not have exactly 2 outputs.
227225
pub(super) fn avoid_uih(
228226
&self,
229-
candidate_inputs: impl IntoIterator<Item = InputPair>,
227+
candidate_inputs: &[InputPair],
230228
) -> Result<InputPair, CoinSelectionError> {
231229
if self.payjoin_psbt.outputs.len() != 2 {
232230
return Err(InternalCoinSelectionError::UnsupportedOutputLength.into());
@@ -258,7 +256,7 @@ impl WantsInputs {
258256
if candidate_min_in > candidate_min_out {
259257
// The candidate avoids UIH2 but conforms to UIH1: Optimal change heuristic.
260258
// It implies the smallest output is the sender's change address.
261-
return Ok(input_pair);
259+
return Ok(input_pair.clone());
262260
}
263261
}
264262

@@ -269,9 +267,9 @@ impl WantsInputs {
269267
/// Returns the first candidate input in the provided list or errors if the list is empty.
270268
fn select_first_candidate(
271269
&self,
272-
candidate_inputs: impl IntoIterator<Item = InputPair>,
270+
candidate_inputs: &[InputPair],
273271
) -> Result<InputPair, CoinSelectionError> {
274-
candidate_inputs.into_iter().next().ok_or(InternalCoinSelectionError::Empty.into())
272+
candidate_inputs.first().cloned().ok_or(InternalCoinSelectionError::Empty.into())
275273
}
276274

277275
/// Contributes the provided list of inputs to the transaction at random indices. If the total input
@@ -584,6 +582,53 @@ mod tests {
584582
);
585583
}
586584

585+
fn candidate_input_from_test_vector(value: Amount) -> InputPair {
586+
let txout = TxOut {
587+
value,
588+
script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(DUMMY20)),
589+
};
590+
let tx = Transaction {
591+
version: bitcoin::transaction::Version::TWO,
592+
lock_time: LockTime::Seconds(Time::MIN),
593+
input: vec![],
594+
output: vec![txout.clone()],
595+
};
596+
let outpoint = OutPoint { txid: tx.compute_txid(), vout: 0 };
597+
InputPair::new(
598+
TxIn { previous_output: outpoint, sequence: Sequence::MAX, ..Default::default() },
599+
Input { witness_utxo: Some(txout), ..Default::default() },
600+
None,
601+
)
602+
.unwrap()
603+
}
604+
605+
#[test]
606+
fn try_preserving_privacy_falls_back_when_avoid_uih_not_found() {
607+
let original = original_from_test_vector();
608+
let wants_inputs = WantsOutputs::new(original, vec![0]).commit_outputs();
609+
let candidates = vec![
610+
candidate_input_from_test_vector(Amount::ONE_SAT),
611+
candidate_input_from_test_vector(Amount::from_sat(2)),
612+
];
613+
614+
let selected = wants_inputs.try_preserving_privacy(candidates.clone()).unwrap();
615+
616+
assert_eq!(selected, candidates[0]);
617+
}
618+
619+
#[test]
620+
fn try_preserving_privacy_falls_back_when_avoid_uih_unsupported() {
621+
let original = original_from_test_vector();
622+
let mut wants_inputs = WantsOutputs::new(original, vec![0]).commit_outputs();
623+
wants_inputs.payjoin_psbt.unsigned_tx.output.pop();
624+
wants_inputs.payjoin_psbt.outputs.pop();
625+
let candidate = candidate_input_from_test_vector(Amount::ONE_SAT);
626+
627+
let selected = wants_inputs.try_preserving_privacy([candidate.clone()]).unwrap();
628+
629+
assert_eq!(selected, candidate);
630+
}
631+
587632
#[test]
588633
fn test_avoid_uih_one_output() {
589634
let original = original_from_test_vector();
@@ -594,14 +639,13 @@ mod tests {
594639
None,
595640
)
596641
.unwrap();
597-
let input_iter = [input].into_iter();
598642
let mut payjoin = WantsOutputs::new(original, vec![0])
599643
.commit_outputs()
600-
.contribute_inputs(input_iter.clone())
644+
.contribute_inputs([input.clone()])
601645
.expect("Failed to contribute inputs");
602646

603647
payjoin.payjoin_psbt.outputs.pop();
604-
let avoid_uih = payjoin.avoid_uih(input_iter);
648+
let avoid_uih = payjoin.avoid_uih(std::slice::from_ref(&input));
605649
assert_eq!(
606650
avoid_uih.unwrap_err(),
607651
CoinSelectionError::from(InternalCoinSelectionError::UnsupportedOutputLength),

0 commit comments

Comments
 (0)