From 2cc61324899771d955555817f223f9ae57426147 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Fri, 24 May 2024 15:44:55 +0300 Subject: [PATCH] Open LN channel from incoming Payjoin tx This commit allows users to schedule a channel that will opened once a Payjoin request received. This can save users 1 extra onchain transaction fees. The Payjoin flow is normal with the following caveats: 1. We use `Payjoin::ProvisionalProposal::substitue_output_address` to point to the multisig output script as retrived from `LdkEvent::FundingGeneratingReady`. 2. We dont try to preserve privacy in Payjoin channel opening transactions. 3. We wait with our response to the Payjoin sender until a `Ldk::Event::FundingTxBroadcastSafe` event is received. --- Cargo.toml | 38 +-- src/builder.rs | 3 +- src/event.rs | 60 ++++- src/lib.rs | 8 + src/payjoin_channel_scheduler.rs | 251 ++++++++++++++++++ src/payjoin_receiver.rs | 120 ++++++++- src/payment/payjoin.rs | 104 +++++++- src/wallet.rs | 12 + tests/integration_tests_payjoin.rs | 7 +- ...tion_tests_payjoin_with_channel_opening.rs | 75 ++++++ 10 files changed, 639 insertions(+), 39 deletions(-) create mode 100644 src/payjoin_channel_scheduler.rs create mode 100644 tests/integration_tests_payjoin_with_channel_opening.rs diff --git a/Cargo.toml b/Cargo.toml index c0212fc4c..3b9ac2796 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,23 +28,23 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.0.123", features = ["std"] } -lightning-invoice = { version = "0.31.0" } -lightning-net-tokio = { version = "0.0.123" } -lightning-persister = { version = "0.0.123" } -lightning-background-processor = { version = "0.0.123", features = ["futures"] } -lightning-rapid-gossip-sync = { version = "0.0.123" } -lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] } -lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } - -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] } -#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["futures"] } -#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["esplora-async"] } -#lightning-liquidity = { git = "https://github.com/lightningdevkit/lightning-liquidity", branch="main", features = ["std"] } +# lightning = { version = "0.0.123", features = ["std"] } +# lightning-invoice = { version = "0.31.0" } +# lightning-net-tokio = { version = "0.0.123" } +# lightning-persister = { version = "0.0.123" } +# lightning-background-processor = { version = "0.0.123", features = ["futures"] } +# lightning-rapid-gossip-sync = { version = "0.0.123" } +# lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] } +# lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } + +lightning = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["std"] } +lightning-invoice = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-net-tokio = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-persister = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-background-processor = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["futures"] } +lightning-rapid-gossip-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-transaction-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["esplora-async"] } +lightning-liquidity = { git = "https://github.com/jbesraa/lightning-liquidity", branch="pj-fixes", features = ["std"] } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-invoice = { path = "../rust-lightning/lightning-invoice" } @@ -78,8 +78,8 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { version = "0.0.123", features = ["std", "_test_utils"] } -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +# lightning = { version = "0.0.123", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["std", "_test_utils"] } electrum-client = { version = "0.15.1", default-features = true } bitcoincore-rpc = { version = "0.17.0", default-features = false } proptest = "1.0.0" diff --git a/src/builder.rs b/src/builder.rs index 81df83320..5c5cf6f68 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1024,12 +1024,13 @@ fn build_with_store_internal( payjoin_receiver = Some(Arc::new(PayjoinReceiver::new( Arc::clone(&logger), Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&config), pj_config.payjoin_directory.clone(), pj_config.payjoin_relay.clone(), pj_config.ohttp_keys.clone(), ))); } - let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); diff --git a/src/event.rs b/src/event.rs index 41cba200a..77fca4e7f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,3 +1,4 @@ +use crate::payjoin_receiver::PayjoinReceiver; use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ @@ -375,6 +376,7 @@ where network_graph: Arc, payment_store: Arc>, peer_store: Arc>, + payjoin_receiver: Option>, runtime: Arc>>, logger: L, config: Arc, @@ -389,8 +391,9 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, - payment_store: Arc>, peer_store: Arc>, - runtime: Arc>>, logger: L, config: Arc, + payment_store: Arc>, payjoin_receiver: Option>, + peer_store: Arc>, runtime: Arc>>, + logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -401,6 +404,7 @@ where output_sweeper, network_graph, payment_store, + payjoin_receiver, peer_store, logger, runtime, @@ -415,6 +419,7 @@ where counterparty_node_id, channel_value_satoshis, output_script, + user_channel_id, .. } => { // Construct the raw transaction with the output that is paid the amount of the @@ -425,6 +430,18 @@ where let cur_height = self.channel_manager.current_best_block().height; let locktime = LockTime::from_height(cur_height).unwrap_or(LockTime::ZERO); + if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + if payjoin_receiver + .set_channel_accepted( + user_channel_id, + &output_script, + temporary_channel_id.0, + ) + .await + { + return; + } + } // Sign the final funding transaction and broadcast it. match self.wallet.create_funding_transaction( output_script, @@ -1087,6 +1104,45 @@ where ); } }, + LdkEvent::FundingTxBroadcastSafe { funding_tx, .. } => { + use crate::io::utils::ohttp_headers; + if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + let is_payjoin_channel = + payjoin_receiver.set_funding_tx_signed(funding_tx.clone()).await; + if let Some((url, body)) = is_payjoin_channel { + log_info!( + self.logger, + "Detected payjoin channel transaction. Sending payjoin sender request for transaction {}", + funding_tx.txid() + ); + let headers = ohttp_headers(); + let client = reqwest::Client::builder().build().unwrap(); + match client.post(url).body(body).headers(headers).send().await { + Ok(response) => { + if response.status().is_success() { + log_info!( + self.logger, + "Responded to 'Payjoin Sender' successfuly" + ); + } else { + log_info!( + self.logger, + "Got unsuccessful response from 'Payjoin Sender': {}", + response.status() + ); + } + }, + Err(e) => { + log_error!( + self.logger, + "Failed to send a response to 'Payjoin Sender': {}", + e + ); + }, + }; + } + } + }, LdkEvent::ChannelPending { channel_id, user_channel_id, diff --git a/src/lib.rs b/src/lib.rs index f3538d0ec..e30206dc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +mod payjoin_channel_scheduler; mod payjoin_receiver; mod payjoin_sender; pub mod payment; @@ -726,6 +727,7 @@ impl Node { Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), Arc::clone(&self.payment_store), + self.payjoin_receiver.clone(), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), Arc::clone(&self.logger), @@ -1111,6 +1113,9 @@ impl Node { payjoin_receiver.map(Arc::clone), Arc::clone(&self.config), Arc::clone(&self.event_queue), + Arc::clone(&self.peer_store), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), ) } @@ -1130,6 +1135,9 @@ impl Node { payjoin_receiver.map(Arc::clone), Arc::clone(&self.config), Arc::clone(&self.event_queue), + Arc::clone(&self.peer_store), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), )) } diff --git a/src/payjoin_channel_scheduler.rs b/src/payjoin_channel_scheduler.rs new file mode 100644 index 000000000..6f30e3252 --- /dev/null +++ b/src/payjoin_channel_scheduler.rs @@ -0,0 +1,251 @@ +use bitcoin::{secp256k1::PublicKey, Network, ScriptBuf, TxOut}; + +#[derive(Clone)] +pub struct PayjoinChannelScheduler { + channels: Vec, +} + +impl PayjoinChannelScheduler { + pub(crate) fn new() -> Self { + Self { channels: vec![] } + } + + pub(crate) fn schedule( + &mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, + channel_id: u128, + ) { + let channel = PayjoinChannel::new(channel_value_satoshi, counterparty_node_id, channel_id); + match channel.state { + ScheduledChannelState::ChannelCreated => { + self.channels.push(channel); + }, + _ => {}, + } + } + + pub(crate) fn set_channel_accepted( + &mut self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + for channel in &mut self.channels { + if channel.channel_id() == channel_id { + channel.state.set_channel_accepted(output_script, temporary_channel_id); + return true; + } + } + false + } + + pub(crate) fn set_funding_tx_created( + &mut self, channel_id: u128, url: &payjoin::Url, body: Vec, + ) -> bool { + for channel in &mut self.channels { + if channel.channel_id() == channel_id { + return channel.state.set_channel_funding_tx_created(url.clone(), body); + } + } + false + } + + pub(crate) fn set_funding_tx_signed( + &mut self, tx: bitcoin::Transaction, + ) -> Option<(payjoin::Url, Vec)> { + for output in tx.output.iter() { + if let Some(mut channel) = self.internal_find_by_tx_out(&output.clone()) { + let info = channel.request_info(); + if info.is_some() && channel.state.set_channel_funding_tx_signed(output.clone()) { + return info; + } + } + } + None + } + + /// Get the next channel matching the given channel amount. + /// + /// The channel must be in accepted state. + /// + /// If more than one channel matches the given channel amount, the channel with the oldest + /// creation date will be returned. + pub(crate) fn get_next_channel( + &self, channel_amount: bitcoin::Amount, network: Network, + ) -> Option<(u128, bitcoin::Address, [u8; 32], bitcoin::Amount, bitcoin::secp256k1::PublicKey)> + { + let channel = self + .channels + .iter() + .filter(|channel| { + channel.channel_value_satoshi() == channel_amount + && channel.is_channel_accepted() + && channel.output_script().is_some() + && channel.temporary_channel_id().is_some() + }) + .min_by_key(|channel| channel.created_at()); + + if let Some(channel) = channel { + let address = bitcoin::Address::from_script(&channel.output_script().unwrap(), network); + if let Ok(address) = address { + return Some(( + channel.channel_id(), + address, + channel.temporary_channel_id().unwrap(), + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + )); + } + }; + None + } + + fn internal_find_by_tx_out(&self, txout: &TxOut) -> Option { + let channel = self.channels.iter().find(|channel| { + return Some(&txout.script_pubkey) == channel.output_script(); + }); + channel.cloned() + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PayjoinChannel { + state: ScheduledChannelState, + channel_value_satoshi: bitcoin::Amount, + channel_id: u128, + counterparty_node_id: PublicKey, + created_at: u64, +} + +impl PayjoinChannel { + pub(crate) fn new( + channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128, + ) -> Self { + Self { + state: ScheduledChannelState::ChannelCreated, + channel_value_satoshi, + channel_id, + counterparty_node_id, + created_at: 0, + } + } + + fn is_channel_accepted(&self) -> bool { + match self.state { + ScheduledChannelState::ChannelAccepted(..) => true, + _ => false, + } + } + + pub(crate) fn channel_value_satoshi(&self) -> bitcoin::Amount { + self.channel_value_satoshi + } + + pub(crate) fn channel_id(&self) -> u128 { + self.channel_id + } + + pub(crate) fn counterparty_node_id(&self) -> PublicKey { + self.counterparty_node_id + } + + pub(crate) fn output_script(&self) -> Option<&ScriptBuf> { + self.state.output_script() + } + + pub(crate) fn temporary_channel_id(&self) -> Option<[u8; 32]> { + self.state.temporary_channel_id() + } + + pub(crate) fn request_info(&self) -> Option<(payjoin::Url, Vec)> { + match &self.state { + ScheduledChannelState::FundingTxCreated(_, url, body) => { + Some((url.clone(), body.clone())) + }, + _ => None, + } + } + + fn created_at(&self) -> u64 { + self.created_at + } +} + +#[derive(Clone, Debug)] +struct FundingTxParams { + output_script: ScriptBuf, + temporary_channel_id: [u8; 32], +} + +impl FundingTxParams { + fn new(output_script: ScriptBuf, temporary_channel_id: [u8; 32]) -> Self { + Self { output_script, temporary_channel_id } + } +} + +#[derive(Clone, Debug)] +enum ScheduledChannelState { + ChannelCreated, + ChannelAccepted(FundingTxParams), + FundingTxCreated(FundingTxParams, payjoin::Url, Vec), + FundingTxSigned(FundingTxParams, ()), +} + +impl ScheduledChannelState { + fn output_script(&self) -> Option<&ScriptBuf> { + match self { + ScheduledChannelState::ChannelAccepted(funding_tx_params) => { + Some(&funding_tx_params.output_script) + }, + ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => { + Some(&funding_tx_params.output_script) + }, + ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => { + Some(&funding_tx_params.output_script) + }, + _ => None, + } + } + + fn temporary_channel_id(&self) -> Option<[u8; 32]> { + match self { + ScheduledChannelState::ChannelAccepted(funding_tx_params) => { + Some(funding_tx_params.temporary_channel_id) + }, + ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => { + Some(funding_tx_params.temporary_channel_id) + }, + ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => { + Some(funding_tx_params.temporary_channel_id) + }, + _ => None, + } + } + + fn set_channel_accepted( + &mut self, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + if let ScheduledChannelState::ChannelCreated = self { + *self = ScheduledChannelState::ChannelAccepted(FundingTxParams::new( + output_script.clone(), + temporary_channel_id, + )); + return true; + } + return false; + } + + fn set_channel_funding_tx_created(&mut self, url: payjoin::Url, body: Vec) -> bool { + if let ScheduledChannelState::ChannelAccepted(funding_tx_params) = self { + *self = ScheduledChannelState::FundingTxCreated(funding_tx_params.clone(), url, body); + return true; + } + return false; + } + + fn set_channel_funding_tx_signed(&mut self, output: TxOut) -> bool { + let mut res = false; + if let ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) = self { + assert_eq!(funding_tx_params.output_script, output.script_pubkey); + *self = ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), ()); + res = true; + } + return res; + } +} diff --git a/src/payjoin_receiver.rs b/src/payjoin_receiver.rs index 71d9ac465..b17411768 100644 --- a/src/payjoin_receiver.rs +++ b/src/payjoin_receiver.rs @@ -1,7 +1,11 @@ use crate::error::Error; use crate::io::utils::ohttp_headers; use crate::logger::FilesystemLogger; -use crate::types::Wallet; +use crate::payjoin_channel_scheduler::{PayjoinChannel, PayjoinChannelScheduler}; +use crate::types::{ChannelManager, Wallet}; +use crate::Config; +use bitcoin::{ScriptBuf, Transaction}; +use lightning::ln::ChannelId; use lightning::log_info; use lightning::util::logger::Logger; use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; @@ -17,6 +21,8 @@ use tokio::sync::RwLock; pub(crate) struct PayjoinReceiver { logger: Arc, wallet: Arc, + channel_manager: Arc, + channel_scheduler: RwLock, /// Directory receiver wish to enroll with payjoin_directory: Url, /// Proxy server receiver wish to make requests through @@ -28,16 +34,21 @@ pub(crate) struct PayjoinReceiver { /// Optional as they can be fetched on behalf of the user if not provided. /// They are required in order to enroll. ohttp_keys: RwLock>, + config: Arc, } impl PayjoinReceiver { pub(crate) fn new( - logger: Arc, wallet: Arc, payjoin_directory: payjoin::Url, - payjoin_relay: payjoin::Url, ohttp_keys: Option, + logger: Arc, wallet: Arc, channel_manager: Arc, + config: Arc, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, ) -> Self { Self { logger, wallet, + channel_manager, + channel_scheduler: RwLock::new(PayjoinChannelScheduler::new()), + config, payjoin_directory, payjoin_relay, enrolled: RwLock::new(None), @@ -85,13 +96,21 @@ impl PayjoinReceiver { Ok(payjoin_uri) } + pub(crate) async fn set_channel_accepted( + &self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + let mut scheduler = self.channel_scheduler.write().await; + scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id) + } + /// After enrolling, we should periodacly check if we have received any Payjoin transactions. /// /// This function will try to fetch pending Payjoin requests from the subdirectory, and if a - /// successful response received, we validate the request as specified in [`BIP78`]. After - /// validation we try to preserve privacy by adding more inputs/outputs to the transaction. - /// Last, we finalise the transaction and send a response back the the Payjoin sender. - /// + /// successful response received, we validate the request as specified in [BIP78]. After + /// validation we check if we have a pending matching channel, and if so, we try fund the channel + /// with the incoming funds from the payjoin request. Otherwise, we accept the Payjoin request + /// normally by trying to preserve privacy, finalise the Payjoin proposal and send it back the + /// the Payjoin sender. /// /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist pub(crate) async fn process_payjoin_request(&self) { @@ -163,6 +182,7 @@ impl PayjoinReceiver { return; }, }; + let original_tx = unchecked_proposal.extract_tx_to_schedule_broadcast(); let provisional_proposal = match self.validate_payjoin_request(unchecked_proposal).await { Ok(proposal) => proposal, @@ -171,7 +191,68 @@ impl PayjoinReceiver { return; }, }; - self.accept_payjoin_transaction(provisional_proposal).await; + let amount = match self.wallet.funds_directed_to_us(&original_tx) { + Ok(a) => a, + Err(e) => { + // This should not happen in practice as the validation checks would fail if + // the sender didnt include us in the outputs + log_info!(self.logger, "Not able to find any ouput directed to us: {}", e); + return; + }, + }; + let mut scheduler = self.channel_scheduler.write().await; + let network = self.config.network; + if let Some(channel) = scheduler.get_next_channel(amount, network) { + log_info!(self.logger, "Found a channel match for incoming Payjoin request"); + let (channel_id, funding_tx_address, temporary_channel_id, _, counterparty_node_id) = + channel; + let mut channel_provisional_proposal = provisional_proposal.clone(); + channel_provisional_proposal.substitute_output_address(funding_tx_address); + let payjoin_proposal = match channel_provisional_proposal + .finalize_proposal(|psbt| Ok(psbt.clone()), None) + { + Ok(proposal) => proposal, + Err(e) => { + dbg!(&e); + return; + }, + }; + let (receiver_request, _) = match payjoin_proposal.clone().extract_v2_req() { + Ok((req, ctx)) => (req, ctx), + Err(e) => { + dbg!(&e); + return; + }, + }; + let tx = payjoin_proposal.psbt().clone().extract_tx(); + scheduler.set_funding_tx_created( + channel_id, + &receiver_request.url, + receiver_request.body, + ); + match self.channel_manager.unsafe_manual_funding_transaction_generated( + &ChannelId::from_bytes(temporary_channel_id), + &counterparty_node_id, + tx.clone(), + ) { + Ok(_) => { + // Created Funding Transaction and waiting for `FundingTxBroadcastSafe` event before returning a response + log_info!(self.logger, "Created channel funding transaction from Payjoin request and waiting for `FundingTxBroadcastSafe`"); + }, + Err(_) => { + log_info!( + self.logger, + "Unable to channel create funding tx from Payjoin request" + ); + }, + } + } else { + log_info!( + self.logger, + "Couldnt match a channel to Payjoin request, accepting normally" + ); + self.accept_payjoin_transaction(provisional_proposal).await; + } } else { log_info!(self.logger, "Payjoin Receiver: Unable to get enrolled object"); } @@ -396,6 +477,29 @@ impl PayjoinReceiver { && self.ohttp_keys.read().await.deref().is_some() } + /// Schedule a channel to opened upon receiving a Payjoin tranasction value with the same + /// channel funding amount. + pub(crate) async fn schedule_channel( + &self, amount: bitcoin::Amount, counterparty_node_id: bitcoin::secp256k1::PublicKey, + channel_id: u128, + ) { + let channel = PayjoinChannel::new(amount, counterparty_node_id, channel_id); + self.channel_scheduler.write().await.schedule( + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + channel.channel_id(), + ); + } + + /// This should only be called upon receiving [`Event::FundingTxBroadcastSafe`] + /// + /// [`Event::FundingTxBroadcastSafe`]: lightning::events::Event::FundingTxBroadcastSafe + pub(crate) async fn set_funding_tx_signed( + &self, funding_tx: Transaction, + ) -> Option<(payjoin::Url, Vec)> { + self.channel_scheduler.write().await.set_funding_tx_signed(funding_tx) + } + /// Validate an incoming Payjoin request as specified in [BIP78]. /// /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist diff --git a/src/payment/payjoin.rs b/src/payment/payjoin.rs index a0bee189e..fb463bf8c 100644 --- a/src/payment/payjoin.rs +++ b/src/payment/payjoin.rs @@ -1,11 +1,17 @@ //! Holds a payment handler allowing to send Payjoin payments. use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; -use crate::types::{EventQueue, PayjoinSender}; +use crate::types::{ChannelManager, EventQueue, PayjoinSender}; use crate::Event; +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::SocketAddress; +use lightning::util::config::{ChannelHandshakeConfig, UserConfig}; use payjoin::PjUri; +use crate::connection::ConnectionManager; +use crate::logger::FilesystemLogger; use crate::payjoin_receiver::PayjoinReceiver; +use crate::peer_store::{PeerInfo, PeerStore}; use crate::{error::Error, Config}; use std::sync::{Arc, RwLock}; @@ -58,14 +64,28 @@ pub struct PayjoinPayment { receiver: Option>, config: Arc, event_queue: Arc, + peer_store: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, } impl PayjoinPayment { pub(crate) fn new( runtime: Arc>>, sender: Option>, receiver: Option>, config: Arc, event_queue: Arc, + peer_store: Arc>>, channel_manager: Arc, + connection_manager: Arc>>, ) -> Self { - Self { runtime, sender, receiver, config, event_queue } + Self { + runtime, + sender, + receiver, + config, + event_queue, + peer_store, + channel_manager, + connection_manager, + } } /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. @@ -193,13 +213,89 @@ impl PayjoinPayment { /// Payjoin sender. /// /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki - pub async fn receive(&self, amount: bitcoin::Amount) -> Result { + pub fn receive(&self, amount: bitcoin::Amount) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } if let Some(receiver) = &self.receiver { - receiver.receive(amount).await + let runtime = rt_lock.as_ref().unwrap(); + runtime.handle().block_on(async { receiver.receive(amount).await }) + } else { + Err(Error::PayjoinReceiverUnavailable) + } + } + + /// Receive on chain Payjoin transaction and open a channel in a single transaction. + /// + /// This method will enroll with the configured Payjoin directory if not already, + /// and before returning a [BIP21] URI pointing to our enrolled subdirectory to share with + /// Payjoin sender, we start the channel opening process and halt it when we receive + /// `accept_channel` from counterparty node. Once the Payjoin request is received, we move + /// forward with the channel opening process. + /// + /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub fn receive_with_channel_opening( + &self, channel_amount_sats: u64, push_msat: Option, announce_channel: bool, + node_id: PublicKey, address: SocketAddress, + ) -> Result { + use rand::Rng; + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + if let Some(receiver) = &self.receiver { + let user_channel_id: u128 = rand::thread_rng().gen::(); + let runtime = rt_lock.as_ref().unwrap(); + runtime.handle().block_on(async { + receiver + .schedule_channel( + bitcoin::Amount::from_sat(channel_amount_sats), + node_id, + user_channel_id, + ) + .await; + }); + let user_config = UserConfig { + channel_handshake_limits: Default::default(), + channel_handshake_config: ChannelHandshakeConfig { + announced_channel: announce_channel, + ..Default::default() + }, + ..Default::default() + }; + let push_msat = push_msat.unwrap_or(0); + let peer_info = PeerInfo { node_id, address }; + + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + runtime.handle().block_on(async { + let _ = con_cm.connect_peer_if_necessary(con_node_id, con_addr).await; + }); + + match self.channel_manager.create_channel( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) { + Ok(_) => { + self.peer_store.add_peer(peer_info)?; + }, + Err(_) => { + return Err(Error::ChannelCreationFailed); + }, + }; + + runtime.handle().block_on(async { + let payjoin_uri = + receiver.receive(bitcoin::Amount::from_sat(channel_amount_sats)).await?; + Ok(payjoin_uri) + }) } else { Err(Error::PayjoinReceiverUnavailable) } diff --git a/src/wallet.rs b/src/wallet.rs index 42498d600..111657bcb 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -151,6 +151,18 @@ where res } + // Returns the total value of all outputs in the given transaction that are directed to us + pub(crate) fn funds_directed_to_us(&self, tx: &Transaction) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let total_value = tx.output.iter().fold(0, |acc, output| { + match locked_wallet.is_mine(&output.script_pubkey) { + Ok(true) => acc + output.value, + _ => acc, + } + }); + Ok(bitcoin::Amount::from_sat(total_value)) + } + pub(crate) fn build_payjoin_transaction( &self, output_script: ScriptBuf, value_sats: u64, ) -> Result { diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs index d57dc7041..9a85897ac 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -28,12 +28,9 @@ fn send_receive_regular_payjoin_transaction() { assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00); assert_eq!(node_a_pj_receiver.next_event(), None); let payjoin_payment = node_a_pj_receiver.payjoin_payment(); - - let payjoin_uri = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { - let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).await.unwrap(); - payjoin_uri - }); + let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).unwrap(); let payjoin_uri = payjoin_uri.to_string(); + dbg!(&payjoin_uri); let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); diff --git a/tests/integration_tests_payjoin_with_channel_opening.rs b/tests/integration_tests_payjoin_with_channel_opening.rs new file mode 100644 index 000000000..1b38edfbd --- /dev/null +++ b/tests/integration_tests_payjoin_with_channel_opening.rs @@ -0,0 +1,75 @@ +mod common; + +use common::{ + expect_channel_pending_event, expect_channel_ready_event, + expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_payjoin_nodes, + wait_for_tx, +}; + +use bitcoin::Amount; +use ldk_node::Event; + +#[test] +fn send_receive_payjoin_transaction_with_channel_opening() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false); + let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap(); + let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b, addr_a], + Amount::from_sat(premine_amount_sat), + ); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(node_a_pj_receiver.next_event(), None); + assert_eq!(node_a_pj_receiver.list_channels().len(), 0); + let payjoin_payment = node_a_pj_receiver.payjoin_payment(); + let node_b_listening_address = + node_b_pj_sender.listening_addresses().unwrap().get(0).unwrap().clone(); + let payjoin_uri = payjoin_payment + .receive_with_channel_opening( + 80_000, + None, + false, + node_b_pj_sender.node_id(), + node_b_listening_address, + ).unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); + assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); + expect_channel_pending_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); + expect_channel_pending_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); + let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + let node_b_balance = node_b_pj_sender.list_balances(); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); + + expect_channel_ready_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); + expect_channel_ready_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); + let channels = node_a_pj_receiver.list_channels(); + let channel = channels.get(0).unwrap(); + assert_eq!(channel.channel_value_sats, 80_000); + assert!(channel.is_channel_ready); + assert!(channel.is_usable); + + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_connected, true); + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_persisted, true); + assert_eq!( + node_a_pj_receiver.list_peers().get(0).unwrap().node_id, + node_b_pj_sender.node_id() + ); + + let invoice_amount_1_msat = 2500_000; + let invoice = + node_b_pj_sender.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap(); + assert!(node_a_pj_receiver.bolt11_payment().send(&invoice).is_ok()); +}