Skip to content

Commit

Permalink
Allow receiver to set minimum fee_rate
Browse files Browse the repository at this point in the history
  Payjoin receiver can set minimum `fee_rate`
  in order to reject payjoin requests that contain
  a psbt which doesnt meet the minimum `fee_rate`
  threshold.

  We add this functionality to `check_broadcast_suitability`
  function which previously was called `check_can_broadcast`.
  • Loading branch information
jbesraa committed Nov 28, 2023
1 parent 4d3bbc6 commit 62f78b0
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 5 deletions.
2 changes: 1 addition & 1 deletion payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions payjoin/src/receive/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InternalRequestError> for RequestError {
Expand Down Expand Up @@ -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),
),
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<FeeRate>,
can_broadcast: impl Fn(&bitcoin::Transaction) -> Result<bool, Error>,
) -> Result<MaybeInputsOwned, Error> {
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 {
Expand Down
18 changes: 17 additions & 1 deletion payjoin/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 62f78b0

Please sign in to comment.