From a20e84d95d5a7034c2c10f3eda67a7478cb03a46 Mon Sep 17 00:00:00 2001 From: DanGould Date: Tue, 11 Feb 2025 11:36:54 -0500 Subject: [PATCH 1/2] Fall back to first candidate if avoid_uih fails try_preserving_privacy is about a best-effort attempt, not a guaranteed success. This implementation is what was already implied by the docstring. So it's a fix. Making avoid_uih and select_first_candidate public later can be public to allow for more granular downstream control. --- payjoin/src/receive/v1/mod.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/payjoin/src/receive/v1/mod.rs b/payjoin/src/receive/v1/mod.rs index 0b782fa2c..b5eb29aea 100644 --- a/payjoin/src/receive/v1/mod.rs +++ b/payjoin/src/receive/v1/mod.rs @@ -408,7 +408,7 @@ impl WantsInputs { /// Proper coin selection allows payjoin to resemble ordinary transactions. /// To ensure the resemblance, a number of heuristics must be avoided. /// - /// UIH "Unnecessary input heuristic" is avoided for multi-output transactions. + /// Attempt to avoid UIH (Unnecessary input heuristic) for 2-output transactions. /// A simple consolidation is otherwise chosen if available. pub fn try_preserving_privacy( &self, @@ -419,17 +419,8 @@ impl WantsInputs { return Err(InternalSelectionError::Empty.into()); } - if self.payjoin_psbt.outputs.len() > 2 { - // This UIH avoidance function supports only - // many-input, n-output transactions such that n <= 2 for now - return Err(InternalSelectionError::TooManyOutputs.into()); - } - - if self.payjoin_psbt.outputs.len() == 2 { - self.avoid_uih(candidate_inputs) - } else { - self.select_first_candidate(candidate_inputs) - } + self.avoid_uih(&mut candidate_inputs) + .or_else(|_| self.select_first_candidate(&mut candidate_inputs)) } /// UIH "Unnecessary input heuristic" is one class of heuristics to avoid. We define @@ -437,10 +428,17 @@ impl WantsInputs { /// BlockSci UIH1 and UIH2: /// if min(in) > min(out) then UIH1 else UIH2 /// + /// + /// This UIH avoidance function supports only + /// many-input, 2-output transactions for now fn avoid_uih( &self, candidate_inputs: impl IntoIterator, ) -> Result { + if self.payjoin_psbt.outputs.len() != 2 { + return Err(InternalSelectionError::TooManyOutputs.into()); + } + let min_out_sats = self .payjoin_psbt .unsigned_tx From 1201492418202574ef7345772314cc091260052a Mon Sep 17 00:00:00 2001 From: DanGould Date: Tue, 11 Feb 2025 11:42:45 -0500 Subject: [PATCH 2/2] Rename UnsupportedOutputLength SelectionError It can be too short too. --- payjoin/src/receive/error.rs | 4 ++-- payjoin/src/receive/v1/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/payjoin/src/receive/error.rs b/payjoin/src/receive/error.rs index 58ac803e0..2f67d3462 100644 --- a/payjoin/src/receive/error.rs +++ b/payjoin/src/receive/error.rs @@ -318,7 +318,7 @@ pub(crate) enum InternalSelectionError { /// No candidates available for selection Empty, /// Current privacy selection implementation only supports 2-output transactions - TooManyOutputs, + UnsupportedOutputLength, /// No selection candidates improve privacy NotFound, } @@ -327,7 +327,7 @@ impl fmt::Display for SelectionError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.0 { InternalSelectionError::Empty => write!(f, "No candidates available for selection"), - InternalSelectionError::TooManyOutputs => write!( + InternalSelectionError::UnsupportedOutputLength => write!( f, "Current privacy selection implementation only supports 2-output transactions" ), diff --git a/payjoin/src/receive/v1/mod.rs b/payjoin/src/receive/v1/mod.rs index b5eb29aea..5d9918684 100644 --- a/payjoin/src/receive/v1/mod.rs +++ b/payjoin/src/receive/v1/mod.rs @@ -436,7 +436,7 @@ impl WantsInputs { candidate_inputs: impl IntoIterator, ) -> Result { if self.payjoin_psbt.outputs.len() != 2 { - return Err(InternalSelectionError::TooManyOutputs.into()); + return Err(InternalSelectionError::UnsupportedOutputLength.into()); } let min_out_sats = self