Skip to content

Commit

Permalink
Contribute multiple inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
spacebear21 committed Aug 1, 2024
1 parent f4f39d6 commit 99e11c8
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 86 deletions.
30 changes: 17 additions & 13 deletions payjoin-cli/src/app/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ impl App {
}

fn try_contributing_inputs(
payjoin: payjoin::receive::WantsInputs,
mut payjoin: payjoin::receive::WantsInputs,
bitcoind: &bitcoincore_rpc::Client,
) -> Result<payjoin::receive::ProvisionalProposal> {
use bitcoin::OutPoint;
Expand All @@ -352,17 +352,21 @@ fn try_contributing_inputs(
.map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout }))
.collect();

let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg")[0];
let selected_utxo = available_inputs
let selected_outpoints = payjoin.try_preserving_privacy(candidate_inputs).expect("gg");

let mut selected_inputs = HashMap::new();
for outpoint in selected_outpoints {
let utxo = available_inputs
.iter()
.find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?;
log::debug!("selected utxo: {:#?}", selected_utxo);

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
Ok(payjoin.contribute_witness_input(txo_to_contribute, selected_outpoint))
.find(|i| i.txid == outpoint.txid && i.vout == outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the selector.")?;
let txo = bitcoin::TxOut {
value: utxo.amount.to_sat(),
script_pubkey: utxo.script_pub_key.clone(),
};
selected_inputs.insert(outpoint, txo);
}
log::debug!("selected inputs: {:#?}", selected_inputs);

Ok(payjoin.contribute_witness_inputs(selected_inputs))
}
30 changes: 17 additions & 13 deletions payjoin-cli/src/app/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ impl App {
}

fn try_contributing_inputs(
payjoin: payjoin::receive::v2::WantsInputs,
mut payjoin: payjoin::receive::v2::WantsInputs,
bitcoind: &bitcoincore_rpc::Client,
) -> Result<payjoin::receive::v2::ProvisionalProposal> {
use bitcoin::OutPoint;
Expand All @@ -367,19 +367,23 @@ fn try_contributing_inputs(
.map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout }))
.collect();

let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg")[0];
let selected_utxo = available_inputs
let selected_outpoints = payjoin.try_preserving_privacy(candidate_inputs).expect("gg");

let mut selected_inputs = HashMap::new();
for outpoint in selected_outpoints {
let utxo = available_inputs
.iter()
.find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?;
log::debug!("selected utxo: {:#?}", selected_utxo);

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
Ok(payjoin.contribute_witness_input(txo_to_contribute, selected_outpoint))
.find(|i| i.txid == outpoint.txid && i.vout == outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the selector.")?;
let txo = bitcoin::TxOut {
value: utxo.amount.to_sat(),
script_pubkey: utxo.script_pub_key.clone(),
};
selected_inputs.insert(outpoint, txo);
}
log::debug!("selected inputs: {:#?}", selected_inputs);

Ok(payjoin.contribute_witness_inputs(selected_inputs))
}

async fn unwrap_ohttp_keys_or_else_fetch(config: &AppConfig) -> Result<payjoin::OhttpKeys> {
Expand Down
70 changes: 45 additions & 25 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ impl WantsOutputs {
payjoin_psbt,
params: self.params,
owned_vouts: self.owned_vouts,
change_amount: None,
})
}
}
Expand All @@ -406,6 +407,8 @@ pub struct WantsInputs {
payjoin_psbt: Psbt,
params: Params,
owned_vouts: Vec<usize>,
// Input excess value to be added back as change to a receiver output
change_amount: Option<u64>,
}

