diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index ec63adbe0..26ab55994 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -100,10 +100,18 @@ interface Bolt11Payment { [Throws=NodeError] void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); [Throws=NodeError] + void claim_for_hash(PaymentHash payment_hash, u64 claimable_amount_msat, PaymentPreimage preimage); + [Throws=NodeError] + void fail_for_hash(PaymentHash payment_hash); + [Throws=NodeError] Bolt11Invoice receive(u64 amount_msat, [ByRef]string description, u32 expiry_secs); [Throws=NodeError] + Bolt11Invoice receive_for_hash(u64 amount_msat, [ByRef]string description, u32 expiry_secs, PaymentHash payment_hash); + [Throws=NodeError] Bolt11Invoice receive_variable_amount([ByRef]string description, u32 expiry_secs); [Throws=NodeError] + Bolt11Invoice receive_variable_amount_for_hash([ByRef]string description, u32 expiry_secs, PaymentHash payment_hash); + [Throws=NodeError] Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); [Throws=NodeError] Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); @@ -222,6 +230,7 @@ interface Event { PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, u64? fee_paid_msat); PaymentFailed(PaymentId? payment_id, PaymentHash payment_hash, PaymentFailureReason? reason); PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat); + PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline); 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); @@ -285,6 +294,7 @@ dictionary PaymentDetails { u64? amount_msat; PaymentDirection direction; PaymentStatus status; + u64 latest_update_timestamp; }; [NonExhaustive] diff --git a/src/event.rs b/src/event.rs index 5cd9e2603..838df4230 100644 --- a/src/event.rs +++ b/src/event.rs @@ -84,6 +84,28 @@ pub enum Event { /// The value, in thousandths of a satoshi, that has been received. amount_msat: u64, }, + /// A payment for a previously-registered payment hash has been received. + /// + /// This needs to be manually claimed by supplying the correct preimage to [`claim_for_hash`]. + /// + /// If the the provided parameters don't match the expectations or the preimage can't be + /// retrieved in time, should be failed-back via [`fail_for_hash`]. + /// + /// Note claiming will necessarily fail after the `claim_deadline` has been reached. + /// + /// [`claim_for_hash`]: crate::payment::Bolt11Payment::claim_for_hash + /// [`fail_for_hash`]: crate::payment::Bolt11Payment::fail_for_hash + PaymentClaimable { + /// A local identifier used to track the payment. + payment_id: PaymentId, + /// The hash of the payment. + payment_hash: PaymentHash, + /// The value, in thousandths of a satoshi, that is claimable. + claimable_amount_msat: u64, + /// The block height at which this payment will be failed back and will no longer be + /// eligible for claiming. + claim_deadline: Option, + }, /// A channel has been created and is pending confirmation on-chain. ChannelPending { /// The `channel_id` of the channel. @@ -156,6 +178,12 @@ impl_writeable_tlv_based_enum!(Event, (1, counterparty_node_id, option), (2, user_channel_id, required), (3, reason, upgradable_option), + }, + (6, PaymentClaimable) => { + (0, payment_hash, required), + (2, payment_id, required), + (4, claimable_amount_msat, required), + (6, claim_deadline, option), }; ); @@ -434,12 +462,31 @@ where receiver_node_id: _, via_channel_id: _, via_user_channel_id: _, - claim_deadline: _, + claim_deadline, onion_fields: _, counterparty_skimmed_fee_msat, } => { let payment_id = PaymentId(payment_hash.0); if let Some(info) = self.payment_store.get(&payment_id) { + if info.direction == PaymentDirection::Outbound { + log_info!( + self.logger, + "Refused inbound payment with ID {}: circular payments are unsupported.", + payment_id + ); + self.channel_manager.fail_htlc_backwards(&payment_hash); + + let update = PaymentDetailsUpdate { + status: Some(PaymentStatus::Failed), + ..PaymentDetailsUpdate::new(payment_id) + }; + self.payment_store.update(&update).unwrap_or_else(|e| { + log_error!(self.logger, "Failed to access payment store: {}", e); + panic!("Failed to access payment store"); + }); + return; + } + if info.status == PaymentStatus::Succeeded || matches!(info.kind, PaymentKind::Spontaneous { .. }) { @@ -500,6 +547,38 @@ where }); return; } + + // If this is known by the store but ChannelManager doesn't know the preimage, + // the payment has been registered via `_for_hash` variants and needs to be manually claimed via + // user interaction. + match info.kind { + PaymentKind::Bolt11 { preimage, .. } => { + if purpose.preimage().is_none() { + debug_assert!( + preimage.is_none(), + "We would have registered the preimage if we knew" + ); + + self.event_queue + .add_event(Event::PaymentClaimable { + payment_id, + payment_hash, + claimable_amount_msat: amount_msat, + claim_deadline, + }) + .unwrap_or_else(|e| { + log_error!( + self.logger, + "Failed to push to event queue: {}", + e + ); + panic!("Failed to push to event queue"); + }); + return; + } + }, + _ => {}, + } } log_info!( @@ -519,19 +598,21 @@ where .. } => { let offer_id = payment_context.offer_id; - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Bolt12Offer { - hash: Some(payment_hash), - preimage: payment_preimage, - secret: Some(payment_secret), - offer_id, - }, - amount_msat: Some(amount_msat), - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, + let kind = PaymentKind::Bolt12Offer { + hash: Some(payment_hash), + preimage: payment_preimage, + secret: Some(payment_secret), + offer_id, }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); + match self.payment_store.insert(payment) { Ok(false) => (), Ok(true) => { @@ -559,17 +640,19 @@ where }, PaymentPurpose::SpontaneousPayment(preimage) => { // Since it's spontaneous, we insert it now into our store. - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Spontaneous { - hash: payment_hash, - preimage: Some(preimage), - }, - amount_msat: Some(amount_msat), - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, + let kind = PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(preimage), }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); + match self.payment_store.insert(payment) { Ok(false) => (), Ok(true) => { diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index c23f7b670..e8d030bc0 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -8,20 +8,23 @@ use crate::error::Error; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; use crate::payment::store::{ - LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, + LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, + PaymentStatus, PaymentStore, }; use crate::peer_store::{PeerInfo, PeerStore}; use crate::types::{ChannelManager, KeysManager}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; -use lightning::ln::PaymentHash; +use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::routing::router::{PaymentParameters, RouteParameters}; use lightning_invoice::{payment, Bolt11Invoice, Currency}; +use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use std::sync::{Arc, RwLock}; +use std::time::SystemTime; /// A payment handler allowing to create and pay [BOLT 11] invoices. /// @@ -102,17 +105,18 @@ impl Bolt11Payment { let amt_msat = invoice.amount_milli_satoshis().unwrap(); log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: payment_secret, - }, - amount_msat: invoice.amount_milli_satoshis(), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: payment_secret, }; + let payment = PaymentDetails::new( + payment_id, + kind, + invoice.amount_milli_satoshis(), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; @@ -123,17 +127,18 @@ impl Bolt11Payment { match e { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: payment_secret, - }, - amount_msat: invoice.amount_milli_satoshis(), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: payment_secret, }; + let payment = PaymentDetails::new( + payment_id, + kind, + invoice.amount_milli_satoshis(), + PaymentDirection::Outbound, + PaymentStatus::Failed, + ); self.payment_store.insert(payment)?; Err(Error::PaymentSendingFailed) @@ -213,17 +218,19 @@ impl Bolt11Payment { payee_pubkey ); - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), - }, - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), }; + + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; Ok(payment_id) @@ -234,17 +241,18 @@ impl Bolt11Payment { match e { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), - }, - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Failed, + ); self.payment_store.insert(payment)?; Err(Error::PaymentSendingFailed) @@ -254,62 +262,233 @@ impl Bolt11Payment { } } + /// Allows to attempt manually claiming payments with the given preimage that have previously + /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`]. + /// + /// This should be called in reponse to a [`PaymentClaimable`] event as soon as the preimage is + /// available. + /// + /// Will check that the payment is known, and that the given preimage and claimable amount + /// match our expectations before attempting to claim the payment, and will return an error + /// otherwise. + /// + /// When claiming the payment has succeeded, a [`PaymentReceived`] event will be emitted. + /// + /// [`receive_for_hash`]: Self::receive_for_hash + /// [`receive_variable_amount_for_hash`]: Self::receive_variable_amount_for_hash + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`PaymentReceived`]: crate::Event::PaymentReceived + pub fn claim_for_hash( + &self, payment_hash: PaymentHash, claimable_amount_msat: u64, preimage: PaymentPreimage, + ) -> Result<(), Error> { + let payment_id = PaymentId(payment_hash.0); + + let expected_payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); + + if expected_payment_hash != payment_hash { + log_error!( + self.logger, + "Failed to manually claim payment as the given preimage doesn't match the hash {}", + payment_hash + ); + return Err(Error::InvalidPaymentPreimage); + } + + if let Some(details) = self.payment_store.get(&payment_id) { + if let Some(expected_amount_msat) = details.amount_msat { + if claimable_amount_msat < expected_amount_msat { + log_error!( + self.logger, + "Failed to manually claim payment {} as the claimable amount is less than expected", + payment_id + ); + return Err(Error::InvalidAmount); + } + } + } else { + log_error!( + self.logger, + "Failed to manually claim unknown payment with hash: {}", + payment_hash + ); + return Err(Error::InvalidPaymentHash); + } + + self.channel_manager.claim_funds(preimage); + Ok(()) + } + + /// Allows to manually fail payments with the given hash that have previously + /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`]. + /// + /// This should be called in reponse to a [`PaymentClaimable`] event if the payment needs to be + /// failed back, e.g., if the correct preimage can't be retrieved in time before the claim + /// deadline has been reached. + /// + /// Will check that the payment is known before failing the payment, and will return an error + /// otherwise. + /// + /// [`receive_for_hash`]: Self::receive_for_hash + /// [`receive_variable_amount_for_hash`]: Self::receive_variable_amount_for_hash + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + pub fn fail_for_hash(&self, payment_hash: PaymentHash) -> Result<(), Error> { + let payment_id = PaymentId(payment_hash.0); + + let update = PaymentDetailsUpdate { + status: Some(PaymentStatus::Failed), + ..PaymentDetailsUpdate::new(payment_id) + }; + + if !self.payment_store.update(&update)? { + log_error!( + self.logger, + "Failed to manually fail unknown payment with hash: {}", + payment_hash + ); + return Err(Error::InvalidPaymentHash); + } + + self.channel_manager.fail_htlc_backwards(&payment_hash); + Ok(()) + } + /// Returns a payable invoice that can be used to request and receive a payment of the amount /// given. + /// + /// The inbound payment will be automatically claimed upon arrival. pub fn receive( &self, amount_msat: u64, description: &str, expiry_secs: u32, ) -> Result { - self.receive_inner(Some(amount_msat), description, expiry_secs) + self.receive_inner(Some(amount_msat), description, expiry_secs, None) + } + + /// Returns a payable invoice that can be used to request a payment of the amount + /// given for the given payment hash. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + pub fn receive_for_hash( + &self, amount_msat: u64, description: &str, expiry_secs: u32, payment_hash: PaymentHash, + ) -> Result { + self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash)) } /// Returns a payable invoice that can be used to request and receive a payment for which the /// amount is to be determined by the user, also known as a "zero-amount" invoice. + /// + /// The inbound payment will be automatically claimed upon arrival. pub fn receive_variable_amount( &self, description: &str, expiry_secs: u32, ) -> Result { - self.receive_inner(None, description, expiry_secs) + self.receive_inner(None, description, expiry_secs, None) + } + + /// Returns a payable invoice that can be used to request a payment for the given payment hash + /// and the amount to be determined by the user, also known as a "zero-amount" invoice. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + pub fn receive_variable_amount_for_hash( + &self, description: &str, expiry_secs: u32, payment_hash: PaymentHash, + ) -> Result { + self.receive_inner(None, description, expiry_secs, Some(payment_hash)) } fn receive_inner( &self, amount_msat: Option, description: &str, expiry_secs: u32, + manual_claim_payment_hash: Option, ) -> Result { let currency = Currency::from(self.config.network); let keys_manager = Arc::clone(&self.keys_manager); - let invoice = match lightning_invoice::utils::create_invoice_from_channelmanager( - &self.channel_manager, - keys_manager, - Arc::clone(&self.logger), - currency, - amount_msat, - description.to_string(), - expiry_secs, - None, - ) { - Ok(inv) => { - log_info!(self.logger, "Invoice created: {}", inv); - inv - }, - Err(e) => { - log_error!(self.logger, "Failed to create invoice: {}", e); - return Err(Error::InvoiceCreationFailed); - }, + let duration = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("for the foreseeable future this shouldn't happen"); + + let invoice = { + let invoice_res = if let Some(payment_hash) = manual_claim_payment_hash { + lightning_invoice::utils::create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash( + &self.channel_manager, + keys_manager, + Arc::clone(&self.logger), + currency, + amount_msat, + description.to_string(), + duration, + expiry_secs, + payment_hash, + None, + ) + } else { + lightning_invoice::utils::create_invoice_from_channelmanager_and_duration_since_epoch( + &self.channel_manager, + keys_manager, + Arc::clone(&self.logger), + currency, + amount_msat, + description.to_string(), + duration, + expiry_secs, + None, + ) + }; + + match invoice_res { + Ok(inv) => { + log_info!(self.logger, "Invoice created: {}", inv); + inv + }, + Err(e) => { + log_error!(self.logger, "Failed to create invoice: {}", e); + return Err(Error::InvoiceCreationFailed); + }, + } }; let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let payment_secret = invoice.payment_secret(); let id = PaymentId(payment_hash.0); - let payment = PaymentDetails { + let preimage = if manual_claim_payment_hash.is_none() { + // If the user hasn't registered a custom payment hash, we're positive ChannelManager + // will know the preimage at this point. + let res = self + .channel_manager + .get_payment_preimage(payment_hash, payment_secret.clone()) + .ok(); + debug_assert!(res.is_some(), "We just let ChannelManager create an inbound payment, it can't have forgotten the preimage by now."); + res + } else { + None + }; + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage, + secret: Some(payment_secret.clone()), + }; + let payment = PaymentDetails::new( id, - kind: PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), - }, - + kind, amount_msat, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - }; - + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; Ok(invoice) @@ -422,24 +601,27 @@ impl Bolt11Payment { // Register payment in payment store. let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let payment_secret = invoice.payment_secret(); let lsp_fee_limits = LSPFeeLimits { max_total_opening_fee_msat: lsp_total_opening_fee, max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, }; let id = PaymentId(payment_hash.0); - let payment = PaymentDetails { + let preimage = + self.channel_manager.get_payment_preimage(payment_hash, payment_secret.clone()).ok(); + let kind = PaymentKind::Bolt11Jit { + hash: payment_hash, + preimage, + secret: Some(payment_secret.clone()), + lsp_fee_limits, + }; + let payment = PaymentDetails::new( id, - kind: PaymentKind::Bolt11Jit { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), - lsp_fee_limits, - }, + kind, amount_msat, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - }; - + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; // Persist LSP peer to make sure we reconnect on restart. diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 35fa3cfb4..5fd1208cc 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -96,13 +96,13 @@ impl Bolt12Payment { secret: None, offer_id: offer.id(), }; - let payment = PaymentDetails { - id: payment_id, + let payment = PaymentDetails::new( + payment_id, kind, - amount_msat: Some(*offer_amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, - }; + Some(*offer_amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; Ok(payment_id) @@ -118,13 +118,13 @@ impl Bolt12Payment { secret: None, offer_id: offer.id(), }; - let payment = PaymentDetails { - id: payment_id, + let payment = PaymentDetails::new( + payment_id, kind, - amount_msat: Some(*offer_amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, - }; + Some(*offer_amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Failed, + ); self.payment_store.insert(payment)?; Err(Error::InvoiceRequestCreationFailed) }, @@ -197,13 +197,13 @@ impl Bolt12Payment { secret: None, offer_id: offer.id(), }; - let payment = PaymentDetails { - id: payment_id, + let payment = PaymentDetails::new( + payment_id, kind, - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, - }; + Some(amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; Ok(payment_id) @@ -219,13 +219,13 @@ impl Bolt12Payment { secret: None, offer_id: offer.id(), }; - let payment = PaymentDetails { - id: payment_id, + let payment = PaymentDetails::new( + payment_id, kind, - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, - }; + Some(amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Failed, + ); self.payment_store.insert(payment)?; Err(Error::PaymentSendingFailed) }, @@ -281,17 +281,16 @@ impl Bolt12Payment { let payment_hash = invoice.payment_hash(); let payment_id = PaymentId(payment_hash.0); - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Bolt12Refund { - hash: Some(payment_hash), - preimage: None, - secret: None, - }, - amount_msat: Some(refund.amount_msats()), - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - }; + let kind = + PaymentKind::Bolt12Refund { hash: Some(payment_hash), preimage: None, secret: None }; + + let payment = PaymentDetails::new( + payment_id, + kind, + Some(refund.amount_msats()), + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; @@ -333,13 +332,13 @@ impl Bolt12Payment { let kind = PaymentKind::Bolt12Refund { hash: None, preimage: None, secret: None }; - let payment = PaymentDetails { - id: payment_id, + let payment = PaymentDetails::new( + payment_id, kind, - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, - }; + Some(amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index fcca8065a..482df42d9 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -77,16 +77,17 @@ impl SpontaneousPayment { Ok(_hash) => { log_info!(self.logger, "Initiated sending {}msat to {}.", amount_msat, node_id); - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Spontaneous { - hash: payment_hash, - preimage: Some(payment_preimage), - }, - status: PaymentStatus::Pending, - direction: PaymentDirection::Outbound, - amount_msat: Some(amount_msat), + let kind = PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(payment_preimage), }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); self.payment_store.insert(payment)?; Ok(payment_id) @@ -97,17 +98,17 @@ impl SpontaneousPayment { match e { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { - let payment = PaymentDetails { - id: payment_id, - kind: PaymentKind::Spontaneous { - hash: payment_hash, - preimage: Some(payment_preimage), - }, - - status: PaymentStatus::Failed, - direction: PaymentDirection::Outbound, - amount_msat: Some(amount_msat), + let kind = PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(payment_preimage), }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + PaymentDirection::Outbound, + PaymentStatus::Failed, + ); self.payment_store.insert(payment)?; Err(Error::PaymentSendingFailed) diff --git a/src/payment/store.rs b/src/payment/store.rs index f7f4942be..eb3ac091f 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -20,6 +20,7 @@ use std::collections::HashMap; use std::iter::FromIterator; use std::ops::Deref; use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Represents a payment. #[derive(Clone, Debug, PartialEq, Eq)] @@ -34,6 +35,21 @@ pub struct PaymentDetails { pub direction: PaymentDirection, /// The status of the payment. pub status: PaymentStatus, + /// The timestamp, in seconds since start of the UNIX epoch, when this entry was last updated. + pub latest_update_timestamp: u64, +} + +impl PaymentDetails { + pub(crate) fn new( + id: PaymentId, kind: PaymentKind, amount_msat: Option, direction: PaymentDirection, + status: PaymentStatus, + ) -> Self { + let latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + Self { id, kind, amount_msat, direction, status, latest_update_timestamp } + } } impl Writeable for PaymentDetails { @@ -48,6 +64,7 @@ impl Writeable for PaymentDetails { (3, self.kind, required), // 4 used to be `secret` before it was moved to `kind` in v0.3.0 (4, None::>, required), + (5, self.latest_update_timestamp, required), (6, self.amount_msat, required), (8, self.direction, required), (10, self.status, required) @@ -58,12 +75,17 @@ impl Writeable for PaymentDetails { impl Readable for PaymentDetails { fn read(reader: &mut R) -> Result { + let unix_time_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); _init_and_read_len_prefixed_tlv_fields!(reader, { (0, id, required), // Used to be `hash` (1, lsp_fee_limits, option), (2, preimage, required), (3, kind_opt, option), (4, secret, required), + (5, latest_update_timestamp, (default_value, unix_time_secs)), (6, amount_msat, required), (8, direction, required), (10, status, required) @@ -72,6 +94,8 @@ impl Readable for PaymentDetails { let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?; let preimage: Option = preimage.0.ok_or(DecodeError::InvalidValue)?; let secret: Option = secret.0.ok_or(DecodeError::InvalidValue)?; + let latest_update_timestamp: u64 = + latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?; let amount_msat: Option = amount_msat.0.ok_or(DecodeError::InvalidValue)?; let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?; let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?; @@ -103,7 +127,7 @@ impl Readable for PaymentDetails { } }; - Ok(PaymentDetails { id, kind, amount_msat, direction, status }) + Ok(PaymentDetails { id, kind, amount_msat, direction, status, latest_update_timestamp }) } } @@ -391,6 +415,11 @@ where payment.status = status; } + payment.latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + self.persist_info(&update.id, payment)?; updated = true; } @@ -487,13 +516,9 @@ mod tests { ) .is_err()); - let payment = PaymentDetails { - id, - kind: PaymentKind::Bolt11 { hash, preimage: None, secret: None }, - amount_msat: None, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - }; + let kind = PaymentKind::Bolt11 { hash, preimage: None, secret: None }; + let payment = + PaymentDetails::new(id, kind, None, PaymentDirection::Inbound, PaymentStatus::Pending); assert_eq!(Ok(false), payment_store.insert(payment.clone())); assert!(payment_store.get(&id).is_some()); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 062d14f61..5959bd58e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -8,10 +8,13 @@ use ldk_node::{ }; use lightning::ln::msgs::SocketAddress; +use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::util::persist::KVStore; use lightning::util::test_utils::TestStore; use lightning_persister::fs_store::FilesystemStore; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; use bitcoin::{Address, Amount, Network, OutPoint, Txid}; use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; @@ -99,6 +102,31 @@ macro_rules! expect_payment_received_event { pub(crate) use expect_payment_received_event; +macro_rules! expect_payment_claimable_event { + ($node: expr, $payment_id: expr, $payment_hash: expr, $claimable_amount_msat: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PaymentClaimable { + payment_id, + payment_hash, + claimable_amount_msat, + .. + } => { + println!("{} got event {:?}", std::stringify!($node), e); + assert_eq!(payment_hash, $payment_hash); + assert_eq!(payment_id, $payment_id); + assert_eq!(claimable_amount_msat, $claimable_amount_msat); + $node.event_handled(); + claimable_amount_msat + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; +} + +pub(crate) use expect_payment_claimable_event; + macro_rules! expect_payment_successful_event { ($node: expr, $payment_id: expr, $fee_paid_msat: expr) => {{ match $node.wait_next_event() { @@ -378,7 +406,7 @@ pub(crate) fn do_channel_full_cycle( let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); - let premine_amount_sat = if expect_anchor_channel { 125_000 } else { 100_000 }; + let premine_amount_sat = if expect_anchor_channel { 2_125_000 } else { 2_100_000 }; premine_and_distribute_funds( &bitcoind, @@ -396,7 +424,7 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_b.next_event(), None); println!("\nA -- connect_open_channel -> B"); - let funding_amount_sat = 80_000; + let funding_amount_sat = 2_080_000; let push_msat = (funding_amount_sat / 2) * 1000; // balance the channel node_a .connect_open_channel( @@ -580,6 +608,89 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + // Test claiming manually registered payments. + let invoice_amount_3_msat = 5_532_000; + let manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash = PaymentHash(Sha256::hash(&manual_preimage.0).to_byte_array()); + let manual_invoice = node_b + .bolt11_payment() + .receive_for_hash(invoice_amount_3_msat, &"asdf", 9217, manual_payment_hash) + .unwrap(); + let manual_payment_id = node_a.bolt11_payment().send(&manual_invoice).unwrap(); + + let claimable_amount_msat = expect_payment_claimable_event!( + node_b, + manual_payment_id, + manual_payment_hash, + invoice_amount_3_msat + ); + node_b + .bolt11_payment() + .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) + .unwrap(); + expect_payment_received_event!(node_b, claimable_amount_msat); + expect_payment_successful_event!(node_a, Some(manual_payment_id), None); + assert_eq!(node_a.payment(&manual_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&manual_payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!( + node_a.payment(&manual_payment_id).unwrap().amount_msat, + Some(invoice_amount_3_msat) + ); + assert!(matches!(node_a.payment(&manual_payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + assert_eq!(node_b.payment(&manual_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&manual_payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!( + node_b.payment(&manual_payment_id).unwrap().amount_msat, + Some(invoice_amount_3_msat) + ); + assert!(matches!(node_b.payment(&manual_payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + + // Test failing manually registered payments. + let invoice_amount_4_msat = 5_532_000; + let manual_fail_preimage = PaymentPreimage([43u8; 32]); + let manual_fail_payment_hash = + PaymentHash(Sha256::hash(&manual_fail_preimage.0).to_byte_array()); + let manual_fail_invoice = node_b + .bolt11_payment() + .receive_for_hash(invoice_amount_3_msat, &"asdf", 9217, manual_fail_payment_hash) + .unwrap(); + let manual_fail_payment_id = node_a.bolt11_payment().send(&manual_fail_invoice).unwrap(); + + expect_payment_claimable_event!( + node_b, + manual_fail_payment_id, + manual_fail_payment_hash, + invoice_amount_4_msat + ); + node_b.bolt11_payment().fail_for_hash(manual_fail_payment_hash).unwrap(); + expect_event!(node_a, PaymentFailed); + assert_eq!(node_a.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed); + assert_eq!( + node_a.payment(&manual_fail_payment_id).unwrap().direction, + PaymentDirection::Outbound + ); + assert_eq!( + node_a.payment(&manual_fail_payment_id).unwrap().amount_msat, + Some(invoice_amount_4_msat) + ); + assert!(matches!( + node_a.payment(&manual_fail_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); + assert_eq!(node_b.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed); + assert_eq!( + node_b.payment(&manual_fail_payment_id).unwrap().direction, + PaymentDirection::Inbound + ); + assert_eq!( + node_b.payment(&manual_fail_payment_id).unwrap().amount_msat, + Some(invoice_amount_4_msat) + ); + assert!(matches!( + node_b.payment(&manual_fail_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); + // Test spontaneous/keysend payments println!("\nA send_spontaneous_payment"); let keysend_amount_msat = 2500_000; @@ -611,8 +722,8 @@ pub(crate) fn do_channel_full_cycle( node_b.payment(&keysend_payment_id).unwrap().kind, PaymentKind::Spontaneous { .. } )); - assert_eq!(node_a.list_payments().len(), 4); - assert_eq!(node_b.list_payments().len(), 5); + assert_eq!(node_a.list_payments().len(), 6); + assert_eq!(node_b.list_payments().len(), 7); println!("\nB close_channel (force: {})", force_close); if force_close { @@ -715,6 +826,7 @@ pub(crate) fn do_channel_full_cycle( let sum_of_all_payments_sat = (push_msat + invoice_amount_1_msat + overpaid_amount_msat + + invoice_amount_3_msat + determined_amount_msat + keysend_amount_msat) / 1000;