diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b6c6e97b1..0e79477a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Pending ## Compatibility Notes +- `Bolt11Invoice::recover_payee_pub_key` now returns `Option` in the public Rust and + FFI APIs. Callers must handle `None` when signature-based payee-key recovery fails; callers + needing the effective payee key should use the appropriate non-recovery accessor when available. - The language bindings now expose `Mnemonic` as an object instead of a string alias. Existing mnemonic phrases must be passed through its fallible constructor, which returns `NodeError::InvalidMnemonic` for invalid input; generated mnemonics can be converted back to a diff --git a/Cargo.toml b/Cargo.toml index 0110c027b0..fccea4a552 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,18 +86,18 @@ uniffi-default = [ #lightning-macros = { version = "0.2.0" } #lightning-dns-resolver = { version = "0.3.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["tokio"], optional = true } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["rest-client", "rpc-client", "tokio"], optional = true } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", optional = true } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", optional = true } +lightning = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", features = ["std"] } +lightning-types = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729" } +lightning-invoice = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", features = ["std"] } +lightning-net-tokio = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729" } +lightning-persister = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", features = ["tokio"], optional = true } +lightning-background-processor = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729" } +lightning-rapid-gossip-sync = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729" } +lightning-block-sync = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", features = ["rest-client", "rpc-client", "tokio"], optional = true } +lightning-transaction-sync = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", optional = true } +lightning-liquidity = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", features = ["std"] } +lightning-macros = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729" } +lightning-dns-resolver = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", optional = true } bdk_chain = { version = "0.23.3", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.2", default-features = false, features = ["async-https-rustls", "tokio"], optional = true } @@ -132,14 +132,14 @@ postgres-native-tls = { version = "0.5", default-features = false, features = [" vss-client = { package = "vss-client-ng", version = "0.6", optional = true } prost = { version = "0.11.6", default-features = false, optional = true} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "1d6ffaa8962391ddc84aeef98bd7439e55ccae9d", optional = true } +bitcoin-payment-instructions = { git = "https://git.rust-bitcoin.org/shaavan/bitcoin-payment-instructions.git", rev = "1d50eb439bf019a3822635d0a7b0d8f548685aa0", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["std", "_test_utils"] } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["tokio"] } +lightning = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", features = ["std", "_test_utils"] } +lightning-persister = { git = "https://git.rust-bitcoin.org/shaavan/rust-lightning", rev = "e09a607cd87f181b1f9a353ddcc4646b8e08b729", features = ["tokio"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 4c4c1a438a..1ca3212fe8 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -176,6 +176,8 @@ enum NodeError { "InvalidMnemonic", "InvalidOfferId", "InvalidNodeId", + "InvalidRecurrenceId", + "InvalidOutboundRecurrence", "InvalidPaymentId", "InvalidPaymentHash", "InvalidPaymentPreimage", @@ -355,6 +357,9 @@ typedef string Address; [Custom] typedef string OfferId; +[Custom] +typedef string RecurrenceId; + [Custom] typedef string PaymentId; diff --git a/src/builder.rs b/src/builder.rs index ab641cf86b..3eb647035f 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -80,7 +80,9 @@ use crate::io::utils::{ #[cfg(feature = "storage-vss")] use crate::io::vss_store::VssStoreBuilder; use crate::io::{ - self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + self, OUTBOUND_RECURRENCE_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + OUTBOUND_RECURRENCE_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; @@ -100,8 +102,8 @@ use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper, - GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, - PendingPaymentStore, + GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, OutboundRecurrenceStore, + PaymentStore, PeerManager, PendingPaymentStore, }; use crate::wallet::persist::{read_address_pool, KVStoreWalletPersister}; use crate::wallet::Wallet; @@ -1524,26 +1526,37 @@ fn build_with_store_internal( let kv_store_ref = Arc::clone(&kv_store); let logger_ref = Arc::clone(&logger); - let (payment_store_res, node_metris_res, pending_payment_store_res, address_pool_res) = runtime - .block_on(async move { - tokio::join!( - read_n_objects( - &*kv_store_ref, - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - PAYMENT_CACHE_WARMUP_COUNT, - Arc::clone(&logger_ref), - ), - read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)), - read_all_objects( - &*kv_store_ref, - PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - Arc::clone(&logger_ref), - ), - read_address_pool(&*kv_store_ref, &*logger_ref) - ) - }); + let ( + payment_store_res, + node_metris_res, + pending_payment_store_res, + outbound_recurrence_res, + address_pool_res, + ) = runtime.block_on(async move { + tokio::join!( + read_n_objects( + &*kv_store_ref, + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PAYMENT_CACHE_WARMUP_COUNT, + Arc::clone(&logger_ref), + ), + read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)), + read_all_objects( + &*kv_store_ref, + PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + Arc::clone(&logger_ref), + ), + read_all_objects( + &*kv_store_ref, + OUTBOUND_RECURRENCE_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + OUTBOUND_RECURRENCE_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + Arc::clone(&logger_ref), + ), + read_address_pool(&*kv_store_ref, &*logger_ref) + ) + }); // Initialize the status fields. let node_metrics = match node_metris_res { @@ -1865,6 +1878,21 @@ fn build_with_store_internal( }, }; + let outbound_recurrence_store = match outbound_recurrence_res { + Ok(outbound_recurrences) => Arc::new(OutboundRecurrenceStore::new( + outbound_recurrences, + KeepAllEntries, + OUTBOUND_RECURRENCE_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), + OUTBOUND_RECURRENCE_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), + Arc::clone(&kv_store), + Arc::clone(&logger), + )), + Err(e) => { + log_error!(logger, "Failed to read outbound recurrence data from store: {}", e); + return Err(BuildError::ReadFailed); + }, + }; + let wallet = Arc::new(Wallet::new( bdk_wallet, wallet_persister, @@ -2484,6 +2512,7 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + outbound_recurrence_store, lnurl_auth, is_running, node_metrics, diff --git a/src/data_store.rs b/src/data_store.rs index a9fe0d0f59..7fd51b3908 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -752,7 +752,7 @@ mod tests { use lightning::util::persist::{PageToken, PaginatedKVStore, PaginatedListResponse}; use lightning::util::test_utils::TestLogger; - use lightning::{impl_writeable_tlv_based, io}; + use lightning::{impl_ser_tlv_based, io}; use tokio::sync::Notify; use super::*; @@ -802,7 +802,7 @@ mod tests { hex_utils::to_vec(s)?.try_into().ok().map(|id| Self { id }) } } - impl_writeable_tlv_based!(TestObjectId, { (0, id, required) }); + impl_ser_tlv_based!(TestObjectId, { (0, id, required) }); struct TestObjectUpdate { id: TestObjectId, @@ -858,7 +858,7 @@ mod tests { } } - impl_writeable_tlv_based!(TestObject, { + impl_ser_tlv_based!(TestObject, { (0, id, required), (2, data, required), (4, extra, option), diff --git a/src/error.rs b/src/error.rs index 485f944c25..52f5836f39 100644 --- a/src/error.rs +++ b/src/error.rs @@ -87,6 +87,10 @@ pub enum Error { InvalidOfferId, /// The given node id is invalid. InvalidNodeId, + /// The given recurrence id is invalid. + InvalidRecurrenceId, + /// The outbound recurrence is not in a valid state for this operation. + InvalidOutboundRecurrence, /// The given payment id is invalid. InvalidPaymentId, /// The given payment hash is invalid. @@ -193,6 +197,10 @@ impl fmt::Display for Error { Self::InvalidMnemonic => write!(f, "The given BIP 39 mnemonic is invalid."), Self::InvalidOfferId => write!(f, "The given offer id is invalid."), Self::InvalidNodeId => write!(f, "The given node id is invalid."), + Self::InvalidRecurrenceId => write!(f, "The given recurrence id is invalid."), + Self::InvalidOutboundRecurrence => { + write!(f, "The outbound recurrence is not in a valid state for this operation.") + }, Self::InvalidPaymentId => write!(f, "The given payment id is invalid."), Self::InvalidPaymentHash => write!(f, "The given payment hash is invalid."), Self::InvalidPaymentPreimage => write!(f, "The given payment preimage is invalid."), diff --git a/src/event.rs b/src/event.rs index 0a35697552..6749ef5c87 100644 --- a/src/event.rs +++ b/src/event.rs @@ -24,19 +24,21 @@ use lightning::events::{ }; use lightning::ln::channelmanager::{PaymentId, TrustedChannelFeatures}; use lightning::ln::types::ChannelId; +use lightning::offers::invoice::Bolt12Invoice; +use lightning::offers::offer::Offer; use lightning::routing::gossip::NodeId; use lightning::sign::EntropySource; use lightning::util::config::{ChannelConfigOverrides, ChannelConfigUpdate}; use lightning::util::errors::APIError; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; -use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; +use lightning::{impl_ser_tlv_based, impl_ser_tlv_based_enum}; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_types::payment::{PaymentHash, PaymentPreimage}; use crate::config::{may_announce_channel, Config, PEER_RECONNECTION_INTERVAL}; use crate::connection::ConnectionManager; -use crate::data_store::DataStoreUpdateResult; +use crate::data_store::{DataStoreUpdateResult, StorableObject}; use crate::fee_estimator::ConfirmationTarget; #[cfg(feature = "uniffi")] use crate::ffi::PaidBolt12Invoice; @@ -48,6 +50,9 @@ use crate::liquidity::LiquiditySource; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; +use crate::payment::outbound_recurrence::{ + OutboundRecurrencePaymentState, OutboundRecurrenceStatus, +}; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; @@ -55,7 +60,8 @@ use crate::payment::PaymentMetadata; use crate::probing::Prober; use crate::runtime::Runtime; use crate::types::{ - CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet, + CustomTlvRecord, DynStore, KeysManager, OnionMessenger, OutboundRecurrenceStore, PaymentStore, + Sweeper, Wallet, }; use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, @@ -84,7 +90,7 @@ pub struct HTLCLocator { pub node_id: Option, } -impl_writeable_tlv_based!(HTLCLocator, { +impl_ser_tlv_based!(HTLCLocator, { (1, channel_id, required), (3, user_channel_id, option), (5, node_id, option), @@ -308,7 +314,7 @@ pub enum Event { }, } -impl_writeable_tlv_based_enum!(Event, +impl_ser_tlv_based_enum!(Event, (0, PaymentSuccessful) => { (0, payment_hash, required), (1, fee_paid_msat, option), @@ -551,6 +557,7 @@ where network_graph: Arc, liquidity_source: Arc>>, payment_store: Arc, + outbound_recurrence_store: Arc, peer_store: Arc>, keys_manager: Arc, static_invoice_store: Option, @@ -572,10 +579,10 @@ where channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, liquidity_source: Arc>>, payment_store: Arc, - peer_store: Arc>, keys_manager: Arc, - static_invoice_store: Option, onion_messenger: Arc, - om_mailbox: Option>, prober: Option>, - runtime: Arc, logger: L, config: Arc, + outbound_recurrence_store: Arc, peer_store: Arc>, + keys_manager: Arc, static_invoice_store: Option, + onion_messenger: Arc, om_mailbox: Option>, + prober: Option>, runtime: Arc, logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -587,6 +594,7 @@ where network_graph, liquidity_source, payment_store, + outbound_recurrence_store, peer_store, keys_manager, static_invoice_store, @@ -691,6 +699,160 @@ where }) } + /// Processes success for the retained outbound attempt without reopening terminal state. + /// + /// The payment record is persisted first by the caller. Replay must still advance an owned + /// attempt even when that payment is already marked successful. Clearing its ownership and + /// retaining the successful ID in one write prevents double counting and stale updates. + async fn advance_outbound_recurrence_on_payment_sent( + &self, payment_id: PaymentId, invoice: &Bolt12Invoice, + ) -> Result<(), ReplayEvent> { + let mut matches = self + .outbound_recurrence_store + .list_filter(|details| { + matches!(details.payment_state, + OutboundRecurrencePaymentState::Active(id) | OutboundRecurrencePaymentState::Failed(id) + if id == payment_id) + }) + .await + .into_iter(); + + let Some(details) = matches.next() else { + // Ordinary payments have no recurrence state. + return Ok(()); + }; + + if matches.len() != 0 { + debug_assert!(false, "multiple recurrences have the same active payment ID?"); + return Ok(()); + } + + let Some(invoice_recurrence) = invoice.invoice_recurrence() else { + debug_assert!(false, "Invoice that should be corresponding to recurrence according to our recurrence store doesn't?"); + return Ok(()); + }; + + match self + .outbound_recurrence_store + .mutate_async(&details.id, |current| async move { + let Some(mut updated) = current else { + log_error!( + self.logger, + "Recurrence disappeared before its payment update was stored" + ); + return Err(Error::InvalidRecurrenceId); + }; + // A late success may follow failure or cancellation. Only the retained attempt + // owns this period; a replay or an older attempt must not advance a newer one. + if updated.last_successful_payment_id == Some(payment_id) + || !matches!(updated.payment_state, + OutboundRecurrencePaymentState::Active(id) | OutboundRecurrencePaymentState::Failed(id) + if id == payment_id) + { + return Ok(None); + } + + let offer = Offer::try_from(updated.original_offer.clone()) + .map_err(|_| Error::InvalidOffer)?; + let recurrence = offer.offer_recurrence().ok_or(Error::InvalidOffer)?; + // paid_count still identifies the period just paid. Looking up the next period + // would complete early at an inclusive limit and may overflow a valid final index. + let paid_period_index = recurrence + .period_index(updated.paid_count, updated.initial_start) + .map_err(|_| Error::InvalidOffer)?; + updated.paid_count = + updated.paid_count.checked_add(1).ok_or(Error::InvalidOutboundRecurrence)?; + updated.basetime.get_or_insert(invoice_recurrence.recurrence_basetime()); + updated.opaque_state = + invoice_recurrence.recurrence_next_state().map(|state| state.to_vec()); + updated.last_successful_payment_id = Some(payment_id); + updated.payment_state = OutboundRecurrencePaymentState::NoActivePayment; + + // Late success records progress while preserving Cancelled, Missed, or Completed. + if updated.status == OutboundRecurrenceStatus::Active + && recurrence + .recurrence_limit + .map(|limit| paid_period_index >= limit.0) + .unwrap_or(false) + { + updated.status = OutboundRecurrenceStatus::Completed; + } + Ok(Some(updated)) + }) + .await + { + Ok(_) => Ok(()), + Err(e) => { + log_error!(self.logger, "Failed to store updated recurrence: {}", e); + Err(ReplayEvent()) + }, + } + } + + /// Marks the unique recurrence with this active payment ID as failed. + /// + /// Leaves unrelated or already-handled attempts unchanged and requests event replay on + /// persistence failure. Returns whether success superseded the failure, in which case the + /// caller must suppress its failure event. Payment-store locks are acquired second. + async fn update_outbound_recurrence_on_payment_failed( + &self, payment_id: PaymentId, + ) -> Result { + let recurrence_id = match self + .outbound_recurrence_store + .list_filter(|details| { + matches!(details.payment_state, + OutboundRecurrencePaymentState::Active(id) | OutboundRecurrencePaymentState::Failed(id) + if id == payment_id) + || details.last_successful_payment_id == Some(payment_id) + }) + .await + .as_slice() + { + [] => return Ok(false), + [details] => details.id, + _ => { + debug_assert!(false, "multiple recurrences have the same active payment ID?"); + return Ok(false); + }, + }; + + let mut success_seen = false; + let success_seen_ref = &mut success_seen; + self.outbound_recurrence_store + .mutate_async(&recurrence_id, |current| async move { + let Some(mut updated) = current else { + log_error!( + self.logger, + "Recurrence disappeared before its failed payment update was stored" + ); + return Err(Error::InvalidRecurrenceId); + }; + // Success may have reached either record since the caller persisted this + // failure. Neither a late failure nor replay may downgrade that outcome. + if updated.last_successful_payment_id == Some(payment_id) + || matches!(self.payment_store.get(&payment_id).await?, + Some(payment) if payment.status == PaymentStatus::Succeeded) + { + *success_seen_ref = true; + return Ok(None); + } + // Recheck under the mutation lock so stale or replayed failures + // cannot overwrite an already-handled or newer attempt. + if updated.payment_state != OutboundRecurrencePaymentState::Active(payment_id) { + return Ok(None); + } + updated.payment_state = OutboundRecurrencePaymentState::Failed(payment_id); + Ok(Some(updated)) + }) + .await + .map_err(|e| { + log_error!(self.logger, "Failed to store updated recurrence: {}", e); + ReplayEvent() + })?; + + Ok(success_seen) + } + async fn resolve_inbound_payment_id( &self, event_payment_id: Option, payment_hash: &PaymentHash, ) -> Result<(PaymentId, Option), ReplayEvent> { @@ -824,6 +986,11 @@ where .lsps2_service() .lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id); }, + LdkEvent::RecurringOfferCancelled { .. } => { + // TODO: Mark the corresponding recurrence as cancelled once the + // recurrence data store is introduced. + debug_assert!(false, "recurrence cancellation handling is not implemented"); + }, LdkEvent::PaymentClaimable { payment_id, payment_hash, @@ -1405,6 +1572,8 @@ where ..PaymentDetailsUpdate::new(payment_id) }; + // Persist the payment before releasing the recurrence's attempt. If the + // recurrence write fails, replay must complete it even though this is idempotent. match self.payment_store.update(update).await { Ok(_) => {}, Err(e) => { @@ -1413,6 +1582,17 @@ where }, }; + if let Some(invoice) = + bolt12_invoice.as_ref().and_then(|paid_invoice| paid_invoice.bolt12_invoice()) + { + if let Err(e) = + self.advance_outbound_recurrence_on_payment_sent(payment_id, invoice).await + { + log_error!(self.logger, "Failed to advance recurrence: replaying event"); + return Err(e); + } + } + match self.payment_store.get(&payment_id).await { Ok(Some(payment)) => { let amount_msat = payment.amount_msat.expect( @@ -1469,13 +1649,38 @@ where status: Some(PaymentStatus::Failed), ..PaymentDetailsUpdate::new(payment_id) }; - match self.payment_store.update(update).await { + let mut already_succeeded = false; + // LDK requires failures following PaymentSent to be ignored. Check and update + // atomically so success cannot be overwritten between a lookup and a write. + match self + .payment_store + .mutate(&payment_id, |current| { + let mut payment = match current { + Some(payment) if payment.status == PaymentStatus::Succeeded => { + already_succeeded = true; + return None; + }, + Some(payment) => payment.clone(), + None => return None, + }; + match payment.update(update) { + true => Some(payment), + false => None, + } + }) + .await + { Ok(_) => {}, Err(e) => { log_error!(self.logger, "Failed to access payment store: {}", e); return Err(ReplayEvent()); }, - }; + } + if already_succeeded + || self.update_outbound_recurrence_on_payment_failed(payment_id).await? + { + return Ok(()); + } let event = Event::PaymentFailed { payment_id, payment_hash, reason }; match self.event_queue.add_event(event).await { @@ -2342,7 +2547,7 @@ mod tests { }, } - impl_writeable_tlv_based_enum!(LegacyEvent, + impl_ser_tlv_based_enum!(LegacyEvent, (5, ChannelClosed) => { (0, channel_id, required), (1, counterparty_node_id, option), diff --git a/src/ffi/types.rs b/src/ffi/types.rs index d213f9d51e..bdc96329bb 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -29,6 +29,7 @@ use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; pub use lightning::ln::types::ChannelId; use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; +use lightning::offers::invoice_request::RecurrenceId; pub use lightning::offers::offer::OfferId; use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; use lightning::offers::payer_proof::{ @@ -1044,6 +1045,22 @@ uniffi::custom_type!(OfferId, String, { }, }); +uniffi::custom_type!(RecurrenceId, String, { + remote, + try_lift: |val| { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(RecurrenceId(bytes)); + } + } + Err(Error::InvalidRecurrenceId.into()) + }, + lower: |obj| { + hex_utils::to_string(&obj.0) + }, +}); + uniffi::custom_type!(PaymentId, String, { remote, try_lift: |val| { @@ -1509,8 +1526,12 @@ impl Bolt11Invoice { .collect() } - /// Recover the payee's public key (only to be used if none was included in the invoice) - pub fn recover_payee_pub_key(&self) -> PublicKey { + /// Recover the payee's public key (only to be used if none was included in the invoice). + /// + /// Signature recovery can fail if the invoice does not contain enough information to recover + /// the key; `None` indicates that recovery failed. Callers needing the invoice's effective + /// payee key should use the appropriate non-recovery accessor when available. + pub fn recover_payee_pub_key(&self) -> Option { self.inner.recover_payee_pub_key() } } diff --git a/src/io/mod.rs b/src/io/mod.rs index c11475c431..aa988e026b 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -37,6 +37,11 @@ pub(crate) const PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_payments"; pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; +/// Outbound recurrence records are persisted under this prefix. +pub(crate) const OUTBOUND_RECURRENCE_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = + "outbound_recurrences"; +pub(crate) const OUTBOUND_RECURRENCE_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; + /// The node metrics will be persisted under this key. pub(crate) const NODE_METRICS_PRIMARY_NAMESPACE: &str = ""; pub(crate) const NODE_METRICS_SECONDARY_NAMESPACE: &str = ""; diff --git a/src/io/utils.rs b/src/io/utils.rs index 30fc0c62d2..8598a0302a 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -779,7 +779,7 @@ mod read_objects_tests { use std::num::NonZeroUsize; use std::sync::Arc; - use lightning::impl_writeable_tlv_based; + use lightning::impl_ser_tlv_based; use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse}; use lightning::util::ser::Writeable; use lightning::util::test_utils::TestLogger; @@ -796,7 +796,7 @@ mod read_objects_tests { struct TestObject { id: u32, } - impl_writeable_tlv_based!(TestObject, { (0, id, required) }); + impl_ser_tlv_based!(TestObject, { (0, id, required) }); /// Writes `num_objects` objects with ascending ids, so that the highest id is the most /// recently created one. diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 6a44be02d9..78466bdfa3 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -21,7 +21,7 @@ use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use bitcoin::key::Secp256k1; use bitcoin::Network; -use lightning::impl_writeable_tlv_based_enum; +use lightning::impl_ser_tlv_based_enum; use lightning::io::{self, Error, ErrorKind}; use lightning::sign::{EntropySource as LdkEntropySource, RandomBytes}; use lightning::util::persist::{ @@ -67,7 +67,7 @@ enum VssSchemaVersion { V1, } -impl_writeable_tlv_based_enum!(VssSchemaVersion, +impl_ser_tlv_based_enum!(VssSchemaVersion, (0, V0) => {}, (1, V1) => {}, ); diff --git a/src/lib.rs b/src/lib.rs index 821304a532..52794e2e57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,7 +152,7 @@ use graph::NetworkGraph; use io::utils::update_and_persist_node_metrics; pub use lightning; use lightning::chain::BlockLocator; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; pub use lightning::ln::channel_state::ChannelShutdownState; @@ -176,8 +176,8 @@ use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ - Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, PaymentDetailsPage, - SpontaneousPayment, + Bolt11Payment, Bolt12Payment, OnchainPayment, OutboundRecurrenceDetails, PaymentDetails, + PaymentDetailsPage, RecurrenceId, SpontaneousPayment, }; #[cfg(feature = "unified-payments")] use payment::{HRNResolver, UnifiedPayment}; @@ -201,6 +201,7 @@ use crate::config::{LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY use crate::ffi::{maybe_deref, maybe_wrap}; use crate::liquidity::Liquidity; use crate::scoring::setup_background_pathfinding_scores_sync; +use crate::types::OutboundRecurrenceStore; use crate::wallet::FundingAmount; #[cfg(not(feature = "uniffi"))] @@ -272,6 +273,7 @@ pub struct Node { peer_store: Arc>>, payment_store: Arc, lnurl_auth: Arc, + outbound_recurrence_store: Arc, is_running: Arc>, node_metrics: Arc, om_mailbox: Option>, @@ -678,6 +680,7 @@ impl Node { Arc::clone(&self.network_graph), Arc::clone(&self.liquidity_source), Arc::clone(&self.payment_store), + Arc::clone(&self.outbound_recurrence_store), Arc::clone(&self.peer_store), Arc::clone(&self.keys_manager), static_invoice_store, @@ -1075,6 +1078,7 @@ impl Node { Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.outbound_recurrence_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -1092,6 +1096,7 @@ impl Node { Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.outbound_recurrence_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -2158,6 +2163,27 @@ impl Node { self.runtime.block_on(self.payment_store.get(payment_id)) } + /// Retrieves an owned snapshot of the outbound recurrence with the given id. + /// + /// Returns `Ok(None)` for an unknown id, or an error if the record cannot be read. Available + /// before the node is started, including after records have been loaded during construction. + /// Changing the returned snapshot does not change the stored recurrence. + pub fn outbound_recurrence( + &self, recurrence_id: &RecurrenceId, + ) -> Result, Error> { + self.runtime.block_on(self.outbound_recurrence_store.get(recurrence_id)) + } + + /// Lists owned snapshots of all known outbound recurrences, in unspecified order. + /// + /// Includes terminal recurrences and failed attempts so callers can recover persisted records + /// after restart or an initiation error, even if the error left them without a recurrence id. + /// Records that were not successfully persisted cannot be recovered this way. Available before + /// the node is started; changing these snapshots does not update stored state. + pub fn list_outbound_recurrences(&self) -> Vec { + self.runtime.block_on(self.outbound_recurrence_store.list_filter(|_| true)) + } + /// Remove the payment with the given id from the store. pub fn remove_payment(&self, payment_id: &PaymentId) -> Result<(), Error> { self.runtime.block_on(self.payment_store.remove(&payment_id)) @@ -2506,7 +2532,7 @@ impl PersistedNodeMetrics { } } -impl_writeable_tlv_based!(NodeMetrics, { +impl_ser_tlv_based!(NodeMetrics, { (0, latest_lightning_wallet_sync_timestamp, option), (1, latest_pathfinding_scores_sync_timestamp, option), (2, latest_onchain_wallet_sync_timestamp, option), @@ -2590,7 +2616,7 @@ mod tests { latest_pathfinding_scores_sync_timestamp: Option, latest_node_announcement_broadcast_timestamp: Option, } - impl_writeable_tlv_based!(OldNodeMetrics, { + impl_ser_tlv_based!(OldNodeMetrics, { (0, latest_lightning_wallet_sync_timestamp, option), (1, latest_pathfinding_scores_sync_timestamp, option), (2, latest_onchain_wallet_sync_timestamp, option), diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs index f1e2378c23..85d9479234 100644 --- a/src/payment/asynchronous/static_invoice_store.rs +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -13,7 +13,7 @@ use std::time::Duration; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use lightning::blinded_path::message::BlindedMessagePath; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::offers::static_invoice::StaticInvoice; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, Writeable}; @@ -28,7 +28,7 @@ struct PersistedStaticInvoice { request_path: BlindedMessagePath, } -impl_writeable_tlv_based!(PersistedStaticInvoice, { +impl_ser_tlv_based!(PersistedStaticInvoice, { (0, invoice, required), (2, request_path, required) }); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 5f0a70c2c6..c167a27daf 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -13,7 +13,7 @@ use std::sync::{Arc, RwLock}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, OptionalBolt11PaymentParams, PaymentId, }; @@ -55,7 +55,7 @@ pub(crate) struct PaymentMetadata { pub(crate) lsps2_parameters: Option, } -impl_writeable_tlv_based!(PaymentMetadata, { +impl_ser_tlv_based!(PaymentMetadata, { (0, lsps2_parameters, option), }); @@ -245,7 +245,7 @@ impl Bolt11Payment { let payee_pubkey = invoice.recover_payee_pub_key(); log_info!( self.logger, - "Initiated sending {} msat to {}", + "Initiated sending {} msat to {:?}", payment_amount_msat, payee_pubkey ); diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 0e994d67ac..9c6083c1d1 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -14,9 +14,14 @@ use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use lightning::blinded_path::message::BlindedMessagePath; -use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; +use lightning::ln::channelmanager::{ + OptionalOfferPaymentParams, PaymentId, RecurrencePaymentParams, +}; use lightning::ln::outbound_payment::Retry; -use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity}; +use lightning::offers::invoice_request::RecurrenceId; +use lightning::offers::offer::{ + Amount, Offer as LdkOffer, OfferFromHrn, Quantity, Recurrence, RecurrenceType, +}; use lightning::offers::parse::Bolt12SemanticError; use lightning::offers::payer_proof::PaidBolt12Invoice as LdkPaidBolt12Invoice; #[cfg(not(feature = "uniffi"))] @@ -24,17 +29,24 @@ use lightning::offers::payer_proof::PayerProof as LdkPayerProof; use lightning::routing::router::RouteParametersConfig; use lightning::sign::{EntropySource, NodeSigner}; #[cfg(feature = "uniffi")] -use lightning::util::ser::{Readable, Writeable}; +use lightning::util::ser::Readable; +use lightning::util::ser::Writeable; use lightning_types::payment::PaymentPreimage; use lightning_types::string::UntrustedString; use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::data_store::StorableObject; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::payment::outbound_recurrence::{ + OutboundRecurrenceDetails, OutboundRecurrencePaymentState, OutboundRecurrenceStatus, +}; +use crate::payment::store::{ + PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, +}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, PaymentStore}; +use crate::types::{ChannelManager, KeysManager, OutboundRecurrenceStore, PaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice; @@ -98,6 +110,7 @@ pub struct Bolt12Payment { channel_manager: Arc, keys_manager: Arc, payment_store: Arc, + outbound_recurrence_store: Arc, config: Arc, is_running: Arc>, logger: Arc, @@ -107,7 +120,8 @@ pub struct Bolt12Payment { impl Bolt12Payment { pub(crate) fn new( runtime: Arc, channel_manager: Arc, - keys_manager: Arc, payment_store: Arc, config: Arc, + keys_manager: Arc, payment_store: Arc, + outbound_recurrence_store: Arc, config: Arc, is_running: Arc>, logger: Arc, async_payments_role: Option, ) -> Self { @@ -116,6 +130,7 @@ impl Bolt12Payment { channel_manager, keys_manager, payment_store, + outbound_recurrence_store, config, is_running, logger, @@ -123,6 +138,500 @@ impl Bolt12Payment { } } + /// Registers an outbound recurrence and submits its initial invoice request. + /// + /// The returned [`RecurrenceId`] identifies the complete recurring relationship and must be + /// retained for its lifetime. The returned [`PaymentId`] identifies only the initial payment + /// attempt. Both records are reserved after validation and persisted before submitting the + /// request. A submission failure retains the failed attempt without advancing paid progress. + /// + /// `quantity` must be supplied exactly when the offer expects it and must satisfy its bounds. + /// `amount_msat` is the total for one period. When omitted, it defaults to the offer's per-item + /// amount multiplied by the quantity (or one when no quantity is expected). Invalid quantities + /// return [`Error::InvalidQuantity`]; missing, zero, overflowing, excessive, or insufficient + /// amounts return [`Error::InvalidAmount`] before any records are reserved. + pub fn initiate_outbound_recurrence( + &self, offer: &Offer, amount_msat: Option, quantity: Option, + payer_note: Option, route_parameters: Option, + initial_start: Option, + ) -> Result<(RecurrenceId, PaymentId), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + + let offer = maybe_deref(offer); + let recurrence = offer.offer_recurrence().ok_or(Error::InvoiceRequestCreationFailed)?; + if recurrence.period_index(0, initial_start).is_err() { + return Err(Error::InvoiceRequestCreationFailed); + } + + // A quantity must be present exactly when the offer requests it. Let the offer enforce + // its bounds rather than accepting zero, an omitted quantity, or an unexpected one. + match quantity { + None if offer.expects_quantity() => return Err(Error::InvalidQuantity), + Some(qty) if !offer.expects_quantity() || !offer.is_valid_quantity(qty) => { + return Err(Error::InvalidQuantity); + }, + _ => {}, + } + + // Offer amounts are per item; the caller's amount is the total for this period. + // Check multiplication even when the caller supplies a total, so overflow cannot bypass + // the minimum required by the offer. + let minimum_amount_msat = match offer.amount() { + Some(Amount::Bitcoin { amount_msats }) => { + Some(amount_msats.checked_mul(quantity.unwrap_or(1)).ok_or(Error::InvalidAmount)?) + }, + Some(_) => return Err(Error::UnsupportedCurrency), + None => None, + }; + let amount_msat = amount_msat.or(minimum_amount_msat).ok_or(Error::InvalidAmount)?; + if amount_msat == 0 + || amount_msat > bitcoin::Amount::MAX_MONEY.to_sat() * 1_000 + || minimum_amount_msat.is_some_and(|minimum| amount_msat < minimum) + { + return Err(Error::InvalidAmount); + } + + let recurrence_id = RecurrenceId(self.keys_manager.get_secure_random_bytes()); + let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes()); + let retry_policy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + + // An offer with an explicit basetime fixes the basetime expected in the first invoice. + // Otherwise, the first invoice's creation time establishes it. + let offer_basetime = match recurrence.recurrence_type { + RecurrenceType::Compulsory(Some(base)) => Some(base.basetime), + _ => None, + }; + + let details = OutboundRecurrenceDetails { + id: recurrence_id, + status: OutboundRecurrenceStatus::Active, + payment_state: OutboundRecurrencePaymentState::Active(payment_id), + original_offer: offer.encode(), + amount_msat, + quantity, + payer_note: payer_note.clone().map(UntrustedString), + basetime: offer_basetime, + initial_start, + paid_count: 0, + opaque_state: None, + retry_policy, + routing_override: route_parameters, + last_successful_payment_id: None, + pay_next_automatically: false, + }; + let kind = PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: offer.id(), + payer_note: payer_note.map(UntrustedString), + quantity, + }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + None, + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); + self.reserve_initial_recurrence(details, payment)?; + + self.runtime + .block_on(self.submit_reserved_outbound_recurrence(recurrence_id, payment_id))?; + Ok((recurrence_id, payment_id)) + } + + /// Reserves both initial records without replacing an existing recurrence or payment. + /// + /// Always acquire the recurrence store's mutation lock before the payment store's lock. + /// Submission may proceed only after both writes succeed. The stores are not transactional: + /// if the recurrence write fails, the payment can remain persisted, but no request is sent. + fn reserve_initial_recurrence( + &self, details: OutboundRecurrenceDetails, payment: PaymentDetails, + ) -> Result<(), Error> { + let recurrence_id = details.id; + self.runtime.block_on(self.outbound_recurrence_store.mutate_async( + &recurrence_id, + |current| async move { + if current.is_some() { + return Err(Error::DuplicatePayment); + } + let payment_id = payment.id; + self.payment_store + .mutate(&payment_id, |current| current.is_none().then_some(payment)) + .await? + .ok_or(Error::DuplicatePayment)?; + Ok(Some(details)) + }, + ))?; + Ok(()) + } + + /// Submits a persisted attempt only while it still owns an active recurrence. + /// + /// Hold the recurrence mutation lock through LDK's synchronous queueing call. Other + /// lifecycle mutations must use this same lock, so a cancellation that wins the lock can + /// prevent an unqueued reservation from being submitted. Read parameters from the current + /// record rather than the caller's earlier snapshot. Acquire payment-store locks second. + async fn submit_reserved_outbound_recurrence( + &self, recurrence_id: RecurrenceId, payment_id: PaymentId, + ) -> Result { + let mut submission_result = Ok(payment_id); + let result = &mut submission_result; + self.outbound_recurrence_store + .mutate_async(&recurrence_id, |current| async move { + let mut details = current.ok_or(Error::InvalidRecurrenceId)?; + if details.status != OutboundRecurrenceStatus::Active + || details.payment_state != OutboundRecurrencePaymentState::Active(payment_id) + { + return Err(Error::PaymentSendingFailed); + } + // Both reservations must still exist. In particular, never submit again after + // an outcome has been persisted but its recurrence update is still pending. + match self.payment_store.get(&payment_id).await? { + Some(payment) if payment.status == PaymentStatus::Pending => {}, + _ => return Err(Error::PaymentSendingFailed), + } + + let offer = LdkOffer::try_from(details.original_offer.clone()) + .map_err(|_| Error::InvalidOffer)?; + let params = RecurrencePaymentParams { + counter: details.paid_count, + start: details.initial_start, + prev_state: details.opaque_state.clone(), + quantity: details.quantity, + expected_invoice_recurrence_basetime: details.basetime, + }; + let optional_params = OptionalOfferPaymentParams { + payer_note: details.payer_note.as_ref().map(|note| note.0.clone()), + route_params_config: details.routing_override.unwrap_or_default(), + retry_strategy: details.retry_policy, + }; + // Persistence can outlast the payment window. Recheck immediately before + // queueing and record an expired reservation as failed without sending it. + let request_result = (|| { + let recurrence = offer.offer_recurrence().ok_or(Error::InvalidOffer)?; + // Initial retries with a known basetime also have a payment window. The + // validator permits an unknown window only before basetime is established. + self.validate_outbound_recurrence_payment_window(&details, &recurrence)?; + self.channel_manager + .pay_for_recurrence( + &offer, + Some(details.amount_msat), + payment_id, + recurrence_id, + params, + optional_params, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to send invoice request: {:?}", e); + match e { + Bolt12SemanticError::DuplicatePaymentId => Error::DuplicatePayment, + _ => Error::InvoiceRequestCreationFailed, + } + }) + })(); + match request_result { + Ok(()) => { + log_info!( + self.logger, + "Initiated sending {}msat to {:?}", + details.amount_msat, + offer.issuer_signing_pubkey() + ); + // Reservation already persisted the active attempt. Avoid another write + // after queueing, when the request may already produce an event. + Ok(None) + }, + Err(e) => { + *result = Err(e); + let payment = self + .payment_store + .mutate(&payment_id, |current| { + let mut payment = match current { + Some(payment) if payment.status != PaymentStatus::Succeeded => { + payment.clone() + }, + _ => return None, + }; + let mut update = PaymentDetailsUpdate::new(payment_id); + update.status = Some(PaymentStatus::Failed); + payment.update(update); + Some(payment) + }) + .await?; + // A success or removal may have raced with submission. Preserve it. + if payment.is_none() { + return Ok(None); + } + details.payment_state = OutboundRecurrencePaymentState::Failed(payment_id); + Ok(Some(details)) + }, + } + }) + .await?; + submission_result + } + + /// Submits the next sequential payment for an active outbound recurrence. + /// + /// The initial payment must have succeeded, no payment may be in progress, and the next + /// period's payment window must be open. Failed attempts may be retried within that window. + /// + /// Returns the [`PaymentId`] when the invoice request is successfully submitted. This does + /// not indicate that the payment has succeeded; inspect its payment record for the outcome. + /// Synchronous submission failures return an error. + pub fn pay_next_outbound_recurrence( + &self, recurrence_id: RecurrenceId, + ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + + let details = self + .runtime + .block_on(self.outbound_recurrence_store.get(&recurrence_id))? + .ok_or(Error::InvalidRecurrenceId)?; + + // Sanity Checks + if details.status != OutboundRecurrenceStatus::Active { + log_error!(self.logger, "Cannot pay the next recurrence: recurrence is not active."); + return Err(Error::InvalidOutboundRecurrence); + } + + if matches!(details.payment_state, OutboundRecurrencePaymentState::Active(_)) { + log_error!( + self.logger, + "Cannot pay the next recurrence: a payment is already active for the current period." + ); + return Err(Error::PaymentSendingFailed); + } + + if details.paid_count == 0 { + log_error!( + self.logger, + "Cannot pay the next recurrence before the initial payment succeeds." + ); + return Err(Error::InvalidOutboundRecurrence); + } + + self.runtime.block_on(self.submit_outbound_recurrence_payment(details)) + } + + /// Retries a failed payment attempt without advancing the recurrence period. + /// + /// The recurrence must be active and its latest attempt must have failed. Its payment + /// window must be open, except for an initial payment whose offer has no explicit basetime: + /// that window is not known until the first payment succeeds. + /// + /// Returns a new [`PaymentId`] when the invoice request is successfully submitted, not when + /// the payment succeeds. Returns [`Error::NotRunning`] if the node is stopped, + /// [`Error::InvalidRecurrenceId`] if the recurrence is unknown, or + /// [`Error::InvalidOutboundRecurrence`] if its lifecycle or attempt state does not permit a retry. + /// Payment-window, submission, and persistence errors are returned to the caller. + pub fn retry_outbound_recurrence( + &self, recurrence_id: RecurrenceId, + ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + + let details = self + .runtime + .block_on(self.outbound_recurrence_store.get(&recurrence_id))? + .ok_or(Error::InvalidRecurrenceId)?; + + // A retry replaces only a failed attempt; it must not start a new period or replace + // an in-progress payment. The shared submission path preserves the period counter. + if details.status != OutboundRecurrenceStatus::Active + || !matches!(details.payment_state, OutboundRecurrencePaymentState::Failed(_)) + { + return Err(Error::InvalidOutboundRecurrence); + } + + self.runtime.block_on(self.submit_outbound_recurrence_payment(details)) + } + + /// Reserves a subsequent attempt before using the shared submission gate. + /// + /// Recheck the caller's period and attempt under the recurrence mutation lock so concurrent + /// calls cannot both reserve the same period or replace a newer attempt. As with initial + /// reservation, acquire payment-store locks second and never overwrite an existing payment. + async fn submit_outbound_recurrence_payment( + &self, expected: OutboundRecurrenceDetails, + ) -> Result { + let recurrence_id = expected.id; + let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes()); + self.outbound_recurrence_store + .mutate_async(&recurrence_id, |current| async move { + let mut details = current.ok_or(Error::InvalidRecurrenceId)?; + if details.status != OutboundRecurrenceStatus::Active + || details.paid_count != expected.paid_count + || details.payment_state != expected.payment_state + { + return Err(Error::InvalidOutboundRecurrence); + } + match details.payment_state { + OutboundRecurrencePaymentState::Active(_) => { + return Err(Error::PaymentSendingFailed) + }, + OutboundRecurrencePaymentState::Failed(previous_id) => { + // A recurrence failure may be persisted before the payment outcome. Wait + // for that outcome instead of replacing an incompletely handled attempt. + match self.payment_store.get(&previous_id).await? { + Some(payment) if payment.status != PaymentStatus::Failed => { + return Err(Error::PaymentSendingFailed); + }, + _ => {}, + } + }, + OutboundRecurrencePaymentState::NoActivePayment => {}, + } + let offer = LdkOffer::try_from(details.original_offer.clone()) + .map_err(|_| Error::InvalidOffer)?; + let recurrence = offer.offer_recurrence().ok_or(Error::InvalidOffer)?; + self.validate_outbound_recurrence_payment_window(&details, &recurrence)?; + + let payment = PaymentDetails::new( + payment_id, + PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: offer.id(), + payer_note: details.payer_note.clone(), + quantity: details.quantity, + }, + Some(details.amount_msat), + None, + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); + self.payment_store + .mutate(&payment_id, |current| current.is_none().then_some(payment)) + .await? + .ok_or(Error::DuplicatePayment)?; + details.payment_state = OutboundRecurrencePaymentState::Active(payment_id); + Ok(Some(details)) + }) + .await?; + self.submit_reserved_outbound_recurrence(recurrence_id, payment_id).await + } + + /// Checks the next outbound period against the offer limit and its payment window. + fn validate_outbound_recurrence_payment_window( + &self, details: &OutboundRecurrenceDetails, recurrence: &Recurrence, + ) -> Result<(), Error> { + let period_index = recurrence + .period_index(details.paid_count, details.initial_start) + .map_err(|_| Error::InvalidOffer)?; + + if recurrence.recurrence_limit.map_or(false, |limit| period_index > limit.0) { + debug_assert!(false, "Recurrence exceeded its limit without being marked completed."); + return Err(Error::InvalidOutboundRecurrence); + } + + // Without an explicit basetime, the first successful invoice establishes the window. + let basetime = match details.basetime { + Some(basetime) => basetime, + None if details.paid_count == 0 + && !matches!(recurrence.recurrence_type, RecurrenceType::Compulsory(Some(_))) => + { + // If the offer has no basetime and the first payment has not succeeded, + // period zero's start time and payment window are not known yet. + // Skip the window check so a failed first payment can be retried. + return Ok(()); + }, + None => return Err(Error::InvalidOffer), + }; + + let (opening, closing) = + recurrence.payment_window(basetime, period_index).map_err(|_| Error::InvalidOffer)?; + + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + + if now < opening { + log_error!(self.logger, "Tried to send recurrence payment too early."); + return Err(Error::PaymentSendingFailed); + } + + if now >= closing { + // TODO: Figure out, where we should ideally be changing recurrence states at appropriate times. + return Err(Error::InvalidOutboundRecurrence); + } + + Ok(()) + } + + /// Cancels an outbound recurrence locally and, after the first successful payment, queues a + /// continuity-preserving cancellation request for the payee. + /// + /// Abandons an active attempt but retains its payment ID until the outcome is known. A late + /// success is still recorded without reopening the recurrence. If queueing remote cancellation + /// fails, local cancellation is persisted before returning the queueing error. + pub fn cancel_outbound_recurrence(&self, recurrence_id: RecurrenceId) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + + let mut cancellation_result = Ok(()); + let result = &mut cancellation_result; + self.runtime.block_on(self.outbound_recurrence_store.mutate_async( + &recurrence_id, + |current| async move { + let mut details = current.ok_or(Error::InvalidRecurrenceId)?; + match details.status { + OutboundRecurrenceStatus::Cancelled => { + log_error!(self.logger, "Tried to cancel an already cancelled recurrence."); + return Err(Error::InvalidOutboundRecurrence); + }, + OutboundRecurrenceStatus::Completed => { + log_error!( + self.logger, + "A completed outbound recurrence cannot be cancelled." + ); + return Err(Error::InvalidOutboundRecurrence); + }, + _ => {}, + } + + // Validate stored offer data before abandoning a payment or queueing a message. + let offer = match details.paid_count { + 0 => None, + _ => Some( + LdkOffer::try_from(details.original_offer.clone()) + .map_err(|_| Error::InvalidOffer)?, + ), + }; + // Submission holds this same mutation lock through queueing. If cancellation + // wins, its persisted status blocks the reservation; otherwise abandon the + // queued attempt. Retain its ID since in-flight HTLCs can still settle. + if let OutboundRecurrencePaymentState::Active(payment_id) = details.payment_state { + self.channel_manager.abandon_payment(payment_id); + } + if let Some(offer) = offer { + // The payee authenticates continuity using the latest successfully paid state. + let params = lightning::ln::channelmanager::RecurrenceCancellationParams { + counter: details.paid_count, + start: details.initial_start, + prev_state: details.opaque_state.clone(), + }; + *result = self + .channel_manager + .cancel_recurrence(&offer, recurrence_id, params) + .map_err(|_| Error::PaymentSendingFailed); + } + // Remote queueing failure must not let the local reservation submit afterward. + details.status = OutboundRecurrenceStatus::Cancelled; + Ok(Some(details)) + }, + ))?; + cancellation_result + } + pub(crate) fn send_using_amount_inner( &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, route_parameters: Option, hrn: Option, diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 13dbe51063..7220f50131 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -13,6 +13,7 @@ mod bolt12; #[cfg(feature = "unified-payments")] mod hrn; mod onchain; +pub(crate) mod outbound_recurrence; pub(crate) mod pending_payment_store; mod spontaneous; pub(crate) mod store; @@ -25,6 +26,10 @@ pub use bolt12::{Bolt12Payment, PayerProofOptions}; #[cfg(feature = "unified-payments")] pub(crate) use hrn::HRNResolver; pub use onchain::OnchainPayment; +pub use outbound_recurrence::{ + OutboundRecurrenceDetails, OutboundRecurrencePaymentState, OutboundRecurrenceStatus, + RecurrenceId, +}; pub(crate) use pending_payment_store::{FundingTxCandidate, PendingPaymentDetails}; pub use spontaneous::SpontaneousPayment; pub use store::{ diff --git a/src/payment/outbound_recurrence.rs b/src/payment/outbound_recurrence.rs new file mode 100644 index 0000000000..00735d5b35 --- /dev/null +++ b/src/payment/outbound_recurrence.rs @@ -0,0 +1,216 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license . You may not use this file except in +// accordance with one or both of these licenses. + +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::outbound_payment::Retry; +pub use lightning::offers::invoice_request::RecurrenceId; +use lightning::routing::router::RouteParametersConfig; +use lightning::{impl_ser_tlv_based, impl_ser_tlv_based_enum}; +use lightning_types::string::UntrustedString; + +use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; +use crate::hex_utils; + +impl StorableObjectId for RecurrenceId { + fn encode_to_hex_str(&self) -> String { + hex_utils::to_string(&self.0) + } + + fn decode_from_hex_str(s: &str) -> Option { + hex_utils::to_vec(s)?.try_into().ok().map(Self) + } +} + +/// The lifecycle status of an outbound recurrence. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundRecurrenceStatus { + /// Outbound recurrence can accept its next payment attempt. + Active, + /// Current payment window closed before a payment succeeded. + Missed, + /// Outbound recurrence will not accept further payments. + Cancelled, + /// Outbound recurrence reached its configured payment limit. + Completed, +} + +impl_ser_tlv_based_enum!(OutboundRecurrenceStatus, + (0, Active) => {}, + (2, Missed) => {}, + (4, Cancelled) => {}, + (6, Completed) => {}, +); + +/// The payment attempt state of an outbound recurrence. +/// +/// Tracks an in-progress attempt or retains the ID of a failed attempt. This is +/// separate from [`OutboundRecurrenceStatus`]: a failed attempt does not by itself +/// end the outbound recurrence or advance its paid period count. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundRecurrencePaymentState { + /// No attempt is in progress and no failed attempt is retained. + /// + /// Used before the first attempt and after a successful payment, whose ID is + /// recorded in [`OutboundRecurrenceDetails::last_successful_payment_id`]. + NoActivePayment, + /// An attempt with this payment ID is in progress. + /// + /// Another attempt must not be started while this attempt remains active. Cancellation + /// retains this ID until the outcome is known, since abandoned HTLCs may still succeed. + Active(PaymentId), + /// The attempt with this payment ID failed and is no longer in progress. + /// + /// Retains the failed ID until another attempt replaces it. A new attempt may + /// be started if the outbound recurrence status and payment window permit it. + Failed(PaymentId), +} + +impl_ser_tlv_based_enum!(OutboundRecurrencePaymentState, + (0, NoActivePayment) => {}, + {2, Active} => (), + {4, Failed} => (), +); + +/// Persisted state for an outbound recurrence, where this node is the payer. +/// +/// Stored using [`RecurrenceId`] as the key. Lookup and list APIs return owned copies; +/// changing a returned value does not update the stored recurrence. +#[derive(Clone, Debug)] +pub struct OutboundRecurrenceDetails { + /// Recurrence ID. + pub id: RecurrenceId, + + /// Current lifecycle status of the outbound recurrence. + pub status: OutboundRecurrenceStatus, + + /// State of the outbound recurrence's payment attempt. + /// + /// Retains the payment ID while an attempt is active or after it fails. + /// Successful attempts are recorded in [`Self::last_successful_payment_id`]. + pub payment_state: OutboundRecurrencePaymentState, + + /// Serialized original offer defining the outbound recurrence terms. + /// + /// Retained to ensure that subsequent payments use the same offer. + pub original_offer: Vec, + + /// Amount in millisatoshis to use for each payment by default. + /// + /// May be increased by the user before the next period begins. If the original + /// offer specifies an amount, this must be at least that amount multiplied by + /// [`Self::quantity`], when a quantity is set. + pub amount_msat: u64, + + /// Quantity to include in each invoice request, if any. + /// + /// May be changed by the user before the next period begins. + pub quantity: Option, + + /// Note to include in each invoice request. + pub payer_note: Option, + + /// UNIX timestamp anchoring period zero of the outbound recurrence. + /// + /// If the offer does not specify a basetime, period zero is anchored to + /// the creation time of the first successfully paid invoice. In that case, + /// this is `None` until the first payment succeeds and `Some` thereafter. + pub basetime: Option, + + /// First outbound recurrence period to pay, if specified. + pub initial_start: Option, + + /// Number of outbound recurrence periods successfully paid. + pub paid_count: u32, + + /// Opaque state provided by the payee for the next invoice request. + pub opaque_state: Option>, + + /// Retry strategy to use when attempting each payment. + pub retry_policy: Retry, + + /// Routing configuration for this outbound recurrence overriding the node-wide default. + pub routing_override: Option, + + /// Payment ID of the most recently completed payment. + pub last_successful_payment_id: Option, + + /// Whether future outbound recurrence periods should be paid automatically. + pub pay_next_automatically: bool, +} + +impl Default for OutboundRecurrenceDetails { + fn default() -> Self { + Self { + id: RecurrenceId([0; 32]), + status: OutboundRecurrenceStatus::Active, + payment_state: OutboundRecurrencePaymentState::NoActivePayment, + original_offer: Vec::new(), + amount_msat: 0, + quantity: None, + payer_note: None, + basetime: None, + initial_start: None, + paid_count: 0, + opaque_state: None, + retry_policy: Retry::Attempts(0), + routing_override: None, + last_successful_payment_id: None, + pay_next_automatically: false, + } + } +} + +impl_ser_tlv_based!(OutboundRecurrenceDetails, { + (0, id, required), + (2, status, required), + (4, payment_state, required), + (6, original_offer, required), + (8, amount_msat, required), + (10, quantity, option), + (12, payer_note, option), + (14, basetime, option), + (16, initial_start, option), + (18, paid_count, required), + (20, opaque_state, option), + (22, retry_policy, required), + (24, routing_override, option), + (26, last_successful_payment_id, option), + (28, pay_next_automatically, required), +}); + +impl StorableObject for OutboundRecurrenceDetails { + type Id = RecurrenceId; + type Update = OutboundRecurrenceDetailsUpdate; + + fn id(&self) -> Self::Id { + self.id + } + + fn update(&mut self, update: Self::Update) -> bool { + if self.id() != update.details.id() { + return false; + } + *self = update.details; + true + } + + fn to_update(&self) -> Self::Update { + OutboundRecurrenceDetailsUpdate { details: self.clone() } + } +} + +/// Update instructions for [`OutboundRecurrenceDetails`]. +#[derive(Clone, Debug)] +pub(crate) struct OutboundRecurrenceDetailsUpdate { + pub details: OutboundRecurrenceDetails, +} + +impl StorableObjectUpdate for OutboundRecurrenceDetailsUpdate { + fn id(&self) -> RecurrenceId { + self.details.id + } +} diff --git a/src/payment/pending_payment_store.rs b/src/payment/pending_payment_store.rs index 30a1135374..a0fad8984c 100644 --- a/src/payment/pending_payment_store.rs +++ b/src/payment/pending_payment_store.rs @@ -6,7 +6,7 @@ // accordance with one or both of these licenses. use bitcoin::Txid; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channelmanager::PaymentId; use crate::data_store::{StorableObject, StorableObjectUpdate}; @@ -30,7 +30,7 @@ pub(crate) struct FundingTxCandidate { pub fee_paid_msat: Option, } -impl_writeable_tlv_based!(FundingTxCandidate, { +impl_ser_tlv_based!(FundingTxCandidate, { (0, txid, required), (2, amount_msat, option), (4, fee_paid_msat, option), @@ -62,7 +62,7 @@ impl PendingPaymentDetails { } } -impl_writeable_tlv_based!(PendingPaymentDetails, { +impl_ser_tlv_based!(PendingPaymentDetails, { (0, details, required), (2, conflicting_txids, optional_vec), (4, candidates, optional_vec), diff --git a/src/payment/store.rs b/src/payment/store.rs index 3163ed15b7..336b968a2c 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -16,8 +16,8 @@ use lightning::ln::types::ChannelId; use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; use lightning::{ - _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, - impl_writeable_tlv_based_enum, write_tlv_fields, + _init_and_read_len_prefixed_tlv_fields, impl_ser_tlv_based, impl_ser_tlv_based_enum, + write_tlv_fields, }; use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning_types::string::UntrustedString; @@ -374,7 +374,7 @@ pub enum PaymentDirection { Outbound, } -impl_writeable_tlv_based_enum!(PaymentDirection, +impl_ser_tlv_based_enum!(PaymentDirection, (0, Inbound) => {}, (1, Outbound) => {} ); @@ -391,7 +391,7 @@ pub enum PaymentStatus { Failed, } -impl_writeable_tlv_based_enum!(PaymentStatus, +impl_ser_tlv_based_enum!(PaymentStatus, (0, Pending) => {}, (2, Succeeded) => {}, (4, Failed) => {} @@ -407,7 +407,7 @@ pub struct Channel { pub channel_id: ChannelId, } -impl_writeable_tlv_based!(Channel, { +impl_ser_tlv_based!(Channel, { (0, counterparty_node_id, required), (2, channel_id, required), }); @@ -467,7 +467,7 @@ pub enum TransactionType { }, } -impl_writeable_tlv_based_enum!(TransactionType, +impl_ser_tlv_based_enum!(TransactionType, (0, Funding) => { (0, channels, optional_vec), }, @@ -649,7 +649,7 @@ pub enum PaymentKind { }, } -impl_writeable_tlv_based_enum!(PaymentKind, +impl_ser_tlv_based_enum!(PaymentKind, (0, Onchain) => { (0, txid, required), (1, tx_type, option), @@ -709,7 +709,7 @@ pub enum ConfirmationStatus { Unconfirmed, } -impl_writeable_tlv_based_enum!(ConfirmationStatus, +impl_ser_tlv_based_enum!(ConfirmationStatus, (0, Confirmed) => { (0, block_hash, required), (2, height, required), @@ -734,7 +734,7 @@ pub struct LSPS2Parameters { pub max_proportional_opening_fee_ppm_msat: Option, } -impl_writeable_tlv_based!(LSPS2Parameters, { +impl_ser_tlv_based!(LSPS2Parameters, { (0, max_total_opening_fee_msat, option), (2, max_proportional_opening_fee_ppm_msat, option), }); @@ -866,7 +866,7 @@ mod tests { pub status: PaymentStatus, } - impl_writeable_tlv_based!(OldPaymentDetails, { + impl_ser_tlv_based!(OldPaymentDetails, { (0, hash, required), (2, preimage, required), (4, secret, required), @@ -965,7 +965,7 @@ mod tests { status: ConfirmationStatus, } - impl_writeable_tlv_based!(OldOnchainKind, { + impl_ser_tlv_based!(OldOnchainKind, { (0, txid, required), (2, status, required), }); @@ -1487,7 +1487,7 @@ mod tests { lsp_fee_limits: LSPS2Parameters, } - impl_writeable_tlv_based!(LegacyBolt11JitKind, { + impl_ser_tlv_based!(LegacyBolt11JitKind, { (0, hash, required), (1, counterparty_skimmed_fee_msat, option), (2, preimage, option), diff --git a/src/payment/unified.rs b/src/payment/unified.rs index e10d57ba00..f43bca50ab 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -260,6 +260,8 @@ impl UnifiedPayment { PaymentMethod::LightningBolt12(_) => 0, PaymentMethod::LightningBolt11(_) => 1, PaymentMethod::OnChain(_) => 2, + PaymentMethod::Bark(_) => 3, + PaymentMethod::Cashu(_) => 4, }); for method in sorted_payment_methods { @@ -338,6 +340,9 @@ impl UnifiedPayment { .await?; return Ok(UnifiedPaymentResult::Onchain { txid }); }, + // Bark and Cashu are valid BIP 21 payment methods, but LDK Node does + // not support sending payments with them yet. Try the next fallback method. + PaymentMethod::Bark(_) | PaymentMethod::Cashu(_) => {}, } } diff --git a/src/peer_store.rs b/src/peer_store.rs index 8345bf7111..2e7fdd240e 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -10,7 +10,7 @@ use std::ops::Deref; use std::sync::{Arc, RwLock}; use bitcoin::secp256k1::PublicKey; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; @@ -163,7 +163,7 @@ pub(crate) struct PeerInfo { pub address: SocketAddress, } -impl_writeable_tlv_based!(PeerInfo, { +impl_ser_tlv_based!(PeerInfo, { (0, node_id, required), (2, address, required), }); diff --git a/src/types.rs b/src/types.rs index 1a61daa109..bd52e67bb8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -13,7 +13,7 @@ use std::sync::{Arc, Mutex}; use bitcoin::secp256k1::PublicKey; use bitcoin::{OutPoint, ScriptBuf}; use lightning::chain::chainmonitor; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channel_state::{ ChannelDetails as LdkChannelDetails, ChannelShutdownState, CounterpartyForwardingInfo, }; @@ -42,6 +42,7 @@ use crate::fee_estimator::OnchainFeeEstimator; use crate::ffi::maybe_wrap; use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::outbound_recurrence::OutboundRecurrenceDetails; use crate::payment::{PaymentDetails, PendingPaymentDetails}; use crate::runtime::RuntimeSpawner; @@ -334,6 +335,9 @@ pub(crate) type BumpTransactionEventHandler = pub(crate) type PaymentStore = DataStore, KeepLeastRecentlyUsed>; +pub(crate) type OutboundRecurrenceStore = + DataStore, KeepAllEntries>; + /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. @@ -703,7 +707,7 @@ pub struct CustomTlvRecord { pub value: Vec, } -impl_writeable_tlv_based!(CustomTlvRecord, { +impl_ser_tlv_based!(CustomTlvRecord, { (0, type_num, required), (2, value, required), }); diff --git a/src/wallet/persist.rs b/src/wallet/persist.rs index 6384d0fcee..296a58683d 100644 --- a/src/wallet/persist.rs +++ b/src/wallet/persist.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use bdk_chain::Merge; use bdk_wallet::{AsyncWalletPersister, ChangeSet}; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, Writeable}; @@ -39,7 +39,7 @@ struct AddressPoolRecord { indices: Vec, } -impl_writeable_tlv_based!(AddressPoolRecord, { +impl_ser_tlv_based!(AddressPoolRecord, { (0, indices, required_vec), });