diff --git a/payjoin-cli/src/app.rs b/payjoin-cli/src/app.rs index a205a9ea..ea953004 100644 --- a/payjoin-cli/src/app.rs +++ b/payjoin-cli/src/app.rs @@ -250,7 +250,7 @@ impl App { )?; // Receive Check 1: Can Broadcast - let proposal = proposal.check_can_broadcast(|tx| { + let proposal = proposal.check_broadcast_suitability(None, |tx| { let raw_tx = bitcoin::consensus::encode::serialize_hex(&tx); let mempool_results = self .bitcoind diff --git a/payjoin/src/receive/error.rs b/payjoin/src/receive/error.rs index ffc91360..6b58524b 100644 --- a/payjoin/src/receive/error.rs +++ b/payjoin/src/receive/error.rs @@ -65,6 +65,10 @@ pub(crate) enum InternalRequestError { /// Original PSBT input has been seen before. Only automatic receivers, aka "interactive" in the spec /// look out for these to prevent probing attacks. InputSeen(bitcoin::OutPoint), + /// Fee rate provided by sender is too low + /// First argument is the fee rate provided by the sender + /// Second argument is the minimum fee rate required by the receiver + FeeTooLow(bitcoin::Amount, bitcoin::FeeRate), } impl From for RequestError { @@ -125,6 +129,11 @@ impl fmt::Display for RequestError { write_error(f, "original-psbt-rejected", &format!("Input Type Error: {}.", e)), InternalRequestError::InputSeen(_) => write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."), + InternalRequestError::FeeTooLow(sender_fee_rate, receiver_fee_rate) => write_error( + f, + "dont meet minimum required fee rate defined by receiver", + &format!("Fee too low: {} < {}.", sender_fee_rate, receiver_fee_rate), + ), } } } diff --git a/payjoin/src/receive/mod.rs b/payjoin/src/receive/mod.rs index 7e16027d..96bd9483 100644 --- a/payjoin/src/receive/mod.rs +++ b/payjoin/src/receive/mod.rs @@ -123,7 +123,7 @@ //! We need to know this transaction is consensus-valid. //! //! ``` -//! let checked_1 = proposal.check_can_broadcast(|tx| { +//! let checked_1 = proposal.check_broadcast_suitability(None, |tx| { //! let raw_tx = bitcoin::consensus::encode::serialize(&tx).to_hex(); //! let mempool_results = self //! .bitcoind @@ -295,7 +295,7 @@ pub trait Headers { /// /// If you are implementing an interactive payment processor, you should get extract the original /// transaction with extract_tx_to_schedule_broadcast() and schedule, followed by checking -/// that the transaction can be broadcast with check_can_broadcast. Otherwise it is safe to +/// that the transaction can be broadcast with check_broadcast_suitability. Otherwise it is safe to /// call assume_interactive_receive to proceed with validation. #[derive(Debug, Clone)] pub struct UncheckedProposal { @@ -360,10 +360,18 @@ impl UncheckedProposal { /// Broadcasting the Original PSBT after some time in the failure case makes incurs sender cost and prevents probing. /// /// Call this after checking downstream. - pub fn check_can_broadcast( + pub fn check_broadcast_suitability( self, + min_feerate: Option, can_broadcast: impl Fn(&bitcoin::Transaction) -> Result, ) -> Result { + if let (Some(receiver_min_feerate), Ok(psbt_feerate)) = (min_feerate, self.psbt.fee()) { + if receiver_min_feerate.to_sat_per_kwu() > psbt_feerate.to_sat() { + return Err(Error::BadRequest( + InternalRequestError::FeeTooLow(psbt_feerate, receiver_min_feerate).into(), + )); + } + } if can_broadcast(&self.psbt.clone().extract_tx())? { Ok(MaybeInputsOwned { psbt: self.psbt, params: self.params }) } else { diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index dd4cc545..5a4e3c46 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -143,8 +143,24 @@ mod integration { let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast(); // Receive Check 1: Can Broadcast + // Here we set receiver min feerate to be higher than the proposal's psbt feerate + // This should fail the check + let receiver_min_feerate = bitcoin::FeeRate::from_sat_per_kwu(283); + assert!(proposal + .clone() + .check_broadcast_suitability(Some(receiver_min_feerate), |tx| { + Ok(receiver + .test_mempool_accept(&[bitcoin::consensus::encode::serialize_hex(&tx)]) + .unwrap() + .first() + .unwrap() + .allowed) + }) + .is_err()); + // Here we set receiver min feerate to be lower than the proposal's psbt feerate + let receiver_min_feerate = bitcoin::FeeRate::from_sat_per_kwu(281); let proposal = proposal - .check_can_broadcast(|tx| { + .check_broadcast_suitability(Some(receiver_min_feerate), |tx| { Ok(receiver .test_mempool_accept(&[bitcoin::consensus::encode::serialize_hex(&tx)]) .unwrap()