From be52bc078c784509ee399c82ba917342069ec1cf Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Mon, 5 May 2025 23:25:38 -0300 Subject: [PATCH 1/6] Introduce PaymentMetadata, PaymentMetadataStore, and PaymentDataWithFallback - Add PaymentMetadata to represent metadata for inbound payments - Add PaymentMetadataStore for persisting PaymentMetadata - Add PaymentDataWithFallback to handle backward compatibility during the transition from legacy bolt11/bolt12 pending payments to the new PaymentMetadata format Issue #425 --- src/io/mod.rs | 2 + src/payment/mod.rs | 3 +- src/payment/store.rs | 640 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 643 insertions(+), 2 deletions(-) diff --git a/src/io/mod.rs b/src/io/mod.rs index 3192dbb86..c0cd55180 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -26,6 +26,8 @@ pub(crate) const PEER_INFO_PERSISTENCE_KEY: &str = "peers"; /// The payment information will be persisted under this prefix. pub(crate) const PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payments"; pub(crate) const PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; +/// The metadata information will be persisted under this prefix. +pub(crate) const PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "metadata"; /// The spendable output information used to persisted under this prefix until LDK Node v0.3.0. pub(crate) const DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = diff --git a/src/payment/mod.rs b/src/payment/mod.rs index b031e37fd..b55c8fd6d 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -19,7 +19,8 @@ pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; pub use spontaneous::SpontaneousPayment; pub use store::{ - ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, + ConfirmationStatus, JitChannelFeeLimits, LSPFeeLimits, PaymentDetails, PaymentDirection, + PaymentKind, PaymentMetadata, PaymentMetadataDetail, PaymentStatus, }; pub use unified_qr::{QrPaymentResult, UnifiedQrPayment}; diff --git a/src/payment/store.rs b/src/payment/store.rs index 2a074031c..9d6eeda33 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -8,21 +8,28 @@ use crate::hex_utils; use crate::io::{ PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::logger::{log_error, LdkLogger}; use crate::types::DynStore; use crate::Error; +use bitcoin::hashes::Hash; +use bitcoin::io::Read; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; +use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::OfferId; -use lightning::util::ser::{Readable, Writeable}; +use lightning::offers::refund::Refund; +use lightning::onion_message::dns_resolution::{DNSSECProof, HumanReadableName}; +use lightning::util::ser::{Hostname, Readable, Writeable, Writer}; use lightning::util::string::UntrustedString; use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, impl_writeable_tlv_based_enum, write_tlv_fields, }; +use lightning_invoice::Bolt11Invoice; use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use bitcoin::{BlockHash, Txid}; @@ -30,6 +37,7 @@ use bitcoin::{BlockHash, Txid}; use std::collections::hash_map; use std::collections::HashMap; use std::ops::Deref; +use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -726,6 +734,570 @@ where } } +/// Represents a payment metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentMetadata { + /// The identifier of this payment. + pub id: PaymentId, + /// The timestamp, in seconds since start of the UNIX epoch, when this entry was last updated. + pub latest_update_timestamp: u64, + /// The direction of the payment. + pub direction: PaymentDirection, + /// The status of the payment. + pub status: PaymentStatus, + /// The metadata detail of the payment. + /// + /// This can be a BOLT 11 invoice, a BOLT 12 offer, or a BOLT 12 refund. + pub payment_metadata_detail: PaymentMetadataDetail, + /// The limits applying to how much fee we allow an LSP to deduct from the payment amount. + pub jit_channel_fee_limit: Option, +} + +impl PaymentMetadata { + pub(crate) fn new( + id: PaymentId, direction: PaymentDirection, payment_metadata_detail: PaymentMetadataDetail, + jit_channel_fee_limit: Option, status: PaymentStatus, + ) -> Self { + let latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + + PaymentMetadata { + id, + latest_update_timestamp, + direction, + payment_metadata_detail, + jit_channel_fee_limit, + status, + } + } + + pub(crate) fn update(&mut self, update: &PaymentMetadataUpdate) -> bool { + debug_assert_eq!( + self.id, update.id, + "We should only ever override payment metadata data for the same payment id" + ); + + let mut updated = false; + + macro_rules! update_if_necessary { + ($val: expr, $update: expr) => { + if $val != $update { + $val = $update; + updated = true; + } + }; + } + + if let Some(preimage_opt) = update.preimage { + match self.payment_metadata_detail { + PaymentMetadataDetail::Bolt11 { ref mut preimage, .. } => { + update_if_necessary!(*preimage, Some(preimage_opt)); + }, + _ => {}, + } + } + + if let Some(hrn_opt) = update.hrn.clone() { + match self.payment_metadata_detail { + PaymentMetadataDetail::Bolt12Offer { ref mut hrn, .. } => { + update_if_necessary!(*hrn, Some(hrn_opt.clone())); + }, + PaymentMetadataDetail::Bolt12Refund { ref mut hrn, .. } => { + update_if_necessary!(*hrn, Some(hrn_opt.clone())); + }, + _ => {}, + } + } + + if let Some(dnssec_proof_opt) = update.dnssec_proof.clone() { + match self.payment_metadata_detail { + PaymentMetadataDetail::Bolt12Offer { ref mut dnssec_proof, .. } => { + update_if_necessary!(*dnssec_proof, Some(dnssec_proof_opt.clone())); + }, + PaymentMetadataDetail::Bolt12Refund { ref mut dnssec_proof, .. } => { + update_if_necessary!(*dnssec_proof, Some(dnssec_proof_opt.clone())); + }, + _ => {}, + } + } + + if let Some(direction) = update.direction { + update_if_necessary!(self.direction, direction); + } + if let Some(counterparty_skimmed_fee_msat) = update.counterparty_skimmed_fee_msat { + if let Some(jit_channel_fee_limit) = &mut self.jit_channel_fee_limit { + update_if_necessary!( + jit_channel_fee_limit.counterparty_skimmed_fee_msat, + Some(counterparty_skimmed_fee_msat) + ); + } else { + updated = true; + } + } + if let Some(state) = &update.status { + update_if_necessary!(self.status, *state); + } + + if updated { + self.latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + } + + updated + } +} + +impl Writeable for PaymentMetadata { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + write_tlv_fields!(writer, { + (0, self.id, required), + (1, self.latest_update_timestamp, required), + (2, self.direction, required), + (3, self.status, required), + (4, self.payment_metadata_detail, required), + (5, self.jit_channel_fee_limit, option), + }); + Ok(()) + } +} + +impl Readable for PaymentMetadata { + fn read(reader: &mut R) -> Result { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, id, required), + (1, latest_update_timestamp, required), + (2, direction, required), + (3, status, required), + (4, payment_metadata_detail, required), + (5, jit_channel_fee_limit, option), + }); + + Ok(PaymentMetadata { + id: id.0.ok_or(DecodeError::InvalidValue)?, + latest_update_timestamp: latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?, + direction: direction.0.ok_or(DecodeError::InvalidValue)?, + status: status.0.ok_or(DecodeError::InvalidValue)?, + payment_metadata_detail: payment_metadata_detail.0.ok_or(DecodeError::InvalidValue)?, + jit_channel_fee_limit, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Defines fee limits for Just-In-Time (JIT) channels opened by a Lightning Service Provider (LSP) +/// to provide inbound liquidity, as per the LSPS2 protocol (bLIP-52). Used in `PaymentMetadata` to +/// track and constrain costs associated with dynamically opened channels for payments. +pub struct JitChannelFeeLimits { + /// The maximal total amount we allow any configured LSP withhold from us when forwarding the + /// payment. + pub max_total_opening_fee_msat: Option, + /// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured + /// LSP withhold from us when forwarding the payment. + pub max_proportional_opening_fee_ppm_msat: Option, + /// The value, in thousands of a satoshi, that was deducted from this payment as an extra + /// fee taken by our channel counterparty. + /// + /// Will only be `Some` once we received the payment. Will always be `None` for LDK Node + /// v0.4 and prior. + pub counterparty_skimmed_fee_msat: Option, +} +impl_writeable_tlv_based!(JitChannelFeeLimits, { + (0, max_total_opening_fee_msat, option), + (1, max_proportional_opening_fee_ppm_msat, option), + (2, counterparty_skimmed_fee_msat, option), +}); + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Represents the metadata details of a payment. +/// +/// This enum encapsulates various types of payment metadata, such as BOLT 11 invoices, +/// BOLT 12 offers, and BOLT 12 refunds, along with their associated details. +pub enum PaymentMetadataDetail { + /// A [BOLT 11] metadata. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + Bolt11 { + /// The invoice associated with the payment. + invoice: String, + /// The pre-image used by the payment. + preimage: Option, + }, + /// A [BOLT 12] 'offer' payment metadata, i.e., a payment metadata for an [`Offer`]. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + /// [`Offer`]: crate::lightning::offers::offer::Offer + Bolt12Offer { + /// The ID of the offer this payment is for. + offer_id: OfferId, + /// The pre-image used by the payment. + preimage: Option, + /// The DNSSEC proof associated with the payment. + dnssec_proof: Option, + /// The human-readable name associated with the payment. + hrn: Option, + /// The quantity of an item requested in the offer. + quantity: Option, + /// The payment hash, i.e., the hash of the `preimage`. + hash: Option, + }, + /// A [BOLT 12] 'refund' payment metadata, i.e., a payment metadata for a [`Refund`]. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + /// [`Refund`]: lightning::offers::refund::Refund + Bolt12Refund { + /// The refund details associated with the payment. + refund: Refund, + /// The human-readable name associated with the refund payment. + hrn: Option, + /// The DNSSEC proof associated with the refund payment. + dnssec_proof: Option, + /// The invoice associated with the refund payment. + invoice: Vec, + /// The pre-image used by the refund payment. + preimage: Option, + }, +} +impl_writeable_tlv_based_enum! { PaymentMetadataDetail, + (0, Bolt11) => { + (0, invoice, required), + (1, preimage, option), + }, + (4, Bolt12Offer) => { + (0, offer_id, required), + (1, preimage, option), + (2, dnssec_proof, option), + (3, hrn, option), + (4, quantity, option), + (5, hash, option), + }, + (6, Bolt12Refund) => { + (0, refund, required), + (1, hrn, option), + (2, dnssec_proof, option), + (4, invoice, required), + (5, preimage, option), + } +} +#[derive(Clone, Debug, PartialEq, Eq)] +/// A wrapper for `DNSSECProof` to enable serialization and deserialization, +/// allowing it to be stored in the payment store. +pub struct DNSSECProofWrapper(pub DNSSECProof); + +impl Writeable for DNSSECProofWrapper { + fn write(&self, w: &mut W) -> Result<(), lightning::io::Error> { + (self.0.name.as_str().len() as u8).write(w)?; + w.write_all(&self.0.name.as_str().as_bytes())?; + self.0.proof.write(w)?; + + Ok(()) + } +} + +impl Readable for DNSSECProofWrapper { + fn read(r: &mut R) -> Result { + let s = Hostname::read(r)?; + let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; + let proof = Vec::::read(r)?; + + Ok(DNSSECProofWrapper(DNSSECProof { name, proof })) + } +} + +pub(crate) struct PaymentMetadataStore +where + L::Target: LdkLogger, +{ + metadata: Mutex>, + kv_store: Arc, + logger: L, +} + +impl PaymentMetadataStore +where + L::Target: LdkLogger, +{ + pub(crate) fn new(metadata: Vec, kv_store: Arc, logger: L) -> Self { + let metadata = Mutex::new(HashMap::from_iter( + metadata.into_iter().map(|payment| (payment.id, payment)), + )); + Self { metadata, kv_store, logger } + } + + pub(crate) fn insert(&self, payment: PaymentMetadata) -> Result { + let mut locked_payments = self.metadata.lock().unwrap(); + + let updated = locked_payments.insert(payment.id, payment.clone()).is_some(); + self.persist_info(&payment.id, &payment)?; + Ok(updated) + } + + pub(crate) fn remove(&self, id: &PaymentId) -> Result<(), Error> { + let removed = self.metadata.lock().unwrap().remove(id).is_some(); + if removed { + let store_key = hex_utils::to_string(&id.0); + self.kv_store + .remove( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key, + false, + ) + .map_err(|e| { + log_error!( + self.logger, + "Removing payment data for key {}/{}/{} failed due to: {}", + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + store_key, + e + ); + Error::PersistenceFailed + })?; + } + Ok(()) + } + + pub(crate) fn get(&self, id: &PaymentId) -> Option { + self.metadata.lock().unwrap().get(id).cloned() + } + + pub(crate) fn update( + &self, update: &PaymentMetadataUpdate, + ) -> Result { + let mut locked_payments_metadata = self.metadata.lock().unwrap(); + + if let Some(payment) = locked_payments_metadata.get_mut(&update.id) { + let updated = payment.update(update); + if updated { + self.persist_info(&update.id, payment)?; + Ok(PaymentStoreUpdateResult::Updated) + } else { + Ok(PaymentStoreUpdateResult::Unchanged) + } + } else { + Ok(PaymentStoreUpdateResult::NotFound) + } + } + + pub(crate) fn list_filter bool>( + &self, f: F, + ) -> Vec { + self.metadata.lock().unwrap().values().filter(f).cloned().collect::>() + } + + fn persist_info(&self, id: &PaymentId, payment: &PaymentMetadata) -> Result<(), Error> { + let store_key = hex_utils::to_string(&id.0); + let data = payment.encode(); + self.kv_store + .write( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key, + &data, + ) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + store_key, + e + ); + Error::PersistenceFailed + })?; + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PaymentMetadataUpdate { + pub id: PaymentId, + pub direction: Option, + pub preimage: Option, + pub status: Option, + pub hrn: Option, // Hashed Recipient Name + pub dnssec_proof: Option, // Prova DNSSEC + pub counterparty_skimmed_fee_msat: Option, +} + +impl PaymentMetadataUpdate { + pub fn new(id: PaymentId) -> Self { + Self { + id, + direction: None, + preimage: None, + status: None, + hrn: None, + dnssec_proof: None, + counterparty_skimmed_fee_msat: None, + } + } +} + +impl From<&PaymentMetadata> for PaymentMetadataUpdate { + fn from(value: &PaymentMetadata) -> Self { + let (preimage, hrn, dnssec_proof) = match &value.payment_metadata_detail { + PaymentMetadataDetail::Bolt11 { preimage, .. } => (preimage, None, None), + PaymentMetadataDetail::Bolt12Offer { preimage, hrn, dnssec_proof, .. } => { + (preimage, hrn.clone(), dnssec_proof.clone()) + }, + PaymentMetadataDetail::Bolt12Refund { preimage, hrn, dnssec_proof, .. } => { + (preimage, hrn.clone(), dnssec_proof.clone()) + }, + }; + let counterparty_skimmed_fee_msat = match value.jit_channel_fee_limit { + Some(ref limit) => limit.counterparty_skimmed_fee_msat, + None => None, + }; + + Self { + id: value.id, + direction: Some(value.direction), + preimage: *preimage, + status: Some(value.status.clone()), + hrn, + dnssec_proof, + counterparty_skimmed_fee_msat, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// A struct that holds both `PaymentDetails` and `PaymentMetadata`. +/// Ensures that if one does not exist, the other must exist, aiding in the backward compatibility +/// between `PaymentDetails` and `PaymentMetadata` for pending payments. +pub struct PaymentDataWithFallback { + pub payment_metadata: Option, + pub payment_details: Option, + pub status: PaymentStatus, + pub direction: PaymentDirection, +} + +impl PaymentDataWithFallback { + pub(crate) fn new( + payment_id: &PaymentId, logger: Arc, metadata_store: Arc>, + payment_store: Arc>, error_fn: impl Fn() -> E, + ) -> Result + where + L: Deref, + L::Target: LdkLogger, + { + let mut payment_metadata = None; + let mut payment_details = None; + let mut status = None; + let mut direction = None; + + if let Some(metadata) = metadata_store.get(&payment_id) { + payment_metadata = Some(metadata.clone()); + status = Some(metadata.status); + direction = Some(metadata.direction); + } + if let Some(payment) = payment_store.get(&payment_id) { + payment_details = Some(payment.clone()); + status = Some(payment.status); + direction = Some(payment.direction); + } + if status.is_none() || direction.is_none() { + log_error!( + logger, + "Payment with id {} not found in either metadata or payment store", + payment_id + ); + return Err(error_fn()); + } + + Ok(Self { + payment_metadata, + payment_details, + direction: direction.unwrap(), + status: status.unwrap(), + }) + } + + /// Retrieves payment details, creating them from payment metadata if they don't exist. + /// Returns an error from `error_fn` if neither details nor metadata are available. + pub fn get_payment_detail(&self, error_fn: impl Fn() -> E) -> Result { + if let Some(detail) = &self.payment_details { + return Ok(detail.clone()); + } else if let Some(metadata) = &self.payment_metadata { + let mut amount_msat = None; + let kind = match &metadata.payment_metadata_detail { + PaymentMetadataDetail::Bolt11 { invoice, preimage } => { + let invoice = Bolt11Invoice::from_str(invoice.as_str()).unwrap(); + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let payment_secret = invoice.payment_secret(); + amount_msat = invoice.amount_milli_satoshis(); + match &metadata.jit_channel_fee_limit { + Some(jit_channel_fee_limit) => { + let lsp_fee_limits = LSPFeeLimits { + max_proportional_opening_fee_ppm_msat: jit_channel_fee_limit + .max_proportional_opening_fee_ppm_msat, + max_total_opening_fee_msat: jit_channel_fee_limit + .max_total_opening_fee_msat, + }; + PaymentKind::Bolt11Jit { + hash: payment_hash, + secret: Some(payment_secret.clone()), + preimage: *preimage, + lsp_fee_limits, + counterparty_skimmed_fee_msat: jit_channel_fee_limit + .counterparty_skimmed_fee_msat, + } + }, + None => PaymentKind::Bolt11 { + hash: payment_hash, + secret: Some(payment_secret.clone()), + preimage: *preimage, + }, + } + }, + PaymentMetadataDetail::Bolt12Offer { + offer_id, preimage, hash, quantity, .. + } => PaymentKind::Bolt12Offer { + hash: *hash, + offer_id: *offer_id, + preimage: *preimage, + quantity: *quantity, + secret: None, + payer_note: None, + }, + PaymentMetadataDetail::Bolt12Refund { refund, preimage, invoice, .. } => { + let invoice = Bolt12Invoice::try_from(invoice.to_vec()).unwrap(); + let payment_hash = PaymentHash(invoice.payment_hash().0); + amount_msat = Some(invoice.amount_msats()); + PaymentKind::Bolt12Refund { + preimage: *preimage, + hash: Some(payment_hash), + secret: None, + payer_note: refund + .payer_note() + .map(|note| UntrustedString(note.0.to_string())), + quantity: refund.quantity(), + } + }, + }; + + return Ok(PaymentDetails::new( + metadata.id, + kind, + amount_msat, + None, + metadata.direction, + metadata.status, + )); + } else { + return Err(error_fn()); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -957,4 +1529,70 @@ mod tests { } } } + + #[test] + fn payment_metadata_info_is_persisted() { + let store: Arc = Arc::new(TestStore::new(false)); + let logger = Arc::new(TestLogger::new()); + let metadata_store = PaymentMetadataStore::new(Vec::new(), Arc::clone(&store), logger); + + let hash = PaymentHash([42u8; 32]); + let id = PaymentId([42u8; 32]); + assert!(metadata_store.get(&id).is_none()); + + let store_key = hex_utils::to_string(&hash.0); + assert!(store + .read( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key + ) + .is_err()); + let payment_metadata_detail = + PaymentMetadataDetail::Bolt11 { invoice: "test".to_string(), preimage: None }; + let payment_metadata = PaymentMetadata::new( + id, + PaymentDirection::Inbound, + payment_metadata_detail, + None, + PaymentStatus::Pending, + ); + + assert_eq!(Ok(false), metadata_store.insert(payment_metadata.clone())); + assert!(metadata_store.get(&id).is_some()); + assert!(store + .read( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key + ) + .is_ok()); + + assert_eq!(Ok(true), metadata_store.insert(payment_metadata.clone())); + assert!(metadata_store.get(&id).is_some()); + + // Check update returns `Updated` + let mut update = PaymentMetadataUpdate::new(id); + update.status = Some(PaymentStatus::Succeeded); + assert_eq!(Ok(PaymentStoreUpdateResult::Updated), metadata_store.update(&update)); + + // Check no-op update yields `Unchanged` + let mut update = PaymentMetadataUpdate::new(id); + update.status = Some(PaymentStatus::Succeeded); + assert_eq!(Ok(PaymentStoreUpdateResult::Unchanged), metadata_store.update(&update)); + + // Check bogus update yields `NotFound` + let bogus_id = PaymentId([84u8; 32]); + let mut update = PaymentMetadataUpdate::new(bogus_id); + update.status = Some(PaymentStatus::Succeeded); + assert_eq!(Ok(PaymentStoreUpdateResult::NotFound), metadata_store.update(&update)); + + assert!(metadata_store.get(&id).is_some()); + + assert_eq!(PaymentStatus::Succeeded, metadata_store.get(&id).unwrap().status); + + // Check remove + assert_eq!(Ok(()), metadata_store.remove(&id)); + assert!(metadata_store.get(&id).is_none()); + } } From 48a760c9b476b1fa301764c70661f57524eb7964 Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Tue, 6 May 2025 11:06:15 -0300 Subject: [PATCH 2/6] Add persisted payment metadata reading in store Issue #425 --- src/builder.rs | 15 ++++++++++++++- src/io/utils.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index 224cc9fa7..1f867184a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -23,7 +23,7 @@ use crate::liquidity::{ }; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; -use crate::payment::store::PaymentStore; +use crate::payment::store::{PaymentMetadataStore, PaymentStore}; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ @@ -1023,6 +1023,18 @@ fn build_with_store_internal( }, }; + let metadata_store = match io::utils::read_metadata(Arc::clone(&kv_store), Arc::clone(&logger)) + { + Ok(metadata) => Arc::new(PaymentMetadataStore::new( + metadata, + Arc::clone(&kv_store), + Arc::clone(&logger), + )), + Err(_) => { + return Err(BuildError::ReadFailed); + }, + }; + let wallet = Arc::new(Wallet::new( bdk_wallet, wallet_persister, @@ -1513,6 +1525,7 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + metadata_store, is_listening, node_metrics, }) diff --git a/src/io/utils.rs b/src/io/utils.rs index b5537ed7d..7c491acbd 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -14,6 +14,7 @@ use crate::io::{ NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, }; use crate::logger::{log_error, LdkLogger, Logger}; +use crate::payment::store::PaymentMetadata; use crate::peer_store::PeerStore; use crate::sweep::DeprecatedSpendableOutputInfo; use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; @@ -232,6 +233,36 @@ where Ok(res) } +/// Read previously persisted payment metadata information from the store. +pub(crate) fn read_metadata( + kv_store: Arc, logger: L, +) -> Result, std::io::Error> +where + L::Target: LdkLogger, +{ + let mut res = Vec::new(); + + for stored_key in kv_store.list( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + )? { + let mut reader = Cursor::new(kv_store.read( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_METADATA_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &stored_key, + )?); + let payment = PaymentMetadata::read(&mut reader).map_err(|e| { + log_error!(logger, "Failed to deserialize PaymentMetadata: {}", e); + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to deserialize PaymentMetadata", + ) + })?; + res.push(payment); + } + Ok(res) +} + /// Read `OutputSweeper` state from the store. pub(crate) fn read_output_sweeper( broadcaster: Arc, fee_estimator: Arc, From 5eafd6c08d47f9119de7363d4d77561baa2b015c Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Tue, 6 May 2025 12:53:24 -0300 Subject: [PATCH 3/6] Add `legacy_payment_store` Cargo feature for compatibility testing Introduces the optional `legacy_payment_store` feature to enable testing of legacy payment flows during the transition to `PaymentMetadata` for inbound payments. Issue #425 --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 9535ddfc8..4609cda8c 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ panic = 'abort' # Abort on panic [features] default = [] +legacy_payment_store = [] [dependencies] lightning = { version = "0.1.0", features = ["std"] } @@ -129,4 +130,5 @@ check-cfg = [ "cfg(tokio_unstable)", "cfg(cln_test)", "cfg(lnd_test)", + "cfg(legacy_payment_store)", ] From 194a542771752639d42c372783d4313e44701c1d Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Tue, 6 May 2025 11:37:42 -0300 Subject: [PATCH 4/6] Persist payment metadata for pending inbound payments instead of using paymentStore Inbound Bolt11 and Bolt12 payments that are pending should now be stored in `PaymentMetadataStore` rather than directly in `PaymentStore`. Once the payment is completed (successful or failed), it is then moved to `PaymentStore`, and its status is updated in `PaymentMetadata`. This change maintains backward compatibility with the previous behavior of storing all payment information directly in `PaymentStore`. issue #425 --- src/event.rs | 303 +++++++++++++++++++++++++++++----------- src/lib.rs | 46 ++++++- src/payment/bolt11.rs | 311 +++++++++++++++++++++++++++++++++++------- src/payment/bolt12.rs | 67 +++++++-- 4 files changed, 585 insertions(+), 142 deletions(-) diff --git a/src/event.rs b/src/event.rs index 00d8441e5..b68f58bfa 100644 --- a/src/event.rs +++ b/src/event.rs @@ -8,8 +8,8 @@ use crate::types::{CustomTlvRecord, DynStore, Sweeper, Wallet}; use crate::{ - hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, - UserChannelId, + hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PaymentMetadataStore, + PeerInfo, PeerStore, UserChannelId, }; use crate::config::{may_announce_channel, Config}; @@ -19,8 +19,9 @@ use crate::liquidity::LiquiditySource; use crate::logger::Logger; use crate::payment::store::{ - PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, - PaymentStore, PaymentStoreUpdateResult, + PaymentDataWithFallback, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, + PaymentMetadataDetail, PaymentMetadataUpdate, PaymentStatus, PaymentStore, + PaymentStoreUpdateResult, }; use crate::io::{ @@ -450,6 +451,7 @@ where network_graph: Arc, liquidity_source: Option>>>, payment_store: Arc>, + metadata_store: Arc>, peer_store: Arc>, runtime: Arc>>>, logger: L, @@ -466,8 +468,9 @@ where channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, - payment_store: Arc>, peer_store: Arc>, - runtime: Arc>>>, logger: L, config: Arc, + payment_store: Arc>, metadata_store: Arc>, + peer_store: Arc>, runtime: Arc>>>, + logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -479,6 +482,7 @@ where network_graph, liquidity_source, payment_store, + metadata_store, peer_store, logger, runtime, @@ -572,7 +576,13 @@ where payment_id: _, } => { let payment_id = PaymentId(payment_hash.0); - if let Some(info) = self.payment_store.get(&payment_id) { + if let Ok(info) = PaymentDataWithFallback::new( + &payment_id, + Arc::new(self.logger.clone()), + self.metadata_store.clone(), + self.payment_store.clone(), + || ReplayEvent(), + ) { if info.direction == PaymentDirection::Outbound { log_info!( self.logger, @@ -595,7 +605,11 @@ where } if info.status == PaymentStatus::Succeeded - || matches!(info.kind, PaymentKind::Spontaneous { .. }) + || info + .payment_details + .as_ref() + .map(|details| matches!(details.kind, PaymentKind::Spontaneous { .. })) + .unwrap_or(false) { log_info!( self.logger, @@ -605,35 +619,34 @@ where ); self.channel_manager.fail_htlc_backwards(&payment_hash); - let update = PaymentDetailsUpdate { - status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_id) - }; - match self.payment_store.update(&update) { - Ok(_) => return Ok(()), - Err(e) => { - log_error!(self.logger, "Failed to access payment store: {}", e); - return Err(ReplayEvent()); - }, - }; + return self.fail_payment(&info, None); } - let max_total_opening_fee_msat = match info.kind { - PaymentKind::Bolt11Jit { lsp_fee_limits, .. } => { - lsp_fee_limits - .max_total_opening_fee_msat - .or_else(|| { + let max_total_opening_fee_msat = info + .payment_details + .as_ref() + .and_then(|details| { + if let PaymentKind::Bolt11Jit { lsp_fee_limits, .. } = &details.kind { + lsp_fee_limits.max_total_opening_fee_msat.or_else(|| { lsp_fee_limits.max_proportional_opening_fee_ppm_msat.and_then( |max_prop_fee| { - // If it's a variable amount payment, compute the actual fee. compute_opening_fee(amount_msat, 0, max_prop_fee) }, ) }) - .unwrap_or(0) - }, - _ => 0, - }; + } else { + None + } + }) + .or_else(|| { + info.payment_metadata.as_ref().and_then(|metadata| { + metadata + .jit_channel_fee_limit + .as_ref() + .and_then(|jit_limits| jit_limits.max_total_opening_fee_msat) + }) + }) + .unwrap_or(0); if counterparty_skimmed_fee_msat > max_total_opening_fee_msat { log_info!( @@ -645,52 +658,67 @@ where ); self.channel_manager.fail_htlc_backwards(&payment_hash); - let update = PaymentDetailsUpdate { - hash: Some(Some(payment_hash)), - status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_id) - }; - match self.payment_store.update(&update) { - Ok(_) => return Ok(()), - Err(e) => { - log_error!(self.logger, "Failed to access payment store: {}", e); - return Err(ReplayEvent()); - }, - }; + return self.fail_payment(&info, Some(payment_hash)); } // If the LSP skimmed anything, update our stored payment. if counterparty_skimmed_fee_msat > 0 { - match info.kind { - PaymentKind::Bolt11Jit { .. } => { - let update = PaymentDetailsUpdate { - counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)), - ..PaymentDetailsUpdate::new(payment_id) + if info.payment_details.is_some() || info.payment_metadata.is_some() { + if let Some(payment_detail) = info.payment_details.clone() { + match payment_detail.kind { + PaymentKind::Bolt11Jit { .. } => { + let update = PaymentDetailsUpdate { + counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)), + ..PaymentDetailsUpdate::new(payment_id) + }; + match self.payment_store.update(&update) { + Ok(_) => (), + Err(e) => { + log_error!(self.logger, "Failed to access payment store: {}", e); + return Err(ReplayEvent()); + }, + }; + } + _ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments."), + } + } + if let Some(payment_metadata) = info.payment_metadata.clone() { + let update = PaymentMetadataUpdate { + counterparty_skimmed_fee_msat: Some( + counterparty_skimmed_fee_msat, + ), + ..PaymentMetadataUpdate::new(payment_metadata.id) }; - match self.payment_store.update(&update) { + match self.metadata_store.update(&update) { Ok(_) => (), Err(e) => { - log_error!(self.logger, "Failed to access payment store: {}", e); + log_error!( + self.logger, + "Failed to access metadata store: {}", + e + ); return Err(ReplayEvent()); }, }; } - _ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments."), + } else { + debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments.") } } // 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, .. } => { + macro_rules! handle_payment_claimable { + ($payment_source:expr, $preimage:expr) => { if purpose.preimage().is_none() { debug_assert!( - preimage.is_none(), + $preimage.is_none(), "We would have registered the preimage if we knew" ); let custom_records = onion_fields + .as_ref() .map(|cf| { cf.custom_tlvs().into_iter().map(|tlv| tlv.into()).collect() }) @@ -712,10 +740,20 @@ where ); return Err(ReplayEvent()); }, - }; + } } - }, - _ => {}, + }; + } + if let Some(payment_detail) = &info.payment_details { + if let PaymentKind::Bolt11 { preimage, .. } = payment_detail.kind { + handle_payment_claimable!(payment_detail, preimage); + } + } else if let Some(payment_metadata) = &info.payment_metadata { + if let PaymentMetadataDetail::Bolt11 { preimage, .. } = + payment_metadata.payment_metadata_detail + { + handle_payment_claimable!(payment_metadata, preimage); + } } } @@ -832,18 +870,15 @@ where ); self.channel_manager.fail_htlc_backwards(&payment_hash); - let update = PaymentDetailsUpdate { - hash: Some(Some(payment_hash)), - status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_id) - }; - match self.payment_store.update(&update) { - Ok(_) => return Ok(()), - Err(e) => { - log_error!(self.logger, "Failed to access payment store: {}", e); - return Err(ReplayEvent()); - }, - }; + if let Ok(info) = PaymentDataWithFallback::new( + &payment_id, + Arc::new(self.logger.clone()), + self.metadata_store.clone(), + self.payment_store.clone(), + || ReplayEvent(), + ) { + return self.fail_payment(&info, Some(payment_hash)); + } } }, LdkEvent::PaymentClaimed { @@ -864,6 +899,25 @@ where hex_utils::to_string(&payment_hash.0), amount_msat, ); + let info = PaymentDataWithFallback::new( + &payment_id, + Arc::new(self.logger.clone()), + self.metadata_store.clone(), + self.payment_store.clone(), + || ReplayEvent(), + )?; + if info.payment_details.is_none() { + let detail = info.get_payment_detail(|| ReplayEvent())?; + if let Err(e) = self.payment_store.insert(detail) { + log_error!( + self.logger, + "Failed to insert payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + }; + } let update = match purpose { PaymentPurpose::Bolt11InvoicePayment { @@ -905,6 +959,18 @@ where }, }; + if info.payment_metadata.is_some() { + let update_metadata = PaymentMetadataUpdate { + preimage: update.preimage.flatten(), + status: Some(PaymentStatus::Succeeded), + ..PaymentMetadataUpdate::new(payment_id) + }; + if let Err(e) = self.metadata_store.update(&update_metadata) { + log_error!(self.logger, "Failed to access payment metadata store: {}", e); + return Err(ReplayEvent()); + } + } + match self.payment_store.update(&update) { Ok(PaymentStoreUpdateResult::Updated) | Ok(PaymentStoreUpdateResult::Unchanged) => ( @@ -1013,18 +1079,15 @@ where reason ); - let update = PaymentDetailsUpdate { - hash: Some(payment_hash), - status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_id) - }; - match self.payment_store.update(&update) { - Ok(_) => {}, - Err(e) => { - log_error!(self.logger, "Failed to access payment store: {}", e); - return Err(ReplayEvent()); - }, - }; + let info = PaymentDataWithFallback::new( + &payment_id, + Arc::new(self.logger.clone()), + self.metadata_store.clone(), + self.payment_store.clone(), + || ReplayEvent(), + )?; + + self.fail_payment(&info, payment_hash)?; let event = Event::PaymentFailed { payment_id: Some(payment_id), payment_hash, reason }; @@ -1483,6 +1546,87 @@ where } Ok(()) } + + fn fail_payment( + &self, info: &PaymentDataWithFallback, payment_hash: Option, + ) -> Result<(), ReplayEvent> { + if let Some(metadata) = &info.payment_metadata { + let update = PaymentMetadataUpdate { + status: Some(PaymentStatus::Failed), + ..PaymentMetadataUpdate::new(metadata.id) + }; + match self.metadata_store.update(&update) { + Ok(_) => {}, + Err(e) => { + log_error!(self.logger, "Failed to access payment metadata store: {}", e); + return Err(ReplayEvent()); + }, + }; + } + + let detail = info.get_payment_detail(|| ReplayEvent())?; + + let kind = if let Some(hash) = payment_hash { + match detail.kind { + PaymentKind::Bolt11 { secret, preimage, .. } => { + PaymentKind::Bolt11 { hash, secret, preimage } + }, + PaymentKind::Bolt12Offer { + offer_id, + preimage, + quantity, + secret, + payer_note, + .. + } => PaymentKind::Bolt12Offer { + hash: Some(hash), + offer_id, + preimage, + quantity, + secret, + payer_note, + }, + PaymentKind::Spontaneous { preimage, .. } => { + PaymentKind::Spontaneous { hash, preimage } + }, + PaymentKind::Bolt11Jit { + secret, + preimage, + lsp_fee_limits, + counterparty_skimmed_fee_msat, + .. + } => PaymentKind::Bolt11Jit { + hash, + secret, + preimage, + lsp_fee_limits, + counterparty_skimmed_fee_msat, + }, + PaymentKind::Bolt12Refund { preimage, quantity, secret, payer_note, .. } => { + PaymentKind::Bolt12Refund { + hash: Some(hash), + preimage, + quantity, + secret, + payer_note, + } + }, + _ => detail.kind, + } + } else { + detail.kind + }; + + let payment_detail = PaymentDetails { status: PaymentStatus::Failed, kind, ..detail }; + + match self.payment_store.insert_or_update(&payment_detail) { + Ok(_) => Ok(()), + Err(e) => { + log_error!(self.logger, "Failed to access payment store: {}", e); + Err(ReplayEvent()) + }, + } + } } #[cfg(test)] @@ -1510,7 +1654,6 @@ mod tests { for _ in 0..5 { assert_eq!(event_queue.wait_next_event(), expected_event); assert_eq!(event_queue.next_event_async().await, expected_event); - assert_eq!(event_queue.next_event(), Some(expected_event.clone())); } // Check we can read back what we persisted. diff --git a/src/lib.rs b/src/lib.rs index 93393585d..a61b159eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,7 +132,7 @@ use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; use liquidity::{LSPS1Liquidity, LiquiditySource}; -use payment::store::PaymentStore; +use payment::store::{PaymentMetadata, PaymentMetadataStore, PaymentStore}; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, UnifiedQrPayment, @@ -197,6 +197,7 @@ pub struct Node { scorer: Arc>, peer_store: Arc>>, payment_store: Arc>>, + metadata_store: Arc>>, is_listening: Arc, node_metrics: Arc>, } @@ -535,6 +536,7 @@ impl Node { Arc::clone(&self.network_graph), self.liquidity_source.clone(), Arc::clone(&self.payment_store), + Arc::clone(&self.metadata_store), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), Arc::clone(&self.logger), @@ -837,6 +839,7 @@ impl Node { Arc::clone(&self.connection_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), + Arc::clone(&self.metadata_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.logger), @@ -854,6 +857,7 @@ impl Node { Arc::clone(&self.connection_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), + Arc::clone(&self.metadata_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.logger), @@ -869,6 +873,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.metadata_store), Arc::clone(&self.logger), ) } @@ -882,6 +887,7 @@ impl Node { Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.metadata_store), Arc::clone(&self.logger), )) } @@ -1487,6 +1493,44 @@ impl Node { self.payment_store.list_filter(|_| true) } + /// Retrieve the details of a specific payment metadata with the given id. + /// + /// Returns `Some` if the payment metadata was known and `None` otherwise. + pub fn payment_metadata(&self, payment_id: &PaymentId) -> Option { + self.metadata_store.get(payment_id) + } + + /// Remove the payment metadata with the given id from the store. + pub fn remove_payment_metadata(&self, payment_id: &PaymentId) -> Result<(), Error> { + self.metadata_store.remove(&payment_id) + } + + /// Retrieves all payment metadata that match the given predicate. + /// + /// For example, you could retrieve all metadata for inbound payments as follows: + /// ``` + /// # use ldk_node::Builder; + /// # use ldk_node::config::Config; + /// # use ldk_node::payment::PaymentDirection; + /// # use ldk_node::bitcoin::Network; + /// # let mut config = Config::default(); + /// # config.network = Network::Regtest; + /// # config.storage_dir_path = "/tmp/ldk_node_test/".to_string(); + /// # let builder = Builder::from_config(config); + /// # let node = builder.build().unwrap(); + /// node.list_payments_metadata_with_filter(|m| m.direction == PaymentDirection::Inbound); + /// ``` + pub fn list_payments_metadata_with_filter bool>( + &self, f: F, + ) -> Vec { + self.metadata_store.list_filter(f) + } + + /// Retrieves all payments. + pub fn list_payments_metadata(&self) -> Vec { + self.metadata_store.list_filter(|_| true) + } + /// Retrieves a list of known peers. pub fn list_peers(&self) -> Vec { let mut peers = Vec::new(); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index ee0f24f05..dea2681a3 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -15,12 +15,13 @@ use crate::error::Error; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{ - LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, - PaymentStatus, PaymentStore, PaymentStoreUpdateResult, + JitChannelFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, + PaymentMetadata, PaymentMetadataDetail, PaymentStatus, PaymentStore, PaymentStoreUpdateResult, }; use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; use crate::types::ChannelManager; +use crate::PaymentMetadataStore; use lightning::ln::bolt11_payment; use lightning::ln::channelmanager::{ @@ -36,8 +37,11 @@ use lightning_invoice::Bolt11InvoiceDescription as LdkBolt11InvoiceDescription; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; +use std::str::FromStr; use std::sync::{Arc, RwLock}; +use super::store::{PaymentDataWithFallback, PaymentMetadataUpdate}; + #[cfg(not(feature = "uniffi"))] type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; #[cfg(feature = "uniffi")] @@ -68,6 +72,7 @@ pub struct Bolt11Payment { connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc>>, + metadata_store: Arc>>, peer_store: Arc>>, config: Arc, logger: Arc, @@ -79,8 +84,9 @@ impl Bolt11Payment { channel_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, - payment_store: Arc>>, peer_store: Arc>>, - config: Arc, logger: Arc, + payment_store: Arc>>, + metadata_store: Arc>>, + peer_store: Arc>>, config: Arc, logger: Arc, ) -> Self { Self { runtime, @@ -88,6 +94,7 @@ impl Bolt11Payment { connection_manager, liquidity_source, payment_store, + metadata_store, peer_store, config, logger, @@ -112,7 +119,7 @@ impl Bolt11Payment { })?; let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); - if let Some(payment) = self.payment_store.get(&payment_id) { + if let Ok(payment) = self.get_payment_data_with_fallback(&payment_id) { if payment.status == PaymentStatus::Pending || payment.status == PaymentStatus::Succeeded { @@ -225,7 +232,7 @@ impl Bolt11Payment { let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); - if let Some(payment) = self.payment_store.get(&payment_id) { + if let Ok(payment) = self.get_payment_data_with_fallback(&payment_id) { if payment.status == PaymentStatus::Pending || payment.status == PaymentStatus::Succeeded { @@ -363,24 +370,39 @@ impl Bolt11Payment { 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 { + let payment = self.get_payment_data_with_fallback(&payment_id)?; + let expected_amount_msat = payment + .payment_details + .and_then(|detail| detail.amount_msat) + .or_else(|| { + payment.payment_metadata.as_ref().and_then(|metadata| { + if let PaymentMetadataDetail::Bolt11 { invoice, .. } = + &metadata.payment_metadata_detail + { + Bolt11Invoice::from_str(invoice.as_str()) + .ok() + .and_then(|invoice| invoice.amount_milli_satoshis()) + } else { + None + } + }) + }) + .ok_or_else(|| { + log_error!( + self.logger, + "Failed to manually claim payment {}: missing amount or metadata", + payment_id + ); + Error::InvalidAmount + })?; + + if claimable_amount_msat < expected_amount_msat { log_error!( self.logger, - "Failed to manually claim unknown payment with hash: {}", - payment_hash + "Failed to manually claim payment {} as the claimable amount is less than expected", + payment_id ); - return Err(Error::InvalidPaymentHash); + return Err(Error::InvalidAmount); } self.channel_manager.claim_funds(preimage); @@ -402,31 +424,69 @@ impl Bolt11Payment { /// [`PaymentClaimable`]: crate::Event::PaymentClaimable pub fn fail_for_hash(&self, payment_hash: PaymentHash) -> Result<(), Error> { let payment_id = PaymentId(payment_hash.0); + let payment = self.get_payment_data_with_fallback(&payment_id)?; + if payment.payment_metadata.is_some() { + let update = PaymentMetadataUpdate { + status: Some(PaymentStatus::Failed), + ..PaymentMetadataUpdate::new(payment_id) + }; + match self.metadata_store.update(&update) { + Ok(PaymentStoreUpdateResult::Updated) | Ok(PaymentStoreUpdateResult::Unchanged) => { + () + }, + Ok(PaymentStoreUpdateResult::NotFound) => { + log_error!( + self.logger, + "Failed to manually fail unknown payment with hash {}", + payment_hash, + ); + return Err(Error::InvalidPaymentHash); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to manually fail payment with hash {}: {}", + payment_hash, + e + ); + return Err(e); + }, + } + } + if payment.payment_details.is_some() { + let update = PaymentDetailsUpdate { + status: Some(PaymentStatus::Failed), + ..PaymentDetailsUpdate::new(payment_id) + }; + match self.payment_store.update(&update) { + Ok(PaymentStoreUpdateResult::Updated) | Ok(PaymentStoreUpdateResult::Unchanged) => { + () + }, + Ok(PaymentStoreUpdateResult::NotFound) => { + log_error!( + self.logger, + "Failed to manually fail unknown payment with hash {}", + payment_hash, + ); + return Err(Error::InvalidPaymentHash); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to manually fail payment with hash {}: {}", + payment_hash, + e + ); + return Err(e); + }, + } + } else { + let detail = PaymentDetails { + status: PaymentStatus::Failed, + ..payment.get_payment_detail(|| Error::InvalidPaymentHash)? + }; - let update = PaymentDetailsUpdate { - status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_id) - }; - - match self.payment_store.update(&update) { - Ok(PaymentStoreUpdateResult::Updated) | Ok(PaymentStoreUpdateResult::Unchanged) => (), - Ok(PaymentStoreUpdateResult::NotFound) => { - log_error!( - self.logger, - "Failed to manually fail unknown payment with hash {}", - payment_hash, - ); - return Err(Error::InvalidPaymentHash); - }, - Err(e) => { - log_error!( - self.logger, - "Failed to manually fail payment with hash {}: {}", - payment_hash, - e - ); - return Err(e); - }, + self.payment_store.insert(detail)?; } self.channel_manager.fail_htlc_backwards(&payment_hash); @@ -498,6 +558,64 @@ impl Bolt11Payment { self.receive_inner(None, description, expiry_secs, Some(payment_hash)) } + #[cfg(not(feature = "legacy_payment_store"))] + pub(crate) fn receive_inner( + &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, + expiry_secs: u32, manual_claim_payment_hash: Option, + ) -> Result { + let invoice = { + let invoice_params = Bolt11InvoiceParameters { + amount_msats: amount_msat, + description: invoice_description.clone(), + invoice_expiry_delta_secs: Some(expiry_secs), + payment_hash: manual_claim_payment_hash, + ..Default::default() + }; + + match self.channel_manager.create_bolt11_invoice(invoice_params) { + 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 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 payment_metadata_detail = + PaymentMetadataDetail::Bolt11 { invoice: invoice.clone().to_string(), preimage }; + + let payment_metadata = PaymentMetadata::new( + id, + PaymentDirection::Inbound, + payment_metadata_detail, + None, + PaymentStatus::Pending, + ); + self.metadata_store.insert(payment_metadata)?; + + Ok(invoice) + } + + #[cfg(feature = "legacy_payment_store")] pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, @@ -604,8 +722,95 @@ impl Bolt11Payment { max_proportional_lsp_fee_limit_ppm_msat, ) } + #[cfg(not(feature = "legacy_payment_store"))] + pub(crate) fn receive_via_jit_channel_inner( + &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, + expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; + + let rt_lock = self.runtime.read().unwrap(); + let runtime = rt_lock.as_ref().unwrap(); - fn receive_via_jit_channel_inner( + let peer_info = PeerInfo { node_id, address }; + + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; + + log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); + + let liquidity_source = Arc::clone(&liquidity_source); + let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = + tokio::task::block_in_place(move || { + runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_receive_to_jit_channel( + amount_msat, + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + ) + .await + .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) + } else { + liquidity_source + .lsps2_receive_variable_amount_to_jit_channel( + description, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + ) + .await + .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) + } + }) + })?; + + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let payment_secret = invoice.payment_secret(); + let id = PaymentId(payment_hash.0); + let preimage = + self.channel_manager.get_payment_preimage(payment_hash, payment_secret.clone()).ok(); + + let payment_metadata_detail = + PaymentMetadataDetail::Bolt11 { invoice: invoice.clone().to_string(), preimage }; + let jit_channel_fee_limit = JitChannelFeeLimits { + counterparty_skimmed_fee_msat: None, + max_total_opening_fee_msat: lsp_total_opening_fee, + max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, + }; + + let payment_metadata = PaymentMetadata::new( + id, + PaymentDirection::Inbound, + payment_metadata_detail, + Some(jit_channel_fee_limit), + PaymentStatus::Pending, + ); + self.metadata_store.insert(payment_metadata)?; + + // Persist LSP peer to make sure we reconnect on restart. + self.peer_store.add_peer(peer_info)?; + + Ok(invoice) + } + + #[cfg(feature = "legacy_payment_store")] + pub(crate) fn receive_via_jit_channel_inner( &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, @@ -665,7 +870,7 @@ 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 { + let lsp_fee_limits = crate::payment::LSPFeeLimits { max_total_opening_fee_msat: lsp_total_opening_fee, max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, }; @@ -778,4 +983,16 @@ impl Bolt11Payment { Ok(()) } + + fn get_payment_data_with_fallback( + &self, payment_id: &PaymentId, + ) -> Result { + PaymentDataWithFallback::new( + payment_id, + Arc::new(self.logger.clone()), + self.metadata_store.clone(), + self.payment_store.clone(), + || Error::InvalidPaymentHash, + ) + } } diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index dbeee0ab8..20e4e339b 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -16,12 +16,14 @@ use crate::payment::store::{ PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, }; use crate::types::ChannelManager; +use crate::PaymentMetadataStore; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::{Amount, Offer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::offers::refund::Refund; +use lightning::util::ser::Writeable; use lightning::util::string::UntrustedString; use rand::RngCore; @@ -30,6 +32,8 @@ use std::num::NonZeroU64; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use super::store::{PaymentMetadata, PaymentMetadataDetail}; + /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// /// Should be retrieved by calling [`Node::bolt12_payment`]. @@ -40,6 +44,7 @@ pub struct Bolt12Payment { runtime: Arc>>>, channel_manager: Arc, payment_store: Arc>>, + metadata_store: Arc>>, logger: Arc, } @@ -47,9 +52,9 @@ impl Bolt12Payment { pub(crate) fn new( runtime: Arc>>>, channel_manager: Arc, payment_store: Arc>>, - logger: Arc, + metadata_store: Arc>>, logger: Arc, ) -> Self { - Self { runtime, channel_manager, payment_store, logger } + Self { runtime, channel_manager, payment_store, metadata_store, logger } } /// Send a payment given an offer. @@ -316,7 +321,7 @@ impl Bolt12Payment { Ok(offer) } - + #[cfg(not(feature = "legacy_payment_store"))] /// Requests a refund payment for the given [`Refund`]. /// /// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to @@ -330,24 +335,22 @@ impl Bolt12Payment { let payment_hash = invoice.payment_hash(); let payment_id = PaymentId(payment_hash.0); - let kind = PaymentKind::Bolt12Refund { - hash: Some(payment_hash), + let payment_metadata_detail = PaymentMetadataDetail::Bolt12Refund { + refund: refund.clone(), + invoice: invoice.encode(), preimage: None, - secret: None, - payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())), - quantity: refund.quantity(), + hrn: None, + dnssec_proof: None, }; - let payment = PaymentDetails::new( + let payment_metadata = PaymentMetadata::new( payment_id, - kind, - Some(refund.amount_msats()), - None, PaymentDirection::Inbound, + payment_metadata_detail, + None, PaymentStatus::Pending, ); - - self.payment_store.insert(payment)?; + self.metadata_store.insert(payment_metadata)?; Ok(invoice) } @@ -416,4 +419,40 @@ impl Bolt12Payment { Ok(refund) } + + #[cfg(feature = "legacy_payment_store")] + /// Requests a refund payment for the given [`Refund`]. + /// + /// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to + /// retrieve the refund). + pub fn request_refund_payment(&self, refund: &Refund) -> Result { + let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| { + log_error!(self.logger, "Failed to request refund payment: {:?}", e); + Error::InvoiceRequestCreationFailed + })?; + + let payment_hash = invoice.payment_hash(); + let payment_id = PaymentId(payment_hash.0); + + let kind = PaymentKind::Bolt12Refund { + hash: Some(payment_hash), + preimage: None, + secret: None, + payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())), + quantity: refund.quantity(), + }; + + let payment = PaymentDetails::new( + payment_id, + kind, + Some(refund.amount_msats()), + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); + + self.payment_store.insert(payment)?; + + Ok(invoice) + } } From 396a9d57c5852642077f75e74b7518819999b08d Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Tue, 6 May 2025 11:58:22 -0300 Subject: [PATCH 5/6] Update test to support new PaymentMetadata rule while preserving legacy behavior The test was modified to be compatible with the new `PaymentMetadata` logic, while still verifying compatibility with the legacy rule that stores payments directly in `PaymentStore`. Issue #425 --- tests/common/mod.rs | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3258df791..c978de0ab 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -14,7 +14,7 @@ use logging::TestLogWriter; use ldk_node::config::{Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; -use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; +use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentMetadataDetail, PaymentStatus}; use ldk_node::{ Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance, }; @@ -653,10 +653,23 @@ pub(crate) fn do_channel_full_cycle( }); assert_eq!(outbound_payments_b.len(), 0); - let inbound_payments_b = node_b.list_payments_with_filter(|p| { - p.direction == PaymentDirection::Inbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) - }); - assert_eq!(inbound_payments_b.len(), 1); + if cfg!(feature = "legacy_payment_store") { + let inbound_payments_b = node_b.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Inbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); + assert_eq!(inbound_payments_b.len(), 1); + } else { + // Payment inbound are saved in the payment metadata store when they are pending + let inbound_metadata_b = node_b.list_payments_metadata_with_filter(|p| { + p.direction == PaymentDirection::Inbound + && matches!(p.payment_metadata_detail, PaymentMetadataDetail::Bolt11 { .. }) + }); + assert_eq!(inbound_metadata_b.len(), 1); + let inbound_payments_b = node_b.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Inbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); + assert_eq!(inbound_payments_b.len(), 0); + } expect_event!(node_a, PaymentSuccessful); expect_event!(node_b, PaymentReceived); @@ -893,10 +906,22 @@ pub(crate) fn do_channel_full_cycle( node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(), 5 ); - assert_eq!( - node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(), - 6 - ); + if cfg!(feature = "legacy_payment_store") { + assert_eq!( + node_b + .list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })) + .len(), + 6 + ); + } else { + assert_eq!( + node_b + .list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })) + .len(), + 5 + ); + } + assert_eq!( node_a .list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Spontaneous { .. })) From 6f2abb19a72a8fc57b7ac5ad2dc5a208c641b773 Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Tue, 6 May 2025 12:01:51 -0300 Subject: [PATCH 6/6] Ci: Add test legacy_payment_store Issue #425 --- .github/workflows/rust.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1c4e6ed15..1bce3c14b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -96,3 +96,7 @@ jobs: if: "matrix.platform != 'windows-latest' && matrix.build-uniffi" run: | RUSTFLAGS="--cfg no_download" cargo test --features uniffi + - name: Test with legacy_payment_store feature on Rust ${{ matrix.toolchain }} + if: "matrix.platform != 'windows-latest'" + run: | + RUSTFLAGS="--cfg no_download" cargo test --features legacy_payment_store