impl WantsInputs {
Expand All @@ -418,7 +421,7 @@ impl WantsInputs {
/// UIH "Unnecessary input heuristic" is avoided for two-output transactions.
/// A simple consolidation is otherwise chosen if available.
pub fn try_preserving_privacy(
&self,
&mut self,
candidate_inputs: HashMap<Amount, OutPoint>,
) -> Result<Vec<OutPoint>, SelectionError> {
if candidate_inputs.is_empty() {
Expand All @@ -436,7 +439,7 @@ impl WantsInputs {
}

fn do_coin_selection(
&self,
&mut self,
candidate_inputs: HashMap<Amount, OutPoint>,
) -> Result<Vec<OutPoint>, SelectionError> {
// Calculate the amount that the receiver must contribute
Expand All @@ -456,6 +459,8 @@ impl WantsInputs {
input_sats += candidate_sats;

if input_sats >= min_input_amount {
// TODO: this doesn't account for fees that might be needed to cover extra weight
self.change_amount = Some(input_sats - min_input_amount);
return Ok(selected_coins);
}
}
Expand All @@ -469,7 +474,7 @@ impl WantsInputs {
// if min(in) > min(out) then UIH1 else UIH2
// https://eprint.iacr.org/2022/589.pdf
fn avoid_uih(
&self,
&mut self,
candidate_inputs: HashMap<Amount, OutPoint>,
) -> Result<Vec<OutPoint>, SelectionError> {
let min_original_out_sats = self
Expand Down Expand Up @@ -498,6 +503,7 @@ impl WantsInputs {
if candidate_min_in > candidate_min_out {
// The candidate avoids UIH2 but conforms to UIH1: Optimal change heuristic.
// It implies the smallest output is the sender's change address.
self.change_amount = Some(candidate_sats);
return Ok(vec![candidate.1]);
}
}
Expand All @@ -507,17 +513,24 @@ impl WantsInputs {
}

fn select_first_candidate(
&self,
&mut self,
candidate_inputs: HashMap<Amount, OutPoint>,
) -> Result<Vec<OutPoint>, SelectionError> {
match candidate_inputs.values().next().cloned() {
Some(outpoint) => Ok(vec![outpoint]),
match candidate_inputs.into_iter().next() {
Some((amount, outpoint)) => {
self.change_amount = Some(amount.to_sat());
Ok(vec![outpoint])
}
None => Err(SelectionError::from(InternalSelectionError::NotFound)),
}
}

pub fn contribute_witness_input(self, txo: TxOut, outpoint: OutPoint) -> ProvisionalProposal {
pub fn contribute_witness_inputs(
self,
inputs: HashMap<OutPoint, TxOut>,
) -> ProvisionalProposal {
let mut payjoin_psbt = self.payjoin_psbt.clone();

// The payjoin proposal must not introduce mixed input sequence numbers
let original_sequence = self
.payjoin_psbt
Expand All @@ -527,26 +540,33 @@ impl WantsInputs {
.map(|input| input.sequence)
.unwrap_or_default();

// Add the value of new receiver input to receiver output
let txo_value = txo.value;
let vout_to_augment =
self.owned_vouts.choose(&mut rand::thread_rng()).expect("owned_vouts is empty");
payjoin_psbt.unsigned_tx.output[*vout_to_augment].value += txo_value;
// Add the receiver change amount to the receiver output, if applicable
if let Some(txo_value) = self.change_amount {
// TODO: ensure that owned_vouts only refers to outpoints actually owned by the
// receiver (e.g. not a forwarded payment)
let vout_to_augment =
self.owned_vouts.choose(&mut rand::thread_rng()).expect("owned_vouts is empty");
payjoin_psbt.unsigned_tx.output[*vout_to_augment].value += txo_value;
}

// Insert contribution at random index for privacy
// Insert contributions at random indices for privacy
let mut rng = rand::thread_rng();
let index = rng.gen_range(0..=self.payjoin_psbt.unsigned_tx.input.len());
payjoin_psbt
.inputs
.insert(index, bitcoin::psbt::Input { witness_utxo: Some(txo), ..Default::default() });
payjoin_psbt.unsigned_tx.input.insert(
index,
bitcoin::TxIn {
previous_output: outpoint,
sequence: original_sequence,
..Default::default()
},
);
for (outpoint, txo) in inputs.into_iter() {
let index = rng.gen_range(0..=self.payjoin_psbt.unsigned_tx.input.len());
payjoin_psbt.inputs.insert(
index,
bitcoin::psbt::Input { witness_utxo: Some(txo), ..Default::default() },
);
payjoin_psbt.unsigned_tx.input.insert(
index,
bitcoin::TxIn {
previous_output: outpoint,
sequence: original_sequence,
..Default::default()
},
);
}

ProvisionalProposal {
original_psbt: self.original_psbt,
payjoin_psbt,
Expand Down
9 changes: 6 additions & 3 deletions payjoin/src/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,14 +426,17 @@ impl WantsInputs {
// if min(in) > min(out) then UIH1 else UIH2
// https://eprint.iacr.org/2022/589.pdf
pub fn try_preserving_privacy(
&self,
&mut self,
candidate_inputs: HashMap<Amount, OutPoint>,
) -> Result<Vec<OutPoint>, SelectionError> {
self.inner.try_preserving_privacy(candidate_inputs)
}

pub fn contribute_witness_input(self, txo: TxOut, outpoint: OutPoint) -> ProvisionalProposal {
let inner = self.inner.contribute_witness_input(txo, outpoint);
pub fn contribute_witness_inputs(
self,
inputs: HashMap<OutPoint, TxOut>,
) -> ProvisionalProposal {
let inner = self.inner.contribute_witness_inputs(inputs);
ProvisionalProposal { inner, context: self.context }
}

Expand Down
64 changes: 32 additions & 32 deletions payjoin/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ mod integration {
})
.expect("Receiver should have at least one output");

let payjoin = payjoin
let mut payjoin = payjoin
.try_substitute_receiver_output(|| {
Ok(receiver
.get_new_address(None, None)
Expand All @@ -166,22 +166,22 @@ mod integration {
.map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout }))
.collect();

let selected_outpoint =
payjoin.try_preserving_privacy(candidate_inputs).expect("gg")[0];
let selected_utxo = available_inputs
.iter()
.find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.unwrap();
let selected_outpoints = payjoin.try_preserving_privacy(candidate_inputs).expect("gg");

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
let outpoint_to_contribute =
bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout };
let payjoin =
payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
let mut selected_inputs = HashMap::new();
for outpoint in selected_outpoints {
let utxo = available_inputs
.iter()
.find(|i| i.txid == outpoint.txid && i.vout == outpoint.vout)
.unwrap();
let txo = bitcoin::TxOut {
value: utxo.amount.to_sat(),
script_pubkey: utxo.script_pub_key.clone(),
};
selected_inputs.insert(outpoint, txo);
}

let payjoin = payjoin.contribute_witness_inputs(selected_inputs);

let payjoin_proposal = payjoin
.finalize_proposal(
Expand Down Expand Up @@ -753,7 +753,7 @@ mod integration {
})
.expect("Receiver should have at least one output");

let payjoin = payjoin
let mut payjoin = payjoin
.try_substitute_receiver_outputs(None)
.expect("Could not substitute outputs");

Expand All @@ -764,22 +764,22 @@ mod integration {
.map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout }))
.collect();

let selected_outpoint =
payjoin.try_preserving_privacy(candidate_inputs).expect("gg")[0];
let selected_utxo = available_inputs
.iter()
.find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.unwrap();
let selected_outpoints = payjoin.try_preserving_privacy(candidate_inputs).expect("gg");

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
let outpoint_to_contribute =
bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout };
let payjoin =
payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
let mut selected_inputs = HashMap::new();
for outpoint in selected_outpoints {
let utxo = available_inputs
.iter()
.find(|i| i.txid == outpoint.txid && i.vout == outpoint.vout)
.unwrap();
let txo = bitcoin::TxOut {
value: utxo.amount.to_sat(),
script_pubkey: utxo.script_pub_key.clone(),
};
selected_inputs.insert(outpoint, txo);
}

let payjoin = payjoin.contribute_witness_inputs(selected_inputs);

let payjoin_proposal = payjoin
.finalize_proposal(
Expand Down

0 comments on commit 99e11c8

Please sign in to comment.