From fa4445468759c069a7226daa90c3092cec59ad9a Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 22 Apr 2024 10:43:23 +0300 Subject: [PATCH 01/11] Add `Wallet::is_mine` Checks whether a script is owned by the node wallet. --- src/wallet.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wallet.rs b/src/wallet.rs index 674cb6786..6502a8003 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -111,6 +111,10 @@ where res } + pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result { + Ok(self.inner.lock().unwrap().is_mine(script)?) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime, From d459c46f3ace0b8a1d2e3b0c5c565b77151191cf Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 22 Apr 2024 10:44:15 +0300 Subject: [PATCH 02/11] Add `Wallet::sign_tx` Sign a transaction with the node wallet. --- src/wallet.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/wallet.rs b/src/wallet.rs index 6502a8003..62fca988c 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -2,6 +2,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger}; use crate::Error; +use bitcoin::psbt::Psbt; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; @@ -115,6 +116,14 @@ where Ok(self.inner.lock().unwrap().is_mine(script)?) } + pub(crate) fn sign_tx(&self, psbt: &Psbt, options: Option) -> Result { + let wallet = self.inner.lock().unwrap(); + let mut psbt = psbt.clone(); + let options = options.unwrap_or_default(); + wallet.sign(&mut psbt, options)?; + Ok(psbt) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime, From ca85529a3c8d620218bea89045bf3507924018f9 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 7 May 2024 15:24:14 +0300 Subject: [PATCH 03/11] Add `Wallet::verify_tx` Verify that a tx meets bitcoinconsensus --- Cargo.toml | 2 +- src/wallet.rs | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4c4422461..7d872b87e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ bdk = { version = "0.29.0", default-features = false, features = ["std", "async- reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } rusqlite = { version = "0.28.0", features = ["bundled"] } -bitcoin = "0.30.2" +bitcoin = { version = "0.30.2", features = ["bitcoinconsensus"] } bip39 = "2.0.0" rand = "0.8.5" diff --git a/src/wallet.rs b/src/wallet.rs index 62fca988c..0e9c5b73d 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -25,8 +25,9 @@ use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; -use bitcoin::{ScriptBuf, Transaction, TxOut, Txid}; +use bitcoin::{bitcoinconsensus, ScriptBuf, Transaction, TxOut, Txid}; +use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; @@ -116,6 +117,29 @@ where Ok(self.inner.lock().unwrap().is_mine(script)?) } + pub async fn verify_tx(&self, tx: Transaction) -> Result<(), Error> { + let serialized_tx = bitcoin::consensus::serialize(&tx); + for (index, input) in tx.input.iter().enumerate() { + let input = input.clone(); + let txid = input.previous_output.txid; + let prev_tx = if let Ok(prev_tx) = self.blockchain.get_tx(&txid).await { + prev_tx.unwrap() + } else { + dbg!("maybe conibase?"); + continue; + }; + let spent_output = prev_tx.output.get(input.previous_output.vout as usize).unwrap(); + bitcoinconsensus::verify( + &spent_output.script_pubkey.to_bytes(), + spent_output.value, + &serialized_tx, + index, + ) + .unwrap(); + } + Ok(()) + } + pub(crate) fn sign_tx(&self, psbt: &Psbt, options: Option) -> Result { let wallet = self.inner.lock().unwrap(); let mut psbt = psbt.clone(); From 5aef166cd1f48b034be41613fa019de3bc0d19c9 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 16 Apr 2024 14:51:14 +0300 Subject: [PATCH 04/11] Add `internal_connect_open_channel` Split the current `connect_open_channel` into two separate functions to utilise the code in future channel opening functions. More precisly, this is to use `internal_connect_open_channel` in a future commit by new `payjoin_connect_open_channel` function. --- src/lib.rs | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3d619cebb..bb3de01cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -980,20 +980,6 @@ impl Node { return Err(Error::InsufficientFunds); } - 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); - - // We need to use our main runtime here as a local runtime might not be around to poll - // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) - })?; - let channel_config = (*(channel_config.unwrap_or_default())).clone().into(); let user_config = UserConfig { channel_handshake_limits: Default::default(), @@ -1007,6 +993,33 @@ impl Node { let push_msat = push_to_counterparty_msat.unwrap_or(0); let user_channel_id: u128 = rand::thread_rng().gen::(); + self.internal_connect_open_channel( + node_id, + channel_amount_sats, + push_msat, + user_channel_id, + address, + Some(user_config), + runtime, + ) + } + + fn internal_connect_open_channel( + &self, node_id: PublicKey, channel_amount_sats: u64, push_msat: u64, user_channel_id: u128, + address: SocketAddress, user_config: Option, runtime: &tokio::runtime::Runtime, + ) -> Result { + 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); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; match self.channel_manager.create_channel( peer_info.node_id, @@ -1014,7 +1027,7 @@ impl Node { push_msat, user_channel_id, None, - Some(user_config), + user_config, ) { Ok(_) => { log_info!( From 020a1eb10e7c2e221cf30b281ada45dd84d872b8 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 16 Apr 2024 17:45:43 +0300 Subject: [PATCH 05/11] Add `ChannelScheduler` and `PayjoinHandler` to `Node` ... --- Cargo.toml | 38 +- bindings/ldk_node.udl | 7 + src/builder.rs | 66 +++- src/channel_scheduler.rs | 329 +++++++++++++++++ src/error.rs | 27 ++ src/event.rs | 18 + src/lib.rs | 135 +++++++ src/payjoin_handler.rs | 542 +++++++++++++++++++++++++++++ src/tx_broadcaster.rs | 78 ++++- src/wallet.rs | 121 +++++-- tests/common/mod.rs | 16 +- tests/integration_tests_payjoin.rs | 211 +++++++++++ tests/integration_tests_rust.rs | 14 +- 13 files changed, 1557 insertions(+), 45 deletions(-) create mode 100644 src/channel_scheduler.rs create mode 100644 src/payjoin_handler.rs create mode 100644 tests/integration_tests_payjoin.rs diff --git a/Cargo.toml b/Cargo.toml index 7d872b87e..7dff4907b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,15 +28,28 @@ 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 = { 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", branch = "danger-funding-generated", features = ["std"] } +lightning-invoice = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-net-tokio = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-persister = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-background-processor = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated", features = ["futures"] } +lightning-rapid-gossip-sync = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated" } +lightning-transaction-sync = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated", features = ["esplora-async-https", "time"] } +#lightning-liquidity = { version = "0.1.0-alpha.1", features = ["std"] } + +# lightning-liquidity = {path = "../../lightning-liquidity" git = "https://github.com/jbesraa/lightning-liquidity", rev = "b6ac60d", features = ["std"] } +lightning-liquidity = { git = "https://github.com/jbesraa/lightning-liquidity", branch = "pj-fixes", features = ["std"] } +# lightning-liquidity = { git = "https://github.com/tnull/lightning-liquidity", rev = "abf7088c0e03221c0f122e797f34802c9e99a3d4", features = ["std"] } + +# payjoin = { git = "https://github.com/jbesraa/rust-payjoin.git", rev = "9e4f454", features = ["v2", "receive", "send"] } #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" } @@ -57,7 +70,7 @@ lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } bdk = { version = "0.29.0", default-features = false, features = ["std", "async-interface", "use-esplora-async", "sqlite-bundled", "keys-bip39"]} -reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] } rusqlite = { version = "0.28.0", features = ["bundled"] } bitcoin = { version = "0.30.2", features = ["bitcoinconsensus"] } bip39 = "2.0.0" @@ -68,6 +81,7 @@ tokio = { version = "1", default-features = false, features = [ "rt-multi-thread esplora-client = { version = "0.6", default-features = false } libc = "0.2" uniffi = { version = "0.26.0", features = ["build"], optional = true } +payjoin = { version = "0.15.0", features = ["v2", "send", "receive"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" @@ -77,12 +91,14 @@ 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 = { version = "0.0.123", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/jbesraa/rust-lightning", branch = "danger-funding-generated", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", 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" regex = "1.5.6" +reqwest = { version = "0.11", default-features = false, features = ["blocking"] } [target.'cfg(not(no_download))'.dev-dependencies] electrsd = { version = "0.26.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 58fab0d52..b73cae720 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -151,6 +151,13 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinReqwest", + "PayjoinValidation", + "PayjoinEnrollment", + "PayjoinUri", + "PayjoinReceiver", + "PayjoinSender", + "BitcoinConsensusFailed", }; dictionary NodeStatus { diff --git a/src/builder.rs b/src/builder.rs index 6d3db420f..738cb3eee 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,3 +1,4 @@ +use crate::channel_scheduler::ChannelScheduler; use crate::config::{ Config, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, DEFAULT_ESPLORA_SERVER_URL, WALLET_KEYS_SEED_LEN, @@ -11,6 +12,7 @@ use crate::io::sqlite_store::SqliteStore; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; +use crate::payjoin_handler::{PayjoinReceiver, PayjoinSender}; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -94,6 +96,13 @@ struct LiquiditySourceConfig { lsps2_service: Option<(SocketAddress, PublicKey, Option)>, } +#[derive(Debug, Clone)] +struct PayjoinConfig { + payjoin_directory: payjoin::Url, + payjoin_relay: payjoin::Url, + ohttp_keys: Option, +} + impl Default for LiquiditySourceConfig { fn default() -> Self { Self { lsps2_service: None } @@ -173,6 +182,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_config: Option, } impl NodeBuilder { @@ -188,12 +198,14 @@ impl NodeBuilder { let chain_data_source_config = None; let gossip_source_config = None; let liquidity_source_config = None; + let payjoin_config = None; Self { config, entropy_source_config, chain_data_source_config, gossip_source_config, liquidity_source_config, + payjoin_config, } } @@ -248,6 +260,16 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync + /// server. + pub fn set_payjoin_config( + &mut self, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, + ) -> &mut Self { + self.payjoin_config = Some(PayjoinConfig { payjoin_directory, payjoin_relay, ohttp_keys }); + self + } + /// Configures the [`Node`] instance to source its inbound liquidity from the given /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. @@ -369,6 +391,7 @@ impl NodeBuilder { seed_bytes, logger, vss_store, + self.payjoin_config.as_ref(), ) } @@ -390,6 +413,7 @@ impl NodeBuilder { seed_bytes, logger, kv_store, + self.payjoin_config.as_ref(), ) } } @@ -460,6 +484,18 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_rgs(rgs_server_url); } + /// Configures the [`Node`] instance to use payjoin. + pub fn set_payjoin_config( + &self, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, + ) { + self.inner.write().unwrap().set_payjoin_config( + payjoin_directory, + payjoin_relay, + ohttp_keys, + ); + } + /// Configures the [`Node`] instance to source its inbound liquidity from the given /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. @@ -523,7 +559,7 @@ fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - logger: Arc, kv_store: Arc, + logger: Arc, kv_store: Arc, payjoin_config: Option<&PayjoinConfig>, ) -> Result { // Initialize the on-chain wallet and chain access let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes) @@ -556,6 +592,7 @@ fn build_with_store_internal( log_error!(logger, "Failed to set up wallet: {}", e); BuildError::WalletSetupFailed })?; + let channel_scheduler = Arc::new(tokio::sync::Mutex::new(ChannelScheduler::new())); let (blockchain, tx_sync, tx_broadcaster, fee_estimator) = match chain_data_source_config { Some(ChainDataSourceConfig::Esplora(server_url)) => { @@ -566,6 +603,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), + Arc::clone(&channel_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -584,6 +622,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), + Arc::clone(&channel_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -973,6 +1012,29 @@ fn build_with_store_internal( }; let (stop_sender, _) = tokio::sync::watch::channel(()); + let (payjoin_receiver, payjoin_sender) = if let Some(payjoin_config) = payjoin_config { + let payjoin_receiver = match PayjoinReceiver::enroll( + &payjoin_config.ohttp_keys, + &payjoin_config.payjoin_directory, + &payjoin_config.payjoin_relay, + Arc::clone(&channel_scheduler), + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&logger), + ) { + Ok(r) => Some(Arc::new(r)), + Err(_e) => None, + }; + let payjoin_sender = PayjoinSender::new( + Arc::clone(&logger), + Arc::clone(&wallet), + &payjoin_config.payjoin_relay, + &payjoin_config.payjoin_directory, + ); + (payjoin_receiver, Some(Arc::new(payjoin_sender))) + } else { + (None, None) + }; let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); @@ -993,6 +1055,8 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_receiver, + payjoin_sender, peer_manager, connection_manager, keys_manager, diff --git a/src/channel_scheduler.rs b/src/channel_scheduler.rs new file mode 100644 index 000000000..5cf5738dd --- /dev/null +++ b/src/channel_scheduler.rs @@ -0,0 +1,329 @@ +use bitcoin::{secp256k1::PublicKey, ScriptBuf, TxOut}; + +#[derive(Clone)] +pub struct ChannelScheduler { + channels: Vec, +} + +impl ChannelScheduler { + /// Create a new empty channel scheduler. + pub fn new() -> Self { + Self { channels: vec![] } + } + /// Schedule a new channel. + /// + /// The channel will be created with `ScheduledChannelState::ChannelCreated` state. + pub fn schedule( + &mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, + channel_id: u128, + ) { + let channel = + ScheduledChannel::new(channel_value_satoshi, counterparty_node_id, channel_id); + match channel.state { + ScheduledChannelState::ChannelCreated => { + self.channels.push(channel); + }, + _ => {}, + } + } + /// Mark a channel as accepted. + /// + /// The channel will be updated to `ScheduledChannelState::ChannelAccepted` state. + pub 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 + } + /// Mark a channel as funding tx created. + /// + /// The channel will be updated to `ScheduledChannelState::FundingTxCreated` state. + pub 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 + } + /// Mark a channel as funding tx signed. + /// + /// The channel will be updated to `ScheduledChannelState::FundingTxSigned` state. + pub 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 the accepted state. + /// + /// If more than one channel matches the given channel amount, the channel with the oldest + /// creation date will be returned. + pub fn get_next_channel( + &self, channel_amount: bitcoin::Amount, + ) -> 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(), + bitcoin::Network::Regtest, // fixme + ); + 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 + } + + /// List all channels. + pub fn list_channels(&self) -> &Vec { + &self.channels + } + + pub fn in_progress(&self) -> bool { + self.channels.iter().any(|channel| !channel.is_channel_accepted()) + } + 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() + } +} + +/// A struct representing a scheduled channel. +#[derive(Clone, Debug)] +pub struct ScheduledChannel { + state: ScheduledChannelState, + channel_value_satoshi: bitcoin::Amount, + channel_id: u128, + counterparty_node_id: PublicKey, + created_at: u64, +} + +impl ScheduledChannel { + pub 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 fn channel_value_satoshi(&self) -> bitcoin::Amount { + self.channel_value_satoshi + } + + /// Get the user channel id. + pub fn channel_id(&self) -> u128 { + self.channel_id + } + + /// Get the counterparty node id. + pub fn counterparty_node_id(&self) -> PublicKey { + self.counterparty_node_id + } + + /// Get the output script. + pub fn output_script(&self) -> Option<&ScriptBuf> { + self.state.output_script() + } + + /// Get the temporary channel id. + pub fn temporary_channel_id(&self) -> Option<[u8; 32]> { + self.state.temporary_channel_id() + } + + /// Get the temporary channel id. + pub fn tx_out(&self) -> Option<&TxOut> { + match &self.state { + ScheduledChannelState::FundingTxSigned(_, txout) => Some(txout), + _ => None, + } + } + + pub 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, TxOut), +} + +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 { + *self = + ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), output.clone()); + res = true; + } + return res; + } +} + +// #[cfg(test)] +// mod tests { +// use std::str::FromStr; + +// use super::*; +// use bitcoin::{ +// psbt::Psbt, +// secp256k1::{self, Secp256k1}, +// }; + +// #[ignore] +// #[test] +// fn test_channel_scheduler() { +// let create_pubkey = || -> PublicKey { +// let secp = Secp256k1::new(); +// PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap()) +// }; +// let channel_value_satoshi = 100; +// let node_id = create_pubkey(); +// let channel_id: u128 = 0; +// let mut channel_scheduler = ChannelScheduler::new(); +// channel_scheduler.schedule( +// bitcoin::Amount::from_sat(channel_value_satoshi), +// node_id, +// channel_id, +// ); +// assert_eq!(channel_scheduler.channels.len(), 1); +// assert_eq!(channel_scheduler.is_channel_created(channel_id), true); +// channel_scheduler.set_channel_accepted( +// channel_id, +// &ScriptBuf::from(vec![1, 2, 3]), +// [0; 32], +// ); +// assert_eq!(channel_scheduler.is_channel_accepted(channel_id), true); +// let str_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; +// let mock_transaction = Psbt::from_str(str_psbt).unwrap(); +// let _our_txout = mock_transaction.clone().extract_tx().output[0].clone(); +// // channel_scheduler.set_funding_tx_created(channel_id, mock_transaction.clone()); +// // let tx_id = mock_transaction.extract_tx().txid(); +// // assert_eq!(channel_scheduler.is_funding_tx_created(&tx_id), true); +// // channel_scheduler.set_funding_tx_signed(tx_id); +// // assert_eq!(channel_scheduler.is_funding_tx_signed(&tx_id), true); +// } +// } diff --git a/src/error.rs b/src/error.rs index 5acc75af8..00be44481 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,20 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Payjoin errors + PayjoinReqwest, + /// Payjoin errors + PayjoinValidation, + /// Payjoin errors + PayjoinEnrollment, + /// Payjoin errors + PayjoinUri, + /// Payjoin errors + PayjoinReceiver, + /// Payjoin errors + PayjoinSender, + /// Payjoin errors + BitcoinConsensusFailed, } impl fmt::Display for Error { @@ -122,10 +136,23 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::PayjoinReqwest => write!(f, "PayjoinLightning: http error"), + Self::PayjoinValidation => write!(f, "PayjoinLightning: payjoin request validation failed."), + Self::PayjoinEnrollment => write!(f, "PayjoinLightning: payjoin enrollment failed. Maybe the configured payjoin directory or payjoin relay are not valid?"), + Self::PayjoinUri => write!(f, "PayjoinLightning: Failed to construct payjoin URI."), + Self::PayjoinSender => write!(f, "Failed to send payjoin."), + Self::PayjoinReceiver => write!(f, "Failed to receive payjoin."), + Self::BitcoinConsensusFailed => write!(f, "Bitcoin consensus failed."), } } } +impl From for Error { + fn from(_e: payjoin::Error) -> Self { + return Self::PayjoinValidation; + } +} + impl std::error::Error for Error {} impl From for Error { diff --git a/src/event.rs b/src/event.rs index 78188452f..41bfb387f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,3 +1,4 @@ +use crate::payjoin_handler::PayjoinReceiver; use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, @@ -322,6 +323,7 @@ where runtime: Arc>>, logger: L, config: Arc, + payjoin_receiver: Option>>, } impl EventHandler @@ -333,6 +335,7 @@ where output_sweeper: Arc, network_graph: Arc, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>, logger: L, config: Arc, + payjoin_receiver: Option>>, ) -> Self { Self { event_queue, @@ -341,6 +344,7 @@ where output_sweeper, network_graph, payment_store, + payjoin_receiver, peer_store, logger, runtime, @@ -355,6 +359,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 @@ -365,6 +370,19 @@ 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, diff --git a/src/lib.rs b/src/lib.rs index bb3de01cf..b496b65e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,7 @@ mod balance; mod builder; +mod channel_scheduler; mod config; mod connection; mod error; @@ -88,6 +89,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +mod payjoin_handler; pub mod payment; mod peer_store; mod sweep; @@ -99,6 +101,7 @@ mod wallet; pub use bip39; pub use bitcoin; +use bitcoin::address::NetworkChecked; pub use lightning; pub use lightning_invoice; @@ -107,7 +110,10 @@ pub use config::{default_config, Config}; pub use error::Error as NodeError; use error::Error; +use channel_scheduler::ScheduledChannel; pub use event::Event; +use payjoin::PjUri; +use payjoin_handler::{PayjoinReceiver, PayjoinSender}; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -181,6 +187,8 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_receiver: Option>>>, + payjoin_sender: Option>>>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -491,6 +499,50 @@ impl Node { }); } + if let Some(payjoin_receiver) = &self.payjoin_receiver { + let mut stop_payjoin_server = self.stop_sender.subscribe(); + let payjoin_receiver = Arc::clone(&payjoin_receiver); + let payjoin_check_interval = 1; + runtime.spawn(async move { + let mut payjoin_interval = + tokio::time::interval(Duration::from_secs(payjoin_check_interval)); + payjoin_interval.reset(); + payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + tokio::select! { + _ = stop_payjoin_server.changed() => { + return; + } + _ = payjoin_interval.tick() => { + let _ = payjoin_receiver.process_payjoin_request().await; + } + } + } + }); + } + + if let Some(payjoin_sender) = &self.payjoin_sender { + let mut stop_payjoin_server = self.stop_sender.subscribe(); + let payjoin_sender = Arc::clone(&payjoin_sender); + let payjoin_check_interval = 1; + runtime.spawn(async move { + let mut payjoin_interval = + tokio::time::interval(Duration::from_secs(payjoin_check_interval)); + payjoin_interval.reset(); + payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + tokio::select! { + _ = stop_payjoin_server.changed() => { + return; + } + _ = payjoin_interval.tick() => { + let _ = payjoin_sender.process_payjoin_response().await; + } + } + } + }); + } + // Regularly reconnect to persisted peers. let connect_cm = Arc::clone(&self.connection_manager); let connect_pm = Arc::clone(&self.peer_manager); @@ -628,6 +680,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), + self.payjoin_receiver.clone(), )); // Setup background processing @@ -697,6 +750,88 @@ impl Node { Ok(()) } + /// Send a payjoin transaction from the node on chain funds to the address as specified in the + /// payjoin URI. + pub fn send_payjoin_transaction( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, + ) -> Result, Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let payjoin_sender = self.payjoin_sender.as_ref().ok_or(Error::PayjoinSender)?; + let psbt = payjoin_sender.create_payjoin_request(payjoin_uri.clone()).unwrap(); + payjoin_sender.send_payjoin_request(payjoin_uri, psbt) + } + + /// Creates a payjoin URI with the given amount that can be used to request a payjoin + /// transaction. + pub fn request_payjoin_transaction(&self, amount_sats: u64) -> Result { + let payjoin_receiver = self.payjoin_receiver.as_ref().ok_or(Error::PayjoinReceiver)?; + payjoin_receiver.payjoin_uri(bitcoin::Amount::from_sat(amount_sats)) + } + + /// Requests a payjoin transaction with the given amount and a corresponding lightning channel + /// opening. + /// + /// This method will attempt to open a channel with the given node if the URI returned is + /// called. If the payjoin transaction is received but the channel opening fails, a normal + /// payjoin transaction will be conducted. + /// + /// The channel opening will start immediately after this method is called and is halted at the + /// `ChannelAccepted` state until the payjoin transaction is received and handeled by + /// `PayjoinReceiver::process_payjoin_request`. + pub fn request_payjoin_transaction_with_channel_opening( + &self, channel_amount_sats: u64, push_msat: Option, announce_channel: bool, + node_id: PublicKey, address: SocketAddress, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let runtime = rt_lock.as_ref().unwrap(); + let user_channel_id: u128 = rand::thread_rng().gen::(); + let payjoin_receiver = self.payjoin_receiver.as_ref().ok_or(Error::PayjoinReceiver)?; + payjoin_receiver.schedule_channel( + bitcoin::Amount::from_sat(channel_amount_sats), + node_id, + user_channel_id, + ); + 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); + self.internal_connect_open_channel( + node_id, + channel_amount_sats, + push_msat, + user_channel_id, + address, + Some(user_config), + runtime, + )?; + let payjoin_uri = self + .payjoin_receiver + .as_ref() + .ok_or(Error::PayjoinReceiver)? + .payjoin_uri(bitcoin::Amount::from_sat(channel_amount_sats))?; + Ok(payjoin_uri) + } + + /// List all scheduled payjoin channels. + pub fn list_scheduled_payjoin_channels(&self) -> Result, Error> { + if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + Ok(payjoin_receiver.list_scheduled_channels()) + } else { + Ok(Vec::new()) + } + } + /// Disconnects all peers, stops all running background tasks, and shuts down [`Node`]. /// /// After this returns most API methods will return [`Error::NotRunning`]. diff --git a/src/payjoin_handler.rs b/src/payjoin_handler.rs new file mode 100644 index 000000000..b1d525572 --- /dev/null +++ b/src/payjoin_handler.rs @@ -0,0 +1,542 @@ +use crate::channel_scheduler::ChannelScheduler; +use crate::error::Error; +use crate::types::{ChannelManager, Wallet}; +use crate::ScheduledChannel; +use bdk::SignOptions; +use bitcoin::address::NetworkChecked; +use bitcoin::psbt::{Input, PartiallySignedTransaction, Psbt}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{ScriptBuf, Txid}; +use lightning::ln::ChannelId; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; +use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; +use payjoin::send::RequestContext; +use payjoin::{OhttpKeys, PjUriBuilder}; +use payjoin::{PjUri, Url}; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderValue; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::Mutex; +use tokio::time::sleep; + +// Payjoin receiver is a node that can receive payjoin requests. +// +// In order to setup a payjoin receiver, you need to enroll in the payjoin directory and receive +// ohttp keys. You can enroll using `PayjoinReceiverSetup::enroll` function. +// +// The payjoin receiver can then process payjoin requests and respond to them. +pub(crate) struct PayjoinReceiver +where + L::Target: Logger, +{ + logger: L, + scheduler: Arc>, + wallet: Arc, + channel_manager: Arc, + enrolled: Enrolled, + ohttp_keys: OhttpKeys, +} + +impl PayjoinReceiver +where + L::Target: Logger, +{ + pub(crate) fn enroll( + ohttp_keys: &Option, payjoin_directory: &Url, payjoin_relay: &Url, + scheduler: Arc>, wallet: Arc, + channel_manager: Arc, logger: L, + ) -> Result, Error> { + let ohttp_keys = match ohttp_keys { + Some(ohttp_keys) => ohttp_keys.clone(), + None => { + let payjoin_directory = + payjoin_directory.join("/ohttp-keys").map_err(|_| Error::PayjoinEnrollment)?; + let res = BlockingHttpClient::new_proxy(payjoin_relay) + .and_then(|c| c.get(&payjoin_directory))?; + OhttpKeys::decode(res.as_slice()).map_err(|_| Error::PayjoinEnrollment)? + }, + }; + let enrolled = { + let mut enroller = Enroller::from_directory_config( + payjoin_directory.clone(), + ohttp_keys.clone(), + payjoin_relay.clone(), + ); + let (req, ctx) = enroller.extract_req()?; + let headers = payjoin_receiver_request_headers(); + let response = BlockingHttpClient::new() + .and_then(|c| c.post(&req.url.to_string(), req.body, headers, None))?; + let enrolled = enroller.process_res(response.as_slice(), ctx)?; + enrolled + }; + Ok(PayjoinReceiver { logger, scheduler, wallet, channel_manager, enrolled, ohttp_keys }) + } + + async fn post_request(url: &payjoin::Url, body: Vec) -> Result, Error> { + let headers = payjoin_receiver_request_headers(); + let client = HttpClient::new()?; + let response = client.post(url, body, headers.clone()).await?; + Ok(response) + } + + async fn fetch_payjoin_request( + &self, + ) -> Result, Error> { + let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1); + let (req, context) = self.enrolled.clone().extract_req().unwrap(); + let payjoin_request = Self::post_request(&req.url, req.body).await?; + let unchecked_proposal = self.enrolled.process_res(payjoin_request.as_slice(), context)?; + match unchecked_proposal { + None => return Ok(None), + Some(unchecked_proposal) => { + let (provisional_proposal, amount_to_us) = + match self.validate_payjoin_request(unchecked_proposal, min_fee_rate).await { + Ok(proposal) => proposal, + Err(_e) => { + return Ok(None); + }, + }; + Ok(Some((provisional_proposal, amount_to_us))) + }, + } + } + + pub(crate) async fn process_payjoin_request(&self) { + let mut scheduler = self.scheduler.lock().await; + if !scheduler.in_progress() { + let (provisional_proposal, amount_to_us) = match self.fetch_payjoin_request().await { + Ok(Some(proposal)) => proposal, + _ => { + return; + }, + }; + let scheduled_channel = scheduler.get_next_channel(amount_to_us); + if let Some(channel) = scheduled_channel { + let (channel_id, address, temporary_channel_id, _, counterparty_node_id) = channel; + let mut channel_provisional_proposal = provisional_proposal.clone(); + channel_provisional_proposal.substitute_output_address(address); + let payjoin_proposal = channel_provisional_proposal + .finalize_proposal(|psbt| Ok(psbt.clone()), None) + .and_then(|mut proposal| { + let (receiver_request, _) = proposal.extract_v2_req().unwrap(); + let tx = proposal.psbt().clone().extract_tx(); + Ok((receiver_request, tx)) + }); + if let Ok(payjoin_proposal) = payjoin_proposal { + if let (true, Ok(())) = ( + scheduler.set_funding_tx_created( + channel_id, + &payjoin_proposal.0.url, + payjoin_proposal.0.body, + ), + self.channel_manager.unsafe_funding_transaction_generated( + &ChannelId::from_bytes(temporary_channel_id), + &counterparty_node_id, + payjoin_proposal.1, + ), + ) { + return; + } + } + } + self.accept_normal_payjoin_request(provisional_proposal).await; + } + } + + async fn accept_normal_payjoin_request(&self, provisional_proposal: ProvisionalProposal) { + let mut finalized_proposal = + match provisional_proposal.finalize_proposal(|psbt| Ok(psbt.clone()), None) { + Ok(proposal) => proposal, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + let (receiver_request, _) = match finalized_proposal.extract_v2_req() { + Ok(req) => req, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + match Self::post_request(&receiver_request.url, receiver_request.body).await { + Ok(_response) => { + log_info!(self.logger, "Payjoin Receiver: Payjoin request sent to sender"); + }, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + }, + } + } + + pub(crate) fn payjoin_uri(&self, amount: bitcoin::Amount) -> Result { + let address = self.wallet.get_new_address()?; + let pj_part = + Url::parse(&self.enrolled.fallback_target()).map_err(|_| Error::PayjoinUri)?; + let payjoin_uri = PjUriBuilder::new(address, pj_part, Some(self.ohttp_keys.clone())) + .amount(amount) + .build(); + Ok(payjoin_uri) + } + + pub(crate) fn schedule_channel( + &self, amount: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128, + ) { + let channel = ScheduledChannel::new(amount, counterparty_node_id, channel_id); + self.scheduler.blocking_lock().schedule( + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + channel.channel_id(), + ); + } + + pub(crate) fn list_scheduled_channels(&self) -> Vec { + self.scheduler.blocking_lock().list_channels().clone() + } + + pub(crate) async fn set_channel_accepted( + &self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + let mut scheduler = self.scheduler.lock().await; + scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id) + } + + async fn validate_payjoin_request( + &self, proposal: UncheckedProposal, min_fee_rate: Option, + ) -> Result<(ProvisionalProposal, bitcoin::Amount), Error> { + let tx = proposal.extract_tx_to_schedule_broadcast(); + let verified = self.wallet.verify_tx(&tx).await; + let amount_to_us = self.wallet.funds_directed_to_us(&tx).unwrap_or_default(); + let proposal = + proposal.check_broadcast_suitability(min_fee_rate, |_t| Ok(verified.is_ok()))?; + let proposal = proposal.check_inputs_not_owned(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + let proposal = match proposal.check_no_mixed_input_scripts() { + Ok(proposal) => proposal, + Err(_) => { + log_error!(self.logger, "Payjoin Receiver: Mixed input scripts"); + return Err(Error::PayjoinReceiver); + }, + }; + let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?; + let original_proposal = proposal.clone().identify_receiver_outputs(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + Ok((original_proposal, amount_to_us)) + } +} +pub fn payjoin_receiver_request_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + let header_value = HeaderValue::from_static("message/ohttp-req"); + headers.insert(reqwest::header::CONTENT_TYPE, header_value); + headers +} + +pub(crate) struct PayjoinSender +where + L::Target: Logger, +{ + logger: L, + wallet: Arc, + payjoin_relay: Url, + payjoin_directory: Url, + pending_requests: Mutex>, +} + +impl PayjoinSender +where + L::Target: Logger, +{ + pub(crate) fn new( + logger: L, wallet: Arc, payjoin_relay: &Url, payjoin_directory: &Url, + ) -> Self { + Self { + logger, + wallet, + payjoin_relay: payjoin_relay.clone(), + payjoin_directory: payjoin_directory.clone(), + pending_requests: Mutex::new(Vec::new()), + } + } + + // Create a payjoin request based on the payjoin URI parameters. This function builds a PSBT + // based on the amount and receiver address extracted from the payjoin URI, that can be used to + // send a payjoin request to the receiver using `PayjoinSender::send_payjoin_request`. + pub(crate) fn create_payjoin_request( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, + ) -> Result { + // Extract amount and receiver address from URI + let amount_to_send = match payjoin_uri.amount { + Some(amount) => amount, + None => { + log_error!(self.logger, "Payjoin Sender: send: No amount found in URI"); + return Err(Error::PayjoinSender); + }, + }; + let receiver_address = payjoin_uri.address.clone().script_pubkey(); + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + let original_psbt = self + .wallet + .build_transaction(receiver_address, amount_to_send.to_sat(), sign_options) + .unwrap(); + Ok(original_psbt) + } + + // Send payjoin transaction based on the payjoin URI parameters. + // + // This function sends the payjoin request to the receiver and saves the context and request in + // the pending_requests field to process the response async. + pub(crate) fn send_payjoin_request( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, original_psbt: Psbt, + ) -> Result, Error> { + let mut request_context = + payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) + .and_then(|b| b.build_non_incentivizing()) + .map_err(|e| { + log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); + Error::PayjoinSender + })?; + let (sender_request, sender_ctx) = + request_context.extract_v2(self.payjoin_directory.clone()).map_err(|e| { + log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); + Error::PayjoinSender + })?; + let (body, url) = (sender_request.body.clone(), sender_request.url.to_string()); + log_info!(self.logger, "Payjoin Sender: send: sending payjoin request to: {}", url); + let mut headers = HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); + let response = BlockingHttpClient::new_proxy(&self.payjoin_relay) + .and_then(|c| c.post(&url, body, headers, Some(std::time::Duration::from_secs(5)))); + if let Ok(response) = response { + let psbt = match sender_ctx.process_response(&mut response.as_slice()) { + Ok(Some(psbt)) => psbt, + _ => { + log_info!( + self.logger, + "Payjoin Sender: No payjoin response found yet. Setting request as pending." + ); + self.queue_request(request_context, original_psbt); + return Ok(None); + }, + }; + return self.finalise_payjoin_tx(psbt, original_psbt.clone()); + } else { + self.queue_request(request_context, original_psbt); + return Ok(None); + } + } + + // Process the payjoin response from the receiver. + // + // After sending the payjoin request to the receiver, we will process the response from the + // receiver and finalise the payjoin transaction. + // + // Because the response from the receiver is asynchronous, this function first checks if there + // is a pending request in the pending_requests field. If there is a pending request, it will + // check if a response was received in the directory and process it accordingly. A successful + // responsonse from the directory but with no payjoin proposal will return Ok(None). If a + // payjoin proposal is found, we will attempt to finalise the payjoin transaction and broadcast + // it. + pub(crate) async fn process_payjoin_response(&self) { + let mut pending_requests = self.pending_requests.lock().await; + let (mut request_context, original_psbt) = match pending_requests.pop() { + Some(request_context) => request_context, + None => { + log_info!(self.logger, "Payjoin Sender: No pending request found. "); + return; + }, + }; + let now = std::time::Instant::now(); + let (psbt, original_psbt) = match self.poll(&mut request_context, original_psbt, now).await + { + Some((psbt, original_psbt)) => (psbt, original_psbt), + None => { + return; + }, + }; + match self.finalise_payjoin_tx(psbt.clone(), original_psbt.clone()) { + Ok(Some(txid)) => { + log_info!(self.logger, "Payjoin Sender: Payjoin transaction broadcasted: {}", txid); + }, + Ok(None) => { + log_info!( + self.logger, + "Payjoin Sender: Was not able to finalise payjoin transaction {}.", + psbt.extract_tx().txid() + ); + }, + Err(e) => { + log_error!( + self.logger, + "Payjoin Sender: Error finalising payjoin transaction: {}", + e + ); + }, + } + } + + async fn poll( + &self, request_context: &mut RequestContext, original_psbt: Psbt, time: Instant, + ) -> Option<(Psbt, Psbt)> { + let duration = std::time::Duration::from_secs(180); + loop { + if time.elapsed() > duration { + log_info!(self.logger, "Payjoin Sender: Polling timed out"); + return None; + } + let (req, ctx) = match request_context.extract_v2(self.payjoin_directory.clone()) { + Ok(req) => req, + Err(e) => { + log_error!(self.logger, "Payjoin Sender: Error extracting v2 request: {}", e); + sleep(std::time::Duration::from_secs(5)).await; + return None; + }, + }; + let mut headers = HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); + let client = HttpClient::new().ok()?; + let response = client.post(&req.url, req.body, headers.clone()).await.ok()?; + let psbt = match ctx.process_response(&mut response.as_slice()) { + Ok(Some(psbt)) => psbt, + Ok(None) => { + log_info!(self.logger, "Payjoin Sender: No pending payjoin response"); + sleep(std::time::Duration::from_secs(5)).await; + continue; + }, + Err(e) => { + log_error!(self.logger, "Payjoin Sender: malformed payjoin response: {}", e); + sleep(std::time::Duration::from_secs(10)).await; + continue; + }, + }; + return Some((psbt, original_psbt.clone())); + } + } + + // finalise the payjoin transaction and broadcast it + fn finalise_payjoin_tx( + &self, mut psbt: Psbt, mut ocean_psbt: Psbt, + ) -> Result, Error> { + // for BDK, we need to reintroduce utxo from original psbt. + // Otherwise we wont be able to sign the transaction. + fn input_pairs( + psbt: &mut PartiallySignedTransaction, + ) -> Box + '_> { + Box::new(psbt.unsigned_tx.input.iter().zip(&mut psbt.inputs)) + } + + // get original inputs from original psbt clone (ocean_psbt) + let mut original_inputs = input_pairs(&mut ocean_psbt).peekable(); + for (proposed_txin, proposed_psbtin) in input_pairs(&mut 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(); + } + } + } + + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + sign_options.try_finalize = true; + let (_is_signed, psbt) = self.wallet.sign_transaction(&psbt, sign_options)?; + let tx = psbt.extract_tx(); + self.wallet.broadcast_transaction(&tx); + let txid = tx.txid(); + Ok(Some(txid)) + } + + fn queue_request(&self, request_context: RequestContext, original_psbt: Psbt) { + log_info!(&self.logger, "Payjoin Sender: saving pending request for txid"); + self.pending_requests.blocking_lock().push((request_context, original_psbt)); + } +} + +pub struct HttpClient { + client: reqwest::Client, +} + +impl HttpClient { + fn new() -> Result { + let client = reqwest::Client::builder().build().map_err(|_| Error::PayjoinReqwest)?; + Ok(Self { client }) + } + + async fn post( + &self, url: &Url, body: Vec, headers: HeaderMap, + ) -> Result, Error> { + Ok(self + .client + .post(url.to_string()) + .headers(headers) + .body(body) + .send() + .await + .and_then(|response| response.error_for_status()) + .map_err(|_| Error::PayjoinReqwest)? + .bytes() + .await + .map_err(|_| Error::PayjoinReqwest)? + .to_vec()) + } +} + +struct BlockingHttpClient { + client: reqwest::blocking::Client, +} + +impl BlockingHttpClient { + fn new() -> Result { + let client = + reqwest::blocking::Client::builder().build().map_err(|_| Error::PayjoinReqwest)?; + Ok(Self { client }) + } + + fn new_proxy(payjoin_relay: &Url) -> Result { + let proxy = + reqwest::Proxy::all(payjoin_relay.to_string()).map_err(|_| Error::PayjoinReqwest)?; + let client = reqwest::blocking::Client::builder() + .proxy(proxy) + .build() + .map_err(|_| Error::PayjoinReqwest)?; + Ok(Self { client }) + } + + fn get(&self, url: &Url) -> Result, Error> { + Ok(self + .client + .get(url.to_string()) + .send() + .and_then(|response| response.error_for_status()) + .map_err(|_| Error::PayjoinReqwest)? + .bytes() + .map_err(|_| Error::PayjoinReqwest)? + .to_vec()) + } + + fn post( + &self, url: &str, body: Vec, headers: HeaderMap, + timeout: Option, + ) -> Result, Error> { + Ok(self + .client + .post(url) + .headers(headers) + .body(body) + .timeout(timeout.unwrap_or(std::time::Duration::from_secs(15))) + .send() + .and_then(|response| response.error_for_status()) + .map_err(|_| Error::PayjoinReqwest)? + .bytes() + .map_err(|_| Error::PayjoinReqwest)? + .to_vec()) + } +} + +// https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 40483f578..8f7af9eb0 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -1,6 +1,9 @@ +use crate::channel_scheduler::ChannelScheduler; use crate::logger::{log_bytes, log_debug, log_error, log_trace, Logger}; +use crate::payjoin_handler::payjoin_receiver_request_headers; use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::log_info; use lightning::util::ser::Writeable; use esplora_client::AsyncClient as EsploraClient; @@ -11,6 +14,7 @@ use tokio::sync::mpsc; use tokio::sync::Mutex; use std::ops::Deref; +use std::sync::Arc; use std::time::Duration; const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; @@ -23,21 +27,91 @@ where queue_receiver: Mutex>>, esplora_client: EsploraClient, logger: L, + channel_scheduler: Arc>, } impl TransactionBroadcaster where L::Target: Logger, { - pub(crate) fn new(esplora_client: EsploraClient, logger: L) -> Self { + pub(crate) fn new( + esplora_client: EsploraClient, logger: L, channel_scheduler: Arc>, + ) -> Self { let (queue_sender, queue_receiver) = mpsc::channel(BCAST_PACKAGE_QUEUE_SIZE); - Self { queue_sender, queue_receiver: Mutex::new(queue_receiver), esplora_client, logger } + Self { + queue_sender, + queue_receiver: Mutex::new(queue_receiver), + esplora_client, + logger, + channel_scheduler, + } } pub(crate) async fn process_queue(&self) { let mut receiver = self.queue_receiver.lock().await; while let Some(next_package) = receiver.recv().await { for tx in &next_package { + if tx.input.iter().any(|input| input.witness.is_empty()) { + log_info!( + self.logger, + "Skipping broadcast of transaction {} with empty witness, checking for payjoin.", + tx.txid() + ); + dbg!("Skipping broadcast of transaction {} with empty witness, checking for payjoin.", tx.txid()); + let is_payjoin_channel = + self.channel_scheduler.lock().await.set_funding_tx_signed(tx.clone()); + if let Some((url, body)) = is_payjoin_channel { + log_info!( + self.logger, + "Detected payjoin channel transaction. Sending payjoin sender request for transaction {}", + tx.txid() + ); + dbg!("Detected payjoin channel transaction. Sending payjoin sender request for transaction {}", tx.txid()); + + let headers = payjoin_receiver_request_headers(); + let client = match reqwest::Client::builder().build() { + Ok(client) => client, + Err(e) => { + log_error!( + self.logger, + "Failed to create reqwest client for payjoin receiver request: {}", + e + ); + continue; + }, + }; + match client.post(url).body(body).headers(headers).send().await { + Ok(res) => { + if res.status().is_success() { + log_info!( + self.logger, + "Successfully sent payjoin receiver request for transaction {}", + tx.txid() + ); + dbg!("Successfully sent payjoin receiver request for transaction {}", tx.txid()); + } else { + dbg!("Failed to send payjoin receiver request for transaction {}: {}", tx.txid(), res.status()); + log_error!( + self.logger, + "Failed to send payjoin receiver request for transaction {}: {}", + tx.txid(), + res.status() + ); + } + }, + Err(e) => { + dbg!("Failed to send payjoin receiver request for transaction {}: {}", tx.txid(), &e); + log_error!( + self.logger, + "Failed to send payjoin receiver request for transaction {}: {}", + tx.txid(), + e + ); + }, + } + continue; + } + } match self.esplora_client.broadcast(tx).await { Ok(()) => { log_trace!(self.logger, "Successfully broadcast transaction {}", tx.txid()); diff --git a/src/wallet.rs b/src/wallet.rs index 0e9c5b73d..91ef255bc 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -27,7 +27,6 @@ use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::{bitcoinconsensus, ScriptBuf, Transaction, TxOut, Txid}; -use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; @@ -113,39 +112,121 @@ where res } + /// Returns the total value of all outputs in the given PSBT that + /// are directed to us in satoshis. + 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 broadcast_transaction(&self, transaction: &Transaction) { + self.broadcaster.broadcast_transactions(&[transaction]); + } + + pub(crate) fn build_transaction( + &self, output_script: ScriptBuf, value_sats: u64, sign_options: SignOptions, + ) -> Result { + let fee_rate = FeeRate::from_sat_per_kwu(1000 as f32); + + let locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); + + tx_builder.add_recipient(output_script, value_sats).fee_rate(fee_rate).enable_rbf(); + + let mut psbt = match tx_builder.finish() { + Ok((psbt, _)) => { + log_trace!(self.logger, "Created PSBT: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create PSBT: {}", err); + return Err(err.into()); + }, + }; + + locked_wallet.sign(&mut psbt, sign_options)?; + + Ok(psbt) + } + pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result { - Ok(self.inner.lock().unwrap().is_mine(script)?) + let locked_wallet = self.inner.lock().unwrap(); + Ok(locked_wallet.is_mine(script)?) } - pub async fn verify_tx(&self, tx: Transaction) -> Result<(), Error> { + /// Verifies that the given transaction meets the bitcoin consensus rules. + pub async fn verify_tx(&self, tx: &Transaction) -> Result<(), Error> { let serialized_tx = bitcoin::consensus::serialize(&tx); + // Loop through all the inputs for (index, input) in tx.input.iter().enumerate() { let input = input.clone(); let txid = input.previous_output.txid; - let prev_tx = if let Ok(prev_tx) = self.blockchain.get_tx(&txid).await { - prev_tx.unwrap() - } else { - dbg!("maybe conibase?"); - continue; + let prev_tx = match self.blockchain.get_tx(&txid).await { + Ok(prev_tx) => prev_tx, + Err(e) => { + log_error!( + self.logger, + "Failed to verify transaction: blockchain error {} for txid {}", + e, + &txid + ); + return Err(Error::BitcoinConsensusFailed); + }, }; - let spent_output = prev_tx.output.get(input.previous_output.vout as usize).unwrap(); - bitcoinconsensus::verify( - &spent_output.script_pubkey.to_bytes(), - spent_output.value, - &serialized_tx, - index, - ) - .unwrap(); + if let Some(prev_tx) = prev_tx { + let spent_output = match prev_tx.output.get(input.previous_output.vout as usize) { + Some(output) => output, + None => { + log_error!( + self.logger, + "Failed to verify transaction: missing output {} in tx {}", + input.previous_output.vout, + txid + ); + return Err(Error::BitcoinConsensusFailed); + }, + }; + match bitcoinconsensus::verify( + &spent_output.script_pubkey.to_bytes(), + spent_output.value, + &serialized_tx, + index, + ) { + Ok(()) => {}, + Err(e) => { + log_error!(self.logger, "Failed to verify transaction: {}", e); + return Err(Error::BitcoinConsensusFailed); + }, + } + } else { + if tx.is_coin_base() { + continue; + } else { + log_error!( + self.logger, + "Failed to verify transaction: missing previous transaction {}", + txid + ); + return Err(Error::BitcoinConsensusFailed); + } + } } Ok(()) } - pub(crate) fn sign_tx(&self, psbt: &Psbt, options: Option) -> Result { + pub(crate) fn sign_transaction( + &self, psbt: &Psbt, options: SignOptions, + ) -> Result<(bool, Psbt), Error> { let wallet = self.inner.lock().unwrap(); let mut psbt = psbt.clone(); - let options = options.unwrap_or_default(); - wallet.sign(&mut psbt, options)?; - Ok(psbt) + let is_signed = wallet.sign(&mut psbt, options)?; + Ok((is_signed, psbt)) } pub(crate) fn create_funding_transaction( diff --git a/tests/common/mod.rs b/tests/common/mod.rs index bcb47accb..02f8074c3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -196,24 +196,32 @@ macro_rules! setup_builder { pub(crate) use setup_builder; -pub(crate) fn setup_two_nodes(electrsd: &ElectrsD, allow_0conf: bool) -> (TestNode, TestNode) { +pub(crate) fn setup_two_nodes( + electrsd: &ElectrsD, allow_0conf: bool, allow_payjoin: bool, +) -> (TestNode, TestNode) { println!("== Node A =="); let config_a = random_config(); - let node_a = setup_node(electrsd, config_a); + let node_a = setup_node(electrsd, config_a, allow_payjoin); println!("\n== Node B =="); let mut config_b = random_config(); if allow_0conf { config_b.trusted_peers_0conf.push(node_a.node_id()); } - let node_b = setup_node(electrsd, config_b); + let node_b = setup_node(electrsd, config_b, allow_payjoin); (node_a, node_b) } -pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { +pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config, allow_payjoin: bool) -> TestNode { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); setup_builder!(builder, config); builder.set_esplora_server(esplora_url.clone()); + // enable payjoin + if allow_payjoin { + let payjoin_directory = payjoin::Url::parse("https://payjo.in").unwrap(); + let payjoin_relay = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); + builder.set_payjoin_config(payjoin_directory, payjoin_relay, None); + } let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); let node = builder.build_with_store(test_sync_store).unwrap(); node.start().unwrap(); diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs new file mode 100644 index 000000000..6e483707d --- /dev/null +++ b/tests/integration_tests_payjoin.rs @@ -0,0 +1,211 @@ +mod common; +use std::{thread::sleep, time::Duration}; + +use crate::common::{ + expect_channel_pending_event, expect_channel_ready_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_two_nodes, wait_for_tx, +}; +use bitcoin::Amount; +use bitcoincore_rpc::{Client as BitcoindClient, RpcApi}; +use common::setup_bitcoind_and_electrsd; +use ldk_node::Event; + +#[test] +fn send_receive_with_channel_opening_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b], + Amount::from_sat(premine_amount_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a.next_event(), None); + assert_eq!(node_a.list_channels().len(), 0); + assert_eq!(node_b.next_event(), None); + assert_eq!(node_b.list_channels().len(), 0); + let funding_amount_sat = 80_000; + let node_b_listening_address = node_b.listening_addresses().unwrap().get(0).unwrap().clone(); + let payjoin_uri = node_a + .request_payjoin_transaction_with_channel_opening( + funding_amount_sat, + None, + false, + node_b.node_id(), + node_b_listening_address, + ) + .unwrap(); + assert!(node_b + .send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked() + ) + .is_ok()); + expect_channel_pending_event!(node_a, node_b.node_id()); + expect_channel_pending_event!(node_b, node_a.node_id()); + let channels = node_a.list_channels(); + let channel = channels.get(0).unwrap(); + wait_for_tx(&electrsd.client, channel.funding_txo.unwrap().txid); + sleep(Duration::from_secs(1)); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + let channels = node_a.list_channels(); + let channel = channels.get(0).unwrap(); + assert_eq!(channel.channel_value_sats, funding_amount_sat); + assert_eq!(channel.confirmations.unwrap(), 6); + assert!(channel.is_channel_ready); + assert!(channel.is_usable); + + assert_eq!(node_a.list_peers().get(0).unwrap().is_connected, true); + assert_eq!(node_a.list_peers().get(0).unwrap().is_persisted, true); + assert_eq!(node_a.list_peers().get(0).unwrap().node_id, node_b.node_id()); + + let invoice_amount_1_msat = 2500_000; + let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap(); + assert!(node_a.bolt11_payment().send(&invoice).is_ok()); +} + +#[test] +fn send_receive_regular_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b], + Amount::from_sat(premine_amount_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert_eq!(node_a.next_event(), None); + assert_eq!(node_a.list_channels().len(), 0); + let payjoin_uri = node_a.request_payjoin_transaction(80_000).unwrap(); + assert!(node_b + .send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked() + ) + .is_ok()); + sleep(Duration::from_secs(3)); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + let node_a_balance = node_a.list_balances(); + let node_b_balance = node_b.list_balances(); + assert_eq!(node_a_balance.total_onchain_balance_sats, 80000); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); +} + +mod payjoin_v1 { + use bitcoin::address::NetworkChecked; + use bitcoin::base64; + use bitcoin::Txid; + use bitcoincore_rpc::Client as BitcoindClient; + use bitcoincore_rpc::RpcApi; + use std::collections::HashMap; + use std::str::FromStr; + + use bitcoincore_rpc::bitcoin::psbt::Psbt; + + pub fn send( + sender_wallet: &BitcoindClient, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, + ) -> Txid { + let amount_to_send = payjoin_uri.amount.unwrap(); + let receiver_address = payjoin_uri.address.clone(); + let mut outputs = HashMap::with_capacity(1); + outputs.insert(receiver_address.to_string(), amount_to_send); + let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions { + lock_unspent: Some(false), + fee_rate: Some(bitcoincore_rpc::bitcoin::Amount::from_sat(10000)), + ..Default::default() + }; + let sender_psbt = sender_wallet + .wallet_create_funded_psbt( + &[], // inputs + &outputs, + None, // locktime + Some(options), + None, + ) + .unwrap(); + let psbt = + sender_wallet.wallet_process_psbt(&sender_psbt.psbt, None, None, None).unwrap().psbt; + let psbt = Psbt::from_str(&psbt).unwrap(); + let (req, ctx) = + payjoin::send::RequestBuilder::from_psbt_and_uri(psbt.clone(), payjoin_uri) + .unwrap() + .build_with_additional_fee( + bitcoincore_rpc::bitcoin::Amount::from_sat(1), + None, + bitcoincore_rpc::bitcoin::FeeRate::MIN, + true, + ) + .unwrap() + .extract_v1() + .unwrap(); + let url_http = req.url.as_str().replace("https", "http"); + let res = reqwest::blocking::Client::new(); + let res = res + .post(&url_http) + .body(req.body.clone()) + .header("content-type", "text/plain") + .send() + .unwrap(); + let res = res.text().unwrap(); + let psbt = ctx.process_response(&mut res.as_bytes()).unwrap(); + let psbt = sender_wallet + .wallet_process_psbt(&base64::encode(psbt.serialize()), None, None, None) + .unwrap() + .psbt; + let tx = sender_wallet.finalize_psbt(&psbt, Some(true)).unwrap().hex.unwrap(); + let txid = sender_wallet.send_raw_transaction(&tx).unwrap(); + txid + } +} + +#[test] +fn receive_payjoin_version_1() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let payjoin_sender_wallet: BitcoindClient = bitcoind.create_wallet("payjoin_sender").unwrap(); + let (node_a, _) = setup_two_nodes(&electrsd, false, true); + let addr_sender = payjoin_sender_wallet.get_new_address(None, None).unwrap().assume_checked(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_sender], + Amount::from_sat(premine_amount_sat), + ); + node_a.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert_eq!( + payjoin_sender_wallet.get_balances().unwrap().mine.trusted.to_sat(), + premine_amount_sat + ); + assert_eq!(node_a.next_event(), None); + assert_eq!(node_a.list_channels().len(), 0); + let pj_uri = node_a.request_payjoin_transaction(80_000).unwrap(); + payjoin_v1::send( + &payjoin_sender_wallet, + payjoin::Uri::try_from(pj_uri.to_string()).unwrap().assume_checked(), + ); + sleep(Duration::from_secs(3)); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + let node_a_balance = node_a.list_balances(); + assert_eq!(node_a_balance.total_onchain_balance_sats, 80000); +} + +// test validation of payjoin transaction fails +// test counterparty doesnt return fundingsigned diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 1820ef76a..5539d7191 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -18,21 +18,21 @@ use std::sync::Arc; #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false); } #[test] fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, true); + let (node_a, node_b) = setup_two_nodes(&electrsd, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true) } #[test] fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -223,7 +223,7 @@ fn start_stop_reinit() { #[test] fn onchain_spend_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -272,7 +272,7 @@ fn onchain_spend_receive() { fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let config = random_config(); - let node = setup_node(&electrsd, config); + let node = setup_node(&electrsd, config, false); // Tests arbitrary message signing and later verification let msg = "OK computer".as_bytes(); @@ -289,7 +289,7 @@ fn connection_restart_behavior() { fn do_connection_restart_behavior(persist: bool) { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -340,7 +340,7 @@ fn do_connection_restart_behavior(persist: bool) { #[test] fn concurrent_connections_succeed() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); From 569b3918a251444944de33fe7dea6fe215a78890 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Sun, 19 May 2024 14:34:46 +0300 Subject: [PATCH 06/11] add pj scheduler --- src/payjoin_scheduler.rs | 328 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 src/payjoin_scheduler.rs diff --git a/src/payjoin_scheduler.rs b/src/payjoin_scheduler.rs new file mode 100644 index 000000000..67918232f --- /dev/null +++ b/src/payjoin_scheduler.rs @@ -0,0 +1,328 @@ +use bitcoin::{secp256k1::PublicKey, ScriptBuf, TxOut}; + +#[derive(Clone)] +pub struct PayjoinScheduler { + channels: Vec, +} + +impl PayjoinScheduler { + /// Create a new empty channel scheduler. + pub fn new() -> Self { + Self { channels: vec![] } + } + /// Schedule a new channel. + /// + /// The channel will be created with `ScheduledChannelState::ChannelCreated` state. + pub 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); + }, + _ => {}, + } + } + /// Mark a channel as accepted. + /// + /// The channel will be updated to `ScheduledChannelState::ChannelAccepted` state. + pub 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 + } + /// Mark a channel as funding tx created. + /// + /// The channel will be updated to `ScheduledChannelState::FundingTxCreated` state. + pub 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 + } + /// Mark a channel as funding tx signed. + /// + /// The channel will be updated to `ScheduledChannelState::FundingTxSigned` state. + pub 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 the accepted state. + /// + /// If more than one channel matches the given channel amount, the channel with the oldest + /// creation date will be returned. + pub fn get_next_channel( + &self, channel_amount: bitcoin::Amount, + ) -> 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(), + bitcoin::Network::Regtest, // fixme + ); + 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 + } + + /// List all channels. + pub fn list_channels(&self) -> &Vec { + &self.channels + } + + pub fn in_progress(&self) -> bool { + self.channels.iter().any(|channel| !channel.is_channel_accepted()) + } + 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() + } +} + +/// A struct representing a scheduled channel. +#[derive(Clone, Debug)] +pub struct PayjoinChannel { + state: ScheduledChannelState, + channel_value_satoshi: bitcoin::Amount, + channel_id: u128, + counterparty_node_id: PublicKey, + created_at: u64, +} + +impl PayjoinChannel { + pub 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 fn channel_value_satoshi(&self) -> bitcoin::Amount { + self.channel_value_satoshi + } + + /// Get the user channel id. + pub fn channel_id(&self) -> u128 { + self.channel_id + } + + /// Get the counterparty node id. + pub fn counterparty_node_id(&self) -> PublicKey { + self.counterparty_node_id + } + + /// Get the output script. + pub fn output_script(&self) -> Option<&ScriptBuf> { + self.state.output_script() + } + + /// Get the temporary channel id. + pub fn temporary_channel_id(&self) -> Option<[u8; 32]> { + self.state.temporary_channel_id() + } + + /// Get the temporary channel id. + pub fn tx_out(&self) -> Option<&TxOut> { + match &self.state { + ScheduledChannelState::FundingTxSigned(_, txout) => Some(txout), + _ => None, + } + } + + pub 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, TxOut), +} + +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 { + *self = + ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), output.clone()); + res = true; + } + return res; + } +} + +// #[cfg(test)] +// mod tests { +// use std::str::FromStr; + +// use super::*; +// use bitcoin::{ +// psbt::Psbt, +// secp256k1::{self, Secp256k1}, +// }; + +// #[ignore] +// #[test] +// fn test_channel_scheduler() { +// let create_pubkey = || -> PublicKey { +// let secp = Secp256k1::new(); +// PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap()) +// }; +// let channel_value_satoshi = 100; +// let node_id = create_pubkey(); +// let channel_id: u128 = 0; +// let mut channel_scheduler = PayjoinScheduler::new(); +// channel_scheduler.schedule( +// bitcoin::Amount::from_sat(channel_value_satoshi), +// node_id, +// channel_id, +// ); +// assert_eq!(channel_scheduler.channels.len(), 1); +// assert_eq!(channel_scheduler.is_channel_created(channel_id), true); +// channel_scheduler.set_channel_accepted( +// channel_id, +// &ScriptBuf::from(vec![1, 2, 3]), +// [0; 32], +// ); +// assert_eq!(channel_scheduler.is_channel_accepted(channel_id), true); +// let str_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; +// let mock_transaction = Psbt::from_str(str_psbt).unwrap(); +// let _our_txout = mock_transaction.clone().extract_tx().output[0].clone(); +// // channel_scheduler.set_funding_tx_created(channel_id, mock_transaction.clone()); +// // let tx_id = mock_transaction.extract_tx().txid(); +// // assert_eq!(channel_scheduler.is_funding_tx_created(&tx_id), true); +// // channel_scheduler.set_funding_tx_signed(tx_id); +// // assert_eq!(channel_scheduler.is_funding_tx_signed(&tx_id), true); +// } +// } From 092dcccc500e25356f083ea5e409d471c46a85e0 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:31:13 +0300 Subject: [PATCH 07/11] add payjoin sender --- src/payjoin_sender.rs | 273 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 src/payjoin_sender.rs diff --git a/src/payjoin_sender.rs b/src/payjoin_sender.rs new file mode 100644 index 000000000..897ceb9b0 --- /dev/null +++ b/src/payjoin_sender.rs @@ -0,0 +1,273 @@ +use bdk::SignOptions; +use bitcoin::address::NetworkChecked; +use bitcoin::psbt::{Input, PartiallySignedTransaction, Psbt}; +use bitcoin::Txid; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; +use payjoin::send::RequestContext; +use payjoin::Url; +use reqwest::header::HeaderMap; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::Mutex; +use tokio::time::sleep; + +use crate::error::Error; +use crate::types::Wallet; + +/// Payjoin Sender +pub(crate) struct PayjoinSender +where + L::Target: Logger, +{ + logger: L, + wallet: Arc, + payjoin_relay: Url, + pending_requests: Mutex>, +} + +impl PayjoinSender +where + L::Target: Logger, +{ + pub(crate) fn new(logger: L, wallet: Arc, payjoin_relay: &Url) -> Self { + Self { + logger, + wallet, + payjoin_relay: payjoin_relay.clone(), + pending_requests: Mutex::new(Vec::new()), + } + } + + // Create a payjoin request based on the payjoin URI parameters. This function builds a PSBT + // based on the amount and receiver address extracted from the payjoin URI, that can be used to + // send a payjoin request to the receiver using `PayjoinSender::send_payjoin_request`. + pub(crate) fn create_payjoin_request( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, + ) -> Result { + // Extract amount and receiver address from URI + let amount_to_send = match payjoin_uri.amount { + Some(amount) => amount, + None => { + log_error!(self.logger, "Payjoin Sender: send: No amount found in URI"); + return Err(Error::PayjoinSender); + }, + }; + let receiver_address = payjoin_uri.address.clone().script_pubkey(); + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + let original_psbt = self + .wallet + .build_transaction(receiver_address, amount_to_send.to_sat(), sign_options) + .unwrap(); + Ok(original_psbt) + } + + // Send payjoin transaction based on the payjoin URI parameters. + // + // This function sends the payjoin request to the receiver and saves the context and request in + // the pending_requests field to process the response async. + pub(crate) fn send_payjoin_request( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, original_psbt: Psbt, + ) -> Result, Error> { + dbg!("payjoin_uri: {}", payjoin_uri.to_string()); + let mut request_context = + payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) + .and_then(|b| b.build_non_incentivizing()) + .map_err(|e| { + dbg!("Error building requestaaa"); + log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); + Error::PayjoinSender + })?; + let (sender_request, sender_ctx) = + request_context.extract_v2(self.payjoin_relay.clone()).map_err(|e| { + dbg!("Error extracting v2 request"); + log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); + Error::PayjoinSender + })?; + let (body, url) = (sender_request.body.clone(), sender_request.url.to_string()); + dbg!("sending payjoin request to: {}", &url); + log_info!(self.logger, "Payjoin Sender: send: sending payjoin request to: {}", url); + let mut headers = HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); + dbg!("making request {}", url.clone()); + let client = reqwest::blocking::Client::new(); + let response = client.post(&url).body(body).headers(headers).send(); + if let Ok(response) = response { + if response.status().is_success() { + let response = match response.bytes() { + Ok(response) => response.to_vec(), + Err(e) => { + dbg!("hereeea {} ", e); + return Ok(Some(original_psbt.extract_tx().txid())); + }, + }; + let psbt = match sender_ctx.process_response(&mut response.as_slice()) { + Ok(Some(psbt)) => psbt, + _ => { + dbg!("No payjoin response found yet22"); + log_info!( + self.logger, + "Payjoin Sender: No payjoin response found yet. Setting request as pending." + ); + self.queue_request(request_context, original_psbt.clone()); + return Ok(Some(original_psbt.extract_tx().txid())); + }, + }; + return self.finalise_payjoin_tx(psbt, original_psbt.clone()); + } + }; + self.queue_request(request_context, original_psbt.clone()); + return Ok(Some(original_psbt.extract_tx().txid())); + } + + pub(crate) async fn process_payjoin_response(&self) { + let mut pending_requests = self.pending_requests.lock().await; + let (mut request_context, original_psbt) = match pending_requests.pop() { + Some(request_context) => request_context, + None => { + log_info!(self.logger, "Payjoin Sender: No pending request found. "); + return; + }, + }; + let now = std::time::Instant::now(); + let (psbt, original_psbt) = match self.poll(&mut request_context, original_psbt, now).await + { + Some((psbt, original_psbt)) => (psbt, original_psbt), + None => { + return; + }, + }; + match self.finalise_payjoin_tx(psbt.clone(), original_psbt.clone()) { + Ok(Some(txid)) => { + log_info!(self.logger, "Payjoin Sender: Payjoin transaction broadcasted: {}", txid); + }, + Ok(None) => { + log_info!( + self.logger, + "Payjoin Sender: Was not able to finalise payjoin transaction {}.", + psbt.extract_tx().txid() + ); + }, + Err(e) => { + log_error!( + self.logger, + "Payjoin Sender: Error finalising payjoin transaction: {}", + e + ); + }, + } + } + + async fn poll( + &self, request_context: &mut RequestContext, original_psbt: Psbt, time: Instant, + ) -> Option<(Psbt, Psbt)> { + let duration = std::time::Duration::from_secs(360); + loop { + if time.elapsed() > duration { + log_info!(self.logger, "Payjoin Sender: Polling timed out"); + return None; + } + + let payjoin_directory = payjoin::Url::parse("https://payjo.in").unwrap(); + let (req, ctx) = match request_context.extract_v2(payjoin_directory.clone()) { + Ok(req) => req, + Err(e) => { + log_error!(self.logger, "Payjoin Sender: Error extracting v2 request: {}", e); + sleep(std::time::Duration::from_secs(3)).await; + continue; + }, + }; + let mut headers = HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); + + let client = reqwest::Client::new(); + let response = + match client.post(req.url).body(req.body).headers(headers.clone()).send().await { + Ok(response) => response, + Err(e) => { + log_error!(self.logger, "Payjoin Sender: Error sending request: {}", e); + sleep(std::time::Duration::from_secs(3)).await; + continue; + }, + }; + let response = match response.bytes().await { + Ok(response) => response.to_vec(), + Err(e) => { + log_error!(self.logger, "Payjoin Sender: Error reading response: {}", e); + sleep(std::time::Duration::from_secs(3)).await; + continue; + }, + }; + if response.is_empty() { + dbg!("No payjoin response found yet1"); + log_info!(self.logger, "Payjoin Sender: No payjoin response found yet"); + sleep(std::time::Duration::from_secs(3)).await; + continue; + } + dbg!("response.len()", response.len()); + if response.len() == 54 { + dbg!("No payjoin response found yet2"); + log_error!(self.logger, "Payjoin Sender: malformed payjoin response"); + sleep(std::time::Duration::from_secs(3)).await; + continue; + } + let psbt = match ctx.process_response(&mut response.as_slice()) { + Ok(Some(psbt)) => psbt, + Ok(None) => { + dbg!("No payjoin response found yet3"); + log_info!(self.logger, "Payjoin Sender: No pending payjoin response"); + sleep(std::time::Duration::from_secs(3)).await; + continue; + }, + Err(e) => { + dbg!("No payjoin response found yet4"); + log_error!(self.logger, "Payjoin Sender: malformed payjoin response: {}", e); + sleep(std::time::Duration::from_secs(3)).await; + continue; + }, + }; + return Some((psbt, original_psbt.clone())); + } + } + + // finalise the payjoin transaction and broadcast it + fn finalise_payjoin_tx( + &self, mut psbt: Psbt, mut ocean_psbt: Psbt, + ) -> Result, Error> { + // for BDK, we need to reintroduce utxo from original psbt. + // Otherwise we wont be able to sign the transaction. + fn input_pairs( + psbt: &mut PartiallySignedTransaction, + ) -> Box + '_> { + Box::new(psbt.unsigned_tx.input.iter().zip(&mut psbt.inputs)) + } + + // get original inputs from original psbt clone (ocean_psbt) + let mut original_inputs = input_pairs(&mut ocean_psbt).peekable(); + for (proposed_txin, proposed_psbtin) in input_pairs(&mut 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(); + } + } + } + + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + sign_options.try_finalize = true; + let (_is_signed, psbt) = self.wallet.sign_transaction(&psbt, sign_options)?; + let tx = psbt.extract_tx(); + self.wallet.broadcast_transaction(&tx); + let txid = tx.txid(); + Ok(Some(txid)) + } + + fn queue_request(&self, request_context: RequestContext, original_psbt: Psbt) { + log_info!(&self.logger, "Payjoin Sender: saving pending request for txid"); + self.pending_requests.blocking_lock().push((request_context, original_psbt)); + } +} From e1799ceabd23938cb0880ae3c2dd91fef96d36c2 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:31:47 +0300 Subject: [PATCH 08/11] add payjoin receivre --- src/payjoin_receiver.rs | 454 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 src/payjoin_receiver.rs diff --git a/src/payjoin_receiver.rs b/src/payjoin_receiver.rs new file mode 100644 index 000000000..0b567e426 --- /dev/null +++ b/src/payjoin_receiver.rs @@ -0,0 +1,454 @@ +use crate::error::Error; +use crate::payjoin_scheduler::PayjoinScheduler; +use crate::types::{ChannelManager, Wallet}; +use crate::PayjoinChannel; +use bitcoin::secp256k1::PublicKey; +use bitcoin::ScriptBuf; +use lightning::ln::ChannelId; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; +use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; +use payjoin::{OhttpKeys, PjUriBuilder}; +use payjoin::{PjUri, Url}; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderValue; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Payjoin Receiver +async fn post_request(url: &payjoin::Url, body: Vec) -> Result, Error> { + let headers = payjoin_receiver_request_headers(); + let client = reqwest::Client::new(); + let response = + client.post(url.to_string()).body(body).headers(headers).send().await.map_err(|e| { + dbg!(&e); + Error::PayjoinReceiver + })?; + let response = response.bytes().await.map_err(|e| { + dbg!(&e); + Error::PayjoinReceiver + })?; + Ok(response.to_vec()) +} + +pub fn enroll_payjoin_receivers( + ohttp_keys: &Option, payjoin_directory: &Url, payjoin_relay: &Url, +) -> Result<(Enrolled, Enrolled, OhttpKeys), Error> { + let ohttp_keys = match ohttp_keys { + Some(ohttp_keys) => ohttp_keys.clone(), + None => { + let payjoin_directory = + payjoin_directory.join("/ohttp-keys").map_err(|_| Error::PayjoinEnrollment)?; + let res = BlockingHttpClient::new_proxy(payjoin_relay) + .and_then(|c| c.get(&payjoin_directory))?; + OhttpKeys::decode(res.as_slice()).map_err(|e| { + dbg!(&e); + Error::PayjoinEnrollment + })? + }, + }; + let enrolled = { + let mut enroller = Enroller::from_directory_config( + payjoin_directory.clone(), + ohttp_keys.clone(), + payjoin_relay.clone(), + ); + let (req, ctx) = enroller.extract_req()?; + dbg!("normal subdirectory", enroller.subdirectory()); + let headers = payjoin_receiver_request_headers(); + let response = BlockingHttpClient::new() + .and_then(|c| c.post(&req.url.to_string(), req.body, headers, None))?; + let response = match response.bytes() { + Ok(response) => response, + Err(e) => { + dbg!(&e); + return Err(Error::PayjoinEnrollment); + }, + }; + let enrolled = enroller.process_res(response.to_vec().as_slice(), ctx)?; + enrolled + }; + + let lightning_enrolled = { + let mut enroller = Enroller::from_directory_config( + payjoin_directory.clone(), + ohttp_keys.clone(), + payjoin_relay.clone(), + ); + dbg!("lightning subdirectory", enroller.subdirectory()); + let (req, ctx) = enroller.extract_req()?; + let headers = payjoin_receiver_request_headers(); + let response = BlockingHttpClient::new() + .and_then(|c| c.post(&req.url.to_string(), req.body, headers, None))?; + let response = match response.bytes() { + Ok(response) => response, + Err(e) => { + dbg!(&e); + return Err(Error::PayjoinEnrollment); + }, + }; + let enrolled = enroller.process_res(response.to_vec().as_slice(), ctx)?; + enrolled + }; + + Ok((enrolled, lightning_enrolled, ohttp_keys)) +} + +pub(crate) struct PayjoinLightningReceiver +where + L::Target: Logger, +{ + logger: L, + scheduler: Arc>, + wallet: Arc, + channel_manager: Arc, + ohttp_keys: OhttpKeys, + enrolled: Enrolled, +} + +impl PayjoinLightningReceiver +where + L::Target: Logger, +{ + pub fn new( + logger: L, wallet: Arc, channel_manager: Arc, + scheduler: Arc>, enrolled: Enrolled, ohttp_keys: OhttpKeys, + ) -> Self { + Self { logger, wallet, channel_manager, scheduler, enrolled, ohttp_keys } + } + + async fn fetch_payjoin_request(&self) -> Result, Error> { + let mut enrolled = self.enrolled.clone(); + let (req, context) = match enrolled.extract_req() { + Ok(req) => req, + Err(e) => { + dbg!("Error: {}", &e); + log_error!(self.logger, "Payjoin Receiver: {}", e); + return Err(Error::PayjoinReceiver); + }, + }; + let payjoin_request = post_request(&req.url, req.body).await?; + Ok(enrolled.process_res(payjoin_request.as_slice(), context)?) + } + + pub(crate) async fn process_payjoin_request(&self) { + let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1); + let mut scheduler = self.scheduler.lock().await; + if scheduler.list_channels().is_empty() {} + if !scheduler.in_progress() { + let unchecked_proposal = match self.fetch_payjoin_request().await { + Ok(Some(proposal)) => proposal, + _ => { + return; + }, + }; + let tx = unchecked_proposal.extract_tx_to_schedule_broadcast(); + match scheduler.add_seen_tx(&tx) { + true => {}, + false => { + dbg!("Input seen before"); + log_error!(self.logger, "Payjoin Receiver: Seen tx before"); + return; + }, + }; + let (provisional_proposal, amount_to_us) = + match self.validate_payjoin_request(unchecked_proposal, min_fee_rate).await { + Ok(proposal) => proposal, + Err(_e) => { + return; + }, + }; + let scheduled_channel = scheduler.get_next_channel(amount_to_us); + if let Some(channel) = scheduled_channel { + let (channel_id, address, temporary_channel_id, _, counterparty_node_id) = channel; + let mut channel_provisional_proposal = provisional_proposal.clone(); + channel_provisional_proposal.substitute_output_address(address); + let payjoin_proposal = match channel_provisional_proposal + .finalize_proposal(|psbt| Ok(psbt.clone()), None) + { + Ok(proposal) => proposal, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + let (receiver_request, _) = match payjoin_proposal.clone().extract_v2_req() { + Ok((req, ctx)) => (req, ctx), + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + let tx = payjoin_proposal.psbt().clone().extract_tx(); + if let (true, Ok(())) = ( + scheduler.set_funding_tx_created( + channel_id, + &receiver_request.url, + receiver_request.body, + ), + self.channel_manager.unsafe_funding_transaction_generated( + &ChannelId::from_bytes(temporary_channel_id), + &counterparty_node_id, + tx.clone(), + ), + ) { + return; + } + } + } + } + + pub(crate) fn payjoin_uri(&self, amount: bitcoin::Amount) -> Result { + let address = self.wallet.get_new_address()?; + let pj_part = Url::parse(&self.enrolled.fallback_target()).map_err(|e| { + dbg!(&e); + Error::PayjoinUri + })?; + let payjoin_uri = PjUriBuilder::new(address, pj_part, Some(self.ohttp_keys.clone())) + .amount(amount) + .build(); + Ok(payjoin_uri) + } + + pub(crate) fn schedule_channel( + &self, amount: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128, + ) { + let channel = PayjoinChannel::new(amount, counterparty_node_id, channel_id); + self.scheduler.blocking_lock().schedule( + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + channel.channel_id(), + ); + } + + pub(crate) fn list_scheduled_channels(&self) -> Vec { + self.scheduler.blocking_lock().list_channels().clone() + } + + pub(crate) async fn set_channel_accepted( + &self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + let mut scheduler = self.scheduler.lock().await; + scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id) + } + + async fn validate_payjoin_request( + &self, proposal: UncheckedProposal, min_fee_rate: Option, + ) -> Result<(ProvisionalProposal, bitcoin::Amount), Error> { + let tx = proposal.extract_tx_to_schedule_broadcast(); + let verified = self.wallet.verify_tx(&tx).await; + let amount_to_us = self.wallet.funds_directed_to_us(&tx).unwrap_or_default(); + let proposal = + proposal.check_broadcast_suitability(min_fee_rate, |_t| Ok(verified.is_ok()))?; + let proposal = proposal.check_inputs_not_owned(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + let proposal = match proposal.check_no_mixed_input_scripts() { + Ok(proposal) => proposal, + Err(_) => { + log_error!(self.logger, "Payjoin Receiver: Mixed input scripts"); + return Err(Error::PayjoinReceiver); + }, + }; + let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?; + let original_proposal = proposal.clone().identify_receiver_outputs(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + Ok((original_proposal, amount_to_us)) + } +} + +pub(crate) struct PayjoinReceiver +where + L::Target: Logger, +{ + logger: L, + wallet: Arc, + enrolled: Enrolled, + ohttp_keys: OhttpKeys, +} + +impl PayjoinReceiver +where + L::Target: Logger, +{ + pub fn new(logger: L, wallet: Arc, enrolled: Enrolled, ohttp_keys: OhttpKeys) -> Self { + Self { logger, wallet, enrolled, ohttp_keys } + } + + async fn fetch_payjoin_request(&self) -> Result, Error> { + dbg!("fetching pajoing request"); + let mut enrolled = self.enrolled.clone(); + dbg!("1"); + let (req, context) = match enrolled.extract_req() { + Ok(req) => req, + Err(e) => { + dbg!("Error: {}", &e); + log_error!(self.logger, "Payjoin Receiver: {}", e); + return Err(Error::PayjoinReceiver); + }, + }; + dbg!("2"); + let payjoin_request = post_request(&req.url, req.body).await?; + dbg!("3"); + let response = match enrolled.process_res(payjoin_request.as_slice(), context) { + Ok(response) => response, + Err(e) => { + dbg!("Error process payjoin request: {}", &e); + log_error!(self.logger, "Payjoin Receiver: {}", e); + return Err(Error::PayjoinReceiver); + }, + }; + dbg!("4"); + Ok(response) + } + + pub(crate) async fn process_payjoin_request(&self) { + dbg!("processing normal payjoin request"); + let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1); + let unchecked_proposal = match self.fetch_payjoin_request().await { + Ok(Some(proposal)) => proposal, + _ => { + dbg!("NONE"); + return; + }, + }; + dbg!("got unchecked proposal"); + let (provisional_proposal, _) = + match self.validate_payjoin_request(unchecked_proposal, min_fee_rate).await { + Ok(proposal) => proposal, + Err(_e) => { + dbg!("Error validating payjoin request"); + return; + }, + }; + dbg!("finalizing payjoin request"); + self.accept_normal_payjoin_request(provisional_proposal).await + } + + async fn accept_normal_payjoin_request(&self, provisional_proposal: ProvisionalProposal) { + let mut finalized_proposal = + match provisional_proposal.finalize_proposal(|psbt| Ok(psbt.clone()), None) { + Ok(proposal) => proposal, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + let (receiver_request, _) = match finalized_proposal.extract_v2_req() { + Ok(req) => req, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + return; + }, + }; + match post_request(&receiver_request.url, receiver_request.body).await { + Ok(_response) => { + log_info!(self.logger, "Payjoin Receiver: Payjoin request sent to sender"); + }, + Err(e) => { + log_error!(self.logger, "Payjoin Receiver: {}", e); + }, + } + } + + pub(crate) fn payjoin_uri(&self, amount: bitcoin::Amount) -> Result { + let address = self.wallet.get_new_address()?; + let pj_part = + Url::parse(&self.enrolled.fallback_target()).map_err(|_| Error::PayjoinUri)?; + let payjoin_uri = PjUriBuilder::new(address, pj_part, Some(self.ohttp_keys.clone())) + .amount(amount) + .build(); + Ok(payjoin_uri) + } + + async fn validate_payjoin_request( + &self, proposal: UncheckedProposal, min_fee_rate: Option, + ) -> Result<(ProvisionalProposal, bitcoin::Amount), Error> { + let tx = proposal.extract_tx_to_schedule_broadcast(); + let verified = self.wallet.verify_tx(&tx).await; + let amount_to_us = self.wallet.funds_directed_to_us(&tx).unwrap_or_default(); + let proposal = + proposal.check_broadcast_suitability(min_fee_rate, |_t| Ok(verified.is_ok()))?; + let proposal = proposal.check_inputs_not_owned(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + let proposal = match proposal.check_no_mixed_input_scripts() { + Ok(proposal) => proposal, + Err(_) => { + log_error!(self.logger, "Payjoin Receiver: Mixed input scripts"); + return Err(Error::PayjoinReceiver); + }, + }; + let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?; + let original_proposal = proposal.clone().identify_receiver_outputs(|script| { + Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) + })?; + Ok((original_proposal, amount_to_us)) + } +} + +pub(crate) fn payjoin_receiver_request_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + let header_value = HeaderValue::from_static("message/ohttp-req"); + headers.insert(reqwest::header::CONTENT_TYPE, header_value); + headers +} + +struct BlockingHttpClient { + client: reqwest::blocking::Client, +} + +impl BlockingHttpClient { + fn new() -> Result { + let client = + reqwest::blocking::Client::builder().build().map_err(|_| Error::PayjoinReqwest)?; + Ok(Self { client }) + } + + fn new_proxy(payjoin_relay: &Url) -> Result { + let proxy = + reqwest::Proxy::all(payjoin_relay.to_string()).map_err(|_| Error::PayjoinReqwest)?; + let client = reqwest::blocking::Client::builder() + .proxy(proxy) + .build() + .map_err(|_| Error::PayjoinReqwest)?; + Ok(Self { client }) + } + + fn get(&self, url: &Url) -> Result, Error> { + Ok(self + .client + .get(url.to_string()) + .send() + .and_then(|response| response.error_for_status()) + .map_err(|e| { + dbg!(&e); + Error::PayjoinReqwest + })? + .bytes() + .map_err(|e| { + dbg!(&e); + Error::PayjoinReqwest + })? + .to_vec()) + } + + fn post( + &self, url: &str, body: Vec, headers: HeaderMap, + timeout: Option, + ) -> Result { + Ok(self + .client + .post(url) + .timeout(timeout.unwrap_or(std::time::Duration::from_secs(35))) + .headers(headers) + .body(body) + .send() + .map_err(|e| { + dbg!(&e); + Error::PayjoinReqwest + })?) + } +} From accf6a84ed5de7dcd5ae109993c667444a118be5 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:32:12 +0300 Subject: [PATCH 09/11] add payjoin scheduler --- src/payjoin_scheduler.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/payjoin_scheduler.rs b/src/payjoin_scheduler.rs index 67918232f..7a24e8d7e 100644 --- a/src/payjoin_scheduler.rs +++ b/src/payjoin_scheduler.rs @@ -1,14 +1,15 @@ -use bitcoin::{secp256k1::PublicKey, ScriptBuf, TxOut}; +use bitcoin::{secp256k1::PublicKey, ScriptBuf, Transaction, TxOut}; #[derive(Clone)] pub struct PayjoinScheduler { channels: Vec, + seen_txs: Vec, } impl PayjoinScheduler { /// Create a new empty channel scheduler. pub fn new() -> Self { - Self { channels: vec![] } + Self { channels: vec![], seen_txs: vec![] } } /// Schedule a new channel. /// @@ -115,6 +116,29 @@ impl PayjoinScheduler { pub fn in_progress(&self) -> bool { self.channels.iter().any(|channel| !channel.is_channel_accepted()) } + + pub fn add_seen_tx(&mut self, tx: &Transaction) -> bool { + for input in tx.input.iter() { + for tx in self.seen_txs.clone() { + if tx.input.contains(&input) { + return false; + } + } + } + self.seen_txs.push(tx.clone()); + return true; + } + + // pub fn seen_outpoints(&self, tx: &bitcoin::Transaction) -> bool { + // tx.input.iter().any(|input| self.seen_outpoints_internal(&input.previous_output)) + // } + + // fn seen_outpoints_internal(&self, outpoint: &bitcoin::OutPoint) -> bool { + // self.seen_txs + // .iter() + // .any(|seen_tx| seen_tx.input.iter().any(|input| &input.previous_output == outpoint)) + // } + 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(); From 773d4976ae49c10ae8848dadb8254d716ec4742c Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:32:36 +0300 Subject: [PATCH 10/11] test payjoin integartion --- tests/integration_tests_payjoin.rs | 209 ++----------------- tests/integration_tests_payjoin_lightning.rs | 79 +++++++ 2 files changed, 98 insertions(+), 190 deletions(-) create mode 100644 tests/integration_tests_payjoin_lightning.rs diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs index 6e483707d..9d8a3aa6e 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -1,83 +1,16 @@ mod common; -use std::{thread::sleep, time::Duration}; use crate::common::{ - expect_channel_pending_event, expect_channel_ready_event, generate_blocks_and_wait, - premine_and_distribute_funds, setup_two_nodes, wait_for_tx, + generate_blocks_and_wait, premine_and_distribute_funds, setup_two_payjoin_nodes, wait_for_tx, }; use bitcoin::Amount; -use bitcoincore_rpc::{Client as BitcoindClient, RpcApi}; use common::setup_bitcoind_and_electrsd; -use ldk_node::Event; - -#[test] -fn send_receive_with_channel_opening_payjoin_transaction() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - let premine_amount_sat = 100_000_00; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_b], - Amount::from_sat(premine_amount_sat), - ); - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - assert_eq!(node_a.next_event(), None); - assert_eq!(node_a.list_channels().len(), 0); - assert_eq!(node_b.next_event(), None); - assert_eq!(node_b.list_channels().len(), 0); - let funding_amount_sat = 80_000; - let node_b_listening_address = node_b.listening_addresses().unwrap().get(0).unwrap().clone(); - let payjoin_uri = node_a - .request_payjoin_transaction_with_channel_opening( - funding_amount_sat, - None, - false, - node_b.node_id(), - node_b_listening_address, - ) - .unwrap(); - assert!(node_b - .send_payjoin_transaction( - payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked() - ) - .is_ok()); - expect_channel_pending_event!(node_a, node_b.node_id()); - expect_channel_pending_event!(node_b, node_a.node_id()); - let channels = node_a.list_channels(); - let channel = channels.get(0).unwrap(); - wait_for_tx(&electrsd.client, channel.funding_txo.unwrap().txid); - sleep(Duration::from_secs(1)); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - let channels = node_a.list_channels(); - let channel = channels.get(0).unwrap(); - assert_eq!(channel.channel_value_sats, funding_amount_sat); - assert_eq!(channel.confirmations.unwrap(), 6); - assert!(channel.is_channel_ready); - assert!(channel.is_usable); - - assert_eq!(node_a.list_peers().get(0).unwrap().is_connected, true); - assert_eq!(node_a.list_peers().get(0).unwrap().is_persisted, true); - assert_eq!(node_a.list_peers().get(0).unwrap().node_id, node_b.node_id()); - - let invoice_amount_1_msat = 2500_000; - let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap(); - assert!(node_a.bolt11_payment().send(&invoice).is_ok()); -} #[test] fn send_receive_regular_payjoin_transaction() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); - let addr_b = node_b.onchain_payment().new_address().unwrap(); + 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 premine_amount_sat = 100_000_00; premine_and_distribute_funds( &bitcoind.client, @@ -85,127 +18,23 @@ fn send_receive_regular_payjoin_transaction() { vec![addr_b], Amount::from_sat(premine_amount_sat), ); - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); - assert_eq!(node_a.next_event(), None); - assert_eq!(node_a.list_channels().len(), 0); - let payjoin_uri = node_a.request_payjoin_transaction(80_000).unwrap(); - assert!(node_b - .send_payjoin_transaction( - payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked() - ) - .is_ok()); - sleep(Duration::from_secs(3)); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - let node_a_balance = node_a.list_balances(); - let node_b_balance = node_b.list_balances(); - assert_eq!(node_a_balance.total_onchain_balance_sats, 80000); - assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); -} - -mod payjoin_v1 { - use bitcoin::address::NetworkChecked; - use bitcoin::base64; - use bitcoin::Txid; - use bitcoincore_rpc::Client as BitcoindClient; - use bitcoincore_rpc::RpcApi; - use std::collections::HashMap; - use std::str::FromStr; - - use bitcoincore_rpc::bitcoin::psbt::Psbt; - - pub fn send( - sender_wallet: &BitcoindClient, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, - ) -> Txid { - let amount_to_send = payjoin_uri.amount.unwrap(); - let receiver_address = payjoin_uri.address.clone(); - let mut outputs = HashMap::with_capacity(1); - outputs.insert(receiver_address.to_string(), amount_to_send); - let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions { - lock_unspent: Some(false), - fee_rate: Some(bitcoincore_rpc::bitcoin::Amount::from_sat(10000)), - ..Default::default() - }; - let sender_psbt = sender_wallet - .wallet_create_funded_psbt( - &[], // inputs - &outputs, - None, // locktime - Some(options), - None, - ) - .unwrap(); - let psbt = - sender_wallet.wallet_process_psbt(&sender_psbt.psbt, None, None, None).unwrap().psbt; - let psbt = Psbt::from_str(&psbt).unwrap(); - let (req, ctx) = - payjoin::send::RequestBuilder::from_psbt_and_uri(psbt.clone(), payjoin_uri) - .unwrap() - .build_with_additional_fee( - bitcoincore_rpc::bitcoin::Amount::from_sat(1), - None, - bitcoincore_rpc::bitcoin::FeeRate::MIN, - true, - ) - .unwrap() - .extract_v1() - .unwrap(); - let url_http = req.url.as_str().replace("https", "http"); - let res = reqwest::blocking::Client::new(); - let res = res - .post(&url_http) - .body(req.body.clone()) - .header("content-type", "text/plain") - .send() - .unwrap(); - let res = res.text().unwrap(); - let psbt = ctx.process_response(&mut res.as_bytes()).unwrap(); - let psbt = sender_wallet - .wallet_process_psbt(&base64::encode(psbt.serialize()), None, None, None) - .unwrap() - .psbt; - let tx = sender_wallet.finalize_psbt(&psbt, Some(true)).unwrap().hex.unwrap(); - let txid = sender_wallet.send_raw_transaction(&tx).unwrap(); - txid - } -} - -#[test] -fn receive_payjoin_version_1() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let payjoin_sender_wallet: BitcoindClient = bitcoind.create_wallet("payjoin_sender").unwrap(); - let (node_a, _) = setup_two_nodes(&electrsd, false, true); - let addr_sender = payjoin_sender_wallet.get_new_address(None, None).unwrap().assume_checked(); - let premine_amount_sat = 100_000_00; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_sender], - Amount::from_sat(premine_amount_sat), - ); - node_a.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); - assert_eq!( - payjoin_sender_wallet.get_balances().unwrap().mine.trusted.to_sat(), - premine_amount_sat - ); - assert_eq!(node_a.next_event(), None); - assert_eq!(node_a.list_channels().len(), 0); - let pj_uri = node_a.request_payjoin_transaction(80_000).unwrap(); - payjoin_v1::send( - &payjoin_sender_wallet, - payjoin::Uri::try_from(pj_uri.to_string()).unwrap().assume_checked(), + 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, 0); + assert_eq!(node_a_pj_receiver.next_event(), None); + assert_eq!(node_a_pj_receiver.list_channels().len(), 0); + let payjoin_uri = node_a_pj_receiver.request_payjoin_transaction(80_000).unwrap(); + let txid = node_b_pj_sender.send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked(), ); - sleep(Duration::from_secs(3)); + dbg!(&txid); + wait_for_tx(&electrsd.client, txid.unwrap().unwrap()); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); - node_a.sync_wallets().unwrap(); - let node_a_balance = node_a.list_balances(); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + let node_a_balance = node_a_pj_receiver.list_balances(); + let node_b_balance = node_b_pj_sender.list_balances(); assert_eq!(node_a_balance.total_onchain_balance_sats, 80000); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); } - -// test validation of payjoin transaction fails -// test counterparty doesnt return fundingsigned diff --git a/tests/integration_tests_payjoin_lightning.rs b/tests/integration_tests_payjoin_lightning.rs new file mode 100644 index 000000000..1427e937c --- /dev/null +++ b/tests/integration_tests_payjoin_lightning.rs @@ -0,0 +1,79 @@ +mod common; +use std::{thread::sleep, time::Duration}; + +use crate::common::{ + expect_channel_pending_event, expect_channel_ready_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_two_payjoin_nodes, wait_for_tx, +}; +use bitcoin::Amount; +use common::setup_bitcoind_and_electrsd; +use ldk_node::Event; + +#[test] +fn send_receive_with_channel_opening_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_payjoin_nodes(&electrsd, false); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b], + Amount::from_sat(premine_amount_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a.next_event(), None); + assert_eq!(node_a.list_channels().len(), 0); + assert_eq!(node_b.next_event(), None); + assert_eq!(node_b.list_channels().len(), 0); + let funding_amount_sat = 80_000; + let node_b_listening_address = node_b.listening_addresses().unwrap().get(0).unwrap().clone(); + let payjoin_uri = node_a.request_payjoin_transaction_with_channel_opening( + funding_amount_sat, + None, + false, + node_b.node_id(), + node_b_listening_address, + ); + let payjoin_uri = match payjoin_uri { + Ok(payjoin_uri) => payjoin_uri, + Err(e) => { + dbg!(&e); + assert!(false); + panic!("should generate payjoin uri"); + }, + }; + assert!(node_b + .send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked() + ) + .is_ok()); + expect_channel_pending_event!(node_a, node_b.node_id()); + expect_channel_pending_event!(node_b, node_a.node_id()); + let channels = node_a.list_channels(); + let channel = channels.get(0).unwrap(); + wait_for_tx(&electrsd.client, channel.funding_txo.unwrap().txid); + sleep(Duration::from_secs(1)); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + let channels = node_a.list_channels(); + let channel = channels.get(0).unwrap(); + assert_eq!(channel.channel_value_sats, funding_amount_sat); + assert_eq!(channel.confirmations.unwrap(), 6); + assert!(channel.is_channel_ready); + assert!(channel.is_usable); + + assert_eq!(node_a.list_peers().get(0).unwrap().is_connected, true); + assert_eq!(node_a.list_peers().get(0).unwrap().is_persisted, true); + assert_eq!(node_a.list_peers().get(0).unwrap().node_id, node_b.node_id()); + + let invoice_amount_1_msat = 2500_000; + let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap(); + assert!(node_a.bolt11_payment().send(&invoice).is_ok()); +} From b8e82ddec65087b3ab4b060634cd4dacf96576de Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:32:43 +0300 Subject: [PATCH 11/11] f --- src/builder.rs | 125 +++-- src/channel_scheduler.rs | 329 ----------- src/event.rs | 6 +- src/lib.rs | 149 +++-- src/payjoin_handler.rs | 542 ------------------- src/payjoin_sender.rs | 249 +++------ src/tx_broadcaster.rs | 12 +- src/wallet.rs | 7 +- tests/common/mod.rs | 66 ++- tests/integration_tests_payjoin.rs | 12 +- tests/integration_tests_payjoin_lightning.rs | 15 +- tests/integration_tests_rust.rs | 14 +- 12 files changed, 385 insertions(+), 1141 deletions(-) delete mode 100644 src/channel_scheduler.rs delete mode 100644 src/payjoin_handler.rs diff --git a/src/builder.rs b/src/builder.rs index 738cb3eee..edc740042 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,4 +1,3 @@ -use crate::channel_scheduler::ChannelScheduler; use crate::config::{ Config, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, DEFAULT_ESPLORA_SERVER_URL, WALLET_KEYS_SEED_LEN, @@ -12,7 +11,11 @@ use crate::io::sqlite_store::SqliteStore; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; -use crate::payjoin_handler::{PayjoinReceiver, PayjoinSender}; +use crate::payjoin_receiver::{ + enroll_payjoin_receivers, PayjoinLightningReceiver, PayjoinReceiver, +}; +use crate::payjoin_scheduler::PayjoinScheduler; +use crate::payjoin_sender::PayjoinSender; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -97,12 +100,17 @@ struct LiquiditySourceConfig { } #[derive(Debug, Clone)] -struct PayjoinConfig { +struct PayjoinReceiverConfig { payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, ohttp_keys: Option, } +#[derive(Debug, Clone)] +struct PayjoinSenderConfig { + payjoin_relay: payjoin::Url, +} + impl Default for LiquiditySourceConfig { fn default() -> Self { Self { lsps2_service: None } @@ -182,7 +190,8 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, - payjoin_config: Option, + payjoin_receiver_config: Option, + payjoin_sender_config: Option, } impl NodeBuilder { @@ -198,14 +207,16 @@ impl NodeBuilder { let chain_data_source_config = None; let gossip_source_config = None; let liquidity_source_config = None; - let payjoin_config = None; + let payjoin_receiver_config = None; + let payjoin_sender_config = None; Self { config, entropy_source_config, chain_data_source_config, gossip_source_config, liquidity_source_config, - payjoin_config, + payjoin_receiver_config, + payjoin_sender_config, } } @@ -262,11 +273,19 @@ impl NodeBuilder { /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync /// server. - pub fn set_payjoin_config( + pub fn set_payjoin_sender_config(&mut self, payjoin_relay: payjoin::Url) -> &mut Self { + self.payjoin_sender_config = Some(PayjoinSenderConfig { payjoin_relay }); + self + } + + /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync + /// server. + pub fn set_payjoin_receiver_config( &mut self, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, ohttp_keys: Option, ) -> &mut Self { - self.payjoin_config = Some(PayjoinConfig { payjoin_directory, payjoin_relay, ohttp_keys }); + self.payjoin_receiver_config = + Some(PayjoinReceiverConfig { payjoin_directory, payjoin_relay, ohttp_keys }); self } @@ -391,7 +410,8 @@ impl NodeBuilder { seed_bytes, logger, vss_store, - self.payjoin_config.as_ref(), + self.payjoin_receiver_config.as_ref(), + self.payjoin_sender_config.as_ref(), ) } @@ -413,7 +433,8 @@ impl NodeBuilder { seed_bytes, logger, kv_store, - self.payjoin_config.as_ref(), + self.payjoin_receiver_config.as_ref(), + self.payjoin_sender_config.as_ref(), ) } } @@ -478,24 +499,29 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_p2p(); } - /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync - /// server. - pub fn set_gossip_source_rgs(&self, rgs_server_url: String) { - self.inner.write().unwrap().set_gossip_source_rgs(rgs_server_url); + /// payjoin sender config + pub fn set_payjoin_sender_config(&self, payjoin_relay: payjoin::Url) { + self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay); } - /// Configures the [`Node`] instance to use payjoin. - pub fn set_payjoin_config( + /// payjoin receiver config + pub fn set_payjoin_receiver_config( &self, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, ohttp_keys: Option, ) { - self.inner.write().unwrap().set_payjoin_config( + self.inner.write().unwrap().set_payjoin_receiver_config( payjoin_directory, payjoin_relay, ohttp_keys, ); } + /// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync + /// server. + pub fn set_gossip_source_rgs(&self, rgs_server_url: String) { + self.inner.write().unwrap().set_gossip_source_rgs(rgs_server_url); + } + /// Configures the [`Node`] instance to source its inbound liquidity from the given /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. @@ -559,7 +585,9 @@ fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - logger: Arc, kv_store: Arc, payjoin_config: Option<&PayjoinConfig>, + logger: Arc, kv_store: Arc, + payjoin_receiver_config: Option<&PayjoinReceiverConfig>, + payjoin_sender_config: Option<&PayjoinSenderConfig>, ) -> Result { // Initialize the on-chain wallet and chain access let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes) @@ -592,7 +620,7 @@ fn build_with_store_internal( log_error!(logger, "Failed to set up wallet: {}", e); BuildError::WalletSetupFailed })?; - let channel_scheduler = Arc::new(tokio::sync::Mutex::new(ChannelScheduler::new())); + let payjoin_scheduler = Arc::new(tokio::sync::Mutex::new(PayjoinScheduler::new())); let (blockchain, tx_sync, tx_broadcaster, fee_estimator) = match chain_data_source_config { Some(ChainDataSourceConfig::Esplora(server_url)) => { @@ -603,7 +631,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), - Arc::clone(&channel_scheduler), + Arc::clone(&payjoin_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -622,7 +650,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), - Arc::clone(&channel_scheduler), + Arc::clone(&payjoin_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -1012,29 +1040,49 @@ fn build_with_store_internal( }; let (stop_sender, _) = tokio::sync::watch::channel(()); - let (payjoin_receiver, payjoin_sender) = if let Some(payjoin_config) = payjoin_config { - let payjoin_receiver = match PayjoinReceiver::enroll( - &payjoin_config.ohttp_keys, - &payjoin_config.payjoin_directory, - &payjoin_config.payjoin_relay, - Arc::clone(&channel_scheduler), - Arc::clone(&wallet), - Arc::clone(&channel_manager), - Arc::clone(&logger), - ) { - Ok(r) => Some(Arc::new(r)), - Err(_e) => None, - }; + let payjoin_sender = if let Some(payjoin_sender_config) = payjoin_sender_config { let payjoin_sender = PayjoinSender::new( Arc::clone(&logger), Arc::clone(&wallet), - &payjoin_config.payjoin_relay, - &payjoin_config.payjoin_directory, + &payjoin_sender_config.payjoin_relay, ); - (payjoin_receiver, Some(Arc::new(payjoin_sender))) + Some(Arc::new(payjoin_sender)) } else { - (None, None) + None }; + let (payjoin_receiver, payjoin_lightning_receiver) = + if let Some(payjoin_receiver_config) = payjoin_receiver_config { + let enrollement = enroll_payjoin_receivers( + &payjoin_receiver_config.ohttp_keys, + &payjoin_receiver_config.payjoin_directory, + &payjoin_receiver_config.payjoin_relay, + ) + .ok(); + if let Some(enrollement) = enrollement { + let (payjoin_enrollement, lightning_enrollement, ohttp_keys) = enrollement; + dbg!("Enrolled payjoin receiver"); + let payjoin_receiver = PayjoinReceiver::new( + Arc::clone(&logger), + Arc::clone(&wallet), + payjoin_enrollement, + ohttp_keys.clone(), + ); + + let payjoin_lightning_receiver = PayjoinLightningReceiver::new( + Arc::clone(&logger), + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&payjoin_scheduler), + lightning_enrollement, + ohttp_keys, + ); + (Some(Arc::new(payjoin_receiver)), Some(Arc::new(payjoin_lightning_receiver))) + } else { + (None, None) + } + } else { + (None, None) + }; let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); @@ -1057,6 +1105,7 @@ fn build_with_store_internal( output_sweeper, payjoin_receiver, payjoin_sender, + payjoin_lightning_receiver, peer_manager, connection_manager, keys_manager, diff --git a/src/channel_scheduler.rs b/src/channel_scheduler.rs deleted file mode 100644 index 5cf5738dd..000000000 --- a/src/channel_scheduler.rs +++ /dev/null @@ -1,329 +0,0 @@ -use bitcoin::{secp256k1::PublicKey, ScriptBuf, TxOut}; - -#[derive(Clone)] -pub struct ChannelScheduler { - channels: Vec, -} - -impl ChannelScheduler { - /// Create a new empty channel scheduler. - pub fn new() -> Self { - Self { channels: vec![] } - } - /// Schedule a new channel. - /// - /// The channel will be created with `ScheduledChannelState::ChannelCreated` state. - pub fn schedule( - &mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, - channel_id: u128, - ) { - let channel = - ScheduledChannel::new(channel_value_satoshi, counterparty_node_id, channel_id); - match channel.state { - ScheduledChannelState::ChannelCreated => { - self.channels.push(channel); - }, - _ => {}, - } - } - /// Mark a channel as accepted. - /// - /// The channel will be updated to `ScheduledChannelState::ChannelAccepted` state. - pub 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 - } - /// Mark a channel as funding tx created. - /// - /// The channel will be updated to `ScheduledChannelState::FundingTxCreated` state. - pub 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 - } - /// Mark a channel as funding tx signed. - /// - /// The channel will be updated to `ScheduledChannelState::FundingTxSigned` state. - pub 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 the accepted state. - /// - /// If more than one channel matches the given channel amount, the channel with the oldest - /// creation date will be returned. - pub fn get_next_channel( - &self, channel_amount: bitcoin::Amount, - ) -> 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(), - bitcoin::Network::Regtest, // fixme - ); - 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 - } - - /// List all channels. - pub fn list_channels(&self) -> &Vec { - &self.channels - } - - pub fn in_progress(&self) -> bool { - self.channels.iter().any(|channel| !channel.is_channel_accepted()) - } - 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() - } -} - -/// A struct representing a scheduled channel. -#[derive(Clone, Debug)] -pub struct ScheduledChannel { - state: ScheduledChannelState, - channel_value_satoshi: bitcoin::Amount, - channel_id: u128, - counterparty_node_id: PublicKey, - created_at: u64, -} - -impl ScheduledChannel { - pub 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 fn channel_value_satoshi(&self) -> bitcoin::Amount { - self.channel_value_satoshi - } - - /// Get the user channel id. - pub fn channel_id(&self) -> u128 { - self.channel_id - } - - /// Get the counterparty node id. - pub fn counterparty_node_id(&self) -> PublicKey { - self.counterparty_node_id - } - - /// Get the output script. - pub fn output_script(&self) -> Option<&ScriptBuf> { - self.state.output_script() - } - - /// Get the temporary channel id. - pub fn temporary_channel_id(&self) -> Option<[u8; 32]> { - self.state.temporary_channel_id() - } - - /// Get the temporary channel id. - pub fn tx_out(&self) -> Option<&TxOut> { - match &self.state { - ScheduledChannelState::FundingTxSigned(_, txout) => Some(txout), - _ => None, - } - } - - pub 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, TxOut), -} - -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 { - *self = - ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), output.clone()); - res = true; - } - return res; - } -} - -// #[cfg(test)] -// mod tests { -// use std::str::FromStr; - -// use super::*; -// use bitcoin::{ -// psbt::Psbt, -// secp256k1::{self, Secp256k1}, -// }; - -// #[ignore] -// #[test] -// fn test_channel_scheduler() { -// let create_pubkey = || -> PublicKey { -// let secp = Secp256k1::new(); -// PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap()) -// }; -// let channel_value_satoshi = 100; -// let node_id = create_pubkey(); -// let channel_id: u128 = 0; -// let mut channel_scheduler = ChannelScheduler::new(); -// channel_scheduler.schedule( -// bitcoin::Amount::from_sat(channel_value_satoshi), -// node_id, -// channel_id, -// ); -// assert_eq!(channel_scheduler.channels.len(), 1); -// assert_eq!(channel_scheduler.is_channel_created(channel_id), true); -// channel_scheduler.set_channel_accepted( -// channel_id, -// &ScriptBuf::from(vec![1, 2, 3]), -// [0; 32], -// ); -// assert_eq!(channel_scheduler.is_channel_accepted(channel_id), true); -// let str_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; -// let mock_transaction = Psbt::from_str(str_psbt).unwrap(); -// let _our_txout = mock_transaction.clone().extract_tx().output[0].clone(); -// // channel_scheduler.set_funding_tx_created(channel_id, mock_transaction.clone()); -// // let tx_id = mock_transaction.extract_tx().txid(); -// // assert_eq!(channel_scheduler.is_funding_tx_created(&tx_id), true); -// // channel_scheduler.set_funding_tx_signed(tx_id); -// // assert_eq!(channel_scheduler.is_funding_tx_signed(&tx_id), true); -// } -// } diff --git a/src/event.rs b/src/event.rs index 41bfb387f..9abdf8709 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,4 +1,4 @@ -use crate::payjoin_handler::PayjoinReceiver; +use crate::payjoin_receiver::PayjoinLightningReceiver; use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, @@ -323,7 +323,7 @@ where runtime: Arc>>, logger: L, config: Arc, - payjoin_receiver: Option>>, + payjoin_receiver: Option>>, } impl EventHandler @@ -335,7 +335,7 @@ where output_sweeper: Arc, network_graph: Arc, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>, logger: L, config: Arc, - payjoin_receiver: Option>>, + payjoin_receiver: Option>>, ) -> Self { Self { event_queue, diff --git a/src/lib.rs b/src/lib.rs index b496b65e7..8a26e06ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,6 @@ mod balance; mod builder; -mod channel_scheduler; mod config; mod connection; mod error; @@ -89,7 +88,10 @@ pub mod io; mod liquidity; mod logger; mod message_handler; -mod payjoin_handler; +// mod payjoin_handler; +mod payjoin_receiver; +mod payjoin_scheduler; +mod payjoin_sender; pub mod payment; mod peer_store; mod sweep; @@ -110,10 +112,11 @@ pub use config::{default_config, Config}; pub use error::Error as NodeError; use error::Error; -use channel_scheduler::ScheduledChannel; pub use event::Event; use payjoin::PjUri; -use payjoin_handler::{PayjoinReceiver, PayjoinSender}; +use payjoin_receiver::{PayjoinLightningReceiver, PayjoinReceiver}; +use payjoin_scheduler::PayjoinChannel; +use payjoin_sender::PayjoinSender; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -189,6 +192,7 @@ pub struct Node { connection_manager: Arc>>, payjoin_receiver: Option>>>, payjoin_sender: Option>>>, + payjoin_lightning_receiver: Option>>>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -502,7 +506,7 @@ impl Node { if let Some(payjoin_receiver) = &self.payjoin_receiver { let mut stop_payjoin_server = self.stop_sender.subscribe(); let payjoin_receiver = Arc::clone(&payjoin_receiver); - let payjoin_check_interval = 1; + let payjoin_check_interval = 10; runtime.spawn(async move { let mut payjoin_interval = tokio::time::interval(Duration::from_secs(payjoin_check_interval)); @@ -521,28 +525,51 @@ impl Node { }); } - if let Some(payjoin_sender) = &self.payjoin_sender { + if let Some(payjoin_lightning_receiver) = &self.payjoin_lightning_receiver { let mut stop_payjoin_server = self.stop_sender.subscribe(); - let payjoin_sender = Arc::clone(&payjoin_sender); - let payjoin_check_interval = 1; + let payjoin_lightning_receiver = Arc::clone(&payjoin_lightning_receiver); + let payjoin_check_interval = 10; runtime.spawn(async move { - let mut payjoin_interval = + let mut lightning_payjoin_interval = tokio::time::interval(Duration::from_secs(payjoin_check_interval)); - payjoin_interval.reset(); - payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + lightning_payjoin_interval.reset(); + lightning_payjoin_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { tokio::select! { _ = stop_payjoin_server.changed() => { return; } - _ = payjoin_interval.tick() => { - let _ = payjoin_sender.process_payjoin_response().await; + _ = lightning_payjoin_interval.tick() => { + let _ = payjoin_lightning_receiver.process_payjoin_request().await; } } } }); } + // if let Some(payjoin_sender) = &self.payjoin_sender { + // let mut stop_payjoin_server = self.stop_sender.subscribe(); + // let payjoin_sender = Arc::clone(&payjoin_sender); + // let payjoin_check_interval = 2; + // runtime.spawn(async move { + // let mut payjoin_interval = + // tokio::time::interval(Duration::from_secs(payjoin_check_interval)); + // payjoin_interval.reset(); + // payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + // loop { + // tokio::select! { + // _ = stop_payjoin_server.changed() => { + // return; + // } + // _ = payjoin_interval.tick() => { + // let _ = payjoin_sender.process_payjoin_response().await; + // } + // } + // } + // }); + // } + // Regularly reconnect to persisted peers. let connect_cm = Arc::clone(&self.connection_manager); let connect_pm = Arc::clone(&self.peer_manager); @@ -680,7 +707,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), - self.payjoin_receiver.clone(), + self.payjoin_lightning_receiver.clone(), )); // Setup background processing @@ -750,25 +777,81 @@ impl Node { Ok(()) } - /// Send a payjoin transaction from the node on chain funds to the address as specified in the - /// payjoin URI. - pub fn send_payjoin_transaction( - &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, + /// This method can be used to send a payjoin transaction as defined + /// in BIP77. + /// + /// The method will construct an `Original PSBT` from the data + /// provided in the `payjoin_uri` and `amount` parameters. The + /// amount must be set either in the `payjoin_uri` or in the + /// `amount` parameter. If the amount is set in both, the paramter + /// amount parameter will be used. + /// + /// After constructing the `Original PSBT`, the method will + /// extract the payjoin request data from the `Original PSBT` + /// utilising the `payjoin` crate. + /// + /// Then we start a background process to that will run for 1 + /// hour, polling the payjoin endpoint every 10 seconds. If an `OK` (ie status code == 200) + /// is received, polling will stop and we will try to process the + /// response from the payjoin receiver. If the response(or `Payjoin Proposal`) is valid, we will finalise the + /// transaction and broadcast it to the network. + /// + /// Notice that the `Txid` returned from this method is the + /// `Original PSBT` transaction id, but the `Payjoin Proposal` + /// transaction id could be different if the receiver changed the + /// transaction. + pub async fn send_payjoin_transaction( + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option, ) -> Result, Error> { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } - let payjoin_sender = self.payjoin_sender.as_ref().ok_or(Error::PayjoinSender)?; - let psbt = payjoin_sender.create_payjoin_request(payjoin_uri.clone()).unwrap(); - payjoin_sender.send_payjoin_request(payjoin_uri, psbt) + let payjoin_sender = Arc::clone(self.payjoin_sender.as_ref().ok_or(Error::PayjoinSender)?); + let original_psbt = payjoin_sender.create_payjoin_request(payjoin_uri.clone(), amount)?; + let txid = original_psbt.clone().unsigned_tx.txid(); + let (request, context) = + payjoin_sender.extract_request_data(payjoin_uri, original_psbt.clone())?; + + let time = std::time::Instant::now(); + let runtime = rt_lock.as_ref().unwrap(); + runtime.spawn(async move { + let response = payjoin_sender.poll(&request, time).await; + if let Some(response) = response { + let psbt = context.process_response(&mut response.as_slice()); + match psbt { + Ok(Some(psbt)) => { + let finalized = + payjoin_sender.finalise_payjoin_tx(psbt, original_psbt.clone()); + if let Ok(txid) = finalized { + let txid: bitcoin::Txid = txid.into(); + return Some(txid); + } + }, + _ => return None, + } + } + None + }); + Ok(Some(txid)) } /// Creates a payjoin URI with the given amount that can be used to request a payjoin /// transaction. pub fn request_payjoin_transaction(&self, amount_sats: u64) -> Result { - let payjoin_receiver = self.payjoin_receiver.as_ref().ok_or(Error::PayjoinReceiver)?; - payjoin_receiver.payjoin_uri(bitcoin::Amount::from_sat(amount_sats)) + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let payjoin_receiver = match self.payjoin_receiver.as_ref() { + Some(pr) => pr, + None => { + dbg!("unable to get payjoin receiver"); + return Err(Error::PayjoinReceiver); + }, + }; + let payjoin_uri = payjoin_receiver.payjoin_uri(bitcoin::Amount::from_sat(amount_sats)); + payjoin_uri } /// Requests a payjoin transaction with the given amount and a corresponding lightning channel @@ -791,7 +874,8 @@ impl Node { } let runtime = rt_lock.as_ref().unwrap(); let user_channel_id: u128 = rand::thread_rng().gen::(); - let payjoin_receiver = self.payjoin_receiver.as_ref().ok_or(Error::PayjoinReceiver)?; + let payjoin_receiver = + self.payjoin_lightning_receiver.as_ref().ok_or(Error::PayjoinReceiver)?; payjoin_receiver.schedule_channel( bitcoin::Amount::from_sat(channel_amount_sats), node_id, @@ -815,23 +899,26 @@ impl Node { Some(user_config), runtime, )?; - let payjoin_uri = self - .payjoin_receiver - .as_ref() - .ok_or(Error::PayjoinReceiver)? - .payjoin_uri(bitcoin::Amount::from_sat(channel_amount_sats))?; + let payjoin_uri = + payjoin_receiver.payjoin_uri(bitcoin::Amount::from_sat(channel_amount_sats))?; Ok(payjoin_uri) } /// List all scheduled payjoin channels. - pub fn list_scheduled_payjoin_channels(&self) -> Result, Error> { - if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + pub fn list_scheduled_payjoin_channels(&self) -> Result, Error> { + if let Some(payjoin_receiver) = self.payjoin_lightning_receiver.clone() { Ok(payjoin_receiver.list_scheduled_channels()) } else { Ok(Vec::new()) } } + /// List all scheduled payjoin channels. + pub fn list_transactions(&self) -> Result, Error> { + let transactions = self.wallet.list_transactions()?; + Ok(transactions) + } + /// Disconnects all peers, stops all running background tasks, and shuts down [`Node`]. /// /// After this returns most API methods will return [`Error::NotRunning`]. diff --git a/src/payjoin_handler.rs b/src/payjoin_handler.rs deleted file mode 100644 index b1d525572..000000000 --- a/src/payjoin_handler.rs +++ /dev/null @@ -1,542 +0,0 @@ -use crate::channel_scheduler::ChannelScheduler; -use crate::error::Error; -use crate::types::{ChannelManager, Wallet}; -use crate::ScheduledChannel; -use bdk::SignOptions; -use bitcoin::address::NetworkChecked; -use bitcoin::psbt::{Input, PartiallySignedTransaction, Psbt}; -use bitcoin::secp256k1::PublicKey; -use bitcoin::{ScriptBuf, Txid}; -use lightning::ln::ChannelId; -use lightning::util::logger::Logger; -use lightning::{log_error, log_info}; -use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; -use payjoin::send::RequestContext; -use payjoin::{OhttpKeys, PjUriBuilder}; -use payjoin::{PjUri, Url}; -use reqwest::header::HeaderMap; -use reqwest::header::HeaderValue; -use std::ops::Deref; -use std::sync::Arc; -use std::time::Instant; -use tokio::sync::Mutex; -use tokio::time::sleep; - -// Payjoin receiver is a node that can receive payjoin requests. -// -// In order to setup a payjoin receiver, you need to enroll in the payjoin directory and receive -// ohttp keys. You can enroll using `PayjoinReceiverSetup::enroll` function. -// -// The payjoin receiver can then process payjoin requests and respond to them. -pub(crate) struct PayjoinReceiver -where - L::Target: Logger, -{ - logger: L, - scheduler: Arc>, - wallet: Arc, - channel_manager: Arc, - enrolled: Enrolled, - ohttp_keys: OhttpKeys, -} - -impl PayjoinReceiver -where - L::Target: Logger, -{ - pub(crate) fn enroll( - ohttp_keys: &Option, payjoin_directory: &Url, payjoin_relay: &Url, - scheduler: Arc>, wallet: Arc, - channel_manager: Arc, logger: L, - ) -> Result, Error> { - let ohttp_keys = match ohttp_keys { - Some(ohttp_keys) => ohttp_keys.clone(), - None => { - let payjoin_directory = - payjoin_directory.join("/ohttp-keys").map_err(|_| Error::PayjoinEnrollment)?; - let res = BlockingHttpClient::new_proxy(payjoin_relay) - .and_then(|c| c.get(&payjoin_directory))?; - OhttpKeys::decode(res.as_slice()).map_err(|_| Error::PayjoinEnrollment)? - }, - }; - let enrolled = { - let mut enroller = Enroller::from_directory_config( - payjoin_directory.clone(), - ohttp_keys.clone(), - payjoin_relay.clone(), - ); - let (req, ctx) = enroller.extract_req()?; - let headers = payjoin_receiver_request_headers(); - let response = BlockingHttpClient::new() - .and_then(|c| c.post(&req.url.to_string(), req.body, headers, None))?; - let enrolled = enroller.process_res(response.as_slice(), ctx)?; - enrolled - }; - Ok(PayjoinReceiver { logger, scheduler, wallet, channel_manager, enrolled, ohttp_keys }) - } - - async fn post_request(url: &payjoin::Url, body: Vec) -> Result, Error> { - let headers = payjoin_receiver_request_headers(); - let client = HttpClient::new()?; - let response = client.post(url, body, headers.clone()).await?; - Ok(response) - } - - async fn fetch_payjoin_request( - &self, - ) -> Result, Error> { - let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1); - let (req, context) = self.enrolled.clone().extract_req().unwrap(); - let payjoin_request = Self::post_request(&req.url, req.body).await?; - let unchecked_proposal = self.enrolled.process_res(payjoin_request.as_slice(), context)?; - match unchecked_proposal { - None => return Ok(None), - Some(unchecked_proposal) => { - let (provisional_proposal, amount_to_us) = - match self.validate_payjoin_request(unchecked_proposal, min_fee_rate).await { - Ok(proposal) => proposal, - Err(_e) => { - return Ok(None); - }, - }; - Ok(Some((provisional_proposal, amount_to_us))) - }, - } - } - - pub(crate) async fn process_payjoin_request(&self) { - let mut scheduler = self.scheduler.lock().await; - if !scheduler.in_progress() { - let (provisional_proposal, amount_to_us) = match self.fetch_payjoin_request().await { - Ok(Some(proposal)) => proposal, - _ => { - return; - }, - }; - let scheduled_channel = scheduler.get_next_channel(amount_to_us); - if let Some(channel) = scheduled_channel { - let (channel_id, address, temporary_channel_id, _, counterparty_node_id) = channel; - let mut channel_provisional_proposal = provisional_proposal.clone(); - channel_provisional_proposal.substitute_output_address(address); - let payjoin_proposal = channel_provisional_proposal - .finalize_proposal(|psbt| Ok(psbt.clone()), None) - .and_then(|mut proposal| { - let (receiver_request, _) = proposal.extract_v2_req().unwrap(); - let tx = proposal.psbt().clone().extract_tx(); - Ok((receiver_request, tx)) - }); - if let Ok(payjoin_proposal) = payjoin_proposal { - if let (true, Ok(())) = ( - scheduler.set_funding_tx_created( - channel_id, - &payjoin_proposal.0.url, - payjoin_proposal.0.body, - ), - self.channel_manager.unsafe_funding_transaction_generated( - &ChannelId::from_bytes(temporary_channel_id), - &counterparty_node_id, - payjoin_proposal.1, - ), - ) { - return; - } - } - } - self.accept_normal_payjoin_request(provisional_proposal).await; - } - } - - async fn accept_normal_payjoin_request(&self, provisional_proposal: ProvisionalProposal) { - let mut finalized_proposal = - match provisional_proposal.finalize_proposal(|psbt| Ok(psbt.clone()), None) { - Ok(proposal) => proposal, - Err(e) => { - log_error!(self.logger, "Payjoin Receiver: {}", e); - return; - }, - }; - let (receiver_request, _) = match finalized_proposal.extract_v2_req() { - Ok(req) => req, - Err(e) => { - log_error!(self.logger, "Payjoin Receiver: {}", e); - return; - }, - }; - match Self::post_request(&receiver_request.url, receiver_request.body).await { - Ok(_response) => { - log_info!(self.logger, "Payjoin Receiver: Payjoin request sent to sender"); - }, - Err(e) => { - log_error!(self.logger, "Payjoin Receiver: {}", e); - }, - } - } - - pub(crate) fn payjoin_uri(&self, amount: bitcoin::Amount) -> Result { - let address = self.wallet.get_new_address()?; - let pj_part = - Url::parse(&self.enrolled.fallback_target()).map_err(|_| Error::PayjoinUri)?; - let payjoin_uri = PjUriBuilder::new(address, pj_part, Some(self.ohttp_keys.clone())) - .amount(amount) - .build(); - Ok(payjoin_uri) - } - - pub(crate) fn schedule_channel( - &self, amount: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128, - ) { - let channel = ScheduledChannel::new(amount, counterparty_node_id, channel_id); - self.scheduler.blocking_lock().schedule( - channel.channel_value_satoshi(), - channel.counterparty_node_id(), - channel.channel_id(), - ); - } - - pub(crate) fn list_scheduled_channels(&self) -> Vec { - self.scheduler.blocking_lock().list_channels().clone() - } - - pub(crate) async fn set_channel_accepted( - &self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], - ) -> bool { - let mut scheduler = self.scheduler.lock().await; - scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id) - } - - async fn validate_payjoin_request( - &self, proposal: UncheckedProposal, min_fee_rate: Option, - ) -> Result<(ProvisionalProposal, bitcoin::Amount), Error> { - let tx = proposal.extract_tx_to_schedule_broadcast(); - let verified = self.wallet.verify_tx(&tx).await; - let amount_to_us = self.wallet.funds_directed_to_us(&tx).unwrap_or_default(); - let proposal = - proposal.check_broadcast_suitability(min_fee_rate, |_t| Ok(verified.is_ok()))?; - let proposal = proposal.check_inputs_not_owned(|script| { - Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) - })?; - let proposal = match proposal.check_no_mixed_input_scripts() { - Ok(proposal) => proposal, - Err(_) => { - log_error!(self.logger, "Payjoin Receiver: Mixed input scripts"); - return Err(Error::PayjoinReceiver); - }, - }; - let proposal = proposal.check_no_inputs_seen_before(|_outpoint| Ok(false))?; - let original_proposal = proposal.clone().identify_receiver_outputs(|script| { - Ok(self.wallet.is_mine(&script.to_owned()).unwrap_or(false)) - })?; - Ok((original_proposal, amount_to_us)) - } -} -pub fn payjoin_receiver_request_headers() -> HeaderMap { - let mut headers = HeaderMap::new(); - let header_value = HeaderValue::from_static("message/ohttp-req"); - headers.insert(reqwest::header::CONTENT_TYPE, header_value); - headers -} - -pub(crate) struct PayjoinSender -where - L::Target: Logger, -{ - logger: L, - wallet: Arc, - payjoin_relay: Url, - payjoin_directory: Url, - pending_requests: Mutex>, -} - -impl PayjoinSender -where - L::Target: Logger, -{ - pub(crate) fn new( - logger: L, wallet: Arc, payjoin_relay: &Url, payjoin_directory: &Url, - ) -> Self { - Self { - logger, - wallet, - payjoin_relay: payjoin_relay.clone(), - payjoin_directory: payjoin_directory.clone(), - pending_requests: Mutex::new(Vec::new()), - } - } - - // Create a payjoin request based on the payjoin URI parameters. This function builds a PSBT - // based on the amount and receiver address extracted from the payjoin URI, that can be used to - // send a payjoin request to the receiver using `PayjoinSender::send_payjoin_request`. - pub(crate) fn create_payjoin_request( - &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, - ) -> Result { - // Extract amount and receiver address from URI - let amount_to_send = match payjoin_uri.amount { - Some(amount) => amount, - None => { - log_error!(self.logger, "Payjoin Sender: send: No amount found in URI"); - return Err(Error::PayjoinSender); - }, - }; - let receiver_address = payjoin_uri.address.clone().script_pubkey(); - let mut sign_options = SignOptions::default(); - sign_options.trust_witness_utxo = true; - let original_psbt = self - .wallet - .build_transaction(receiver_address, amount_to_send.to_sat(), sign_options) - .unwrap(); - Ok(original_psbt) - } - - // Send payjoin transaction based on the payjoin URI parameters. - // - // This function sends the payjoin request to the receiver and saves the context and request in - // the pending_requests field to process the response async. - pub(crate) fn send_payjoin_request( - &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, original_psbt: Psbt, - ) -> Result, Error> { - let mut request_context = - payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) - .and_then(|b| b.build_non_incentivizing()) - .map_err(|e| { - log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); - Error::PayjoinSender - })?; - let (sender_request, sender_ctx) = - request_context.extract_v2(self.payjoin_directory.clone()).map_err(|e| { - log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); - Error::PayjoinSender - })?; - let (body, url) = (sender_request.body.clone(), sender_request.url.to_string()); - log_info!(self.logger, "Payjoin Sender: send: sending payjoin request to: {}", url); - let mut headers = HeaderMap::new(); - headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); - let response = BlockingHttpClient::new_proxy(&self.payjoin_relay) - .and_then(|c| c.post(&url, body, headers, Some(std::time::Duration::from_secs(5)))); - if let Ok(response) = response { - let psbt = match sender_ctx.process_response(&mut response.as_slice()) { - Ok(Some(psbt)) => psbt, - _ => { - log_info!( - self.logger, - "Payjoin Sender: No payjoin response found yet. Setting request as pending." - ); - self.queue_request(request_context, original_psbt); - return Ok(None); - }, - }; - return self.finalise_payjoin_tx(psbt, original_psbt.clone()); - } else { - self.queue_request(request_context, original_psbt); - return Ok(None); - } - } - - // Process the payjoin response from the receiver. - // - // After sending the payjoin request to the receiver, we will process the response from the - // receiver and finalise the payjoin transaction. - // - // Because the response from the receiver is asynchronous, this function first checks if there - // is a pending request in the pending_requests field. If there is a pending request, it will - // check if a response was received in the directory and process it accordingly. A successful - // responsonse from the directory but with no payjoin proposal will return Ok(None). If a - // payjoin proposal is found, we will attempt to finalise the payjoin transaction and broadcast - // it. - pub(crate) async fn process_payjoin_response(&self) { - let mut pending_requests = self.pending_requests.lock().await; - let (mut request_context, original_psbt) = match pending_requests.pop() { - Some(request_context) => request_context, - None => { - log_info!(self.logger, "Payjoin Sender: No pending request found. "); - return; - }, - }; - let now = std::time::Instant::now(); - let (psbt, original_psbt) = match self.poll(&mut request_context, original_psbt, now).await - { - Some((psbt, original_psbt)) => (psbt, original_psbt), - None => { - return; - }, - }; - match self.finalise_payjoin_tx(psbt.clone(), original_psbt.clone()) { - Ok(Some(txid)) => { - log_info!(self.logger, "Payjoin Sender: Payjoin transaction broadcasted: {}", txid); - }, - Ok(None) => { - log_info!( - self.logger, - "Payjoin Sender: Was not able to finalise payjoin transaction {}.", - psbt.extract_tx().txid() - ); - }, - Err(e) => { - log_error!( - self.logger, - "Payjoin Sender: Error finalising payjoin transaction: {}", - e - ); - }, - } - } - - async fn poll( - &self, request_context: &mut RequestContext, original_psbt: Psbt, time: Instant, - ) -> Option<(Psbt, Psbt)> { - let duration = std::time::Duration::from_secs(180); - loop { - if time.elapsed() > duration { - log_info!(self.logger, "Payjoin Sender: Polling timed out"); - return None; - } - let (req, ctx) = match request_context.extract_v2(self.payjoin_directory.clone()) { - Ok(req) => req, - Err(e) => { - log_error!(self.logger, "Payjoin Sender: Error extracting v2 request: {}", e); - sleep(std::time::Duration::from_secs(5)).await; - return None; - }, - }; - let mut headers = HeaderMap::new(); - headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); - let client = HttpClient::new().ok()?; - let response = client.post(&req.url, req.body, headers.clone()).await.ok()?; - let psbt = match ctx.process_response(&mut response.as_slice()) { - Ok(Some(psbt)) => psbt, - Ok(None) => { - log_info!(self.logger, "Payjoin Sender: No pending payjoin response"); - sleep(std::time::Duration::from_secs(5)).await; - continue; - }, - Err(e) => { - log_error!(self.logger, "Payjoin Sender: malformed payjoin response: {}", e); - sleep(std::time::Duration::from_secs(10)).await; - continue; - }, - }; - return Some((psbt, original_psbt.clone())); - } - } - - // finalise the payjoin transaction and broadcast it - fn finalise_payjoin_tx( - &self, mut psbt: Psbt, mut ocean_psbt: Psbt, - ) -> Result, Error> { - // for BDK, we need to reintroduce utxo from original psbt. - // Otherwise we wont be able to sign the transaction. - fn input_pairs( - psbt: &mut PartiallySignedTransaction, - ) -> Box + '_> { - Box::new(psbt.unsigned_tx.input.iter().zip(&mut psbt.inputs)) - } - - // get original inputs from original psbt clone (ocean_psbt) - let mut original_inputs = input_pairs(&mut ocean_psbt).peekable(); - for (proposed_txin, proposed_psbtin) in input_pairs(&mut 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(); - } - } - } - - let mut sign_options = SignOptions::default(); - sign_options.trust_witness_utxo = true; - sign_options.try_finalize = true; - let (_is_signed, psbt) = self.wallet.sign_transaction(&psbt, sign_options)?; - let tx = psbt.extract_tx(); - self.wallet.broadcast_transaction(&tx); - let txid = tx.txid(); - Ok(Some(txid)) - } - - fn queue_request(&self, request_context: RequestContext, original_psbt: Psbt) { - log_info!(&self.logger, "Payjoin Sender: saving pending request for txid"); - self.pending_requests.blocking_lock().push((request_context, original_psbt)); - } -} - -pub struct HttpClient { - client: reqwest::Client, -} - -impl HttpClient { - fn new() -> Result { - let client = reqwest::Client::builder().build().map_err(|_| Error::PayjoinReqwest)?; - Ok(Self { client }) - } - - async fn post( - &self, url: &Url, body: Vec, headers: HeaderMap, - ) -> Result, Error> { - Ok(self - .client - .post(url.to_string()) - .headers(headers) - .body(body) - .send() - .await - .and_then(|response| response.error_for_status()) - .map_err(|_| Error::PayjoinReqwest)? - .bytes() - .await - .map_err(|_| Error::PayjoinReqwest)? - .to_vec()) - } -} - -struct BlockingHttpClient { - client: reqwest::blocking::Client, -} - -impl BlockingHttpClient { - fn new() -> Result { - let client = - reqwest::blocking::Client::builder().build().map_err(|_| Error::PayjoinReqwest)?; - Ok(Self { client }) - } - - fn new_proxy(payjoin_relay: &Url) -> Result { - let proxy = - reqwest::Proxy::all(payjoin_relay.to_string()).map_err(|_| Error::PayjoinReqwest)?; - let client = reqwest::blocking::Client::builder() - .proxy(proxy) - .build() - .map_err(|_| Error::PayjoinReqwest)?; - Ok(Self { client }) - } - - fn get(&self, url: &Url) -> Result, Error> { - Ok(self - .client - .get(url.to_string()) - .send() - .and_then(|response| response.error_for_status()) - .map_err(|_| Error::PayjoinReqwest)? - .bytes() - .map_err(|_| Error::PayjoinReqwest)? - .to_vec()) - } - - fn post( - &self, url: &str, body: Vec, headers: HeaderMap, - timeout: Option, - ) -> Result, Error> { - Ok(self - .client - .post(url) - .headers(headers) - .body(body) - .timeout(timeout.unwrap_or(std::time::Duration::from_secs(15))) - .send() - .and_then(|response| response.error_for_status()) - .map_err(|_| Error::PayjoinReqwest)? - .bytes() - .map_err(|_| Error::PayjoinReqwest)? - .to_vec()) - } -} - -// https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist diff --git a/src/payjoin_sender.rs b/src/payjoin_sender.rs index 897ceb9b0..8cb87bfae 100644 --- a/src/payjoin_sender.rs +++ b/src/payjoin_sender.rs @@ -1,22 +1,22 @@ +/// An implementation of payjoin v2 sender as described in BIP-77. use bdk::SignOptions; use bitcoin::address::NetworkChecked; use bitcoin::psbt::{Input, PartiallySignedTransaction, Psbt}; use bitcoin::Txid; use lightning::util::logger::Logger; use lightning::{log_error, log_info}; -use payjoin::send::RequestContext; +use payjoin::send::ContextV2; use payjoin::Url; -use reqwest::header::HeaderMap; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::StatusCode; use std::ops::Deref; use std::sync::Arc; use std::time::Instant; -use tokio::sync::Mutex; use tokio::time::sleep; use crate::error::Error; use crate::types::Wallet; -/// Payjoin Sender pub(crate) struct PayjoinSender where L::Target: Logger, @@ -24,7 +24,6 @@ where logger: L, wallet: Arc, payjoin_relay: Url, - pending_requests: Mutex>, } impl PayjoinSender @@ -32,210 +31,131 @@ where L::Target: Logger, { pub(crate) fn new(logger: L, wallet: Arc, payjoin_relay: &Url) -> Self { - Self { - logger, - wallet, - payjoin_relay: payjoin_relay.clone(), - pending_requests: Mutex::new(Vec::new()), - } + Self { logger, wallet, payjoin_relay: payjoin_relay.clone() } } - // Create a payjoin request based on the payjoin URI parameters. This function builds a PSBT + // Create payjoin request based on the payjoin URI parameters. This function builds a PSBT // based on the amount and receiver address extracted from the payjoin URI, that can be used to // send a payjoin request to the receiver using `PayjoinSender::send_payjoin_request`. pub(crate) fn create_payjoin_request( - &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, + &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, amount: Option, ) -> Result { - // Extract amount and receiver address from URI - let amount_to_send = match payjoin_uri.amount { - Some(amount) => amount, - None => { - log_error!(self.logger, "Payjoin Sender: send: No amount found in URI"); - return Err(Error::PayjoinSender); - }, + let amount_to_send = match (amount, payjoin_uri.amount) { + (Some(amount), _) => amount, + (None, Some(amount)) => amount, + (None, None) => return Err(Error::PayjoinSender), }; let receiver_address = payjoin_uri.address.clone().script_pubkey(); let mut sign_options = SignOptions::default(); sign_options.trust_witness_utxo = true; - let original_psbt = self - .wallet - .build_transaction(receiver_address, amount_to_send.to_sat(), sign_options) - .unwrap(); + let original_psbt = self.wallet.build_transaction( + receiver_address, + amount_to_send.to_sat(), + sign_options, + )?; Ok(original_psbt) } - // Send payjoin transaction based on the payjoin URI parameters. - // - // This function sends the payjoin request to the receiver and saves the context and request in - // the pending_requests field to process the response async. - pub(crate) fn send_payjoin_request( + pub(crate) fn extract_request_data( &self, payjoin_uri: payjoin::Uri<'static, NetworkChecked>, original_psbt: Psbt, - ) -> Result, Error> { - dbg!("payjoin_uri: {}", payjoin_uri.to_string()); + ) -> Result<(payjoin::send::Request, ContextV2), Error> { let mut request_context = payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) .and_then(|b| b.build_non_incentivizing()) .map_err(|e| { - dbg!("Error building requestaaa"); - log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); + dbg!(&e); + log_error!( + self.logger, + "Payjoin Sender: send: Error building payjoin request {}", + e + ); Error::PayjoinSender })?; let (sender_request, sender_ctx) = request_context.extract_v2(self.payjoin_relay.clone()).map_err(|e| { - dbg!("Error extracting v2 request"); - log_error!(self.logger, "Payjoin Sender: send: Error building request: {}", e); - Error::PayjoinSender - })?; - let (body, url) = (sender_request.body.clone(), sender_request.url.to_string()); - dbg!("sending payjoin request to: {}", &url); - log_info!(self.logger, "Payjoin Sender: send: sending payjoin request to: {}", url); - let mut headers = HeaderMap::new(); - headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); - dbg!("making request {}", url.clone()); - let client = reqwest::blocking::Client::new(); - let response = client.post(&url).body(body).headers(headers).send(); - if let Ok(response) = response { - if response.status().is_success() { - let response = match response.bytes() { - Ok(response) => response.to_vec(), - Err(e) => { - dbg!("hereeea {} ", e); - return Ok(Some(original_psbt.extract_tx().txid())); - }, - }; - let psbt = match sender_ctx.process_response(&mut response.as_slice()) { - Ok(Some(psbt)) => psbt, - _ => { - dbg!("No payjoin response found yet22"); - log_info!( - self.logger, - "Payjoin Sender: No payjoin response found yet. Setting request as pending." - ); - self.queue_request(request_context, original_psbt.clone()); - return Ok(Some(original_psbt.extract_tx().txid())); - }, - }; - return self.finalise_payjoin_tx(psbt, original_psbt.clone()); - } - }; - self.queue_request(request_context, original_psbt.clone()); - return Ok(Some(original_psbt.extract_tx().txid())); - } - - pub(crate) async fn process_payjoin_response(&self) { - let mut pending_requests = self.pending_requests.lock().await; - let (mut request_context, original_psbt) = match pending_requests.pop() { - Some(request_context) => request_context, - None => { - log_info!(self.logger, "Payjoin Sender: No pending request found. "); - return; - }, - }; - let now = std::time::Instant::now(); - let (psbt, original_psbt) = match self.poll(&mut request_context, original_psbt, now).await - { - Some((psbt, original_psbt)) => (psbt, original_psbt), - None => { - return; - }, - }; - match self.finalise_payjoin_tx(psbt.clone(), original_psbt.clone()) { - Ok(Some(txid)) => { - log_info!(self.logger, "Payjoin Sender: Payjoin transaction broadcasted: {}", txid); - }, - Ok(None) => { - log_info!( - self.logger, - "Payjoin Sender: Was not able to finalise payjoin transaction {}.", - psbt.extract_tx().txid() - ); - }, - Err(e) => { + dbg!(&e); log_error!( self.logger, - "Payjoin Sender: Error finalising payjoin transaction: {}", + "Payjoin Sender: send: Error extracting payjoin request: {}", e ); - }, - } + Error::PayjoinSender + })?; + Ok((sender_request, sender_ctx)) } - async fn poll( - &self, request_context: &mut RequestContext, original_psbt: Psbt, time: Instant, - ) -> Option<(Psbt, Psbt)> { - let duration = std::time::Duration::from_secs(360); + pub(crate) async fn poll( + &self, request: &payjoin::send::Request, time: Instant, + ) -> Option> { + let duration = std::time::Duration::from_secs(3600); + let sleep = || sleep(std::time::Duration::from_secs(10)); loop { if time.elapsed() > duration { log_info!(self.logger, "Payjoin Sender: Polling timed out"); return None; } + let client = reqwest::Client::new(); - let payjoin_directory = payjoin::Url::parse("https://payjo.in").unwrap(); - let (req, ctx) = match request_context.extract_v2(payjoin_directory.clone()) { - Ok(req) => req, + let response = match client + .post(request.url.clone()) + .body(request.body.clone()) + .headers(ohttp_req_header()) + .send() + .await + { + Ok(response) => response, Err(e) => { - log_error!(self.logger, "Payjoin Sender: Error extracting v2 request: {}", e); - sleep(std::time::Duration::from_secs(3)).await; + log_info!(self.logger, "Payjoin Sender: Error polling request: {}", e); + sleep().await; + continue; + }, + }; + let response = match response.error_for_status() { + Ok(response) => response, + Err(e) => { + log_info!(self.logger, "Payjoin Sender: Status Error polling request: {}", e); + sleep().await; continue; }, }; - let mut headers = HeaderMap::new(); - headers.insert(reqwest::header::CONTENT_TYPE, "text/plain".parse().unwrap()); - let client = reqwest::Client::new(); - let response = - match client.post(req.url).body(req.body).headers(headers.clone()).send().await { - Ok(response) => response, + if response.status() == StatusCode::OK { + let response = match response.bytes().await { + Ok(response) => response.to_vec(), Err(e) => { - log_error!(self.logger, "Payjoin Sender: Error sending request: {}", e); - sleep(std::time::Duration::from_secs(3)).await; + log_info!( + self.logger, + "Payjoin Sender: Error reading polling response: {}", + e + ); + sleep().await; continue; }, }; - let response = match response.bytes().await { - Ok(response) => response.to_vec(), - Err(e) => { - log_error!(self.logger, "Payjoin Sender: Error reading response: {}", e); - sleep(std::time::Duration::from_secs(3)).await; + if response.is_empty() { + log_info!(self.logger, "Payjoin Sender: Got empty response while polling"); + sleep().await; continue; - }, - }; - if response.is_empty() { - dbg!("No payjoin response found yet1"); - log_info!(self.logger, "Payjoin Sender: No payjoin response found yet"); - sleep(std::time::Duration::from_secs(3)).await; - continue; - } - dbg!("response.len()", response.len()); - if response.len() == 54 { - dbg!("No payjoin response found yet2"); - log_error!(self.logger, "Payjoin Sender: malformed payjoin response"); - sleep(std::time::Duration::from_secs(3)).await; + } + return Some(response); + } else { + log_info!( + self.logger, + "Payjoin Sender: Error sending request, got status code + {}", + response.status() + ); + sleep().await; continue; } - let psbt = match ctx.process_response(&mut response.as_slice()) { - Ok(Some(psbt)) => psbt, - Ok(None) => { - dbg!("No payjoin response found yet3"); - log_info!(self.logger, "Payjoin Sender: No pending payjoin response"); - sleep(std::time::Duration::from_secs(3)).await; - continue; - }, - Err(e) => { - dbg!("No payjoin response found yet4"); - log_error!(self.logger, "Payjoin Sender: malformed payjoin response: {}", e); - sleep(std::time::Duration::from_secs(3)).await; - continue; - }, - }; - return Some((psbt, original_psbt.clone())); } } // finalise the payjoin transaction and broadcast it - fn finalise_payjoin_tx( - &self, mut psbt: Psbt, mut ocean_psbt: Psbt, - ) -> Result, Error> { + pub(crate) fn finalise_payjoin_tx( + &self, mut psbt: Psbt, ocean_psbt: Psbt, + ) -> Result { + let mut ocean_psbt = ocean_psbt.clone(); // for BDK, we need to reintroduce utxo from original psbt. // Otherwise we wont be able to sign the transaction. fn input_pairs( @@ -263,11 +183,12 @@ where let tx = psbt.extract_tx(); self.wallet.broadcast_transaction(&tx); let txid = tx.txid(); - Ok(Some(txid)) + Ok(txid) } +} - fn queue_request(&self, request_context: RequestContext, original_psbt: Psbt) { - log_info!(&self.logger, "Payjoin Sender: saving pending request for txid"); - self.pending_requests.blocking_lock().push((request_context, original_psbt)); - } +fn ohttp_req_header() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, HeaderValue::from_static("message/ohttp-req")); + headers } diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 8f7af9eb0..559044f9e 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -1,6 +1,6 @@ -use crate::channel_scheduler::ChannelScheduler; use crate::logger::{log_bytes, log_debug, log_error, log_trace, Logger}; -use crate::payjoin_handler::payjoin_receiver_request_headers; +use crate::payjoin_receiver::payjoin_receiver_request_headers; +use crate::payjoin_scheduler::PayjoinScheduler; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::log_info; @@ -27,7 +27,7 @@ where queue_receiver: Mutex>>, esplora_client: EsploraClient, logger: L, - channel_scheduler: Arc>, + payjoin_scheduler: Arc>, } impl TransactionBroadcaster @@ -35,7 +35,7 @@ where L::Target: Logger, { pub(crate) fn new( - esplora_client: EsploraClient, logger: L, channel_scheduler: Arc>, + esplora_client: EsploraClient, logger: L, payjoin_scheduler: Arc>, ) -> Self { let (queue_sender, queue_receiver) = mpsc::channel(BCAST_PACKAGE_QUEUE_SIZE); Self { @@ -43,7 +43,7 @@ where queue_receiver: Mutex::new(queue_receiver), esplora_client, logger, - channel_scheduler, + payjoin_scheduler, } } @@ -59,7 +59,7 @@ where ); dbg!("Skipping broadcast of transaction {} with empty witness, checking for payjoin.", tx.txid()); let is_payjoin_channel = - self.channel_scheduler.lock().await.set_funding_tx_signed(tx.clone()); + self.payjoin_scheduler.lock().await.set_funding_tx_signed(tx.clone()); if let Some((url, body)) = is_payjoin_channel { log_info!( self.logger, diff --git a/src/wallet.rs b/src/wallet.rs index 91ef255bc..19ecff796 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -17,7 +17,7 @@ use lightning::util::message_signing; use bdk::blockchain::EsploraBlockchain; use bdk::database::BatchDatabase; use bdk::wallet::AddressIndex; -use bdk::FeeRate; +use bdk::{FeeRate, TransactionDetails}; use bdk::{SignOptions, SyncOptions}; use bitcoin::bech32::u5; @@ -160,6 +160,11 @@ where Ok(locked_wallet.is_mine(script)?) } + pub(crate) fn list_transactions(&self) -> Result, Error> { + let locked_wallet = self.inner.lock().unwrap(); + Ok(locked_wallet.list_transactions(false)?) + } + /// Verifies that the given transaction meets the bitcoin consensus rules. pub async fn verify_tx(&self, tx: &Transaction) -> Result<(), Error> { let serialized_tx = bitcoin::consensus::serialize(&tx); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 02f8074c3..7c3fdb8a4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -196,32 +196,74 @@ macro_rules! setup_builder { pub(crate) use setup_builder; -pub(crate) fn setup_two_nodes( - electrsd: &ElectrsD, allow_0conf: bool, allow_payjoin: bool, -) -> (TestNode, TestNode) { +pub(crate) fn setup_two_nodes(electrsd: &ElectrsD, allow_0conf: bool) -> (TestNode, TestNode) { println!("== Node A =="); let config_a = random_config(); - let node_a = setup_node(electrsd, config_a, allow_payjoin); + let node_a = setup_node(electrsd, config_a); println!("\n== Node B =="); let mut config_b = random_config(); if allow_0conf { config_b.trusted_peers_0conf.push(node_a.node_id()); } - let node_b = setup_node(electrsd, config_b, allow_payjoin); + let node_b = setup_node(electrsd, config_b); (node_a, node_b) } -pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config, allow_payjoin: bool) -> TestNode { +pub(crate) fn setup_two_payjoin_nodes( + electrsd: &ElectrsD, allow_0conf: bool, +) -> (TestNode, TestNode) { + println!("== Node A =="); + let config_a = random_config(); + let node_a_payjoin_receiver = setup_payjoin_receiver_node(electrsd, config_a); + + println!("\n== Node B =="); + let mut config_b = random_config(); + if allow_0conf { + config_b.trusted_peers_0conf.push(node_a_payjoin_receiver.node_id()); + } + let node_b_payjoin_sender = setup_payjoin_sender_node(electrsd, config_b); + (node_a_payjoin_receiver, node_b_payjoin_sender) +} + +pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); setup_builder!(builder, config); builder.set_esplora_server(esplora_url.clone()); - // enable payjoin - if allow_payjoin { - let payjoin_directory = payjoin::Url::parse("https://payjo.in").unwrap(); - let payjoin_relay = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); - builder.set_payjoin_config(payjoin_directory, payjoin_relay, None); - } + let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); + let node = builder.build_with_store(test_sync_store).unwrap(); + node.start().unwrap(); + assert!(node.status().is_running); + assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); + node +} + +pub(crate) fn setup_payjoin_sender_node(electrsd: &ElectrsD, config: Config) -> TestNode { + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + setup_builder!(builder, config); + builder.set_esplora_server(esplora_url.clone()); + let payjoin_relay = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); + builder.set_payjoin_sender_config(payjoin_relay); + let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); + let node = builder.build_with_store(test_sync_store).unwrap(); + node.start().unwrap(); + assert!(node.status().is_running); + assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); + node +} + +pub(crate) fn setup_payjoin_receiver_node(electrsd: &ElectrsD, config: Config) -> TestNode { + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + setup_builder!(builder, config); + builder.set_esplora_server(esplora_url.clone()); + let payjoin_directory = payjoin::Url::parse("https://payjo.in").unwrap(); + let payjoin_relay = payjoin::Url::parse("https://pj.bobspacebkk.com").unwrap(); + let payjoin_ohttp_keys = [ + 1, 0, 32, 221, 207, 106, 162, 243, 25, 188, 252, 203, 135, 197, 199, 128, 63, 42, 243, 165, + 134, 237, 41, 143, 66, 243, 218, 152, 36, 239, 18, 139, 158, 40, 27, 0, 4, 0, 1, 0, 3, + ]; + let payjoin_ohttp_keys = payjoin::OhttpKeys::decode(payjoin_ohttp_keys.as_slice()).unwrap(); + builder.set_payjoin_receiver_config(payjoin_directory, payjoin_relay, Some(payjoin_ohttp_keys)); let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); let node = builder.build_with_store(test_sync_store).unwrap(); node.start().unwrap(); diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs index 9d8a3aa6e..51b9bc081 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -25,9 +25,15 @@ fn send_receive_regular_payjoin_transaction() { assert_eq!(node_a_pj_receiver.next_event(), None); assert_eq!(node_a_pj_receiver.list_channels().len(), 0); let payjoin_uri = node_a_pj_receiver.request_payjoin_transaction(80_000).unwrap(); - let txid = node_b_pj_sender.send_payjoin_transaction( - payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked(), - ); + let txid = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { + let txid = node_b_pj_sender + .send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked(), + None, + ) + .await; + txid + }); dbg!(&txid); wait_for_tx(&electrsd.client, txid.unwrap().unwrap()); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); diff --git a/tests/integration_tests_payjoin_lightning.rs b/tests/integration_tests_payjoin_lightning.rs index 1427e937c..d904a098c 100644 --- a/tests/integration_tests_payjoin_lightning.rs +++ b/tests/integration_tests_payjoin_lightning.rs @@ -46,11 +46,16 @@ fn send_receive_with_channel_opening_payjoin_transaction() { panic!("should generate payjoin uri"); }, }; - assert!(node_b - .send_payjoin_transaction( - payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked() - ) - .is_ok()); + + let _ = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { + let txid = node_b + .send_payjoin_transaction( + payjoin::Uri::try_from(payjoin_uri.to_string()).unwrap().assume_checked(), + None, + ) + .await; + txid + }); expect_channel_pending_event!(node_a, node_b.node_id()); expect_channel_pending_event!(node_b, node_a.node_id()); let channels = node_a.list_channels(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 5539d7191..1820ef76a 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -18,21 +18,21 @@ use std::sync::Arc; #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false); } #[test] fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, true, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, true); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true) } #[test] fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -223,7 +223,7 @@ fn start_stop_reinit() { #[test] fn onchain_spend_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -272,7 +272,7 @@ fn onchain_spend_receive() { fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let config = random_config(); - let node = setup_node(&electrsd, config, false); + let node = setup_node(&electrsd, config); // Tests arbitrary message signing and later verification let msg = "OK computer".as_bytes(); @@ -289,7 +289,7 @@ fn connection_restart_behavior() { fn do_connection_restart_behavior(persist: bool) { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -340,7 +340,7 @@ fn do_connection_restart_behavior(persist: bool) { #[test] fn concurrent_connections_succeed() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b);