Skip to content

Commit

Permalink
Abstract privacy-preserving candidate selection
Browse files Browse the repository at this point in the history
Avoid UIH for multiple inputs, and just select any input where UIH does
not apply.
  • Loading branch information
DanGould committed May 21, 2024
1 parent 46e86e4 commit 44d67a4
Showing 1 changed file with 56 additions and 45 deletions.
101 changes: 56 additions & 45 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,8 @@ impl ProvisionalProposal {
/// 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 one class of them to avoid. We define
/// UIH1 and UIH2 according to the BlockSci practice
/// BlockSci UIH1 and UIH2:
// if min(out) < min(in) then UIH1 else UIH2
// https://eprint.iacr.org/2022/589.pdf
/// UIH "Unnecessary input heuristic" is avoided for multi-output transactions.
/// A simple consolidation is otherwise chosen if available.
pub fn try_preserving_privacy(
&self,
candidate_inputs: HashMap<Amount, OutPoint>,
Expand All @@ -365,52 +362,66 @@ impl ProvisionalProposal {
// This UIH avoidance function supports only
// many-input, n-output transactions such that n <= 2 for now
return Err(SelectionError::from(InternalSelectionError::TooManyOutputs));
} else if self.payjoin_psbt.outputs.len() == 2 {
let min_original_out_sats = self
.payjoin_psbt
.unsigned_tx
.output
.iter()
.map(|output| output.value)
.min()
.unwrap_or_else(|| Amount::MAX_MONEY.to_sat());

let min_original_in_sats = self
.payjoin_psbt
.input_pairs()
.filter_map(|input| input.previous_txout().ok().map(|txo| txo.value))
.min()
.unwrap_or_else(|| Amount::MAX_MONEY.to_sat());

// Assume many-input, two output to select the vout for now
let prior_payment_sats =
self.payjoin_psbt.unsigned_tx.output[self.owned_vouts[0]].value;
for candidate in candidate_inputs {
// TODO bound loop by timeout / iterations

let candidate_sats = candidate.0.to_sat();
let candidate_min_out =
min(min_original_out_sats, prior_payment_sats + candidate_sats);
let candidate_min_in = min(min_original_in_sats, candidate_sats);

if candidate_min_out < candidate_min_in {
// The candidate avoids UIH2 but conforms to UIH1: Optimal change heuristic.
// It implies the smallest output is the sender's change address.
return Ok(candidate.1);
} else {
// The candidate conforms to UIH2: Unnecessary input
// and could be identified as a potential payjoin
continue;
}
}
}

if self.payjoin_psbt.outputs.len() == 2 {
self.avoid_uih(candidate_inputs)
} else {
return Ok(candidate_inputs.values().next().expect("empty already checked").clone());
self.select_first_candidate(candidate_inputs)
}
}

/// UIH "Unnecessary input heuristic" is one class of them to avoid. We define
/// UIH1 and UIH2 according to the BlockSci practice
/// BlockSci UIH1 and UIH2:
// if min(out) < min(in) then UIH1 else UIH2
// https://eprint.iacr.org/2022/589.pdf
fn avoid_uih(
&self,
candidate_inputs: HashMap<Amount, OutPoint>,
) -> Result<OutPoint, SelectionError> {
let min_original_out_sats = self
.payjoin_psbt
.unsigned_tx
.output
.iter()
.map(|output| output.value)
.min()
.unwrap_or_else(|| Amount::MAX_MONEY.to_sat());

let min_original_in_sats = self
.payjoin_psbt
.input_pairs()
.filter_map(|input| input.previous_txout().ok().map(|txo| txo.value))
.min()
.unwrap_or_else(|| Amount::MAX_MONEY.to_sat());

let prior_payment_sats = self.payjoin_psbt.unsigned_tx.output[self.owned_vouts[0]].value;

for candidate in candidate_inputs {
let candidate_sats = candidate.0.to_sat();
let candidate_min_out = min(min_original_out_sats, prior_payment_sats + candidate_sats);
let candidate_min_in = min(min_original_in_sats, candidate_sats);

if candidate_min_out < candidate_min_in {
return Ok(candidate.1);
}
}

// No suitable privacy preserving selection found
Err(SelectionError::from(InternalSelectionError::NotFound))
}

fn select_first_candidate(
&self,
candidate_inputs: HashMap<Amount, OutPoint>,
) -> Result<OutPoint, SelectionError> {
candidate_inputs
.values()
.next()
.cloned()
.ok_or_else(|| SelectionError::from(InternalSelectionError::NotFound))
}

pub fn contribute_witness_input(&mut self, txo: TxOut, outpoint: OutPoint) {
// The payjoin proposal must not introduce mixed input sequence numbers
let original_sequence = self
Expand Down

0 comments on commit 44d67a4

Please sign in to comment.