Skip to content

Commit

Permalink
Implement receiver fee contributions
Browse files Browse the repository at this point in the history
Modify `apply_fee` to enable fee contributions from the receiver for
fees not covered by the sender. This includes
- any fees that pay for additional inputs, in excess of the sender's
  max_additional_fee_contribution.
- all fees that pay for additional *outputs*.
- in the case where the sender doesn't have a fee output (e.g. a sweep
  transaction), all tx fees are deducted from the receiver output.
  • Loading branch information
spacebear21 committed Aug 26, 2024
1 parent c21d563 commit c595f59
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 18 deletions.
59 changes: 48 additions & 11 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use std::collections::HashMap;
use bitcoin::base64::prelude::BASE64_STANDARD;
use bitcoin::base64::Engine;
use bitcoin::psbt::Psbt;
use bitcoin::{Amount, FeeRate, OutPoint, Script, TxOut};
use bitcoin::{Amount, FeeRate, OutPoint, Script, TxOut, Weight};

mod error;
mod optional_parameters;
Expand Down Expand Up @@ -653,25 +653,27 @@ impl ProvisionalProposal {
.input_pairs()
.next()
.ok_or(InternalRequestError::OriginalPsbtNotBroadcastable)?;
// Calculate the additional fee contribution
let txo = input_pair.previous_txout().map_err(InternalRequestError::PrevTxOut)?;
let input_type = InputType::from_spent_input(txo, &self.payjoin_psbt.inputs[0])
.map_err(InternalRequestError::InputType)?;
let contribution_weight = input_type.expected_input_weight();
let input_count = self.payjoin_psbt.inputs.len() - self.original_psbt.inputs.len();
log::trace!("input_count : {}", input_count);
let weight_per_input = input_type.expected_input_weight();
log::trace!("weight_per_input : {}", weight_per_input);
let contribution_weight = weight_per_input * input_count as u64;
log::trace!("contribution_weight: {}", contribution_weight);
let mut additional_fee = contribution_weight * min_feerate;
let max_additional_fee_contribution =
self.params.additional_fee_contribution.unwrap_or_default().0;
if additional_fee >= max_additional_fee_contribution {
// Cap fee at the sender's contribution to simplify this method
additional_fee = max_additional_fee_contribution;
}
let additional_fee = contribution_weight * min_feerate;
log::trace!("additional_fee: {}", additional_fee);

if additional_fee > Amount::ZERO {
log::trace!(
"self.params.additional_fee_contribution: {:?}",
self.params.additional_fee_contribution
);
if let Some((_, additional_fee_output_index)) = self.params.additional_fee_contribution
let mut receiver_additional_fee = additional_fee;
if let Some((max_additional_fee_contribution, additional_fee_output_index)) =
self.params.additional_fee_contribution
{
// Find the sender's specified output in the original psbt.
// This step is necessary because the sender output may have shifted if new
Expand All @@ -686,10 +688,45 @@ impl ProvisionalProposal {
.iter()
.position(|txo| txo.script_pubkey == sender_fee_output.script_pubkey)
.expect("Sender output is missing from payjoin PSBT");
// Determine the additional amount that the sender will pay in fees
let sender_additional_fee = min(max_additional_fee_contribution, additional_fee);
log::trace!("sender_additional_fee: {}", sender_additional_fee);
// Remove additional miner fee from the sender's specified output
self.payjoin_psbt.unsigned_tx.output[sender_fee_vout].value -= additional_fee;
self.payjoin_psbt.unsigned_tx.output[sender_fee_vout].value -=
sender_additional_fee;
receiver_additional_fee -= sender_additional_fee;
}

// The receiver covers any additional fees if applicable
if receiver_additional_fee > Amount::ZERO {
log::trace!("receiver_additional_fee: {}", receiver_additional_fee);
// Remove additional miner fee from the receiver's specified output
self.payjoin_psbt.unsigned_tx.output[self.change_vout].value -=
receiver_additional_fee;
}
}

// Calculate additional output weight
let payjoin_outputs_weight = self
.payjoin_psbt
.unsigned_tx
.output
.iter()
.fold(Weight::ZERO, |acc, txo| acc + txo.weight());
let original_outputs_weight = self
.original_psbt
.unsigned_tx
.output
.iter()
.fold(Weight::ZERO, |acc, txo| acc + txo.weight());
let contribution_weight = payjoin_outputs_weight - original_outputs_weight;
log::trace!("contribution_weight: {}", contribution_weight);
let additional_fee = contribution_weight * min_feerate;
log::trace!("additional_fee: {}", additional_fee);
if additional_fee > Amount::ZERO {
// Remove additional miner fee from the receiver's specified output
self.payjoin_psbt.unsigned_tx.output[self.change_vout].value -= additional_fee;
}
Ok(&self.payjoin_psbt)
}

Expand Down
11 changes: 4 additions & 7 deletions payjoin/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,7 @@ mod integration {
log::info!("sent");

// Check resulting transaction and balances
// NOTE: No one is contributing fees for the receiver input because the sender has
// no change output and the receiver doesn't contribute fees. Temporary workaround
// is to ensure the sender overpays in the original psbt for the receiver's input.
let network_fees = psbt.fee()?;
let network_fees = predicted_tx_weight(&payjoin_tx) * FeeRate::BROADCAST_MIN;
// Sender sent the entire value of their utxo to receiver (minus fees)
assert_eq!(payjoin_tx.input.len(), 2);
assert_eq!(payjoin_tx.output.len(), 1);
Expand Down Expand Up @@ -719,9 +716,9 @@ mod integration {
outputs.insert(pj_uri.address.to_string(), Amount::from_btc(50.0)?);
let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions {
lock_unspent: Some(true),
// The current API doesn't let the receiver pay for additional fees,
// so we double the minimum relay fee to ensure that the sender pays for the receiver's inputs
fee_rate: Some(Amount::from_sat((DEFAULT_MIN_RELAY_TX_FEE * 2).into())),
// The minimum relay feerate ensures that tests fail if the receiver would add inputs/outputs
// that cannot be covered by the sender's additional fee contributions.
fee_rate: Some(Amount::from_sat(DEFAULT_MIN_RELAY_TX_FEE.into())),
subtract_fee_from_outputs: vec![0],
..Default::default()
};
Expand Down

0 comments on commit c595f59

Please sign in to comment.