From b7598ae7d947a6593f01649a08e29b0d78253b4e Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 4 Sep 2025 16:20:50 +0200 Subject: [PATCH] Add onion mailbox for async receivers This introduces an in-memory mailbox to hold onion messages until the receiver comes online. This is required for async payment `held_htlc_available` messages. The mailbox is bounded by a maximum number of peers and a maximum number of messages per peer. --- bindings/ldk_node.udl | 9 ++- src/builder.rs | 101 ++++++++++++++++++++----- src/config.rs | 16 +++- src/event.rs | 40 ++++++++-- src/lib.rs | 16 ++-- src/payment/asynchronous/mod.rs | 1 + src/payment/asynchronous/om_mailbox.rs | 99 ++++++++++++++++++++++++ src/payment/bolt12.rs | 16 ++-- tests/common/mod.rs | 11 ++- tests/integration_tests_rust.rs | 37 +++++++-- 10 files changed, 298 insertions(+), 48 deletions(-) create mode 100644 src/payment/asynchronous/om_mailbox.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 9f0ef697e..a6d867e5a 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,7 +13,6 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; RouteParametersConfig? route_parameters; - boolean async_payment_services_enabled; }; dictionary AnchorChannelsConfig { @@ -96,6 +95,8 @@ interface Builder { [Throws=BuildError] void set_node_alias(string node_alias); [Throws=BuildError] + void set_async_payments_role(AsyncPaymentsRole? role); + [Throws=BuildError] Node build(); [Throws=BuildError] Node build_with_fs_store(); @@ -356,6 +357,7 @@ enum BuildError { "WalletSetupFailed", "LoggerSetupFailed", "NetworkMismatch", + "AsyncPaymentsConfigMismatch", }; [Trait] @@ -720,6 +722,11 @@ enum Currency { "Signet", }; +enum AsyncPaymentsRole { + "Client", + "Server", +}; + dictionary RouteHintHop { PublicKey src_node_id; u64 short_channel_id; diff --git a/src/builder.rs b/src/builder.rs index b99c44cec..7bca0c2c6 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -7,9 +7,9 @@ use crate::chain::ChainSource; use crate::config::{ - default_user_config, may_announce_channel, AnnounceError, BitcoindRestClientConfig, Config, - ElectrumSyncConfig, EsploraSyncConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, - DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, + default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, + BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, + DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, }; use crate::connection::ConnectionManager; @@ -27,6 +27,7 @@ use crate::liquidity::{ }; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; use crate::peer_store::PeerStore; use crate::runtime::Runtime; use crate::tx_broadcaster::TransactionBroadcaster; @@ -191,6 +192,8 @@ pub enum BuildError { LoggerSetupFailed, /// The given network does not match the node's previously configured network. NetworkMismatch, + /// The role of the node in an asynchronous payments context is not compatible with the current configuration. + AsyncPaymentsConfigMismatch, } impl fmt::Display for BuildError { @@ -219,6 +222,12 @@ impl fmt::Display for BuildError { Self::NetworkMismatch => { write!(f, "Given network does not match the node's previously configured network.") }, + Self::AsyncPaymentsConfigMismatch => { + write!( + f, + "The async payments role is not compatible with the current configuration." + ) + }, } } } @@ -240,6 +249,7 @@ pub struct NodeBuilder { gossip_source_config: Option, liquidity_source_config: Option, log_writer_config: Option, + async_payments_role: Option, runtime_handle: Option, } @@ -266,6 +276,7 @@ impl NodeBuilder { liquidity_source_config, log_writer_config, runtime_handle, + async_payments_role: None, } } @@ -544,6 +555,21 @@ impl NodeBuilder { Ok(self) } + /// Sets the role of the node in an asynchronous payments context. + /// + /// See for more information about the async payments protocol. + pub fn set_async_payments_role( + &mut self, role: Option, + ) -> Result<&mut Self, BuildError> { + if let Some(AsyncPaymentsRole::Server) = role { + may_announce_channel(&self.config) + .map_err(|_| BuildError::AsyncPaymentsConfigMismatch)?; + } + + self.async_payments_role = role; + Ok(self) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result { @@ -700,6 +726,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.async_payments_role, seed_bytes, runtime, logger, @@ -732,6 +759,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.async_payments_role, seed_bytes, runtime, logger, @@ -989,6 +1017,13 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_node_alias(node_alias).map(|_| ()) } + /// Sets the role of the node in an asynchronous payments context. + pub fn set_async_payments_role( + &self, role: Option, + ) -> Result<(), BuildError> { + self.inner.write().unwrap().set_async_payments_role(role).map(|_| ()) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result, BuildError> { @@ -1082,8 +1117,9 @@ impl ArcedNodeBuilder { fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, - liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - runtime: Arc, logger: Arc, kv_store: Arc, + liquidity_source_config: Option<&LiquiditySourceConfig>, + async_payments_role: Option, seed_bytes: [u8; 64], runtime: Arc, + logger: Arc, kv_store: Arc, ) -> Result { optionally_install_rustls_cryptoprovider(); @@ -1378,8 +1414,14 @@ fn build_with_store_internal( 100; } - if config.async_payment_services_enabled { - user_config.accept_forwards_to_priv_channels = true; + if let Some(role) = async_payments_role { + match role { + AsyncPaymentsRole::Server => { + user_config.accept_forwards_to_priv_channels = true; + user_config.enable_htlc_hold = true; + }, + AsyncPaymentsRole::Client => user_config.hold_outbound_htlcs_at_next_hop = true, + } } let message_router = @@ -1452,17 +1494,32 @@ fn build_with_store_internal( } // Initialize the PeerManager - let onion_messenger: Arc = Arc::new(OnionMessenger::new( - Arc::clone(&keys_manager), - Arc::clone(&keys_manager), - Arc::clone(&logger), - Arc::clone(&channel_manager), - message_router, - Arc::clone(&channel_manager), - Arc::clone(&channel_manager), - IgnoringMessageHandler {}, - IgnoringMessageHandler {}, - )); + let onion_messenger: Arc = + if let Some(AsyncPaymentsRole::Server) = async_payments_role { + Arc::new(OnionMessenger::new_with_offline_peer_interception( + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&logger), + Arc::clone(&channel_manager), + message_router, + Arc::clone(&channel_manager), + Arc::clone(&channel_manager), + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + )) + } else { + Arc::new(OnionMessenger::new( + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&logger), + Arc::clone(&channel_manager), + message_router, + Arc::clone(&channel_manager), + Arc::clone(&channel_manager), + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + )) + }; let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); // Initialize the GossipSource @@ -1649,6 +1706,12 @@ fn build_with_store_internal( }, }; + let om_mailbox = if let Some(AsyncPaymentsRole::Server) = async_payments_role { + Some(Arc::new(OnionMessageMailbox::new())) + } else { + None + }; + let (stop_sender, _) = tokio::sync::watch::channel(()); let (background_processor_stop_sender, _) = tokio::sync::watch::channel(()); let is_running = Arc::new(RwLock::new(false)); @@ -1681,6 +1744,8 @@ fn build_with_store_internal( is_running, is_listening, node_metrics, + om_mailbox, + async_payments_role, }) } diff --git a/src/config.rs b/src/config.rs index bb0bd56ba..88b70815d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -179,8 +179,6 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub route_parameters: Option, - /// Whether to enable the static invoice service to support async payment reception for clients. - pub async_payment_services_enabled: bool, } impl Default for Config { @@ -195,7 +193,6 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), route_parameters: None, node_alias: None, - async_payment_services_enabled: false, } } } @@ -537,6 +534,19 @@ impl From for LdkMaxDustHTLCExposure { } } +#[derive(Debug, Clone, Copy)] +/// The role of the node in an asynchronous payments context. +/// +/// See for more information about the async payments protocol. +pub enum AsyncPaymentsRole { + /// Node acts a client in an async payments context. This means that if possible, it will instruct its peers to hold + /// HTLCs for it, so that it can go offline. + Client, + /// Node acts as a server in an async payments context. This means that it will hold async payments HTLCs and onion + /// messages for its peers. + Server, +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/src/event.rs b/src/event.rs index cd9146379..1d1acfafa 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,7 +5,8 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::types::{CustomTlvRecord, DynStore, PaymentStore, Sweeper, Wallet}; +use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet}; use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, UserChannelId, @@ -459,6 +460,8 @@ where logger: L, config: Arc, static_invoice_store: Option, + onion_messenger: Arc, + om_mailbox: Option>, } impl EventHandler @@ -472,7 +475,8 @@ where output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>, - static_invoice_store: Option, runtime: Arc, logger: L, + static_invoice_store: Option, onion_messenger: Arc, + om_mailbox: Option>, runtime: Arc, logger: L, config: Arc, ) -> Self { Self { @@ -490,6 +494,8 @@ where runtime, config, static_invoice_store, + onion_messenger, + om_mailbox, } } @@ -1491,11 +1497,33 @@ where self.bump_tx_event_handler.handle_event(&bte).await; }, - LdkEvent::OnionMessageIntercepted { .. } => { - debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); + LdkEvent::OnionMessageIntercepted { peer_node_id, message } => { + if let Some(om_mailbox) = self.om_mailbox.as_ref() { + om_mailbox.onion_message_intercepted(peer_node_id, message); + } else { + log_trace!( + self.logger, + "Onion message intercepted, but no onion message mailbox available" + ); + } }, - LdkEvent::OnionMessagePeerConnected { .. } => { - debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); + LdkEvent::OnionMessagePeerConnected { peer_node_id } => { + if let Some(om_mailbox) = self.om_mailbox.as_ref() { + let messages = om_mailbox.onion_message_peer_connected(peer_node_id); + + for message in messages { + if let Err(e) = + self.onion_messenger.forward_onion_message(message, &peer_node_id) + { + log_trace!( + self.logger, + "Failed to forward onion message to peer {}: {:?}", + peer_node_id, + e + ); + } + } + } }, LdkEvent::PersistStaticInvoice { diff --git a/src/lib.rs b/src/lib.rs index e7e27273b..046343231 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,8 +127,8 @@ pub use builder::NodeBuilder as Builder; use chain::ChainSource; use config::{ - default_user_config, may_announce_channel, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, - PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, + default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config, + NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; use event::{EventHandler, EventQueue}; @@ -136,6 +136,7 @@ use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; use liquidity::{LSPS1Liquidity, LiquiditySource}; +use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, @@ -205,6 +206,8 @@ pub struct Node { is_running: Arc>, is_listening: Arc, node_metrics: Arc>, + om_mailbox: Option>, + async_payments_role: Option, } impl Node { @@ -499,7 +502,8 @@ impl Node { Arc::clone(&self.logger), )); - let static_invoice_store = if self.config.async_payment_services_enabled { + let static_invoice_store = if let Some(AsyncPaymentsRole::Server) = self.async_payments_role + { Some(StaticInvoiceStore::new(Arc::clone(&self.kv_store))) } else { None @@ -517,6 +521,8 @@ impl Node { Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), static_invoice_store, + Arc::clone(&self.onion_messenger), + self.om_mailbox.clone(), Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), @@ -826,9 +832,9 @@ impl Node { Bolt12Payment::new( Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), - Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), + self.async_payments_role, ) } @@ -840,9 +846,9 @@ impl Node { Arc::new(Bolt12Payment::new( Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), - Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), + self.async_payments_role, )) } diff --git a/src/payment/asynchronous/mod.rs b/src/payment/asynchronous/mod.rs index ebb7a4bd3..c28f6e243 100644 --- a/src/payment/asynchronous/mod.rs +++ b/src/payment/asynchronous/mod.rs @@ -5,5 +5,6 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +pub(crate) mod om_mailbox; mod rate_limiter; pub(crate) mod static_invoice_store; diff --git a/src/payment/asynchronous/om_mailbox.rs b/src/payment/asynchronous/om_mailbox.rs new file mode 100644 index 000000000..9a7478706 --- /dev/null +++ b/src/payment/asynchronous/om_mailbox.rs @@ -0,0 +1,99 @@ +use std::collections::{HashMap, VecDeque}; +use std::sync::Mutex; + +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::OnionMessage; + +pub(crate) struct OnionMessageMailbox { + map: Mutex>>, +} + +impl OnionMessageMailbox { + const MAX_MESSAGES_PER_PEER: usize = 30; + const MAX_PEERS: usize = 300; + + pub fn new() -> Self { + Self { map: Mutex::new(HashMap::with_capacity(Self::MAX_PEERS)) } + } + + pub(crate) fn onion_message_intercepted(&self, peer_node_id: PublicKey, message: OnionMessage) { + let mut map = self.map.lock().unwrap(); + + let queue = map.entry(peer_node_id).or_insert_with(VecDeque::new); + if queue.len() >= Self::MAX_MESSAGES_PER_PEER { + queue.pop_front(); + } + queue.push_back(message); + + // Enforce a peers limit. If exceeded, evict the peer with the longest queue. + if map.len() > Self::MAX_PEERS { + let peer_to_remove = + map.iter().max_by_key(|(_, queue)| queue.len()).map(|(peer, _)| *peer).unwrap(); + + map.remove(&peer_to_remove); + } + } + + pub(crate) fn onion_message_peer_connected( + &self, peer_node_id: PublicKey, + ) -> Vec { + let mut map = self.map.lock().unwrap(); + + if let Some(queue) = map.remove(&peer_node_id) { + queue.into() + } else { + Vec::new() + } + } + + #[cfg(test)] + pub(crate) fn is_empty(&self) -> bool { + let map = self.map.lock().unwrap(); + map.is_empty() + } +} + +#[cfg(test)] +mod tests { + use bitcoin::key::Secp256k1; + use bitcoin::secp256k1::{PublicKey, SecretKey}; + use lightning::onion_message; + + use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; + + #[test] + fn onion_message_mailbox() { + let mailbox = OnionMessageMailbox::new(); + + let secp = Secp256k1::new(); + let sk_bytes = [12; 32]; + let sk = SecretKey::from_slice(&sk_bytes).unwrap(); + let peer_node_id = PublicKey::from_secret_key(&secp, &sk); + + let blinding_sk = SecretKey::from_slice(&[13; 32]).unwrap(); + let blinding_point = PublicKey::from_secret_key(&secp, &blinding_sk); + + let message_sk = SecretKey::from_slice(&[13; 32]).unwrap(); + let message_point = PublicKey::from_secret_key(&secp, &message_sk); + + let message = lightning::ln::msgs::OnionMessage { + blinding_point, + onion_routing_packet: onion_message::packet::Packet { + version: 0, + public_key: message_point, + hop_data: vec![1, 2, 3], + hmac: [0; 32], + }, + }; + mailbox.onion_message_intercepted(peer_node_id, message.clone()); + + let messages = mailbox.onion_message_peer_connected(peer_node_id); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0], message); + + assert!(mailbox.is_empty()); + + let messages = mailbox.onion_message_peer_connected(peer_node_id); + assert_eq!(messages.len(), 0); + } +} diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 601c03d7d..6cb2f0b85 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -9,7 +9,7 @@ //! //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::config::{AsyncPaymentsRole, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; @@ -57,16 +57,17 @@ pub struct Bolt12Payment { channel_manager: Arc, payment_store: Arc, is_running: Arc>, - config: Arc, logger: Arc, + async_payments_role: Option, } impl Bolt12Payment { pub(crate) fn new( channel_manager: Arc, payment_store: Arc, - config: Arc, is_running: Arc>, logger: Arc, + is_running: Arc>, logger: Arc, + async_payments_role: Option, ) -> Self { - Self { channel_manager, payment_store, config, is_running, logger } + Self { channel_manager, payment_store, is_running, logger, async_payments_role } } /// Send a payment given an offer. @@ -554,8 +555,11 @@ impl Bolt12Payment { fn blinded_paths_for_async_recipient_internal( &self, recipient_id: Vec, ) -> Result, Error> { - if !self.config.async_payment_services_enabled { - return Err(Error::AsyncPaymentServicesDisabled); + match self.async_payments_role { + Some(AsyncPaymentsRole::Server) => {}, + _ => { + return Err(Error::AsyncPaymentServicesDisabled); + }, } self.channel_manager diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0a1e8cbd2..aa09b86d0 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod logging; use logging::TestLogWriter; -use ldk_node::config::{Config, ElectrumSyncConfig, EsploraSyncConfig}; +use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ @@ -310,6 +310,13 @@ pub(crate) fn setup_two_nodes( pub(crate) fn setup_node( chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, +) -> TestNode { + setup_node_for_async_payments(chain_source, config, seed_bytes, None) +} + +pub(crate) fn setup_node_for_async_payments( + chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, + async_payments_role: Option, ) -> TestNode { setup_builder!(builder, config.node_config); match chain_source { @@ -375,6 +382,8 @@ pub(crate) fn setup_node( } } + builder.set_async_payments_role(async_payments_role).unwrap(); + let test_sync_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into())); let node = builder.build_with_store(test_sync_store).unwrap(); node.start().unwrap(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index f2e8407cd..63fc737b3 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -16,10 +16,11 @@ use common::{ logging::{init_log_logger, validate_log_entry, TestLogWriter}, open_channel, open_channel_push_amt, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + setup_node, setup_node_for_async_payments, setup_two_nodes, wait_for_tx, TestChainSource, + TestSyncStore, }; -use ldk_node::config::EsploraSyncConfig; +use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, @@ -1132,7 +1133,7 @@ fn simple_bolt12_send_receive() { } #[test] -fn static_invoice_server() { +fn async_payment() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); @@ -1141,20 +1142,33 @@ fn static_invoice_server() { config_sender.node_config.node_alias = None; config_sender.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); - let node_sender = setup_node(&chain_source, config_sender, None); + let node_sender = setup_node_for_async_payments( + &chain_source, + config_sender, + None, + Some(AsyncPaymentsRole::Client), + ); let mut config_sender_lsp = random_config(true); - config_sender_lsp.node_config.async_payment_services_enabled = true; config_sender_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); - let node_sender_lsp = setup_node(&chain_source, config_sender_lsp, None); + let node_sender_lsp = setup_node_for_async_payments( + &chain_source, + config_sender_lsp, + None, + Some(AsyncPaymentsRole::Server), + ); let mut config_receiver_lsp = random_config(true); - config_receiver_lsp.node_config.async_payment_services_enabled = true; config_receiver_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); - let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp, None); + let node_receiver_lsp = setup_node_for_async_payments( + &chain_source, + config_receiver_lsp, + None, + Some(AsyncPaymentsRole::Server), + ); let mut config_receiver = random_config(true); config_receiver.node_config.listening_addresses = None; @@ -1241,9 +1255,16 @@ fn static_invoice_server() { std::thread::sleep(std::time::Duration::from_millis(100)); }; + node_receiver.stop().unwrap(); + let payment_id = node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None).unwrap(); + // Sleep to allow the payment reach a state where the htlc is held and waiting for the receiver to come online. + std::thread::sleep(std::time::Duration::from_millis(3000)); + + node_receiver.start().unwrap(); + expect_payment_successful_event!(node_sender, Some(payment_id), None); }