From d8ecb216adb1641c52b6087ae5beb15a8b86a417 Mon Sep 17 00:00:00 2001 From: DanGould Date: Wed, 12 Jul 2023 14:29:11 -0400 Subject: [PATCH 1/3] Add back original PSBT input to payjoin proposal Unlike Bitcoin Core's walletprocesspsbt RPC, BKD's finalize_psbt only checks if the script in the PSBT input map matches the descriptor and does not check whether it has control of the OutPoint specified in the unsigned_tx's TxIn. So the original_psbt input data needs to be added back into payjoin_psbt without overwriting receiver input. BIP 78 spec clears script data from payjoin proposal. --- src/bitcoin/payment.rs | 47 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/bitcoin/payment.rs b/src/bitcoin/payment.rs index 8f5a3027..8c9a7e85 100644 --- a/src/bitcoin/payment.rs +++ b/src/bitcoin/payment.rs @@ -1,6 +1,15 @@ use anyhow::{anyhow, Result}; -use bdk::{wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails}; -use bitcoin::consensus::serialize; + +use bdk::{ + database::AnyDatabase, wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails, Wallet, +}; + +use bitcoin::consensus::{ + consensus::serialize, + psbt::{Input, Psbt}, + TxIn, +}; + use payjoin::{PjUri, PjUriExt}; use crate::{ @@ -59,7 +68,7 @@ pub async fn create_payjoin( // TODO use fee_rate let pj_params = payjoin::sender::Configuration::non_incentivizing(); - let (req, ctx) = pj_uri.create_pj_request(original_psbt, pj_params)?; + let (req, ctx) = pj_uri.create_pj_request(original_psbt.clone(), pj_params)?; info!("Built PayJoin request"); let response = reqwest::Client::new() .post(req.url) @@ -77,6 +86,7 @@ pub async fn create_payjoin( } let payjoin_psbt = ctx.process_response(res.as_bytes())?; + let payjoin_psbt = add_back_original_input(&original_psbt, payjoin_psbt); debug!( "Proposed PayJoin PSBT:", @@ -87,3 +97,34 @@ pub async fn create_payjoin( Ok(tx) } + +/// Unlike Bitcoin Core's walletprocesspsbt RPC, BDK's finalize_psbt only checks +/// if the script in the PSBT input map matches the descriptor and does not +/// check whether it has control of the OutPoint specified in the unsigned_tx's +/// TxIn. So the original_psbt input data needs to be added back into +/// payjoin_psbt without overwriting receiver input. +fn add_back_original_input(original_psbt: &Psbt, payjoin_psbt: Psbt) -> Psbt { + // input_pairs is only used here. It may be added to payjoin, rust-bitcoin, or BDK in time. + fn input_pairs(psbt: &Psbt) -> Box + '_> { + Box::new( + psbt.unsigned_tx + .input + .iter() + .cloned() // Clone each TxIn for better ergonomics than &muts + .zip(psbt.inputs.iter().cloned()), // Clone each Input too + ) + } + + let mut original_inputs = input_pairs(&original_psbt).peekable(); + + for (proposed_txin, mut proposed_psbtin) in input_pairs(&payjoin_psbt) { + if let Some((original_txin, original_psbtin)) = original_inputs.peek() { + if proposed_txin.previous_output == original_txin.previous_output { + proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone(); + proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone(); + } + original_inputs.next(); + } + } + payjoin_psbt +} From cc796ed3742375deea93c330f7eceedc43b01b16 Mon Sep 17 00:00:00 2001 From: DanGould Date: Wed, 12 Jul 2023 14:49:32 -0400 Subject: [PATCH 2/3] Bump payjoin v0.8.0 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- src/bitcoin/payment.rs | 10 ++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b40fabf7..ef7c2cbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2262,9 +2262,9 @@ checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" [[package]] name = "payjoin" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5770c48c69af29fc8fa9f9ccddd9aee7daf2616ba196651913939671fdcf1f" +checksum = "e076c820f8780985db6a2b1f6c0914350becc79aa3e071872bcd4bc17e7e0f06" dependencies = [ "base64 0.13.1", "bip21", diff --git a/Cargo.toml b/Cargo.toml index fdbb7da8..e91dd66c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ miniscript_crate = { package = "miniscript", version = "9.0.1", features = [ ] } nostr-sdk = "0.22.0" once_cell = "1.17.1" -payjoin = { version = "0.7.0", features = ["sender"] } +payjoin = { version = "0.8.0", features = ["send"] } percent-encoding = "2.2.0" postcard = { version = "1.0.4", features = ["alloc"] } pretty_env_logger = "0.5.0" diff --git a/src/bitcoin/payment.rs b/src/bitcoin/payment.rs index 8c9a7e85..2523d2e1 100644 --- a/src/bitcoin/payment.rs +++ b/src/bitcoin/payment.rs @@ -1,10 +1,8 @@ use anyhow::{anyhow, Result}; -use bdk::{ - database::AnyDatabase, wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails, Wallet, -}; +use bdk::{wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails}; -use bitcoin::consensus::{ +use bitcoin::{ consensus::serialize, psbt::{Input, Psbt}, TxIn, @@ -67,7 +65,7 @@ pub async fn create_payjoin( info!("Original PSBT successfully signed"); // TODO use fee_rate - let pj_params = payjoin::sender::Configuration::non_incentivizing(); + let pj_params = payjoin::send::Configuration::non_incentivizing(); let (req, ctx) = pj_uri.create_pj_request(original_psbt.clone(), pj_params)?; info!("Built PayJoin request"); let response = reqwest::Client::new() @@ -85,7 +83,7 @@ pub async fn create_payjoin( return Err(anyhow!("Error performing payjoin: {res}")); } - let payjoin_psbt = ctx.process_response(res.as_bytes())?; + let payjoin_psbt = ctx.process_response(&mut res.as_bytes())?; let payjoin_psbt = add_back_original_input(&original_psbt, payjoin_psbt); debug!( From 7b5aa38d9d1179074fe1724c1ab5a91a937eb13c Mon Sep 17 00:00:00 2001 From: DanGould Date: Wed, 12 Jul 2023 16:33:35 -0400 Subject: [PATCH 3/3] Apply additional fee recommendation --- src/bitcoin/payment.rs | 47 +++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/bitcoin/payment.rs b/src/bitcoin/payment.rs index 2523d2e1..43f05305 100644 --- a/src/bitcoin/payment.rs +++ b/src/bitcoin/payment.rs @@ -5,10 +5,10 @@ use bdk::{wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails}; use bitcoin::{ consensus::serialize, psbt::{Input, Psbt}, - TxIn, + Amount, TxIn, }; -use payjoin::{PjUri, PjUriExt}; +use payjoin::{send::Configuration, PjUri, PjUriExt}; use crate::{ bitcoin::{ @@ -49,23 +49,56 @@ pub async fn create_payjoin( fee_rate: Option, pj_uri: PjUri<'_>, // TODO specify Uri ) -> Result { + let enacted_fee_rate = fee_rate.unwrap_or_default(); let (psbt, details) = { let locked_wallet = wallet.lock().await; let mut builder = locked_wallet.build_tx(); - for invoice in invoices { + for invoice in &invoices { builder.add_recipient(invoice.address.script_pubkey(), invoice.amount); } - builder.enable_rbf().fee_rate(fee_rate.unwrap_or_default()); + builder.enable_rbf().fee_rate(enacted_fee_rate); builder.finish()? }; debug!(format!("Request PayJoin transaction: {details:#?}")); debug!("Unsigned Original PSBT:", base64::encode(&serialize(&psbt))); - let original_psbt = sign_original_psbt(wallet, psbt).await?; + let original_psbt = sign_original_psbt(wallet, psbt.clone()).await?; info!("Original PSBT successfully signed"); - // TODO use fee_rate - let pj_params = payjoin::send::Configuration::non_incentivizing(); + let additional_fee_index = psbt + .outputs + .clone() + .into_iter() + .enumerate() + .find(|(_, output)| { + invoices.iter().all(|invoice| { + output.redeem_script != Some(invoice.address.script_pubkey()) + && output.witness_script != Some(invoice.address.script_pubkey()) + }) + }) + .map(|(i, _)| i); + + let pj_params = match additional_fee_index { + Some(index) => { + let amount_available = psbt + .clone() + .unsigned_tx + .output + .get(index) + .map(|o| Amount::from_sat(o.value)) + .unwrap_or_default(); + const P2TR_INPUT_WEIGHT: usize = 58; // bitmask is taproot only + let recommended_fee = Amount::from_sat(enacted_fee_rate.fee_wu(P2TR_INPUT_WEIGHT)); + let max_additional_fee = std::cmp::min( + recommended_fee, + amount_available, // offer amount available if recommendation is not + ); + + Configuration::with_fee_contribution(max_additional_fee, Some(index)) + } + None => Configuration::non_incentivizing(), + }; + let (req, ctx) = pj_uri.create_pj_request(original_psbt.clone(), pj_params)?; info!("Built PayJoin request"); let response = reqwest::Client::new()