From ced7777e9ac93caff4e68404496bbbf182f19b53 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:31:13 +0300 Subject: [PATCH 1/7] Allow to send payjoin transactions Implements the payjoin sender as describe in BIP77. This would allow the on chain wallet linked to LDK node to send payjoin transactions. --- Cargo.toml | 1 + bindings/ldk_node.udl | 16 +++ src/builder.rs | 44 +++++++- src/config.rs | 9 ++ src/error.rs | 25 ++++ src/event.rs | 31 +++++ src/io/utils.rs | 9 ++ src/lib.rs | 47 +++++++- src/payment/mod.rs | 2 + src/payment/payjoin/mod.rs | 220 ++++++++++++++++++++++++++++++++++++ src/payment/payjoin/send.rs | 76 +++++++++++++ src/payment/store.rs | 5 +- src/types.rs | 2 + src/wallet.rs | 60 ++++++++++ 14 files changed, 543 insertions(+), 4 deletions(-) create mode 100644 src/payment/payjoin/mod.rs create mode 100644 src/payment/payjoin/send.rs diff --git a/Cargo.toml b/Cargo.toml index d4a87b2a2..8e7bc921a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr esplora-client = { version = "0.6", default-features = false } libc = "0.2" uniffi = { version = "0.26.0", features = ["build"], optional = true } +payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 2723db573..d17b0cd66 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -63,6 +63,7 @@ interface Node { Bolt12Payment bolt12_payment(); SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); + PayjoinPayment payjoin_payment(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -148,6 +149,13 @@ interface OnchainPayment { Txid send_all_to_address([ByRef]Address address); }; +interface PayjoinPayment { + [Throws=NodeError] + void send(string payjoin_uri); + [Throws=NodeError] + void send_with_amount(string payjoin_uri, u64 amount_sats); +}; + [Error] enum NodeError { "AlreadyRunning", @@ -196,6 +204,11 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "PayjoinUnavailable", + "PayjoinUriInvalid", + "PayjoinRequestMissingAmount", + "PayjoinRequestCreationFailed", + "PayjoinResponseProcessingFailed", }; dictionary NodeStatus { @@ -227,6 +240,7 @@ enum BuildError { "KVStoreSetupFailed", "WalletSetupFailed", "LoggerSetupFailed", + "InvalidPayjoinConfig", }; [Enum] @@ -238,6 +252,8 @@ interface Event { ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason); + PayjoinTxSendSuccess(Txid txid); + PayjoinTxSendFailed(string reason); }; enum PaymentFailureReason { diff --git a/src/builder.rs b/src/builder.rs index a2a93aa79..7d48d835b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,6 +11,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::payment::payjoin::send::PayjoinSender; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -93,6 +94,11 @@ struct LiquiditySourceConfig { lsps2_service: Option<(SocketAddress, PublicKey, Option)>, } +#[derive(Debug, Clone)] +struct PayjoinConfig { + payjoin_relay: payjoin::Url, +} + impl Default for LiquiditySourceConfig { fn default() -> Self { Self { lsps2_service: None } @@ -132,6 +138,8 @@ pub enum BuildError { WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// Invalid Payjoin configuration. + InvalidPayjoinConfig, } impl fmt::Display for BuildError { @@ -152,6 +160,10 @@ impl fmt::Display for BuildError { Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."), Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."), Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."), + Self::InvalidPayjoinConfig => write!( + f, + "Invalid Payjoin configuration. Make sure the provided arguments are valid URLs." + ), } } } @@ -172,6 +184,7 @@ pub struct NodeBuilder { chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, + payjoin_config: Option, } impl NodeBuilder { @@ -187,12 +200,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, } } @@ -247,6 +262,14 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to enable payjoin transactions. + pub fn set_payjoin_config(&mut self, payjoin_relay: String) -> Result<&mut Self, BuildError> { + let payjoin_relay = + payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?; + self.payjoin_config = Some(PayjoinConfig { payjoin_relay }); + Ok(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. @@ -365,6 +388,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.payjoin_config.as_ref(), seed_bytes, logger, vss_store, @@ -386,6 +410,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.payjoin_config.as_ref(), seed_bytes, logger, kv_store, @@ -453,6 +478,11 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_p2p(); } + /// Configures the [`Node`] instance to enable payjoin transactions. + pub fn set_payjoin_config(&self, payjoin_relay: String) -> Result<(), BuildError> { + self.inner.write().unwrap().set_payjoin_config(payjoin_relay).map(|_| ()) + } + /// 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) { @@ -521,8 +551,9 @@ impl ArcedNodeBuilder { 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, + liquidity_source_config: Option<&LiquiditySourceConfig>, + payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc, + kv_store: Arc, ) -> Result { // Initialize the on-chain wallet and chain access let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes) @@ -966,6 +997,14 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let mut payjoin_sender = None; + if let Some(pj_config) = payjoin_config { + payjoin_sender = Some(Arc::new(PayjoinSender::new( + Arc::clone(&logger), + pj_config.payjoin_relay.clone(), + ))); + } + let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); @@ -987,6 +1026,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, + payjoin_sender, peer_manager, connection_manager, keys_manager, diff --git a/src/config.rs b/src/config.rs index d0e72080f..3d4cb6e5e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,6 +40,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6; // The time in-between peer reconnection attempts. pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10); +// The time before payjoin sender requests timeout. +pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +// The time before payjoin sender try to send the next request. +pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3); + +// The total time payjoin sender try to send a request. +pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60); + // The time in-between RGS sync attempts. pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60); diff --git a/src/error.rs b/src/error.rs index a8671d9a7..1ea1b5e64 100644 --- a/src/error.rs +++ b/src/error.rs @@ -95,6 +95,16 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Failed to access Payjoin sender object. + PayjoinUnavailable, + /// Payjoin URI is invalid. + PayjoinUriInvalid, + /// Amount is neither user-provided nor defined in the URI. + PayjoinRequestMissingAmount, + /// Failed to build a Payjoin request. + PayjoinRequestCreationFailed, + /// Payjoin response processing failed. + PayjoinResponseProcessingFailed, } impl fmt::Display for Error { @@ -162,6 +172,21 @@ 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::PayjoinUnavailable => { + write!(f, "Failed to access Payjoin sender object. Make sure you have enabled Payjoin sending support.") + }, + Self::PayjoinRequestMissingAmount => { + write!(f, "Amount is neither user-provided nor defined in the URI.") + }, + Self::PayjoinRequestCreationFailed => { + write!(f, "Failed construct a Payjoin request") + }, + Self::PayjoinUriInvalid => { + write!(f, "The provided Payjoin URI is invalid") + }, + Self::PayjoinResponseProcessingFailed => { + write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored") + }, } } } diff --git a/src/event.rs b/src/event.rs index 838df4230..30051a2b1 100644 --- a/src/event.rs +++ b/src/event.rs @@ -143,6 +143,28 @@ pub enum Event { /// This will be `None` for events serialized by LDK Node v0.2.1 and prior. reason: Option, }, + /// A Payjoin transaction has been successfully sent. + /// + /// This event is emitted when we send a Payjoin transaction and it was accepted by the + /// receiver, and then finalised and broadcasted by us. + PayjoinTxSendSuccess { + /// Transaction ID of the successfully sent Payjoin transaction. + txid: bitcoin::Txid, + }, + /// Failed to send Payjoin transaction. + /// + /// This event is emitted when our attempt to send Payjoin transaction fail. + PayjoinTxSendFailed { + /// Reason for the failure. + reason: String, + }, + /// Failed to send Payjoin transaction. + /// + /// This event is emitted when our attempt to send Payjoin transaction fail. + PayjoinPaymentPending { + /// Transaction ID of the successfully sent Payjoin transaction. + txid: bitcoin::Txid, + }, } impl_writeable_tlv_based_enum!(Event, @@ -184,6 +206,15 @@ impl_writeable_tlv_based_enum!(Event, (2, payment_id, required), (4, claimable_amount_msat, required), (6, claim_deadline, option), + }, + (7, PayjoinTxSendSuccess) => { + (0, txid, required), + }, + (8, PayjoinTxSendFailed) => { + (0, reason, required), + }, + (9, PayjoinPaymentPending) => { + (0, txid, required), }; ); diff --git a/src/io/utils.rs b/src/io/utils.rs index 77cc56f55..d298318f5 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -511,6 +511,15 @@ pub(crate) fn check_namespace_key_validity( Ok(()) } +pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + headers +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index de2a0badf..b689426ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,7 @@ pub use error::Error as NodeError; use error::Error; pub use event::Event; +use payment::payjoin::send::PayjoinSender; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -133,7 +134,10 @@ use gossip::GossipSource; use graph::NetworkGraph; use liquidity::LiquiditySource; use payment::store::PaymentStore; -use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; +use payment::{ + Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails, + SpontaneousPayment, +}; use peer_store::{PeerInfo, PeerStore}; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator, @@ -185,6 +189,7 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, + payjoin_sender: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -1063,6 +1068,46 @@ impl Node { )) } + /// Returns a payment handler allowing to send payjoin payments. + /// + /// In order to utilize the Payjoin functionality, it's necessary + /// to configure your node using [`set_payjoin_config`]. + /// + /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config + #[cfg(not(feature = "uniffi"))] + pub fn payjoin_payment(&self) -> PayjoinPayment { + let payjoin_sender = self.payjoin_sender.as_ref(); + PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_sender.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.event_queue), + Arc::clone(&self.logger), + Arc::clone(&self.wallet), + Arc::clone(&self.tx_broadcaster), + ) + } + + /// Returns a payment handler allowing to send payjoin payments. + /// + /// In order to utilize the Payjoin functionality, it's necessary + /// to configure your node using [`set_payjoin_config`]. + /// + /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config + #[cfg(feature = "uniffi")] + pub fn payjoin_payment(&self) -> Arc { + let payjoin_sender = self.payjoin_sender.as_ref(); + Arc::new(PayjoinPayment::new( + Arc::clone(&self.runtime), + payjoin_sender.map(Arc::clone), + Arc::clone(&self.config), + Arc::clone(&self.event_queue), + Arc::clone(&self.logger), + Arc::clone(&self.wallet), + Arc::clone(&self.tx_broadcaster), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 1862bf2df..aa681a67d 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -3,9 +3,11 @@ mod bolt11; mod bolt12; mod onchain; +pub(crate) mod payjoin; mod spontaneous; pub(crate) mod store; +pub use self::payjoin::PayjoinPayment; pub use bolt11::Bolt11Payment; pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs new file mode 100644 index 000000000..eb2d4d667 --- /dev/null +++ b/src/payment/payjoin/mod.rs @@ -0,0 +1,220 @@ +//! Holds a payment handler allowing to send Payjoin payments. + +use lightning::chain::chaininterface::BroadcasterInterface; + +use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::types::{Broadcaster, EventQueue, Wallet}; +use crate::Event; +use crate::{error::Error, Config}; + +use std::sync::{Arc, RwLock}; + +use self::send::PayjoinSender; + +pub(crate) mod send; + +/// A payment handler allowing to send Payjoin payments. +/// +/// Payjoin transactions can be used to improve privacy by breaking the common-input-ownership +/// heuristic when Payjoin receivers contribute input(s) to the transaction. They can also be used to +/// save on fees, as the Payjoin receiver can direct the incoming funds to open a lightning +/// channel, forwards the funds to another address, or simply consolidate UTXOs. +/// +/// Payjoin [`BIP77`] implementation. Compatible also with previous Payjoin version [`BIP78`]. +/// +/// Should be retrieved by calling [`Node::payjoin_payment`]. +/// +/// In a Payjoin, both the sender and receiver contribute inputs to the transaction in a +/// coordinated manner. The Payjoin mechanism is also called pay-to-endpoint(P2EP). +/// +/// The Payjoin receiver endpoint address is communicated through a [`BIP21`] URI, along with the +/// payment address and amount. In the Payjoin process, parties edit, sign and pass iterations of +/// the transaction between each other, before a final version is broadcasted by the Payjoin +/// sender. [`BIP77`] codifies a protocol with 2 iterations (or one round of interaction beyond +/// address sharing). +/// +/// [`BIP77`] Defines the Payjoin process to happen asynchronously, with the Payjoin receiver +/// enrolling with a Payjoin Directory to receive Payjoin requests. The Payjoin sender can then +/// make requests through a proxy server, Payjoin Relay, to the Payjoin receiver even if the +/// receiver is offline. This mechanism requires the Payjoin sender to regulary check for responses +/// from the Payjoin receiver as implemented in [`Node::payjoin_payment::send`]. +/// +/// A Payjoin Relay is a proxy server that forwards Payjoin requests from the Payjoin sender to the +/// Payjoin receiver subdirectory. A Payjoin Relay can be run by anyone. Public Payjoin Relay servers are: +/// - +/// +/// A Payjoin directory is a service that allows Payjoin receivers to receive Payjoin requests +/// offline. A Payjoin directory can be run by anyone. Public Payjoin Directory servers are: +/// - +/// +/// For futher information on Payjoin, please refer to the BIPs included in this documentation. Or +/// visit the [Payjoin website](https://payjoin.org). +/// +/// [`Node::payjoin_payment`]: crate::Node::payjoin_payment +/// [`Node::payjoin_payment::send`]: crate::payment::PayjoinPayment::send +/// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +/// [`BIP78`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki +/// [`BIP77`]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki +pub struct PayjoinPayment { + runtime: Arc>>, + sender: Option>, + config: Arc, + event_queue: Arc, + logger: Arc, + wallet: Arc, + tx_broadcaster: Arc, +} + +impl PayjoinPayment { + pub(crate) fn new( + runtime: Arc>>, sender: Option>, + config: Arc, event_queue: Arc, logger: Arc, + wallet: Arc, tx_broadcaster: Arc, + ) -> Self { + Self { runtime, sender, config, event_queue, logger, wallet, tx_broadcaster } + } + + /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. + /// + /// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters + /// set. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constucting the Payjoin request and sending it in the background. The result of the + /// operation will be communicated through the event queue. If the Payjoin request is + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// + /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. + /// If the Payjoin receiver does not respond within this duration, the process is considered + /// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as + /// part of our request in a regular transaction if we timed out, or for any other reason. The + /// Payjoin sender should monitor the blockchain for such transactions and handle them + /// accordingly. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + pub fn send(&self, payjoin_uri: String) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + let payjoin_sender = self.sender.as_ref().ok_or(Error::PayjoinUnavailable)?; + let payjoin_uri = + payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid)?; + let payjoin_uri = + payjoin_uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; + let amount_to_send = match payjoin_uri.amount { + Some(amount) => amount, + None => return Err(Error::PayjoinRequestMissingAmount), + }; + let original_psbt = self.wallet.build_payjoin_transaction( + payjoin_uri.address.script_pubkey(), + amount_to_send.to_sat(), + )?; + let payjoin_sender = Arc::clone(payjoin_sender); + let runtime = rt_lock.as_ref().unwrap(); + let event_queue = Arc::clone(&self.event_queue); + let tx_broadcaster = Arc::clone(&self.tx_broadcaster); + let wallet = Arc::clone(&self.wallet); + let logger = Arc::clone(&self.logger); + let payjoin_relay = payjoin_sender.payjoin_relay().clone(); + runtime.spawn(async move { + let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL); + loop { + tokio::select! { + _ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => { + let _ = event_queue.add_event(Event::PayjoinTxSendFailed { + reason: "Payjoin request timed out.".to_string(), + }); + break; + } + _ = interval.tick() => { + let payjoin_uri = payjoin_uri.clone(); + + let (request, context) = + payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) + .and_then(|b| b.build_non_incentivizing()) + .and_then(|mut c| c.extract_v2(payjoin_relay.clone())) + .map_err(|_e| Error::PayjoinRequestCreationFailed).unwrap(); + match payjoin_sender.send_request(&request).await { + Some(response) => { + match context.process_response(&mut response.as_slice()) { + Ok(Some(payjoin_proposal_psbt)) => { + let payjoin_proposal_psbt = &mut payjoin_proposal_psbt.clone(); + let is_signed = wallet.sign_payjoin_proposal(payjoin_proposal_psbt, &mut original_psbt.clone()).unwrap(); + if is_signed { + let tx = payjoin_proposal_psbt.clone().extract_tx(); + tx_broadcaster.broadcast_transactions(&[&tx]); + let txid = tx.txid(); + let _ = event_queue.add_event(Event::PayjoinPaymentPending { txid }); + } else { + let _ = event_queue + .add_event(Event::PayjoinTxSendFailed { reason: "Unable to sign proposal".to_string(), }); + break; + } + }, + Err(e) => { + let _ = event_queue + .add_event(Event::PayjoinTxSendFailed { reason: e.to_string() }); + log_error!(logger, "Error processing Payjoin response: {}", e); + break; + }, + Ok(None) => { + log_info!(logger, "Payjoin response received, waiting for next response."); + continue; + } + } + }, + None => { + continue; + }, + }; + } + } + } + }); + return Ok(()); + } + + /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. + /// + /// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters + /// set. + /// + /// This method will ignore the amount specified in the `payjoin_uri` and use the `amount_sats` + /// instead. The `amount_sats` argument is expected to be in satoshis. + /// + /// Due to the asynchronous nature of the Payjoin process, this method will return immediately + /// after constucting the Payjoin request and sending it in the background. The result of the + /// operation will be communicated through the event queue. If the Payjoin request is + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// + /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. + /// If the Payjoin receiver does not respond within this duration, the process is considered + /// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as + /// part of our request in a regular transaction if we timed out, or for any other reason. The + /// Payjoin sender should monitor the blockchain for such transactions and handle them + /// accordingly. + /// + /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + pub fn send_with_amount(&self, payjoin_uri: String, amount_sats: u64) -> Result<(), Error> { + let payjoin_uri = match payjoin::Uri::try_from(payjoin_uri) { + Ok(uri) => uri, + Err(_) => return Err(Error::PayjoinUriInvalid), + }; + let mut payjoin_uri = match payjoin_uri.require_network(self.config.network) { + Ok(uri) => uri, + Err(_) => return Err(Error::InvalidNetwork), + }; + payjoin_uri.amount = Some(bitcoin::Amount::from_sat(amount_sats)); + self.send(payjoin_uri.to_string()) + } +} diff --git a/src/payment/payjoin/send.rs b/src/payment/payjoin/send.rs new file mode 100644 index 000000000..f981b3ec2 --- /dev/null +++ b/src/payment/payjoin/send.rs @@ -0,0 +1,76 @@ +use crate::config::{PAYJOIN_REQUEST_TIMEOUT, PAYJOIN_RETRY_INTERVAL}; +use crate::io::utils::ohttp_headers; +use crate::logger::FilesystemLogger; + +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; + +use std::sync::Arc; + +pub(crate) struct PayjoinSender { + logger: Arc, + payjoin_relay: payjoin::Url, +} + +impl PayjoinSender { + pub(crate) fn new(logger: Arc, payjoin_relay: payjoin::Url) -> Self { + Self { logger, payjoin_relay } + } + + pub(crate) fn payjoin_relay(&self) -> &payjoin::Url { + &self.payjoin_relay + } + + pub(crate) async fn send_request(&self, request: &payjoin::Request) -> Option> { + let response = match reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => response, + Err(e) => { + log_error!( + self.logger, + "Error trying to poll Payjoin response: {}, retrying in {} seconds", + e, + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + }, + }; + if response.status() == reqwest::StatusCode::OK { + match response.bytes().await.and_then(|r| Ok(r.to_vec())) { + Ok(response) => { + if response.is_empty() { + log_info!( + self.logger, + "Got empty response while polling Payjoin response, retrying in {} seconds", PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + } + return Some(response); + }, + Err(e) => { + log_error!( + self.logger, + "Error reading polling Payjoin response: {}, retrying in {} seconds", + e, + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + }, + }; + } else { + log_info!( + self.logger, + "Got status code {} while polling Payjoin response, retrying in {} seconds", + response.status(), + PAYJOIN_RETRY_INTERVAL.as_secs() + ); + return None; + } + } +} diff --git a/src/payment/store.rs b/src/payment/store.rs index eb3ac091f..3f0481d9b 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -232,6 +232,8 @@ pub enum PaymentKind { /// The pre-image used by the payment. preimage: Option, }, + /// A Payjoin payment. + Payjoin, } impl_writeable_tlv_based_enum!(PaymentKind, @@ -261,7 +263,8 @@ impl_writeable_tlv_based_enum!(PaymentKind, (0, hash, option), (2, preimage, option), (4, secret, option), - }; + }, + (12, Payjoin) => {}; ); /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. diff --git a/src/types.rs b/src/types.rs index 0c2faeb78..b1ef67b9c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -72,6 +72,8 @@ pub(crate) type Wallet = crate::wallet::Wallet< Arc, >; +pub(crate) type EventQueue = crate::event::EventQueue>; + pub(crate) type KeysManager = crate::wallet::WalletKeysManager< bdk::database::SqliteDatabase, Arc, diff --git a/src/wallet.rs b/src/wallet.rs index 0da3f6db8..180f7361e 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -3,6 +3,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger}; use crate::config::BDK_WALLET_SYNC_TIMEOUT_SECS; use crate::Error; +use bitcoin::psbt::Psbt; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; use lightning::events::bump_transaction::{Utxo, WalletSource}; @@ -149,6 +150,65 @@ where res } + pub(crate) fn build_payjoin_transaction( + &self, output_script: ScriptBuf, value_sats: u64, + ) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let network = locked_wallet.network(); + let fee_rate = match network { + bitcoin::Network::Regtest => 1000.0, + _ => self + .fee_estimator + .get_est_sat_per_1000_weight(ConfirmationTarget::OutputSpendingFee) as f32, + }; + let fee_rate = FeeRate::from_sat_per_kwu(fee_rate); + 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 Payjoin transaction: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create Payjoin transaction: {}", err); + return Err(err.into()); + }, + }; + locked_wallet.sign(&mut psbt, SignOptions::default())?; + Ok(psbt) + } + + pub(crate) fn sign_payjoin_proposal( + &self, payjoin_proposal_psbt: &mut Psbt, original_psbt: &mut Psbt, + ) -> Result { + // BDK only signs scripts that match its target descriptor by iterating through input map. + // The BIP 78 spec makes receiver clear sender input map UTXOs, so process_response will + // fail unless they're cleared. A PSBT unsigned_tx.input references input OutPoints and + // not a Script, so the sender signer must either be able to sign based on OutPoint UTXO + // lookup or otherwise re-introduce the Script from original_psbt. Since BDK PSBT signer + // only checks Input map Scripts for match against its descriptor, it won't sign if they're + // empty. Re-add the scripts from the original_psbt in order for BDK to sign properly. + // reference: https://github.com/bitcoindevkit/bdk-cli/pull/156#discussion_r1261300637 + let mut original_inputs = + original_psbt.unsigned_tx.input.iter().zip(&mut original_psbt.inputs).peekable(); + for (proposed_txin, proposed_psbtin) in + payjoin_proposal_psbt.unsigned_tx.input.iter().zip(&mut payjoin_proposal_psbt.inputs) + { + 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 wallet = self.inner.lock().unwrap(); + let is_signed = wallet.sign(payjoin_proposal_psbt, SignOptions::default())?; + Ok(is_signed) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime, From a8efc7f29eef44f29b3b2b5119d0ef9de3478893 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Mon, 20 May 2024 21:31:13 +0300 Subject: [PATCH 2/7] Allow to send payjoin transactions Implements the payjoin sender as describe in BIP77. This would allow the on chain wallet linked to LDK node to send payjoin transactions. --- bindings/ldk_node.udl | 16 +- src/builder.rs | 13 +- src/error.rs | 6 + src/event.rs | 80 +++++++-- src/lib.rs | 16 +- src/payment/payjoin/handler.rs | 319 +++++++++++++++++++++++++++++++++ src/payment/payjoin/mod.rs | 124 +++++-------- src/types.rs | 2 + src/uniffi_types.rs | 15 +- src/wallet.rs | 13 +- 10 files changed, 487 insertions(+), 117 deletions(-) create mode 100644 src/payment/payjoin/handler.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d17b0cd66..b4abdf1fc 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -252,8 +252,9 @@ interface Event { ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason); - PayjoinTxSendSuccess(Txid txid); - PayjoinTxSendFailed(string reason); + PayjoinPaymentPending(Txid txid, u64 amount, ScriptBuf receipient); + PayjoinPaymentSuccess(Txid txid, u64 amount, ScriptBuf receipient); + PayjoinPaymentFailed(Txid? txid, u64 amount, ScriptBuf receipient, PayjoinPaymentFailureReason reason); }; enum PaymentFailureReason { @@ -265,6 +266,13 @@ enum PaymentFailureReason { "UnexpectedError", }; +enum PayjoinPaymentFailureReason { + "Timeout", + "TransactionFinalisationFailed", + "InvalidReceiverResponse", +}; + + [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); @@ -290,6 +298,7 @@ interface PaymentKind { Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id); Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); + Payjoin(); }; enum PaymentDirection { @@ -515,3 +524,6 @@ typedef string Mnemonic; [Custom] typedef string UntrustedString; + +[Custom] +typedef string ScriptBuf; diff --git a/src/builder.rs b/src/builder.rs index 7d48d835b..9a8531db4 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,7 +11,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::payment::payjoin::send::PayjoinSender; +use crate::payment::payjoin::handler::PayjoinHandler; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; @@ -997,11 +997,16 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); - let mut payjoin_sender = None; + let mut payjoin_handler = None; if let Some(pj_config) = payjoin_config { - payjoin_sender = Some(Arc::new(PayjoinSender::new( + payjoin_handler = Some(Arc::new(PayjoinHandler::new( + Arc::clone(&tx_broadcaster), Arc::clone(&logger), pj_config.payjoin_relay.clone(), + Arc::clone(&tx_sync), + Arc::clone(&event_queue), + Arc::clone(&wallet), + Arc::clone(&payment_store), ))); } @@ -1026,7 +1031,7 @@ fn build_with_store_internal( channel_manager, chain_monitor, output_sweeper, - payjoin_sender, + payjoin_handler, peer_manager, connection_manager, keys_manager, diff --git a/src/error.rs b/src/error.rs index 1ea1b5e64..cbfa3c2d8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -207,3 +207,9 @@ impl From for Error { Self::TxSyncFailed } } + +impl From for Error { + fn from(_e: reqwest::Error) -> Self { + Self::PayjoinRequestCreationFailed + } +} diff --git a/src/event.rs b/src/event.rs index 30051a2b1..32db5663e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -24,6 +24,7 @@ use lightning::events::{ClosureReason, PaymentPurpose}; use lightning::events::{Event as LdkEvent, PaymentFailureReason}; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; +use lightning::ln::msgs::DecodeError; use lightning::ln::{ChannelId, PaymentHash}; use lightning::routing::gossip::NodeId; use lightning::util::errors::APIError; @@ -143,30 +144,72 @@ pub enum Event { /// This will be `None` for events serialized by LDK Node v0.2.1 and prior. reason: Option, }, + /// Failed to send Payjoin transaction. + /// + /// This event is emitted when our attempt to send Payjoin transaction fail. + PayjoinPaymentPending { + /// Transaction ID of the successfully sent Payjoin transaction. + txid: bitcoin::Txid, + /// docs + amount: u64, + /// docs + receipient: bitcoin::ScriptBuf, + }, /// A Payjoin transaction has been successfully sent. /// /// This event is emitted when we send a Payjoin transaction and it was accepted by the /// receiver, and then finalised and broadcasted by us. - PayjoinTxSendSuccess { + PayjoinPaymentSuccess { /// Transaction ID of the successfully sent Payjoin transaction. txid: bitcoin::Txid, + /// docs + amount: u64, + /// docs + receipient: bitcoin::ScriptBuf, }, /// Failed to send Payjoin transaction. /// /// This event is emitted when our attempt to send Payjoin transaction fail. - PayjoinTxSendFailed { - /// Reason for the failure. - reason: String, - }, - /// Failed to send Payjoin transaction. - /// - /// This event is emitted when our attempt to send Payjoin transaction fail. - PayjoinPaymentPending { + PayjoinPaymentFailed { /// Transaction ID of the successfully sent Payjoin transaction. - txid: bitcoin::Txid, + txid: Option, + /// docs + amount: u64, + /// docs + receipient: bitcoin::ScriptBuf, + /// Reason for the failure. + reason: PayjoinPaymentFailureReason, }, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PayjoinPaymentFailureReason { + Timeout, + TransactionFinalisationFailed, + InvalidReceiverResponse, +} + +impl Readable for PayjoinPaymentFailureReason { + fn read(reader: &mut R) -> Result { + match u8::read(reader)? { + 0 => Ok(Self::Timeout), + 1 => Ok(Self::TransactionFinalisationFailed), + 2 => Ok(Self::InvalidReceiverResponse), + _ => Err(DecodeError::InvalidValue), + } + } +} + +impl Writeable for PayjoinPaymentFailureReason { + fn write(&self, writer: &mut W) -> Result<(), std::io::Error> { + match *self { + Self::Timeout => 0u8.write(writer), + Self::TransactionFinalisationFailed => 1u8.write(writer), + Self::InvalidReceiverResponse => 2u8.write(writer), + } + } +} + impl_writeable_tlv_based_enum!(Event, (0, PaymentSuccessful) => { (0, payment_hash, required), @@ -207,14 +250,21 @@ impl_writeable_tlv_based_enum!(Event, (4, claimable_amount_msat, required), (6, claim_deadline, option), }, - (7, PayjoinTxSendSuccess) => { + (7, PayjoinPaymentPending) => { (0, txid, required), + (2, amount, required), + (4, receipient, required), }, - (8, PayjoinTxSendFailed) => { - (0, reason, required), - }, - (9, PayjoinPaymentPending) => { + (8, PayjoinPaymentSuccess) => { (0, txid, required), + (2, amount, required), + (4, receipient, required), + }, + (9, PayjoinPaymentFailed) => { + (0, amount, required), + (1, txid, option), + (2, receipient, required), + (4, reason, required), }; ); diff --git a/src/lib.rs b/src/lib.rs index b689426ef..674f71b02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,7 +109,7 @@ pub use error::Error as NodeError; use error::Error; pub use event::Event; -use payment::payjoin::send::PayjoinSender; +use payment::payjoin::handler::PayjoinHandler; pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; @@ -189,7 +189,7 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, connection_manager: Arc>>, - payjoin_sender: Option>, + payjoin_handler: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -1076,15 +1076,13 @@ impl Node { /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config #[cfg(not(feature = "uniffi"))] pub fn payjoin_payment(&self) -> PayjoinPayment { - let payjoin_sender = self.payjoin_sender.as_ref(); + let payjoin_handler = self.payjoin_handler.as_ref(); PayjoinPayment::new( Arc::clone(&self.runtime), - payjoin_sender.map(Arc::clone), + payjoin_handler.map(Arc::clone), Arc::clone(&self.config), - Arc::clone(&self.event_queue), Arc::clone(&self.logger), Arc::clone(&self.wallet), - Arc::clone(&self.tx_broadcaster), ) } @@ -1096,15 +1094,13 @@ impl Node { /// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config #[cfg(feature = "uniffi")] pub fn payjoin_payment(&self) -> Arc { - let payjoin_sender = self.payjoin_sender.as_ref(); + let payjoin_handler = self.payjoin_handler.as_ref(); Arc::new(PayjoinPayment::new( Arc::clone(&self.runtime), - payjoin_sender.map(Arc::clone), + payjoin_handler.map(Arc::clone), Arc::clone(&self.config), - Arc::clone(&self.event_queue), Arc::clone(&self.logger), Arc::clone(&self.wallet), - Arc::clone(&self.tx_broadcaster), )) } diff --git a/src/payment/payjoin/handler.rs b/src/payment/payjoin/handler.rs new file mode 100644 index 000000000..3f1f96b7f --- /dev/null +++ b/src/payment/payjoin/handler.rs @@ -0,0 +1,319 @@ +use crate::config::PAYJOIN_REQUEST_TIMEOUT; +use crate::error::Error; +use crate::event::PayjoinPaymentFailureReason; +use crate::logger::FilesystemLogger; +use crate::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::types::{Broadcaster, ChainSource, EventQueue, PaymentStore, Wallet}; +use crate::Event; + +use bitcoin::address::NetworkChecked; +use bitcoin::block::Header; +use bitcoin::psbt::Psbt; +use bitcoin::{Address, Amount, BlockHash, Script, Transaction, Txid}; + +use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; +use lightning::chain::transaction::TransactionData; +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::ln::channelmanager::PaymentId; +use lightning::log_error; +use lightning::util::logger::Logger; +use payjoin::send::{ContextV2, RequestBuilder}; +use rand::RngCore; + +use std::sync::{Arc, RwLock}; + +pub(crate) struct PayjoinHandler { + tx_broadcaster: Arc, + logger: Arc, + payjoin_relay: payjoin::Url, + chain_source: Arc, + transactions: RwLock>, + event_queue: Arc, + wallet: Arc, + payment_store: Arc, +} + +impl PayjoinHandler { + pub(crate) fn new( + tx_broadcaster: Arc, logger: Arc, + payjoin_relay: payjoin::Url, chain_source: Arc, event_queue: Arc, + wallet: Arc, payment_store: Arc, + ) -> Self { + Self { + tx_broadcaster, + logger, + payjoin_relay, + transactions: RwLock::new(Vec::new()), + chain_source, + event_queue, + wallet, + payment_store, + } + } + + pub(crate) async fn send_payjoin_transaction( + &self, original_psbt: &mut Psbt, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result, Error> { + let (request, context) = + RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri.clone()) + .and_then(|b| b.build_non_incentivizing()) + .and_then(|mut c| c.extract_v2(self.payjoin_relay.clone())) + .map_err(|_e| Error::PayjoinRequestCreationFailed) + .unwrap(); + let mut random_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut random_bytes); + self.payment_store.insert(PaymentDetails::new( + PaymentId(random_bytes), + PaymentKind::Payjoin, + payjoin_uri.amount.map(|a| a.to_sat()), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ))?; + let response = send_payjoin_ohttp_request(&request).await?; + self.handle_payjoin_transaction_response(response, context, original_psbt, payjoin_uri) + .await + } + + pub(crate) async fn handle_payjoin_transaction_response( + &self, response: Vec, context: ContextV2, original_psbt: &mut Psbt, + payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result, Error> { + let amount = match payjoin_uri.amount { + Some(amt) => amt.to_sat(), + None => return Err(Error::PayjoinRequestMissingAmount), + }; + match context.process_response(&mut response.as_slice()) { + Ok(Some(pj_proposal)) => { + let pj_proposal = &mut pj_proposal.clone(); + let tx = + self.finalise_tx(pj_proposal, &mut original_psbt.clone(), payjoin_uri.clone())?; + self.tx_broadcaster.broadcast_transactions(&[&tx]); + let txid = tx.txid(); + let _ = self.event_queue.add_event(Event::PayjoinPaymentPending { + txid, + amount, + receipient: payjoin_uri.address.clone().into(), + }); + Ok(Some(txid)) + }, + Ok(None) => Ok(None), + Err(_e) => { + let _ = self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: None, + amount, + receipient: payjoin_uri.address.clone().into(), + reason: PayjoinPaymentFailureReason::InvalidReceiverResponse, + }); + return Err(Error::PayjoinResponseProcessingFailed); + }, + } + } + + fn finalise_tx( + &self, payjoin_proposal: &mut Psbt, original_psbt: &mut Psbt, + payjoin_uri: payjoin::Uri, + ) -> Result { + let wallet = self.wallet.clone(); + wallet.sign_payjoin_proposal(payjoin_proposal, original_psbt)?; + let tx = payjoin_proposal.clone().extract_tx(); + if let Some(our_output) = + tx.output.iter().find(|output| wallet.is_mine(&output.script_pubkey).unwrap_or(false)) + { + let mut transactions = self.transactions.write().unwrap(); + let pj_tx = PayjoinTransaction::new( + tx.clone(), + payjoin_uri.address, + payjoin_uri.amount.unwrap_or_default(), + ); + transactions.push(pj_tx); + self.register_tx(&tx.txid(), &our_output.script_pubkey); + Ok(tx) + } else { + Err(Error::PaymentSendingFailed) + } + } + + pub(crate) fn timeout_payjoin_transaction( + &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, + ) -> Result<(), Error> { + let amount = match payjoin_uri.amount { + Some(amt) => amt.to_sat(), + None => return Err(Error::PayjoinRequestMissingAmount), + }; + let _ = self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: None, + receipient: payjoin_uri.address.clone().into(), + amount, + reason: PayjoinPaymentFailureReason::Timeout, + }); + Ok(()) + } + + fn internal_transactions_confirmed( + &self, header: &Header, txdata: &TransactionData, height: u32, + ) { + let (_, tx) = txdata[0]; + let confirmed_tx_txid = tx.txid(); + let mut transactions = self.transactions.write().unwrap(); + if let Some(position) = + transactions.iter().position(|o| o.txid() == Some(confirmed_tx_txid)) + { + let tx = transactions.remove(position); + tx.to_pending_threshold_confirmations(height, header.block_hash()); + } else { + log_error!( + self.logger, + "Notified about UNTRACKED confirmed payjoin transaction {}", + confirmed_tx_txid + ); + }; + } + + fn internal_best_block_updated(&self, height: u32) { + let mut transactions = self.transactions.write().unwrap(); + transactions.retain(|tx| { + if let (Some(first_conf), Some(txid)) = (tx.first_confirmation_height(), tx.txid()) { + if height - first_conf >= ANTI_REORG_DELAY { + let _ = self.event_queue.add_event(Event::PayjoinPaymentSuccess { + txid, + amount: tx.amount().to_sat(), + receipient: tx.receiver().into(), + }); + false + } else { + true + } + } else { + true + } + }); + } + + fn internal_get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + let state_lock = self.transactions.read().unwrap(); + state_lock + .iter() + .filter_map(|o| match o { + PayjoinTransaction::PendingThresholdConfirmations { + tx, + first_confirmation_height, + first_confirmation_hash, + .. + } => Some(( + tx.clone().txid(), + first_confirmation_height.clone(), + Some(first_confirmation_hash.clone()), + )), + _ => None, + }) + .collect::>() + } +} + +#[derive(Clone, Debug)] +enum PayjoinTransaction { + // PendingReceiverResponse, + PendingFirstConfirmation { + tx: Transaction, + receiver: Address, + amount: Amount, + }, + PendingThresholdConfirmations { + tx: Transaction, + receiver: Address, + amount: Amount, + first_confirmation_height: u32, + first_confirmation_hash: BlockHash, + }, +} + +impl PayjoinTransaction { + fn new(tx: Transaction, receiver: Address, amount: Amount) -> Self { + PayjoinTransaction::PendingFirstConfirmation { tx, receiver, amount } + } + fn txid(&self) -> Option { + match self { + PayjoinTransaction::PendingFirstConfirmation { tx, .. } => Some(tx.txid()), + PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => Some(tx.txid()), + } + } + fn first_confirmation_height(&self) -> Option { + match self { + PayjoinTransaction::PendingFirstConfirmation { .. } => None, + PayjoinTransaction::PendingThresholdConfirmations { + first_confirmation_height, .. + } => Some(*first_confirmation_height), + } + } + fn amount(&self) -> Amount { + match self { + PayjoinTransaction::PendingFirstConfirmation { amount, .. } => *amount, + PayjoinTransaction::PendingThresholdConfirmations { amount, .. } => *amount, + } + } + fn receiver(&self) -> Address { + match self { + PayjoinTransaction::PendingFirstConfirmation { receiver, .. } => receiver.clone(), + PayjoinTransaction::PendingThresholdConfirmations { receiver, .. } => receiver.clone(), + } + } + + fn to_pending_threshold_confirmations(&self, height: u32, hash: BlockHash) -> Self { + match self { + PayjoinTransaction::PendingFirstConfirmation { tx, receiver, amount } => { + PayjoinTransaction::PendingThresholdConfirmations { + tx: tx.clone(), + receiver: receiver.clone(), + amount: *amount, + first_confirmation_height: height, + first_confirmation_hash: hash, + } + }, + _ => unreachable!(), + } + } +} + +impl Filter for PayjoinHandler { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.chain_source.register_tx(txid, script_pubkey); + } + + fn register_output(&self, output: WatchedOutput) { + self.chain_source.register_output(output); + } +} + +impl Confirm for PayjoinHandler { + fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) { + self.internal_transactions_confirmed(header, txdata, height); + } + + fn best_block_updated(&self, _header: &Header, height: u32) { + self.internal_best_block_updated(height); + } + + fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { + self.internal_get_relevant_txids() + } + + fn transaction_unconfirmed(&self, _txid: &Txid) {} +} + +async fn send_payjoin_ohttp_request(request: &payjoin::Request) -> Result, Error> { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("message/ohttp-req"), + ); + let response = reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(headers) + .send() + .await?; + let response = response.error_for_status()?.bytes().await?; + Ok(response.to_vec()) +} diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs index eb2d4d667..9d93debf4 100644 --- a/src/payment/payjoin/mod.rs +++ b/src/payment/payjoin/mod.rs @@ -1,18 +1,18 @@ //! Holds a payment handler allowing to send Payjoin payments. -use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::util::logger::Logger; +use lightning::{log_error, log_info}; use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; -use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; -use crate::types::{Broadcaster, EventQueue, Wallet}; -use crate::Event; +use crate::logger::FilesystemLogger; +use crate::types::Wallet; use crate::{error::Error, Config}; use std::sync::{Arc, RwLock}; -use self::send::PayjoinSender; +pub(crate) mod handler; -pub(crate) mod send; +use handler::PayjoinHandler; /// A payment handler allowing to send Payjoin payments. /// @@ -58,21 +58,19 @@ pub(crate) mod send; /// [`BIP77`]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki pub struct PayjoinPayment { runtime: Arc>>, - sender: Option>, + handler: Option>, config: Arc, - event_queue: Arc, logger: Arc, wallet: Arc, - tx_broadcaster: Arc, } impl PayjoinPayment { pub(crate) fn new( - runtime: Arc>>, sender: Option>, - config: Arc, event_queue: Arc, logger: Arc, - wallet: Arc, tx_broadcaster: Arc, + runtime: Arc>>, + handler: Option>, config: Arc, logger: Arc, + wallet: Arc, ) -> Self { - Self { runtime, sender, config, event_queue, logger, wallet, tx_broadcaster } + Self { runtime, handler, config, logger, wallet } } /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. @@ -83,8 +81,8 @@ impl PayjoinPayment { /// Due to the asynchronous nature of the Payjoin process, this method will return immediately /// after constucting the Payjoin request and sending it in the background. The result of the /// operation will be communicated through the event queue. If the Payjoin request is - /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. - /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// successful, [`Event::PayjoinPaymentSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinPaymentFailed`] is added. /// /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. /// If the Payjoin receiver does not respond within this duration, the process is considered @@ -95,84 +93,46 @@ impl PayjoinPayment { /// /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki - /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess - /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + /// [`Event::PayjoinPaymentSuccess`]: crate::Event::PayjoinPaymentSuccess + /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed pub fn send(&self, payjoin_uri: String) -> Result<(), Error> { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } - let payjoin_sender = self.sender.as_ref().ok_or(Error::PayjoinUnavailable)?; let payjoin_uri = - payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid)?; - let payjoin_uri = - payjoin_uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; - let amount_to_send = match payjoin_uri.amount { - Some(amount) => amount, - None => return Err(Error::PayjoinRequestMissingAmount), - }; - let original_psbt = self.wallet.build_payjoin_transaction( - payjoin_uri.address.script_pubkey(), - amount_to_send.to_sat(), - )?; - let payjoin_sender = Arc::clone(payjoin_sender); - let runtime = rt_lock.as_ref().unwrap(); - let event_queue = Arc::clone(&self.event_queue); - let tx_broadcaster = Arc::clone(&self.tx_broadcaster); - let wallet = Arc::clone(&self.wallet); + payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then( + |uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork), + )?; + let mut original_psbt = self.wallet.build_payjoin_transaction(payjoin_uri.clone())?; + let payjoin_handler = self.handler.as_ref().ok_or(Error::PayjoinUnavailable)?; + let payjoin_handler = Arc::clone(payjoin_handler); let logger = Arc::clone(&self.logger); - let payjoin_relay = payjoin_sender.payjoin_relay().clone(); - runtime.spawn(async move { + log_info!(logger, "Sending Payjoin request to: {}", payjoin_uri.address); + rt_lock.as_ref().unwrap().spawn(async move { let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL); loop { tokio::select! { _ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => { - let _ = event_queue.add_event(Event::PayjoinTxSendFailed { - reason: "Payjoin request timed out.".to_string(), - }); + log_error!(logger, "Payjoin request timed out."); + let _ = payjoin_handler.timeout_payjoin_transaction(payjoin_uri.clone()); break; } _ = interval.tick() => { - let payjoin_uri = payjoin_uri.clone(); - - let (request, context) = - payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri) - .and_then(|b| b.build_non_incentivizing()) - .and_then(|mut c| c.extract_v2(payjoin_relay.clone())) - .map_err(|_e| Error::PayjoinRequestCreationFailed).unwrap(); - match payjoin_sender.send_request(&request).await { - Some(response) => { - match context.process_response(&mut response.as_slice()) { - Ok(Some(payjoin_proposal_psbt)) => { - let payjoin_proposal_psbt = &mut payjoin_proposal_psbt.clone(); - let is_signed = wallet.sign_payjoin_proposal(payjoin_proposal_psbt, &mut original_psbt.clone()).unwrap(); - if is_signed { - let tx = payjoin_proposal_psbt.clone().extract_tx(); - tx_broadcaster.broadcast_transactions(&[&tx]); - let txid = tx.txid(); - let _ = event_queue.add_event(Event::PayjoinPaymentPending { txid }); - } else { - let _ = event_queue - .add_event(Event::PayjoinTxSendFailed { reason: "Unable to sign proposal".to_string(), }); - break; - } - }, - Err(e) => { - let _ = event_queue - .add_event(Event::PayjoinTxSendFailed { reason: e.to_string() }); - log_error!(logger, "Error processing Payjoin response: {}", e); - break; - }, - Ok(None) => { - log_info!(logger, "Payjoin response received, waiting for next response."); - continue; - } - } + match payjoin_handler.send_payjoin_transaction(&mut original_psbt, payjoin_uri.clone()).await { + Ok(Some(_)) => { + log_info!(logger, "Payjoin transaction sent successfully."); + break + }, + Ok(None) => { + log_info!(logger, "No Payjoin response yet."); + continue }, - None => { - continue; - }, - }; + Err(e) => { + log_error!(logger, "Failed to process Payjoin receiver response: {}.", e); + break; + } + } } } } @@ -191,8 +151,8 @@ impl PayjoinPayment { /// Due to the asynchronous nature of the Payjoin process, this method will return immediately /// after constucting the Payjoin request and sending it in the background. The result of the /// operation will be communicated through the event queue. If the Payjoin request is - /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. - /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. + /// successful, [`Event::PayjoinPaymentSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinPaymentFailed`] is added. /// /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. /// If the Payjoin receiver does not respond within this duration, the process is considered @@ -203,8 +163,8 @@ impl PayjoinPayment { /// /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki - /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess - /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed + /// [`Event::PayjoinPaymentSuccess`]: crate::Event::PayjoinPaymentSuccess + /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed pub fn send_with_amount(&self, payjoin_uri: String, amount_sats: u64) -> Result<(), Error> { let payjoin_uri = match payjoin::Uri::try_from(payjoin_uri) { Ok(uri) => uri, diff --git a/src/types.rs b/src/types.rs index b1ef67b9c..ad21e2310 100644 --- a/src/types.rs +++ b/src/types.rs @@ -142,6 +142,8 @@ pub(crate) type BumpTransactionEventHandler = Arc, >; +pub(crate) type PaymentStore = crate::payment::store::PaymentStore>; + /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 9dd7e5699..f5fccbc20 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -1,3 +1,4 @@ +pub use crate::event::PayjoinPaymentFailureReason; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; @@ -11,7 +12,7 @@ pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; -pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid}; +pub use bitcoin::{Address, BlockHash, Network, OutPoint, ScriptBuf, Txid}; pub use bip39::Mnemonic; @@ -31,6 +32,18 @@ use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; use std::str::FromStr; +impl UniffiCustomTypeConverter for ScriptBuf { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(ScriptBuf::from_hex(&val).map_err(|_| Error::InvalidPublicKey)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_hex_string() + } +} + impl UniffiCustomTypeConverter for PublicKey { type Builtin = String; diff --git a/src/wallet.rs b/src/wallet.rs index 180f7361e..6ef136141 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -22,7 +22,7 @@ use bdk::wallet::AddressIndex; use bdk::{Balance, FeeRate}; use bdk::{SignOptions, SyncOptions}; -use bitcoin::address::{Payload, WitnessVersion}; +use bitcoin::address::{NetworkChecked, Payload, WitnessVersion}; use bitcoin::bech32::u5; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; @@ -151,10 +151,12 @@ where } pub(crate) fn build_payjoin_transaction( - &self, output_script: ScriptBuf, value_sats: u64, + &self, payjoin_uri: payjoin::Uri, ) -> Result { let locked_wallet = self.inner.lock().unwrap(); let network = locked_wallet.network(); + let output_script = payjoin_uri.address.script_pubkey(); + let amount = payjoin_uri.amount.ok_or(Error::PayjoinRequestMissingAmount)?.to_sat(); let fee_rate = match network { bitcoin::Network::Regtest => 1000.0, _ => self @@ -164,7 +166,7 @@ where let fee_rate = FeeRate::from_sat_per_kwu(fee_rate); 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(); + tx_builder.add_recipient(output_script, amount).fee_rate(fee_rate).enable_rbf(); let mut psbt = match tx_builder.finish() { Ok((psbt, _)) => { log_trace!(self.logger, "Created Payjoin transaction: {:?}", psbt); @@ -209,6 +211,11 @@ where Ok(is_signed) } + pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + Ok(locked_wallet.is_mine(script)?) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, locktime: LockTime, From ef8e5862630af40826689288354f476dae508b47 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 11 Jun 2024 07:52:22 +0300 Subject: [PATCH 3/7] Add ability to receive Payjoin tx Implements the payjoin receiver part as describe in BIP77. This would allow the on chain wallet linked to LDK node to receive payjoin transactions. Receiving a payjoin transaction requires first to enroll with the configured Payjoin directory and listening to our enrolled subdirectory for upcoming request. When a request received, we validate it as specified in BIP78, prepare our Payjoin proposal and send it back to the payjoin sender via the subdirectory. --- Cargo.toml | 2 +- bindings/ldk_node.udl | 3 + src/builder.rs | 29 +- src/error.rs | 15 + src/lib.rs | 33 ++- src/payjoin_receiver.rs | 436 +++++++++++++++++++++++++++++ src/payment/payjoin/mod.rs | 34 ++- src/wallet.rs | 40 ++- tests/common/mod.rs | 48 ++++ tests/integration_tests_payjoin.rs | 45 +++ 10 files changed, 671 insertions(+), 14 deletions(-) create mode 100644 src/payjoin_receiver.rs create mode 100644 tests/integration_tests_payjoin.rs diff --git a/Cargo.toml b/Cargo.toml index 8e7bc921a..c0212fc4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr esplora-client = { version = "0.6", default-features = false } libc = "0.2" uniffi = { version = "0.26.0", features = ["build"], optional = true } -payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2"] } +payjoin = { version = "0.16.0", default-features = false, features = ["send", "receive", "v2"] } [target.'cfg(vss)'.dependencies] vss-client = "0.2" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b4abdf1fc..f08c1161e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -209,6 +209,9 @@ enum NodeError { "PayjoinRequestMissingAmount", "PayjoinRequestCreationFailed", "PayjoinResponseProcessingFailed", + "PayjoinReceiverUnavailable", + "PayjoinReceiverRequestValidationFailed", + "PayjoinReceiverEnrollementFailed" }; dictionary NodeStatus { diff --git a/src/builder.rs b/src/builder.rs index 9a8531db4..8fa22423e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,6 +11,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_receiver::PayjoinReceiver; use crate::payment::payjoin::handler::PayjoinHandler; use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; @@ -96,7 +97,9 @@ struct LiquiditySourceConfig { #[derive(Debug, Clone)] struct PayjoinConfig { + payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, } impl Default for LiquiditySourceConfig { @@ -263,10 +266,23 @@ impl NodeBuilder { } /// Configures the [`Node`] instance to enable payjoin transactions. - pub fn set_payjoin_config(&mut self, payjoin_relay: String) -> Result<&mut Self, BuildError> { + pub fn set_payjoin_config( + &mut self, payjoin_directory: String, payjoin_relay: String, ohttp_keys: Option, + ) -> Result<&mut Self, BuildError> { let payjoin_relay = payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?; - self.payjoin_config = Some(PayjoinConfig { payjoin_relay }); + let payjoin_directory = payjoin::Url::parse(&payjoin_directory) + .map_err(|_| BuildError::InvalidPayjoinConfig)?; + let ohttp_keys = if let Some(ohttp_keys) = ohttp_keys { + let keys = match payjoin::OhttpKeys::decode(ohttp_keys.as_bytes()) { + Ok(keys) => keys, + Err(_) => return Err(BuildError::InvalidPayjoinConfig), + }; + Some(keys) + } else { + None + }; + self.payjoin_config = Some(PayjoinConfig { payjoin_directory, payjoin_relay, ohttp_keys }); Ok(self) } @@ -998,6 +1014,7 @@ fn build_with_store_internal( let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); let mut payjoin_handler = None; + let mut payjoin_receiver = None; if let Some(pj_config) = payjoin_config { payjoin_handler = Some(Arc::new(PayjoinHandler::new( Arc::clone(&tx_broadcaster), @@ -1008,6 +1025,13 @@ fn build_with_store_internal( Arc::clone(&wallet), Arc::clone(&payment_store), ))); + payjoin_receiver = Some(Arc::new(PayjoinReceiver::new( + Arc::clone(&logger), + Arc::clone(&wallet), + pj_config.payjoin_directory.clone(), + pj_config.payjoin_relay.clone(), + pj_config.ohttp_keys.clone(), + ))); } let is_listening = Arc::new(AtomicBool::new(false)); @@ -1032,6 +1056,7 @@ fn build_with_store_internal( chain_monitor, output_sweeper, payjoin_handler, + payjoin_receiver, peer_manager, connection_manager, keys_manager, diff --git a/src/error.rs b/src/error.rs index cbfa3c2d8..5510a51ee 100644 --- a/src/error.rs +++ b/src/error.rs @@ -105,6 +105,12 @@ pub enum Error { PayjoinRequestCreationFailed, /// Payjoin response processing failed. PayjoinResponseProcessingFailed, + /// Failed to access payjoin receiver object. + PayjoinReceiverUnavailable, + /// Failed to enroll payjoin receiver. + PayjoinReceiverEnrollementFailed, + /// Failed to validate an incoming payjoin request. + PayjoinReceiverRequestValidationFailed, } impl fmt::Display for Error { @@ -187,6 +193,15 @@ impl fmt::Display for Error { Self::PayjoinResponseProcessingFailed => { write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored") }, + Self::PayjoinReceiverUnavailable => { + write!(f, "Failed to access payjoin receiver object. Make sure you have enabled Payjoin receiving support.") + }, + Self::PayjoinReceiverRequestValidationFailed => { + write!(f, "Failed to validate an incoming payjoin request. Payjoin sender request didnt pass the payjoin validation steps.") + }, + Self::PayjoinReceiverEnrollementFailed => { + write!(f, "Failed to enroll payjoin receiver. Make sure the configured Payjoin directory & Payjoin relay are available.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 674f71b02..291780359 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +mod payjoin_receiver; pub mod payment; mod peer_store; mod sweep; @@ -109,6 +110,7 @@ pub use error::Error as NodeError; use error::Error; pub use event::Event; +use payjoin_receiver::PayjoinReceiver; use payment::payjoin::handler::PayjoinHandler; pub use types::ChannelConfig; @@ -190,6 +192,7 @@ pub struct Node { peer_manager: Arc, connection_manager: Arc>>, payjoin_handler: Option>, + payjoin_receiver: Option>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, @@ -690,6 +693,30 @@ impl Node { Arc::clone(&self.logger), )); + // Check every 5 seconds if we have received a payjoin transaction to our enrolled + // subdirectory with the configured Payjoin directory. + 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 = 5; + 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; + } + } + } + }); + } + let event_handler = Arc::new(EventHandler::new( Arc::clone(&self.event_queue), Arc::clone(&self.wallet), @@ -1077,9 +1104,11 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn payjoin_payment(&self) -> PayjoinPayment { let payjoin_handler = self.payjoin_handler.as_ref(); + let payjoin_receiver = self.payjoin_receiver.as_ref(); PayjoinPayment::new( Arc::clone(&self.runtime), payjoin_handler.map(Arc::clone), + payjoin_receiver.map(Arc::clone), Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.wallet), @@ -1095,9 +1124,11 @@ impl Node { #[cfg(feature = "uniffi")] pub fn payjoin_payment(&self) -> Arc { let payjoin_handler = self.payjoin_handler.as_ref(); + let payjoin_receiver = self.payjoin_receiver.as_ref(); Arc::new(PayjoinPayment::new( Arc::clone(&self.runtime), - payjoin_handler.map(Arc::clone), + payjoin_sender.map(Arc::clone), + payjoin_receiver.map(Arc::clone), Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.wallet), diff --git a/src/payjoin_receiver.rs b/src/payjoin_receiver.rs new file mode 100644 index 000000000..86e0d6675 --- /dev/null +++ b/src/payjoin_receiver.rs @@ -0,0 +1,436 @@ +use crate::error::Error; +use crate::io::utils::ohttp_headers; +use crate::logger::FilesystemLogger; +use crate::types::Wallet; +use lightning::log_info; +use lightning::util::logger::Logger; +use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; +use payjoin::{OhttpKeys, PjUriBuilder}; +use payjoin::{PjUri, Url}; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// Implements Payjoin protocol as specified in [BIP77] +/// +/// [BIP77]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki +pub(crate) struct PayjoinReceiver { + logger: Arc, + wallet: Arc, + /// Directory receiver wish to enroll with + payjoin_directory: Url, + /// Proxy server receiver wish to make requests through + payjoin_relay: Url, + /// Enrollement object indicates a successful enrollement if is defined. + enrolled: RwLock>, + /// Directory keys. + /// + /// Optional as they can be fetched on behalf of the user if not provided. + /// They are required in order to enroll. + ohttp_keys: RwLock>, +} + +impl PayjoinReceiver { + pub(crate) fn new( + logger: Arc, wallet: Arc, payjoin_directory: payjoin::Url, + payjoin_relay: payjoin::Url, ohttp_keys: Option, + ) -> Self { + Self { + logger, + wallet, + payjoin_directory, + payjoin_relay, + enrolled: RwLock::new(None), + ohttp_keys: RwLock::new(ohttp_keys), + } + } + + /// Before receiving Payjoin transactions we `enroll` with a Payjoin directory + /// and we acquire a subdirectory we can receive Payjoin transactions to while offline. + /// + /// This function returns [BIP21] URI with Payjoin parameters. + /// + /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub(crate) async fn receive(&self, amount: bitcoin::Amount) -> Result { + if !self.is_enrolled().await { + self.enroll().await?; + } + let enrolled = self.enrolled.read().await; + let enrolled = match enrolled.as_ref() { + Some(enrolled) => enrolled, + None => { + log_info!(self.logger, "Payjoin Receiver: Not enrolled"); + return Err(Error::PayjoinReceiverUnavailable); + }, + }; + let fallback_target = enrolled.fallback_target(); + let ohttp_keys = self.ohttp_keys.read().await; + let ohttp_keys = match ohttp_keys.as_ref() { + Some(okeys) => okeys, + None => { + log_info!(self.logger, "Payjoin Receiver: No ohttp keys"); + return Err(Error::PayjoinReceiverUnavailable); + }, + }; + let address = self.wallet.get_new_address()?; + let pj_part = match payjoin::Url::parse(&fallback_target) { + Ok(pj_part) => pj_part, + Err(_) => { + log_info!(self.logger, "Payjoin Receiver: Invalid fallback target"); + return Err(Error::PayjoinReceiverUnavailable); + }, + }; + let payjoin_uri = + PjUriBuilder::new(address, pj_part, Some(ohttp_keys.clone())).amount(amount).build(); + Ok(payjoin_uri) + } + + /// After enrolling, we should periodacly check if we have received any Payjoin transactions. + /// + /// This function will try to fetch pending Payjoin requests from the subdirectory, and if a + /// successful response received, we validate the request as specified in [`BIP78`]. After + /// validation we try to preserve privacy by adding more inputs/outputs to the transaction. + /// Last, we finalise the transaction and send a response back the the Payjoin sender. + /// + /// + /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist + pub(crate) async fn process_payjoin_request(&self) { + let mut enrolled = self.enrolled.write().await; + if let Some(mut enrolled) = enrolled.take() { + let (req, context) = match enrolled.extract_req() { + Ok(req) => req, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to extract enrollement request and context{}", + e + ); + return; + }, + }; + + let client = reqwest::Client::new(); + let response = match client + .post(req.url.to_string()) + .body(req.body) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to fetch payjoin request {}", + e + ); + return; + }, + }; + if response.status() != reqwest::StatusCode::OK { + log_info!( + self.logger, + "Payjoin Receiver: Got non-200 response from directory server {}", + response.status() + ); + return; + }; + let response = match response.bytes().await { + Ok(response) => response, + Err(e) => { + log_info!(self.logger, "Payjoin Receiver: Error reading response {}", e); + return; + }, + }; + if response.is_empty() { + log_info!(self.logger, "Payjoin Receiver: Empty response from directory server"); + return; + }; + let response = match enrolled.process_res(response.to_vec().as_slice(), context) { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to process payjoin request {}", + e + ); + return; + }, + }; + let unchecked_proposal = match response { + Some(proposal) => proposal, + None => { + return; + }, + }; + let provisional_proposal = match self.validate_payjoin_request(unchecked_proposal).await + { + Ok(proposal) => proposal, + Err(e) => { + log_info!(self.logger, "Payjoin Validation: {}", e); + return; + }, + }; + self.accept_payjoin_transaction(provisional_proposal).await; + } else { + log_info!(self.logger, "Payjoin Receiver: Unable to get enrolled object"); + } + } + + async fn accept_payjoin_transaction(&self, mut provisional_proposal: ProvisionalProposal) { + // Preserve privacy + let (candidate_inputs, utxo_set) = match self.wallet.payjoin_receiver_candidate_input() { + Ok(a) => a, + Err(e) => { + log_info!(self.logger, "Didnt find candidate inputs: {}", e); + return; + }, + }; + match provisional_proposal.try_preserving_privacy(candidate_inputs) { + Ok(selected_outpoint) => { + if let Some(selected_utxo) = utxo_set.iter().find(|i| { + i.outpoint.txid == selected_outpoint.txid + && i.outpoint.vout == selected_outpoint.vout + }) { + let txo_to_contribute = bitcoin::TxOut { + value: selected_utxo.txout.value, + script_pubkey: selected_utxo.txout.script_pubkey.clone(), + }; + let outpoint_to_contribute = bitcoin::OutPoint { + txid: selected_utxo.outpoint.txid, + vout: selected_utxo.outpoint.vout, + }; + provisional_proposal + .contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + } + }, + Err(_) => { + log_info!(self.logger, "Failed to select utxos to improve payjoin request privacy. Payjoin proceeds regardless"); + }, + }; + // Finalise Payjoin Proposal + let mut payjoin_proposal = match provisional_proposal.finalize_proposal( + |psbt| { + self.wallet.prepare_payjoin_proposal(psbt.clone()).map_err(|e| { + log_info!(self.logger, "Payjoin Receiver: Unable to sign proposal {}", e); + payjoin::Error::Server(e.into()) + }) + }, + None, + ) { + Ok(proposal) => proposal, + Err(e) => { + log_info!(self.logger, "Payjoin Receiver: Unable to finalize proposal {}", e); + return; + }, + }; + + let (receiver_request, _) = match payjoin_proposal.extract_v2_req() { + Ok(req) => req, + Err(e) => { + log_info!(self.logger, "Payjoin Receiver: Unable to extract V2 request {}", e); + return; + }, + }; + // Send Payjoin Proposal response back to Payjoin sender + match reqwest::Client::new() + .post(&receiver_request.url.to_string()) + .body(receiver_request.body) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => { + if response.status() == reqwest::StatusCode::OK { + log_info!(self.logger, "Payjoin Receiver: Payjoin response sent to sender"); + } else { + log_info!( + self.logger, + "Payjoin Receiver: Got non-200 response from directory {}", + response.status() + ); + } + }, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to make request to directory {}", + e + ); + }, + }; + } + + /// Enrolls a Payjoin receiver with the specified Payjoin directory. + /// + /// If directory `ohttp_keys` are not provided, they will be fetched from the directory using + /// the Payjoin relay as proxy to improve privacy and not expose users IP address. + async fn enroll(&self) -> Result<(), Error> { + let ohttp_keys = match self.ohttp_keys.read().await.deref() { + Some(okeys) => okeys.clone(), + None => { + let payjoin_directory = &self.payjoin_directory; + let payjoin_directory = match payjoin_directory.join("/ohttp-keys") { + Ok(payjoin_directory) => payjoin_directory, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to construct ohttp keys url {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let proxy = match reqwest::Proxy::all(self.payjoin_relay.to_string()) { + Ok(proxy) => proxy, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to construct reqwest proxy {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let client = match reqwest::Client::builder().proxy(proxy).build() { + Ok(client) => client, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to construct reqwest client {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let response = match client.get(payjoin_directory).send().await { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Unable to make request to fetch ohttp keys {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + if response.status() != reqwest::StatusCode::OK { + log_info!( + self.logger, + "Payjoin Receiver: Got non 200 response when fetching ohttp keys {}", + response.status() + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + } + let response = match response.bytes().await { + Ok(response) => response, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: Error reading ohttp keys response {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + OhttpKeys::decode(response.to_vec().as_slice()).map_err(|e| { + log_info!(self.logger, "Payjoin Receiver: Unable to decode ohttp keys {}", e); + Error::PayjoinReceiverEnrollementFailed + })? + }, + }; + let mut enroller = Enroller::from_directory_config( + self.payjoin_directory.clone(), + ohttp_keys.clone(), + self.payjoin_relay.clone(), + ); + let (req, ctx) = match enroller.extract_req() { + Ok(req) => req, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: unable to extract enrollement request {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let response = match reqwest::Client::new() + .post(&req.url.to_string()) + .body(req.body) + .headers(ohttp_headers()) + .send() + .await + { + Ok(response) => response, + Err(_) => { + log_info!(self.logger, "Payjoin Receiver: unable to make enrollement request"); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + let response = match response.bytes().await { + Ok(response) => response, + Err(_) => { + panic!("Error reading response"); + }, + }; + let enrolled = match enroller.process_res(response.to_vec().as_slice(), ctx) { + Ok(enrolled) => enrolled, + Err(e) => { + log_info!( + self.logger, + "Payjoin Receiver: unable to process enrollement response {}", + e + ); + return Err(Error::PayjoinReceiverEnrollementFailed); + }, + }; + + *self.ohttp_keys.write().await = Some(ohttp_keys); + *self.enrolled.write().await = Some(enrolled); + Ok(()) + } + + async fn is_enrolled(&self) -> bool { + self.enrolled.read().await.deref().is_some() + && self.ohttp_keys.read().await.deref().is_some() + } + + /// Validate an incoming Payjoin request as specified in [BIP78]. + /// + /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist + async fn validate_payjoin_request( + &self, proposal: UncheckedProposal, + ) -> Result { + let wallet = &self.wallet; + let proposal = proposal.assume_interactive_receiver(); + let proposal = proposal + .check_inputs_not_owned(|script| { + Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false)) + }) + .map_err(|e| { + log_info!(self.logger, "Inputs owned by us check failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + let proposal = proposal.check_no_mixed_input_scripts().map_err(|e| { + log_info!(self.logger, "Mixed input scripts check failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + // Fixme: discuss how to handle this, instead of the Ok(false) we should have a way to + // store seen outpoints and check against them + let proposal = + proposal.check_no_inputs_seen_before(|_outpoint| Ok(false)).map_err(|e| { + log_info!(self.logger, "Inputs seen before check failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + let provisional_proposal = proposal + .identify_receiver_outputs(|script| { + Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false)) + }) + .map_err(|e| { + log_info!(self.logger, "Identify receiver outputs failed {}", e); + Error::PayjoinReceiverRequestValidationFailed + })?; + Ok(provisional_proposal) + } +} diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs index 9d93debf4..63b4f3609 100644 --- a/src/payment/payjoin/mod.rs +++ b/src/payment/payjoin/mod.rs @@ -1,11 +1,11 @@ //! Holds a payment handler allowing to send Payjoin payments. -use lightning::util::logger::Logger; -use lightning::{log_error, log_info}; - use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; -use crate::logger::FilesystemLogger; +use crate::logger::{log_info, log_error, FilesystemLogger, Logger}; use crate::types::Wallet; +use payjoin::PjUri; + +use crate::payjoin_receiver::PayjoinReceiver; use crate::{error::Error, Config}; use std::sync::{Arc, RwLock}; @@ -59,6 +59,7 @@ use handler::PayjoinHandler; pub struct PayjoinPayment { runtime: Arc>>, handler: Option>, + receiver: Option>, config: Arc, logger: Arc, wallet: Arc, @@ -67,10 +68,10 @@ pub struct PayjoinPayment { impl PayjoinPayment { pub(crate) fn new( runtime: Arc>>, - handler: Option>, config: Arc, logger: Arc, - wallet: Arc, + handler: Option>, receiver: Option>, + config: Arc, logger: Arc, wallet: Arc, ) -> Self { - Self { runtime, handler, config, logger, wallet } + Self { runtime, handler, receiver, config, logger, wallet } } /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. @@ -177,4 +178,23 @@ impl PayjoinPayment { payjoin_uri.amount = Some(bitcoin::Amount::from_sat(amount_sats)); self.send(payjoin_uri.to_string()) } + + /// Receive onchain Payjoin transaction. + /// + /// This method will enroll with the configured Payjoin directory if not already, + /// and returns a [BIP21] URI pointing to our enrolled subdirectory that you can share with + /// Payjoin sender. + /// + /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub async fn receive(&self, amount: bitcoin::Amount) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + if let Some(receiver) = &self.receiver { + receiver.receive(amount).await + } else { + Err(Error::PayjoinReceiverUnavailable) + } + } } diff --git a/src/wallet.rs b/src/wallet.rs index 6ef136141..a1694d345 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -19,7 +19,7 @@ use lightning::util::message_signing; use bdk::blockchain::EsploraBlockchain; use bdk::database::BatchDatabase; use bdk::wallet::AddressIndex; -use bdk::{Balance, FeeRate}; +use bdk::{Balance, FeeRate, LocalUtxo}; use bdk::{SignOptions, SyncOptions}; use bitcoin::address::{NetworkChecked, Payload, WitnessVersion}; @@ -35,6 +35,7 @@ use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::{ScriptBuf, Transaction, TxOut, Txid}; +use std::collections::{BTreeMap, HashMap}; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; @@ -164,7 +165,6 @@ where .get_est_sat_per_1000_weight(ConfirmationTarget::OutputSpendingFee) as f32, }; let fee_rate = FeeRate::from_sat_per_kwu(fee_rate); - let locked_wallet = self.inner.lock().unwrap(); let mut tx_builder = locked_wallet.build_tx(); tx_builder.add_recipient(output_script, amount).fee_rate(fee_rate).enable_rbf(); let mut psbt = match tx_builder.finish() { @@ -205,12 +205,46 @@ where } } } - let wallet = self.inner.lock().unwrap(); let is_signed = wallet.sign(payjoin_proposal_psbt, SignOptions::default())?; Ok(is_signed) } + // Returns a list of unspent outputs that can be used as inputs to improve the privacy of a + // payjoin transaction. + pub(crate) fn payjoin_receiver_candidate_input( + &self, + ) -> Result<(HashMap, Vec), Error> { + let locked_wallet = self.inner.lock().unwrap(); + let utxo_set = locked_wallet.list_unspent()?; + let candidate_inputs = utxo_set + .iter() + .filter_map(|utxo| { + if !utxo.is_spent { + Some((bitcoin::Amount::from_sat(utxo.txout.value), utxo.outpoint)) + } else { + None + } + }) + .collect(); + Ok((candidate_inputs, utxo_set)) + } + + pub(crate) fn prepare_payjoin_proposal(&self, mut psbt: Psbt) -> Result { + let wallet = self.inner.lock().unwrap(); + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + wallet.sign(&mut psbt, sign_options)?; + // Clear derivation paths from the PSBT as required by BIP78/BIP77 + psbt.inputs.iter_mut().for_each(|i| { + i.bip32_derivation = BTreeMap::new(); + }); + psbt.outputs.iter_mut().for_each(|o| { + o.bip32_derivation = BTreeMap::new(); + }); + Ok(psbt) + } + pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result { let locked_wallet = self.inner.lock().unwrap(); Ok(locked_wallet.is_mine(script)?) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 5959bd58e..a56982023 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -147,6 +147,23 @@ macro_rules! expect_payment_successful_event { pub(crate) use expect_payment_successful_event; +macro_rules! expect_payjoin_tx_sent_successfully_event { + ($node: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinTxSendSuccess { txid } => { + println!("{} got event {:?}", $node.node_id(), e); + $node.event_handled(); + txid + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; +} + +pub(crate) use expect_payjoin_tx_sent_successfully_event; + pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let bitcoind_exe = env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect( @@ -258,6 +275,22 @@ pub(crate) fn setup_two_nodes( (node_a, node_b) } +pub(crate) fn setup_two_payjoin_nodes( + electrsd: &ElectrsD, allow_0conf: bool, +) -> (TestNode, TestNode) { + println!("== Node A =="); + let config_a = random_config(false); + let node_a_payjoin_receiver = setup_payjoin_node(electrsd, config_a); + + println!("\n== Node B =="); + let mut config_b = random_config(false); + if allow_0conf { + config_b.trusted_peers_0conf.push(node_a_payjoin_receiver.node_id()); + } + let node_b_payjoin_sender = setup_payjoin_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); @@ -270,6 +303,21 @@ pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { node } +pub(crate) fn setup_payjoin_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 = "https://payjo.in".to_string(); + let payjoin_relay = "https://pj.bobspacebkk.com".to_string(); + builder.set_payjoin_config(payjoin_directory, payjoin_relay, None).unwrap(); + 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 generate_blocks_and_wait( bitcoind: &BitcoindClient, electrs: &E, num: usize, ) { diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs new file mode 100644 index 000000000..d57dc7041 --- /dev/null +++ b/tests/integration_tests_payjoin.rs @@ -0,0 +1,45 @@ +mod common; + +use common::{ + expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_payjoin_nodes, + wait_for_tx, +}; + +use bitcoin::Amount; +use ldk_node::Event; + +#[test] +fn send_receive_regular_payjoin_transaction() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false); + let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap(); + let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b, addr_a], + Amount::from_sat(premine_amount_sat), + ); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(node_a_pj_receiver.next_event(), None); + let payjoin_payment = node_a_pj_receiver.payjoin_payment(); + + let payjoin_uri = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { + let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).await.unwrap(); + payjoin_uri + }); + let payjoin_uri = payjoin_uri.to_string(); + let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); + assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); + let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_b_pj_sender.sync_wallets().unwrap(); + let node_b_balance = node_b_pj_sender.list_balances(); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); +} From bd8074fa9e009361527d24270a52c7f3ba826eb6 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Fri, 24 May 2024 15:44:55 +0300 Subject: [PATCH 4/7] Open LN channel from incoming Payjoin tx This commit allows users to schedule a channel that will opened once a Payjoin request received. This can save users 1 extra onchain transaction fees. The Payjoin flow is normal with the following caveats: 1. We use `Payjoin::ProvisionalProposal::substitue_output_address` to point to the multisig output script as retrived from `LdkEvent::FundingGeneratingReady`. 2. We dont try to preserve privacy in Payjoin channel opening transactions. 3. We wait with our response to the Payjoin sender until a `Ldk::Event::FundingTxBroadcastSafe` event is received. --- Cargo.toml | 38 +-- src/builder.rs | 3 +- src/event.rs | 60 ++++- src/lib.rs | 8 + src/payjoin_channel_scheduler.rs | 251 ++++++++++++++++++ src/payjoin_receiver.rs | 120 ++++++++- src/payment/payjoin/mod.rs | 96 ++++++- src/wallet.rs | 12 + tests/integration_tests_payjoin.rs | 7 +- ...tion_tests_payjoin_with_channel_opening.rs | 75 ++++++ 10 files changed, 630 insertions(+), 40 deletions(-) create mode 100644 src/payjoin_channel_scheduler.rs create mode 100644 tests/integration_tests_payjoin_with_channel_opening.rs diff --git a/Cargo.toml b/Cargo.toml index c0212fc4c..3b9ac2796 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,23 +28,23 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.0.123", features = ["std"] } -lightning-invoice = { version = "0.31.0" } -lightning-net-tokio = { version = "0.0.123" } -lightning-persister = { version = "0.0.123" } -lightning-background-processor = { version = "0.0.123", features = ["futures"] } -lightning-rapid-gossip-sync = { version = "0.0.123" } -lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] } -lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } - -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] } -#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["futures"] } -#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["esplora-async"] } -#lightning-liquidity = { git = "https://github.com/lightningdevkit/lightning-liquidity", branch="main", features = ["std"] } +# lightning = { version = "0.0.123", features = ["std"] } +# lightning-invoice = { version = "0.31.0" } +# lightning-net-tokio = { version = "0.0.123" } +# lightning-persister = { version = "0.0.123" } +# lightning-background-processor = { version = "0.0.123", features = ["futures"] } +# lightning-rapid-gossip-sync = { version = "0.0.123" } +# lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] } +# lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] } + +lightning = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["std"] } +lightning-invoice = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-net-tokio = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-persister = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-background-processor = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["futures"] } +lightning-rapid-gossip-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" } +lightning-transaction-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["esplora-async"] } +lightning-liquidity = { git = "https://github.com/jbesraa/lightning-liquidity", branch="pj-fixes", features = ["std"] } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-invoice = { path = "../rust-lightning/lightning-invoice" } @@ -78,8 +78,8 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { version = "0.0.123", features = ["std", "_test_utils"] } -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +# lightning = { version = "0.0.123", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["std", "_test_utils"] } electrum-client = { version = "0.15.1", default-features = true } bitcoincore-rpc = { version = "0.17.0", default-features = false } proptest = "1.0.0" diff --git a/src/builder.rs b/src/builder.rs index 8fa22423e..de812d619 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1028,12 +1028,13 @@ fn build_with_store_internal( payjoin_receiver = Some(Arc::new(PayjoinReceiver::new( Arc::clone(&logger), Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&config), pj_config.payjoin_directory.clone(), pj_config.payjoin_relay.clone(), pj_config.ohttp_keys.clone(), ))); } - let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); diff --git a/src/event.rs b/src/event.rs index 32db5663e..06713baa0 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,3 +1,4 @@ +use crate::payjoin_receiver::PayjoinReceiver; use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ @@ -435,6 +436,7 @@ where network_graph: Arc, payment_store: Arc>, peer_store: Arc>, + payjoin_receiver: Option>, runtime: Arc>>, logger: L, config: Arc, @@ -449,8 +451,9 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, - payment_store: Arc>, peer_store: Arc>, - runtime: Arc>>, logger: L, config: Arc, + payment_store: Arc>, payjoin_receiver: Option>, + peer_store: Arc>, runtime: Arc>>, + logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -461,6 +464,7 @@ where output_sweeper, network_graph, payment_store, + payjoin_receiver, peer_store, logger, runtime, @@ -475,6 +479,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 @@ -485,6 +490,18 @@ where let cur_height = self.channel_manager.current_best_block().height; let locktime = LockTime::from_height(cur_height).unwrap_or(LockTime::ZERO); + if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + if payjoin_receiver + .set_channel_accepted( + user_channel_id, + &output_script, + temporary_channel_id.0, + ) + .await + { + return; + } + } // Sign the final funding transaction and broadcast it. match self.wallet.create_funding_transaction( output_script, @@ -1147,6 +1164,45 @@ where ); } }, + LdkEvent::FundingTxBroadcastSafe { funding_tx, .. } => { + use crate::io::utils::ohttp_headers; + if let Some(payjoin_receiver) = self.payjoin_receiver.clone() { + let is_payjoin_channel = + payjoin_receiver.set_funding_tx_signed(funding_tx.clone()).await; + if let Some((url, body)) = is_payjoin_channel { + log_info!( + self.logger, + "Detected payjoin channel transaction. Sending payjoin sender request for transaction {}", + funding_tx.txid() + ); + let headers = ohttp_headers(); + let client = reqwest::Client::builder().build().unwrap(); + match client.post(url).body(body).headers(headers).send().await { + Ok(response) => { + if response.status().is_success() { + log_info!( + self.logger, + "Responded to 'Payjoin Sender' successfuly" + ); + } else { + log_info!( + self.logger, + "Got unsuccessful response from 'Payjoin Sender': {}", + response.status() + ); + } + }, + Err(e) => { + log_error!( + self.logger, + "Failed to send a response to 'Payjoin Sender': {}", + e + ); + }, + }; + } + } + }, LdkEvent::ChannelPending { channel_id, user_channel_id, diff --git a/src/lib.rs b/src/lib.rs index 291780359..be1b6987b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +mod payjoin_channel_scheduler; mod payjoin_receiver; pub mod payment; mod peer_store; @@ -726,6 +727,7 @@ impl Node { Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), Arc::clone(&self.payment_store), + self.payjoin_receiver.clone(), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), Arc::clone(&self.logger), @@ -1112,6 +1114,9 @@ impl Node { Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.wallet), + Arc::clone(&self.peer_store), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), ) } @@ -1132,6 +1137,9 @@ impl Node { Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.wallet), + Arc::clone(&self.peer_store), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), )) } diff --git a/src/payjoin_channel_scheduler.rs b/src/payjoin_channel_scheduler.rs new file mode 100644 index 000000000..6f30e3252 --- /dev/null +++ b/src/payjoin_channel_scheduler.rs @@ -0,0 +1,251 @@ +use bitcoin::{secp256k1::PublicKey, Network, ScriptBuf, TxOut}; + +#[derive(Clone)] +pub struct PayjoinChannelScheduler { + channels: Vec, +} + +impl PayjoinChannelScheduler { + pub(crate) fn new() -> Self { + Self { channels: vec![] } + } + + pub(crate) fn schedule( + &mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, + channel_id: u128, + ) { + let channel = PayjoinChannel::new(channel_value_satoshi, counterparty_node_id, channel_id); + match channel.state { + ScheduledChannelState::ChannelCreated => { + self.channels.push(channel); + }, + _ => {}, + } + } + + pub(crate) fn set_channel_accepted( + &mut self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + for channel in &mut self.channels { + if channel.channel_id() == channel_id { + channel.state.set_channel_accepted(output_script, temporary_channel_id); + return true; + } + } + false + } + + pub(crate) fn set_funding_tx_created( + &mut self, channel_id: u128, url: &payjoin::Url, body: Vec, + ) -> bool { + for channel in &mut self.channels { + if channel.channel_id() == channel_id { + return channel.state.set_channel_funding_tx_created(url.clone(), body); + } + } + false + } + + pub(crate) fn set_funding_tx_signed( + &mut self, tx: bitcoin::Transaction, + ) -> Option<(payjoin::Url, Vec)> { + for output in tx.output.iter() { + if let Some(mut channel) = self.internal_find_by_tx_out(&output.clone()) { + let info = channel.request_info(); + if info.is_some() && channel.state.set_channel_funding_tx_signed(output.clone()) { + return info; + } + } + } + None + } + + /// Get the next channel matching the given channel amount. + /// + /// The channel must be in accepted state. + /// + /// If more than one channel matches the given channel amount, the channel with the oldest + /// creation date will be returned. + pub(crate) fn get_next_channel( + &self, channel_amount: bitcoin::Amount, network: Network, + ) -> Option<(u128, bitcoin::Address, [u8; 32], bitcoin::Amount, bitcoin::secp256k1::PublicKey)> + { + let channel = self + .channels + .iter() + .filter(|channel| { + channel.channel_value_satoshi() == channel_amount + && channel.is_channel_accepted() + && channel.output_script().is_some() + && channel.temporary_channel_id().is_some() + }) + .min_by_key(|channel| channel.created_at()); + + if let Some(channel) = channel { + let address = bitcoin::Address::from_script(&channel.output_script().unwrap(), network); + if let Ok(address) = address { + return Some(( + channel.channel_id(), + address, + channel.temporary_channel_id().unwrap(), + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + )); + } + }; + None + } + + fn internal_find_by_tx_out(&self, txout: &TxOut) -> Option { + let channel = self.channels.iter().find(|channel| { + return Some(&txout.script_pubkey) == channel.output_script(); + }); + channel.cloned() + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PayjoinChannel { + state: ScheduledChannelState, + channel_value_satoshi: bitcoin::Amount, + channel_id: u128, + counterparty_node_id: PublicKey, + created_at: u64, +} + +impl PayjoinChannel { + pub(crate) fn new( + channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128, + ) -> Self { + Self { + state: ScheduledChannelState::ChannelCreated, + channel_value_satoshi, + channel_id, + counterparty_node_id, + created_at: 0, + } + } + + fn is_channel_accepted(&self) -> bool { + match self.state { + ScheduledChannelState::ChannelAccepted(..) => true, + _ => false, + } + } + + pub(crate) fn channel_value_satoshi(&self) -> bitcoin::Amount { + self.channel_value_satoshi + } + + pub(crate) fn channel_id(&self) -> u128 { + self.channel_id + } + + pub(crate) fn counterparty_node_id(&self) -> PublicKey { + self.counterparty_node_id + } + + pub(crate) fn output_script(&self) -> Option<&ScriptBuf> { + self.state.output_script() + } + + pub(crate) fn temporary_channel_id(&self) -> Option<[u8; 32]> { + self.state.temporary_channel_id() + } + + pub(crate) fn request_info(&self) -> Option<(payjoin::Url, Vec)> { + match &self.state { + ScheduledChannelState::FundingTxCreated(_, url, body) => { + Some((url.clone(), body.clone())) + }, + _ => None, + } + } + + fn created_at(&self) -> u64 { + self.created_at + } +} + +#[derive(Clone, Debug)] +struct FundingTxParams { + output_script: ScriptBuf, + temporary_channel_id: [u8; 32], +} + +impl FundingTxParams { + fn new(output_script: ScriptBuf, temporary_channel_id: [u8; 32]) -> Self { + Self { output_script, temporary_channel_id } + } +} + +#[derive(Clone, Debug)] +enum ScheduledChannelState { + ChannelCreated, + ChannelAccepted(FundingTxParams), + FundingTxCreated(FundingTxParams, payjoin::Url, Vec), + FundingTxSigned(FundingTxParams, ()), +} + +impl ScheduledChannelState { + fn output_script(&self) -> Option<&ScriptBuf> { + match self { + ScheduledChannelState::ChannelAccepted(funding_tx_params) => { + Some(&funding_tx_params.output_script) + }, + ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => { + Some(&funding_tx_params.output_script) + }, + ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => { + Some(&funding_tx_params.output_script) + }, + _ => None, + } + } + + fn temporary_channel_id(&self) -> Option<[u8; 32]> { + match self { + ScheduledChannelState::ChannelAccepted(funding_tx_params) => { + Some(funding_tx_params.temporary_channel_id) + }, + ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => { + Some(funding_tx_params.temporary_channel_id) + }, + ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => { + Some(funding_tx_params.temporary_channel_id) + }, + _ => None, + } + } + + fn set_channel_accepted( + &mut self, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + if let ScheduledChannelState::ChannelCreated = self { + *self = ScheduledChannelState::ChannelAccepted(FundingTxParams::new( + output_script.clone(), + temporary_channel_id, + )); + return true; + } + return false; + } + + fn set_channel_funding_tx_created(&mut self, url: payjoin::Url, body: Vec) -> bool { + if let ScheduledChannelState::ChannelAccepted(funding_tx_params) = self { + *self = ScheduledChannelState::FundingTxCreated(funding_tx_params.clone(), url, body); + return true; + } + return false; + } + + fn set_channel_funding_tx_signed(&mut self, output: TxOut) -> bool { + let mut res = false; + if let ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) = self { + assert_eq!(funding_tx_params.output_script, output.script_pubkey); + *self = ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), ()); + res = true; + } + return res; + } +} diff --git a/src/payjoin_receiver.rs b/src/payjoin_receiver.rs index 86e0d6675..c1ef51061 100644 --- a/src/payjoin_receiver.rs +++ b/src/payjoin_receiver.rs @@ -1,7 +1,11 @@ use crate::error::Error; use crate::io::utils::ohttp_headers; use crate::logger::FilesystemLogger; -use crate::types::Wallet; +use crate::payjoin_channel_scheduler::{PayjoinChannel, PayjoinChannelScheduler}; +use crate::types::{ChannelManager, Wallet}; +use crate::Config; +use bitcoin::{ScriptBuf, Transaction}; +use lightning::ln::ChannelId; use lightning::log_info; use lightning::util::logger::Logger; use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal}; @@ -17,6 +21,8 @@ use tokio::sync::RwLock; pub(crate) struct PayjoinReceiver { logger: Arc, wallet: Arc, + channel_manager: Arc, + channel_scheduler: RwLock, /// Directory receiver wish to enroll with payjoin_directory: Url, /// Proxy server receiver wish to make requests through @@ -28,16 +34,21 @@ pub(crate) struct PayjoinReceiver { /// Optional as they can be fetched on behalf of the user if not provided. /// They are required in order to enroll. ohttp_keys: RwLock>, + config: Arc, } impl PayjoinReceiver { pub(crate) fn new( - logger: Arc, wallet: Arc, payjoin_directory: payjoin::Url, - payjoin_relay: payjoin::Url, ohttp_keys: Option, + logger: Arc, wallet: Arc, channel_manager: Arc, + config: Arc, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url, + ohttp_keys: Option, ) -> Self { Self { logger, wallet, + channel_manager, + channel_scheduler: RwLock::new(PayjoinChannelScheduler::new()), + config, payjoin_directory, payjoin_relay, enrolled: RwLock::new(None), @@ -85,13 +96,21 @@ impl PayjoinReceiver { Ok(payjoin_uri) } + pub(crate) async fn set_channel_accepted( + &self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32], + ) -> bool { + let mut scheduler = self.channel_scheduler.write().await; + scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id) + } + /// After enrolling, we should periodacly check if we have received any Payjoin transactions. /// /// This function will try to fetch pending Payjoin requests from the subdirectory, and if a - /// successful response received, we validate the request as specified in [`BIP78`]. After - /// validation we try to preserve privacy by adding more inputs/outputs to the transaction. - /// Last, we finalise the transaction and send a response back the the Payjoin sender. - /// + /// successful response received, we validate the request as specified in [BIP78]. After + /// validation we check if we have a pending matching channel, and if so, we try fund the channel + /// with the incoming funds from the payjoin request. Otherwise, we accept the Payjoin request + /// normally by trying to preserve privacy, finalise the Payjoin proposal and send it back the + /// the Payjoin sender. /// /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist pub(crate) async fn process_payjoin_request(&self) { @@ -163,6 +182,7 @@ impl PayjoinReceiver { return; }, }; + let original_tx = unchecked_proposal.extract_tx_to_schedule_broadcast(); let provisional_proposal = match self.validate_payjoin_request(unchecked_proposal).await { Ok(proposal) => proposal, @@ -171,7 +191,68 @@ impl PayjoinReceiver { return; }, }; - self.accept_payjoin_transaction(provisional_proposal).await; + let amount = match self.wallet.funds_directed_to_us(&original_tx) { + Ok(a) => a, + Err(e) => { + // This should not happen in practice as the validation checks would fail if + // the sender didnt include us in the outputs + log_info!(self.logger, "Not able to find any ouput directed to us: {}", e); + return; + }, + }; + let mut scheduler = self.channel_scheduler.write().await; + let network = self.config.network; + if let Some(channel) = scheduler.get_next_channel(amount, network) { + log_info!(self.logger, "Found a channel match for incoming Payjoin request"); + let (channel_id, funding_tx_address, temporary_channel_id, _, counterparty_node_id) = + channel; + let mut channel_provisional_proposal = provisional_proposal.clone(); + channel_provisional_proposal.substitute_output_address(funding_tx_address); + let payjoin_proposal = match channel_provisional_proposal + .finalize_proposal(|psbt| Ok(psbt.clone()), None) + { + Ok(proposal) => proposal, + Err(e) => { + dbg!(&e); + return; + }, + }; + let (receiver_request, _) = match payjoin_proposal.clone().extract_v2_req() { + Ok((req, ctx)) => (req, ctx), + Err(e) => { + dbg!(&e); + return; + }, + }; + let tx = payjoin_proposal.psbt().clone().extract_tx(); + scheduler.set_funding_tx_created( + channel_id, + &receiver_request.url, + receiver_request.body, + ); + match self.channel_manager.unsafe_manual_funding_transaction_generated( + &ChannelId::from_bytes(temporary_channel_id), + &counterparty_node_id, + tx.clone(), + ) { + Ok(_) => { + // Created Funding Transaction and waiting for `FundingTxBroadcastSafe` event before returning a response + log_info!(self.logger, "Created channel funding transaction from Payjoin request and waiting for `FundingTxBroadcastSafe`"); + }, + Err(_) => { + log_info!( + self.logger, + "Unable to channel create funding tx from Payjoin request" + ); + }, + } + } else { + log_info!( + self.logger, + "Couldnt match a channel to Payjoin request, accepting normally" + ); + self.accept_payjoin_transaction(provisional_proposal).await; + } } else { log_info!(self.logger, "Payjoin Receiver: Unable to get enrolled object"); } @@ -396,6 +477,29 @@ impl PayjoinReceiver { && self.ohttp_keys.read().await.deref().is_some() } + /// Schedule a channel to opened upon receiving a Payjoin tranasction value with the same + /// channel funding amount. + pub(crate) async fn schedule_channel( + &self, amount: bitcoin::Amount, counterparty_node_id: bitcoin::secp256k1::PublicKey, + channel_id: u128, + ) { + let channel = PayjoinChannel::new(amount, counterparty_node_id, channel_id); + self.channel_scheduler.write().await.schedule( + channel.channel_value_satoshi(), + channel.counterparty_node_id(), + channel.channel_id(), + ); + } + + /// This should only be called upon receiving [`Event::FundingTxBroadcastSafe`] + /// + /// [`Event::FundingTxBroadcastSafe`]: lightning::events::Event::FundingTxBroadcastSafe + pub(crate) async fn set_funding_tx_signed( + &self, funding_tx: Transaction, + ) -> Option<(payjoin::Url, Vec)> { + self.channel_scheduler.write().await.set_funding_tx_signed(funding_tx) + } + /// Validate an incoming Payjoin request as specified in [BIP78]. /// /// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs index 63b4f3609..cc91c9f5e 100644 --- a/src/payment/payjoin/mod.rs +++ b/src/payment/payjoin/mod.rs @@ -1,11 +1,16 @@ //! Holds a payment handler allowing to send Payjoin payments. use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; -use crate::logger::{log_info, log_error, FilesystemLogger, Logger}; -use crate::types::Wallet; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::types::{ChannelManager, Wallet}; +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::SocketAddress; +use lightning::util::config::{ChannelHandshakeConfig, UserConfig}; use payjoin::PjUri; +use crate::connection::ConnectionManager; use crate::payjoin_receiver::PayjoinReceiver; +use crate::peer_store::{PeerInfo, PeerStore}; use crate::{error::Error, Config}; use std::sync::{Arc, RwLock}; @@ -63,6 +68,9 @@ pub struct PayjoinPayment { config: Arc, logger: Arc, wallet: Arc, + peer_store: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, } impl PayjoinPayment { @@ -70,8 +78,10 @@ impl PayjoinPayment { runtime: Arc>>, handler: Option>, receiver: Option>, config: Arc, logger: Arc, wallet: Arc, + peer_store: Arc>>, channel_manager: Arc, + connection_manager: Arc>>, ) -> Self { - Self { runtime, handler, receiver, config, logger, wallet } + Self { runtime, handler, receiver, config, logger, wallet, peer_store, channel_manager, connection_manager } } /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. @@ -186,13 +196,89 @@ impl PayjoinPayment { /// Payjoin sender. /// /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki - pub async fn receive(&self, amount: bitcoin::Amount) -> Result { + pub fn receive(&self, amount: bitcoin::Amount) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } if let Some(receiver) = &self.receiver { - receiver.receive(amount).await + let runtime = rt_lock.as_ref().unwrap(); + runtime.handle().block_on(async { receiver.receive(amount).await }) + } else { + Err(Error::PayjoinReceiverUnavailable) + } + } + + /// Receive on chain Payjoin transaction and open a channel in a single transaction. + /// + /// This method will enroll with the configured Payjoin directory if not already, + /// and before returning a [BIP21] URI pointing to our enrolled subdirectory to share with + /// Payjoin sender, we start the channel opening process and halt it when we receive + /// `accept_channel` from counterparty node. Once the Payjoin request is received, we move + /// forward with the channel opening process. + /// + /// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub fn receive_with_channel_opening( + &self, channel_amount_sats: u64, push_msat: Option, announce_channel: bool, + node_id: PublicKey, address: SocketAddress, + ) -> Result { + use rand::Rng; + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + if let Some(receiver) = &self.receiver { + let user_channel_id: u128 = rand::thread_rng().gen::(); + let runtime = rt_lock.as_ref().unwrap(); + runtime.handle().block_on(async { + receiver + .schedule_channel( + bitcoin::Amount::from_sat(channel_amount_sats), + node_id, + user_channel_id, + ) + .await; + }); + let user_config = UserConfig { + channel_handshake_limits: Default::default(), + channel_handshake_config: ChannelHandshakeConfig { + announced_channel: announce_channel, + ..Default::default() + }, + ..Default::default() + }; + let push_msat = push_msat.unwrap_or(0); + let peer_info = PeerInfo { node_id, address }; + + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + runtime.handle().block_on(async { + let _ = con_cm.connect_peer_if_necessary(con_node_id, con_addr).await; + }); + + match self.channel_manager.create_channel( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) { + Ok(_) => { + self.peer_store.add_peer(peer_info)?; + }, + Err(_) => { + return Err(Error::ChannelCreationFailed); + }, + }; + + runtime.handle().block_on(async { + let payjoin_uri = + receiver.receive(bitcoin::Amount::from_sat(channel_amount_sats)).await?; + Ok(payjoin_uri) + }) } else { Err(Error::PayjoinReceiverUnavailable) } diff --git a/src/wallet.rs b/src/wallet.rs index a1694d345..c8e5ddb23 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -151,6 +151,18 @@ where res } + // Returns the total value of all outputs in the given transaction that are directed to us + pub(crate) fn funds_directed_to_us(&self, tx: &Transaction) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let total_value = tx.output.iter().fold(0, |acc, output| { + match locked_wallet.is_mine(&output.script_pubkey) { + Ok(true) => acc + output.value, + _ => acc, + } + }); + Ok(bitcoin::Amount::from_sat(total_value)) + } + pub(crate) fn build_payjoin_transaction( &self, payjoin_uri: payjoin::Uri, ) -> Result { diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs index d57dc7041..9a85897ac 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -28,12 +28,9 @@ fn send_receive_regular_payjoin_transaction() { assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00); assert_eq!(node_a_pj_receiver.next_event(), None); let payjoin_payment = node_a_pj_receiver.payjoin_payment(); - - let payjoin_uri = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { - let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).await.unwrap(); - payjoin_uri - }); + let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).unwrap(); let payjoin_uri = payjoin_uri.to_string(); + dbg!(&payjoin_uri); let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); diff --git a/tests/integration_tests_payjoin_with_channel_opening.rs b/tests/integration_tests_payjoin_with_channel_opening.rs new file mode 100644 index 000000000..1b38edfbd --- /dev/null +++ b/tests/integration_tests_payjoin_with_channel_opening.rs @@ -0,0 +1,75 @@ +mod common; + +use common::{ + expect_channel_pending_event, expect_channel_ready_event, + expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_payjoin_nodes, + wait_for_tx, +}; + +use bitcoin::Amount; +use ldk_node::Event; + +#[test] +fn send_receive_payjoin_transaction_with_channel_opening() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false); + let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap(); + let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b, addr_a], + Amount::from_sat(premine_amount_sat), + ); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(node_a_pj_receiver.next_event(), None); + assert_eq!(node_a_pj_receiver.list_channels().len(), 0); + let payjoin_payment = node_a_pj_receiver.payjoin_payment(); + let node_b_listening_address = + node_b_pj_sender.listening_addresses().unwrap().get(0).unwrap().clone(); + let payjoin_uri = payjoin_payment + .receive_with_channel_opening( + 80_000, + None, + false, + node_b_pj_sender.node_id(), + node_b_listening_address, + ).unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); + assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); + expect_channel_pending_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); + expect_channel_pending_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); + let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + let node_b_balance = node_b_pj_sender.list_balances(); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); + + expect_channel_ready_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); + expect_channel_ready_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); + let channels = node_a_pj_receiver.list_channels(); + let channel = channels.get(0).unwrap(); + assert_eq!(channel.channel_value_sats, 80_000); + assert!(channel.is_channel_ready); + assert!(channel.is_usable); + + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_connected, true); + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_persisted, true); + assert_eq!( + node_a_pj_receiver.list_peers().get(0).unwrap().node_id, + node_b_pj_sender.node_id() + ); + + let invoice_amount_1_msat = 2500_000; + let invoice = + node_b_pj_sender.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap(); + assert!(node_a_pj_receiver.bolt11_payment().send(&invoice).is_ok()); +} From 87211050b20d7b366266e587d6bb2ef157020ac8 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Tue, 11 Jun 2024 14:43:13 +0300 Subject: [PATCH 5/7] Add Payjoin example ..to run the example `cargo run --example ldk-node-with-payjoin-support` --- Cargo.toml | 5 ++ examples/ldk-node-with-payjoin-support.rs | 71 +++++++++++++++++++ tests/integration_tests_payjoin.rs | 32 +++++++++ ...tion_tests_payjoin_with_channel_opening.rs | 3 +- 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 examples/ldk-node-with-payjoin-support.rs diff --git a/Cargo.toml b/Cargo.toml index 3b9ac2796..aa43bca67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,3 +102,8 @@ panic = "abort" [profile.dev] panic = "abort" + +[[example]] +name = "ldk-node-with-payjoin-support" +path = "examples/ldk-node-with-payjoin-support.rs" + diff --git a/examples/ldk-node-with-payjoin-support.rs b/examples/ldk-node-with-payjoin-support.rs new file mode 100644 index 000000000..627f172b6 --- /dev/null +++ b/examples/ldk-node-with-payjoin-support.rs @@ -0,0 +1,71 @@ +use ldk_node::bitcoin::Network; +use ldk_node::{Builder, LogLevel}; + +fn main() { + let mut builder = Builder::new(); + builder.set_log_level(LogLevel::Gossip); + builder.set_network(Network::Testnet); + builder.set_esplora_server("https://blockstream.info/testnet/api".to_string()); + builder.set_gossip_source_rgs( + "https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string(), + ); + + // Payjoin directory is needed only if you are setting up Payjoin receiver, + // not required for Payjoin sender. + let payjoin_directory = "https://payjo.in".to_string(); + // Payjoin relay is required for both Payjoin receiver and sender. + let payjoin_relay = "https://pj.bobspacebkk.com".to_string(); + + // Enable sending payjoin transactions + // builder.set_payjoin_sender_config(payjoin_relay.clone()); + // ohttp keys refer to the Payjoin directory keys that are needed for the Payjoin receiver + // enrollement. If those keys are not provided the node will attempt to fetch them for you. + // let ohttp_keys = None; + // Enable receiving payjoin transactions + builder.set_payjoin_config(payjoin_directory, payjoin_relay); + + let node = builder.build().unwrap(); + + node.start().unwrap(); + + // Receiving payjoin transaction + let payjoin_payment = node.payjoin_payment(); + let amount_to_receive = bitcoin::Amount::from_sat(1000); + let payjoin_uri = payjoin_payment.receive(amount_to_receive).unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + + println!("Payjoin URI: {}", payjoin_uri); + + //** Open a channel from incoming payjoin transactions ***// + // let payjoin_payment = node.payjoin_payment(); + // let channel_amount_sats = bitcoin::Amount::from_sat(10000); + // use bitcoin::secp256k1::PublicKey; + // use lightning::ln::msgs::SocketAddress; + // let counterparty_node_id: PublicKey = unimplemented!(); + // let counterparty_address: SocketAddress = unimplemented!(); + // let payjoin_uri = match payjoin_payment.receive_with_channel_opening(channel_amount_sats, None, true, + // counterparty_node_id, counterparty_address, + // ).await { + // Ok(a) => a, + // Err(e) => { + // panic!("{}", e); + // }, + // }; + // let payjoin_uri = payjoin_uri.to_string(); + // println!("Payjoin URI: {}", payjoin_uri); + + //** Sending payjoin transaction **// + // let payjoin_uri = payjoin::Uri::try_from(payjoin_uri).unwrap(); + // match payjoin_payment.send(payjoin_uri, None, None).await { + // Ok(Some(txid)) => { + // dbg!("Sent transaction and got a response. Transaction completed") + // }, + // Ok(None) => { + // dbg!("Sent transaction and got no response. We will keep polling the response for the next 24hours") + // }, + // Err(e) => { + // dbg!(e); + // } + // } + node.stop().unwrap(); +} diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs index 9a85897ac..f482c5382 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -40,3 +40,35 @@ fn send_receive_regular_payjoin_transaction() { let node_b_balance = node_b_pj_sender.list_balances(); assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); } + +#[test] +fn send_payjoin_with_amount() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false); + let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap(); + let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 100_000_00; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_b, addr_a], + Amount::from_sat(premine_amount_sat), + ); + node_a_pj_receiver.sync_wallets().unwrap(); + node_b_pj_sender.sync_wallets().unwrap(); + assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00); + assert_eq!(node_a_pj_receiver.next_event(), None); + let payjoin_payment = node_a_pj_receiver.payjoin_payment(); + let payjoin_uri = payjoin_payment.receive(Amount::from_sat(100_000_000)).unwrap(); + let payjoin_uri = payjoin_uri.to_string(); + dbg!(&payjoin_uri); + let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); + assert!(sender_payjoin_payment.send_with_amount(payjoin_uri, 80_000).is_ok()); + let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_b_pj_sender.sync_wallets().unwrap(); + let node_b_balance = node_b_pj_sender.list_balances(); + assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); +} diff --git a/tests/integration_tests_payjoin_with_channel_opening.rs b/tests/integration_tests_payjoin_with_channel_opening.rs index 1b38edfbd..d57868927 100644 --- a/tests/integration_tests_payjoin_with_channel_opening.rs +++ b/tests/integration_tests_payjoin_with_channel_opening.rs @@ -39,7 +39,8 @@ fn send_receive_payjoin_transaction_with_channel_opening() { false, node_b_pj_sender.node_id(), node_b_listening_address, - ).unwrap(); + ) + .unwrap(); let payjoin_uri = payjoin_uri.to_string(); let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); From a839688cfb87c8c657d14800a3c398b21950478b Mon Sep 17 00:00:00 2001 From: jbesraa Date: Wed, 10 Jul 2024 19:04:05 +0300 Subject: [PATCH 6/7] f --- src/payment/payjoin/mod.rs | 12 +++++++++++- tests/common/mod.rs | 2 +- ...integration_tests_payjoin_with_channel_opening.rs | 5 +---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs index cc91c9f5e..d37020c07 100644 --- a/src/payment/payjoin/mod.rs +++ b/src/payment/payjoin/mod.rs @@ -81,7 +81,17 @@ impl PayjoinPayment { peer_store: Arc>>, channel_manager: Arc, connection_manager: Arc>>, ) -> Self { - Self { runtime, handler, receiver, config, logger, wallet, peer_store, channel_manager, connection_manager } + Self { + runtime, + handler, + receiver, + config, + logger, + wallet, + peer_store, + channel_manager, + connection_manager, + } } /// Send a Payjoin transaction to the address specified in the `payjoin_uri`. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a56982023..44f7de420 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -150,7 +150,7 @@ pub(crate) use expect_payment_successful_event; macro_rules! expect_payjoin_tx_sent_successfully_event { ($node: expr) => {{ match $node.wait_next_event() { - ref e @ Event::PayjoinTxSendSuccess { txid } => { + ref e @ Event::PayjoinPaymentSuccess { txid, .. } => { println!("{} got event {:?}", $node.node_id(), e); $node.event_handled(); txid diff --git a/tests/integration_tests_payjoin_with_channel_opening.rs b/tests/integration_tests_payjoin_with_channel_opening.rs index d57868927..ad2214b6f 100644 --- a/tests/integration_tests_payjoin_with_channel_opening.rs +++ b/tests/integration_tests_payjoin_with_channel_opening.rs @@ -64,10 +64,7 @@ fn send_receive_payjoin_transaction_with_channel_opening() { assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_connected, true); assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_persisted, true); - assert_eq!( - node_a_pj_receiver.list_peers().get(0).unwrap().node_id, - node_b_pj_sender.node_id() - ); + assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().node_id, node_b_pj_sender.node_id()); let invoice_amount_1_msat = 2500_000; let invoice = From b9ba8fb90bef54d53472d29d6d1195dca0349fa8 Mon Sep 17 00:00:00 2001 From: jbesraa Date: Fri, 12 Jul 2024 13:56:50 +0300 Subject: [PATCH 7/7] f --- src/builder.rs | 2 - src/event.rs | 3 + src/lib.rs | 21 +- src/payment/payjoin/handler.rs | 391 ++++++++---------- src/payment/payjoin/mod.rs | 96 +++-- src/tx_broadcaster.rs | 4 +- tests/common/mod.rs | 17 + tests/integration_tests_payjoin.rs | 31 +- ...tion_tests_payjoin_with_channel_opening.rs | 8 +- 9 files changed, 311 insertions(+), 262 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index de812d619..ce456d4c2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1017,8 +1017,6 @@ fn build_with_store_internal( let mut payjoin_receiver = None; if let Some(pj_config) = payjoin_config { payjoin_handler = Some(Arc::new(PayjoinHandler::new( - Arc::clone(&tx_broadcaster), - Arc::clone(&logger), pj_config.payjoin_relay.clone(), Arc::clone(&tx_sync), Arc::clone(&event_queue), diff --git a/src/event.rs b/src/event.rs index 06713baa0..bb04d55f8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -188,6 +188,7 @@ pub enum PayjoinPaymentFailureReason { Timeout, TransactionFinalisationFailed, InvalidReceiverResponse, + RequestFailed, } impl Readable for PayjoinPaymentFailureReason { @@ -196,6 +197,7 @@ impl Readable for PayjoinPaymentFailureReason { 0 => Ok(Self::Timeout), 1 => Ok(Self::TransactionFinalisationFailed), 2 => Ok(Self::InvalidReceiverResponse), + 3 => Ok(Self::RequestFailed), _ => Err(DecodeError::InvalidValue), } } @@ -207,6 +209,7 @@ impl Writeable for PayjoinPaymentFailureReason { Self::Timeout => 0u8.write(writer), Self::TransactionFinalisationFailed => 1u8.write(writer), Self::InvalidReceiverResponse => 2u8.write(writer), + Self::RequestFailed => 3u8.write(writer), } } } diff --git a/src/lib.rs b/src/lib.rs index be1b6987b..71d2f0953 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -374,6 +374,10 @@ impl Node { let archive_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); let sync_logger = Arc::clone(&self.logger); + let sync_payjoin = match &self.payjoin_handler { + Some(pjh) => Some(Arc::clone(pjh)), + None => None, + }; let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height); let mut stop_sync = self.stop_sender.subscribe(); @@ -393,11 +397,14 @@ impl Node { return; } _ = wallet_sync_interval.tick() => { - let confirmables = vec![ + let mut confirmables = vec![ &*sync_cman as &(dyn Confirm + Sync + Send), &*sync_cmon as &(dyn Confirm + Sync + Send), &*sync_sweeper as &(dyn Confirm + Sync + Send), ]; + if let Some(sync_payjoin) = sync_payjoin.as_ref() { + confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); + } let now = Instant::now(); let timeout_fut = tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), tx_sync.sync(confirmables)); match timeout_fut.await { @@ -1114,9 +1121,11 @@ impl Node { Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.wallet), + Arc::clone(&self.tx_broadcaster), Arc::clone(&self.peer_store), Arc::clone(&self.channel_manager), Arc::clone(&self.connection_manager), + Arc::clone(&self.payment_store), ) } @@ -1132,14 +1141,16 @@ impl Node { let payjoin_receiver = self.payjoin_receiver.as_ref(); Arc::new(PayjoinPayment::new( Arc::clone(&self.runtime), - payjoin_sender.map(Arc::clone), + payjoin_handler.map(Arc::clone), payjoin_receiver.map(Arc::clone), Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.wallet), + Arc::clone(&self.tx_broadcaster), Arc::clone(&self.peer_store), Arc::clone(&self.channel_manager), Arc::clone(&self.connection_manager), + Arc::clone(&self.payment_store), )) } @@ -1344,11 +1355,15 @@ impl Node { let fee_estimator = Arc::clone(&self.fee_estimator); let sync_sweeper = Arc::clone(&self.output_sweeper); let sync_logger = Arc::clone(&self.logger); - let confirmables = vec![ + let sync_payjoin = &self.payjoin_handler.as_ref(); + let mut confirmables = vec![ &*sync_cman as &(dyn Confirm + Sync + Send), &*sync_cmon as &(dyn Confirm + Sync + Send), &*sync_sweeper as &(dyn Confirm + Sync + Send), ]; + if let Some(sync_payjoin) = sync_payjoin { + confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send)); + } let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); let sync_fee_rate_update_timestamp = Arc::clone(&self.latest_fee_rate_cache_update_timestamp); diff --git a/src/payment/payjoin/handler.rs b/src/payment/payjoin/handler.rs index 3f1f96b7f..d9956ad12 100644 --- a/src/payment/payjoin/handler.rs +++ b/src/payment/payjoin/handler.rs @@ -1,31 +1,80 @@ +use lightning::ln::channelmanager::PaymentId; + use crate::config::PAYJOIN_REQUEST_TIMEOUT; use crate::error::Error; use crate::event::PayjoinPaymentFailureReason; -use crate::logger::FilesystemLogger; -use crate::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; -use crate::types::{Broadcaster, ChainSource, EventQueue, PaymentStore, Wallet}; +use crate::io::utils::ohttp_headers; +use crate::payment::store::PaymentDetailsUpdate; +use crate::payment::PaymentStatus; +use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet}; use crate::Event; use bitcoin::address::NetworkChecked; use bitcoin::block::Header; use bitcoin::psbt::Psbt; use bitcoin::{Address, Amount, BlockHash, Script, Transaction, Txid}; - -use lightning::chain::chaininterface::BroadcasterInterface; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::chain::transaction::TransactionData; -use lightning::chain::{Confirm, Filter, WatchedOutput}; -use lightning::ln::channelmanager::PaymentId; -use lightning::log_error; -use lightning::util::logger::Logger; -use payjoin::send::{ContextV2, RequestBuilder}; -use rand::RngCore; +use lightning::chain::{Filter, WatchedOutput}; use std::sync::{Arc, RwLock}; +#[derive(Clone, Debug)] +enum PayjoinTransaction { + PendingFirstConfirmation { + original_psbt: Psbt, + tx: Transaction, + receiver: Address, + amount: Amount, + }, + PendingThresholdConfirmations { + original_psbt: Psbt, + tx: Transaction, + receiver: Address, + amount: Amount, + first_confirmation_height: u32, + first_confirmation_hash: BlockHash, + }, +} + +impl PayjoinTransaction { + fn txid(&self) -> Option { + match self { + PayjoinTransaction::PendingFirstConfirmation { tx, .. } => Some(tx.txid()), + PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => Some(tx.txid()), + } + } + fn original_psbt(&self) -> &Psbt { + match self { + PayjoinTransaction::PendingFirstConfirmation { original_psbt, .. } => original_psbt, + PayjoinTransaction::PendingThresholdConfirmations { original_psbt, .. } => { + original_psbt + }, + } + } + fn first_confirmation_height(&self) -> Option { + match self { + PayjoinTransaction::PendingFirstConfirmation { .. } => None, + PayjoinTransaction::PendingThresholdConfirmations { + first_confirmation_height, .. + } => Some(*first_confirmation_height), + } + } + fn amount(&self) -> Amount { + match self { + PayjoinTransaction::PendingFirstConfirmation { amount, .. } => *amount, + PayjoinTransaction::PendingThresholdConfirmations { amount, .. } => *amount, + } + } + fn receiver(&self) -> Address { + match self { + PayjoinTransaction::PendingFirstConfirmation { receiver, .. } => receiver.clone(), + PayjoinTransaction::PendingThresholdConfirmations { receiver, .. } => receiver.clone(), + } + } +} + pub(crate) struct PayjoinHandler { - tx_broadcaster: Arc, - logger: Arc, payjoin_relay: payjoin::Url, chain_source: Arc, transactions: RwLock>, @@ -36,13 +85,10 @@ pub(crate) struct PayjoinHandler { impl PayjoinHandler { pub(crate) fn new( - tx_broadcaster: Arc, logger: Arc, payjoin_relay: payjoin::Url, chain_source: Arc, event_queue: Arc, wallet: Arc, payment_store: Arc, ) -> Self { Self { - tx_broadcaster, - logger, payjoin_relay, transactions: RwLock::new(Vec::new()), chain_source, @@ -52,142 +98,121 @@ impl PayjoinHandler { } } - pub(crate) async fn send_payjoin_transaction( - &self, original_psbt: &mut Psbt, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, - ) -> Result, Error> { - let (request, context) = - RequestBuilder::from_psbt_and_uri(original_psbt.clone(), payjoin_uri.clone()) - .and_then(|b| b.build_non_incentivizing()) - .and_then(|mut c| c.extract_v2(self.payjoin_relay.clone())) - .map_err(|_e| Error::PayjoinRequestCreationFailed) - .unwrap(); - let mut random_bytes = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut random_bytes); - self.payment_store.insert(PaymentDetails::new( - PaymentId(random_bytes), - PaymentKind::Payjoin, - payjoin_uri.amount.map(|a| a.to_sat()), - PaymentDirection::Outbound, - PaymentStatus::Pending, - ))?; - let response = send_payjoin_ohttp_request(&request).await?; - self.handle_payjoin_transaction_response(response, context, original_psbt, payjoin_uri) - .await + pub(crate) async fn send_request( + &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, original_psbt: &mut Psbt, + ) -> Result, Error> { + let (request, context) = payjoin::send::RequestBuilder::from_psbt_and_uri( + original_psbt.clone(), + payjoin_uri.clone(), + ) + .and_then(|b| b.build_non_incentivizing()) + .and_then(|mut c| c.extract_v2(self.payjoin_relay.clone())) + .map_err(|_e| Error::PayjoinRequestCreationFailed)?; + let response = reqwest::Client::new() + .post(request.url.clone()) + .body(request.body.clone()) + .timeout(PAYJOIN_REQUEST_TIMEOUT) + .headers(ohttp_headers()) + .send() + .await?; + let response = response.error_for_status()?; + let response = response.bytes().await?; + let response = response.to_vec(); + context + .process_response(&mut response.as_slice()) + .map_err(|_e| Error::PayjoinResponseProcessingFailed) } - pub(crate) async fn handle_payjoin_transaction_response( - &self, response: Vec, context: ContextV2, original_psbt: &mut Psbt, - payjoin_uri: payjoin::Uri<'_, NetworkChecked>, - ) -> Result, Error> { - let amount = match payjoin_uri.amount { - Some(amt) => amt.to_sat(), - None => return Err(Error::PayjoinRequestMissingAmount), - }; - match context.process_response(&mut response.as_slice()) { - Ok(Some(pj_proposal)) => { - let pj_proposal = &mut pj_proposal.clone(); - let tx = - self.finalise_tx(pj_proposal, &mut original_psbt.clone(), payjoin_uri.clone())?; - self.tx_broadcaster.broadcast_transactions(&[&tx]); - let txid = tx.txid(); - let _ = self.event_queue.add_event(Event::PayjoinPaymentPending { - txid, - amount, - receipient: payjoin_uri.address.clone().into(), - }); - Ok(Some(txid)) - }, - Ok(None) => Ok(None), - Err(_e) => { - let _ = self.event_queue.add_event(Event::PayjoinPaymentFailed { - txid: None, - amount, - receipient: payjoin_uri.address.clone().into(), - reason: PayjoinPaymentFailureReason::InvalidReceiverResponse, - }); - return Err(Error::PayjoinResponseProcessingFailed); - }, - } + pub(crate) fn handle_request_failure( + &self, payjoin_uri: payjoin::Uri, original_psbt: &Psbt, + ) -> Result<(), Error> { + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: Some(original_psbt.unsigned_tx.txid()), + receipient: payjoin_uri.address.clone().into(), + amount: payjoin_uri.amount.unwrap().to_sat(), + reason: PayjoinPaymentFailureReason::RequestFailed, + }) + } + + pub(crate) fn handle_request_timeout( + &self, payjoin_uri: payjoin::Uri, original_psbt: &Psbt, + ) -> Result<(), Error> { + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: Some(original_psbt.unsigned_tx.txid()), + receipient: payjoin_uri.address.clone().into(), + amount: payjoin_uri.amount.unwrap().to_sat(), + reason: PayjoinPaymentFailureReason::Timeout, + }) } - fn finalise_tx( + pub(crate) fn process_response( &self, payjoin_proposal: &mut Psbt, original_psbt: &mut Psbt, payjoin_uri: payjoin::Uri, ) -> Result { let wallet = self.wallet.clone(); wallet.sign_payjoin_proposal(payjoin_proposal, original_psbt)?; let tx = payjoin_proposal.clone().extract_tx(); - if let Some(our_output) = - tx.output.iter().find(|output| wallet.is_mine(&output.script_pubkey).unwrap_or(false)) - { - let mut transactions = self.transactions.write().unwrap(); - let pj_tx = PayjoinTransaction::new( - tx.clone(), - payjoin_uri.address, - payjoin_uri.amount.unwrap_or_default(), - ); - transactions.push(pj_tx); - self.register_tx(&tx.txid(), &our_output.script_pubkey); + let our_input = + tx.output.iter().find(|output| wallet.is_mine(&output.script_pubkey).unwrap_or(false)); + if let Some(our_input) = our_input { + self.transactions.write().unwrap().push(PayjoinTransaction::PendingFirstConfirmation { + original_psbt: original_psbt.clone(), + tx: tx.clone(), + receiver: payjoin_uri.address.clone(), + amount: payjoin_uri.amount.unwrap_or_default(), + }); + let txid = tx.txid(); + self.register_tx(&txid, &our_input.script_pubkey); + self.event_queue.add_event(Event::PayjoinPaymentPending { + txid, + amount: payjoin_uri.amount.unwrap_or_default().to_sat(), + receipient: payjoin_uri.address.clone().into(), + })?; Ok(tx) } else { - Err(Error::PaymentSendingFailed) + self.event_queue.add_event(Event::PayjoinPaymentFailed { + txid: None, + amount: payjoin_uri.amount.unwrap_or_default().to_sat(), + receipient: payjoin_uri.address.clone().into(), + reason: PayjoinPaymentFailureReason::TransactionFinalisationFailed, + })?; + Err(Error::PayjoinReceiverRequestValidationFailed) // fixeror } } - pub(crate) fn timeout_payjoin_transaction( - &self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, - ) -> Result<(), Error> { - let amount = match payjoin_uri.amount { - Some(amt) => amt.to_sat(), - None => return Err(Error::PayjoinRequestMissingAmount), - }; - let _ = self.event_queue.add_event(Event::PayjoinPaymentFailed { - txid: None, - receipient: payjoin_uri.address.clone().into(), - amount, - reason: PayjoinPaymentFailureReason::Timeout, - }); - Ok(()) - } - fn internal_transactions_confirmed( &self, header: &Header, txdata: &TransactionData, height: u32, ) { let (_, tx) = txdata[0]; let confirmed_tx_txid = tx.txid(); let mut transactions = self.transactions.write().unwrap(); - if let Some(position) = - transactions.iter().position(|o| o.txid() == Some(confirmed_tx_txid)) - { - let tx = transactions.remove(position); - tx.to_pending_threshold_confirmations(height, header.block_hash()); - } else { - log_error!( - self.logger, - "Notified about UNTRACKED confirmed payjoin transaction {}", - confirmed_tx_txid - ); + let position = match transactions.iter().position(|o| o.txid() == Some(confirmed_tx_txid)) { + Some(position) => position, + None => { + return; + }, + }; + let pj_tx = transactions.remove(position); + match pj_tx { + PayjoinTransaction::PendingFirstConfirmation { + ref tx, + receiver, + amount, + original_psbt, + } => { + transactions.push(PayjoinTransaction::PendingThresholdConfirmations { + original_psbt, + tx: tx.clone(), + receiver, + amount, + first_confirmation_height: height, + first_confirmation_hash: header.block_hash(), + }); + }, + _ => { + unreachable!() + }, }; - } - - fn internal_best_block_updated(&self, height: u32) { - let mut transactions = self.transactions.write().unwrap(); - transactions.retain(|tx| { - if let (Some(first_conf), Some(txid)) = (tx.first_confirmation_height(), tx.txid()) { - if height - first_conf >= ANTI_REORG_DELAY { - let _ = self.event_queue.add_event(Event::PayjoinPaymentSuccess { - txid, - amount: tx.amount().to_sat(), - receipient: tx.receiver().into(), - }); - false - } else { - true - } - } else { - true - } - }); } fn internal_get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { @@ -209,69 +234,30 @@ impl PayjoinHandler { }) .collect::>() } -} -#[derive(Clone, Debug)] -enum PayjoinTransaction { - // PendingReceiverResponse, - PendingFirstConfirmation { - tx: Transaction, - receiver: Address, - amount: Amount, - }, - PendingThresholdConfirmations { - tx: Transaction, - receiver: Address, - amount: Amount, - first_confirmation_height: u32, - first_confirmation_hash: BlockHash, - }, -} - -impl PayjoinTransaction { - fn new(tx: Transaction, receiver: Address, amount: Amount) -> Self { - PayjoinTransaction::PendingFirstConfirmation { tx, receiver, amount } - } - fn txid(&self) -> Option { - match self { - PayjoinTransaction::PendingFirstConfirmation { tx, .. } => Some(tx.txid()), - PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => Some(tx.txid()), - } - } - fn first_confirmation_height(&self) -> Option { - match self { - PayjoinTransaction::PendingFirstConfirmation { .. } => None, - PayjoinTransaction::PendingThresholdConfirmations { - first_confirmation_height, .. - } => Some(*first_confirmation_height), - } - } - fn amount(&self) -> Amount { - match self { - PayjoinTransaction::PendingFirstConfirmation { amount, .. } => *amount, - PayjoinTransaction::PendingThresholdConfirmations { amount, .. } => *amount, - } - } - fn receiver(&self) -> Address { - match self { - PayjoinTransaction::PendingFirstConfirmation { receiver, .. } => receiver.clone(), - PayjoinTransaction::PendingThresholdConfirmations { receiver, .. } => receiver.clone(), - } - } - - fn to_pending_threshold_confirmations(&self, height: u32, hash: BlockHash) -> Self { - match self { - PayjoinTransaction::PendingFirstConfirmation { tx, receiver, amount } => { - PayjoinTransaction::PendingThresholdConfirmations { - tx: tx.clone(), - receiver: receiver.clone(), - amount: *amount, - first_confirmation_height: height, - first_confirmation_hash: hash, + fn internal_best_block_updated(&self, height: u32) { + let mut transactions = self.transactions.write().unwrap(); + transactions.retain(|tx| { + if let (Some(first_conf), Some(txid)) = (tx.first_confirmation_height(), tx.txid()) { + if height - first_conf >= ANTI_REORG_DELAY { + let payment_id: [u8; 32] = + tx.original_psbt().unsigned_tx.txid()[..].try_into().unwrap(); + let mut update_details = PaymentDetailsUpdate::new(PaymentId(payment_id)); + update_details.status = Some(PaymentStatus::Succeeded); + let _ = self.payment_store.update(&update_details); + let _ = self.event_queue.add_event(Event::PayjoinPaymentSuccess { + txid, + amount: tx.amount().to_sat(), + receipient: tx.receiver().into(), + }); + false + } else { + true } - }, - _ => unreachable!(), - } + } else { + true + } + }); } } @@ -285,11 +271,13 @@ impl Filter for PayjoinHandler { } } -impl Confirm for PayjoinHandler { +impl lightning::chain::Confirm for PayjoinHandler { fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) { self.internal_transactions_confirmed(header, txdata, height); } + fn transaction_unconfirmed(&self, _txid: &Txid) {} + fn best_block_updated(&self, _header: &Header, height: u32) { self.internal_best_block_updated(height); } @@ -297,23 +285,4 @@ impl Confirm for PayjoinHandler { fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { self.internal_get_relevant_txids() } - - fn transaction_unconfirmed(&self, _txid: &Txid) {} -} - -async fn send_payjoin_ohttp_request(request: &payjoin::Request) -> Result, Error> { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("message/ohttp-req"), - ); - let response = reqwest::Client::new() - .post(request.url.clone()) - .body(request.body.clone()) - .timeout(PAYJOIN_REQUEST_TIMEOUT) - .headers(headers) - .send() - .await?; - let response = response.error_for_status()?.bytes().await?; - Ok(response.to_vec()) } diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs index d37020c07..3c71f61d1 100644 --- a/src/payment/payjoin/mod.rs +++ b/src/payment/payjoin/mod.rs @@ -1,8 +1,12 @@ //! Holds a payment handler allowing to send Payjoin payments. +use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::ln::channelmanager::PaymentId; +use lightning::log_error; + use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL}; -use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; -use crate::types::{ChannelManager, Wallet}; +use crate::logger::{FilesystemLogger, Logger}; +use crate::types::{Broadcaster, ChannelManager, PaymentStore, Wallet}; use bitcoin::secp256k1::PublicKey; use lightning::ln::msgs::SocketAddress; use lightning::util::config::{ChannelHandshakeConfig, UserConfig}; @@ -19,6 +23,8 @@ pub(crate) mod handler; use handler::PayjoinHandler; +use super::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; + /// A payment handler allowing to send Payjoin payments. /// /// Payjoin transactions can be used to improve privacy by breaking the common-input-ownership @@ -63,34 +69,40 @@ use handler::PayjoinHandler; /// [`BIP77`]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki pub struct PayjoinPayment { runtime: Arc>>, - handler: Option>, + payjoin_handler: Option>, receiver: Option>, config: Arc, logger: Arc, wallet: Arc, + tx_broadcaster: Arc, peer_store: Arc>>, channel_manager: Arc, connection_manager: Arc>>, + payment_store: Arc, } impl PayjoinPayment { pub(crate) fn new( runtime: Arc>>, - handler: Option>, receiver: Option>, + payjoin_handler: Option>, receiver: Option>, config: Arc, logger: Arc, wallet: Arc, - peer_store: Arc>>, channel_manager: Arc, + tx_broadcaster: Arc, peer_store: Arc>>, + channel_manager: Arc, connection_manager: Arc>>, + payment_store: Arc, ) -> Self { Self { runtime, - handler, + payjoin_handler, receiver, config, logger, wallet, + tx_broadcaster, peer_store, channel_manager, connection_manager, + payment_store, } } @@ -102,8 +114,8 @@ impl PayjoinPayment { /// Due to the asynchronous nature of the Payjoin process, this method will return immediately /// after constucting the Payjoin request and sending it in the background. The result of the /// operation will be communicated through the event queue. If the Payjoin request is - /// successful, [`Event::PayjoinPaymentSuccess`] event will be added to the event queue. - /// Otherwise, [`Event::PayjoinPaymentFailed`] is added. + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. /// /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. /// If the Payjoin receiver does not respond within this duration, the process is considered @@ -114,45 +126,59 @@ impl PayjoinPayment { /// /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki - /// [`Event::PayjoinPaymentSuccess`]: crate::Event::PayjoinPaymentSuccess - /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed pub fn send(&self, payjoin_uri: String) -> Result<(), Error> { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } + let payjoin_handler = self.payjoin_handler.as_ref().ok_or(Error::PayjoinUnavailable)?; let payjoin_uri = payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then( |uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork), )?; - let mut original_psbt = self.wallet.build_payjoin_transaction(payjoin_uri.clone())?; - let payjoin_handler = self.handler.as_ref().ok_or(Error::PayjoinUnavailable)?; + let original_psbt = self.wallet.build_payjoin_transaction(payjoin_uri.clone())?; let payjoin_handler = Arc::clone(payjoin_handler); + let runtime = rt_lock.as_ref().unwrap(); + let tx_broadcaster = Arc::clone(&self.tx_broadcaster); let logger = Arc::clone(&self.logger); - log_info!(logger, "Sending Payjoin request to: {}", payjoin_uri.address); - rt_lock.as_ref().unwrap().spawn(async move { + let payment_store = Arc::clone(&self.payment_store); + let payment_id = original_psbt.unsigned_tx.txid()[..].try_into().unwrap(); + payment_store.insert(PaymentDetails::new( + PaymentId(payment_id), + PaymentKind::Payjoin, + payjoin_uri.amount.map(|a| a.to_sat()), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ))?; + runtime.spawn(async move { let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL); loop { tokio::select! { _ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => { - log_error!(logger, "Payjoin request timed out."); - let _ = payjoin_handler.timeout_payjoin_transaction(payjoin_uri.clone()); + let _ = payjoin_handler.handle_request_timeout(payjoin_uri.clone(), &original_psbt); break; } _ = interval.tick() => { - match payjoin_handler.send_payjoin_transaction(&mut original_psbt, payjoin_uri.clone()).await { - Ok(Some(_)) => { - log_info!(logger, "Payjoin transaction sent successfully."); - break + let payjoin_uri = payjoin_uri.clone(); + match payjoin_handler.send_request(payjoin_uri.clone(), &mut original_psbt.clone()).await { + Ok(Some(mut proposal)) => { + let _ = payjoin_handler.process_response(&mut proposal, &mut original_psbt.clone(), payjoin_uri).inspect(|tx| { + tx_broadcaster.broadcast_transactions(&[&tx]); + }).inspect_err(|e| { + log_error!(logger, "Failed to process Payjoin response: {}", e); + }); + break; }, Ok(None) => { - log_info!(logger, "No Payjoin response yet."); - continue - }, + continue; + } Err(e) => { - log_error!(logger, "Failed to process Payjoin receiver response: {}.", e); + log_error!(logger, "Failed to send Payjoin request : {}", e); + let _ = payjoin_handler.handle_request_failure(payjoin_uri.clone(), &original_psbt); break; - } + }, } } } @@ -172,8 +198,8 @@ impl PayjoinPayment { /// Due to the asynchronous nature of the Payjoin process, this method will return immediately /// after constucting the Payjoin request and sending it in the background. The result of the /// operation will be communicated through the event queue. If the Payjoin request is - /// successful, [`Event::PayjoinPaymentSuccess`] event will be added to the event queue. - /// Otherwise, [`Event::PayjoinPaymentFailed`] is added. + /// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue. + /// Otherwise, [`Event::PayjoinTxSendFailed`] is added. /// /// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`. /// If the Payjoin receiver does not respond within this duration, the process is considered @@ -184,17 +210,13 @@ impl PayjoinPayment { /// /// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki - /// [`Event::PayjoinPaymentSuccess`]: crate::Event::PayjoinPaymentSuccess - /// [`Event::PayjoinPaymentFailed`]: crate::Event::PayjoinPaymentFailed + /// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess + /// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed pub fn send_with_amount(&self, payjoin_uri: String, amount_sats: u64) -> Result<(), Error> { - let payjoin_uri = match payjoin::Uri::try_from(payjoin_uri) { - Ok(uri) => uri, - Err(_) => return Err(Error::PayjoinUriInvalid), - }; - let mut payjoin_uri = match payjoin_uri.require_network(self.config.network) { - Ok(uri) => uri, - Err(_) => return Err(Error::InvalidNetwork), - }; + let mut payjoin_uri = + payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then( + |uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork), + )?; payjoin_uri.amount = Some(bitcoin::Amount::from_sat(amount_sats)); self.send(payjoin_uri.to_string()) } diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 4492bcfc6..2a3867ebc 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -114,8 +114,10 @@ where { fn broadcast_transactions(&self, txs: &[&Transaction]) { let package = txs.iter().map(|&t| t.clone()).collect::>(); - self.queue_sender.try_send(package).unwrap_or_else(|e| { + let ret = self.queue_sender.try_send(package).unwrap_or_else(|e| { + dbg!(&e); log_error!(self.logger, "Failed to broadcast transactions: {}", e); }); + dbg!(&ret); } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 44f7de420..edccad541 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -147,6 +147,23 @@ macro_rules! expect_payment_successful_event { pub(crate) use expect_payment_successful_event; +macro_rules! expect_payjoin_tx_pending_event { + ($node: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PayjoinPaymentPending { txid, .. } => { + println!("{} got event {:?}", $node.node_id(), e); + $node.event_handled(); + txid + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; +} + +pub(crate) use expect_payjoin_tx_pending_event; + macro_rules! expect_payjoin_tx_sent_successfully_event { ($node: expr) => {{ match $node.wait_next_event() { diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs index f482c5382..43ec91c8f 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -7,7 +7,12 @@ use common::{ }; use bitcoin::Amount; -use ldk_node::Event; +use ldk_node::{ + payment::{PaymentDirection, PaymentKind, PaymentStatus}, + Event, +}; + +use crate::common::expect_payjoin_tx_pending_event; #[test] fn send_receive_regular_payjoin_transaction() { @@ -33,10 +38,22 @@ fn send_receive_regular_payjoin_transaction() { dbg!(&payjoin_uri); let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); - let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + let txid = expect_payjoin_tx_pending_event!(node_b_pj_sender); + let payments = node_b_pj_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.amount_msat, Some(80_000)); + assert_eq!(payment.status, PaymentStatus::Pending); + assert_eq!(payment.direction, PaymentDirection::Outbound); + assert_eq!(payment.kind, PaymentKind::Payjoin); wait_for_tx(&electrsd.client, txid); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3); + node_b_pj_sender.sync_wallets().unwrap(); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4); node_b_pj_sender.sync_wallets().unwrap(); + let payments = node_b_pj_sender.list_payments(); + let payment = payments.first().unwrap(); + assert_eq!(payment.status, PaymentStatus::Succeeded); + expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); let node_b_balance = node_b_pj_sender.list_balances(); assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); } @@ -65,10 +82,12 @@ fn send_payjoin_with_amount() { dbg!(&payjoin_uri); let sender_payjoin_payment = node_b_pj_sender.payjoin_payment(); assert!(sender_payjoin_payment.send_with_amount(payjoin_uri, 80_000).is_ok()); - let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); - wait_for_tx(&electrsd.client, txid); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + let _txid = expect_payjoin_tx_pending_event!(node_b_pj_sender); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3); + node_b_pj_sender.sync_wallets().unwrap(); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4); node_b_pj_sender.sync_wallets().unwrap(); + let _txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); let node_b_balance = node_b_pj_sender.list_balances(); assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); } diff --git a/tests/integration_tests_payjoin_with_channel_opening.rs b/tests/integration_tests_payjoin_with_channel_opening.rs index ad2214b6f..5e1ad4fa4 100644 --- a/tests/integration_tests_payjoin_with_channel_opening.rs +++ b/tests/integration_tests_payjoin_with_channel_opening.rs @@ -10,6 +10,8 @@ use common::{ use bitcoin::Amount; use ldk_node::Event; +use crate::common::expect_payjoin_tx_pending_event; + #[test] fn send_receive_payjoin_transaction_with_channel_opening() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -46,16 +48,18 @@ fn send_receive_payjoin_transaction_with_channel_opening() { assert!(sender_payjoin_payment.send(payjoin_uri).is_ok()); expect_channel_pending_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); expect_channel_pending_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); - let txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); + let txid = expect_payjoin_tx_pending_event!(node_b_pj_sender); wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1); + node_b_pj_sender.sync_wallets().unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); node_a_pj_receiver.sync_wallets().unwrap(); node_b_pj_sender.sync_wallets().unwrap(); let node_b_balance = node_b_pj_sender.list_balances(); assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000); - expect_channel_ready_event!(node_a_pj_receiver, node_b_pj_sender.node_id()); expect_channel_ready_event!(node_b_pj_sender, node_a_pj_receiver.node_id()); + let _ = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender); let channels = node_a_pj_receiver.list_channels(); let channel = channels.get(0).unwrap(); assert_eq!(channel.channel_value_sats, 80_000);