From 94d4bc3ea46df0386bca3b84912564563adeb1c3 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 25 Jun 2026 18:58:23 +0530 Subject: [PATCH 1/4] [plumb] Add forced async invoice refresh selection Async receive offers currently decide static invoice refreshes from timer-based freshness only. Channel changes need a separate selector so callers can rebuild server-side invoices without waiting for the normal age threshold. Add a cache helper that returns used and pending offers for forced invoice refresh. Ready offers stay on the existing offer rotation path because they have not been returned to the application yet. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex --- .../src/offers/async_receive_offer_cache.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index 367cdb68fc8..02c73c2733b 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -469,6 +469,32 @@ impl AsyncReceiveOfferCache { }) } + /// Returns cached offers whose static invoices should be refreshed after a local channel change. + pub(super) fn offers_needing_forced_invoice_refresh( + &self, + ) -> impl Iterator { + self.offers_with_idx().filter_map(move |(_, offer)| { + let needs_invoice_update = match offer.status { + // Used offers may already be published by the application. Keep their server-side + // invoices aligned with our current channels instead of waiting for the timer + // threshold. + OfferStatus::Used { .. } => true, + // Pending offers have already been sent to the server, but are not confirmed yet. + // Re-sending them is safe and matches the normal timer retry behavior. + OfferStatus::Pending => true, + // Ready offers have not been handed to the application yet. They are rotated by the + // offer-refresh path, so forcing invoice updates for them would mostly create extra + // server churn without helping published offers. + OfferStatus::Ready { .. } => false, + }; + if needs_invoice_update { + Some((&offer.offer, offer.offer_nonce, &offer.update_static_invoice_path)) + } else { + None + } + }) + } + /// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice /// server, which indicates that a new offer was persisted by the server and they are ready to /// serve the corresponding static invoice to payers on our behalf. From d32002a89a3b7de23914a349101a9e01050fdf3a Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 27 Jun 2026 17:38:18 +0530 Subject: [PATCH 2/4] [plumb] Thread forced static invoice refresh through flow The cache can identify offers whose static invoices should be rebuilt immediately, but callers still need one canonical path that turns those offers into ServeStaticInvoice messages. Thread a forced-refresh entry point through OffersMessageFlow and ChannelManager while reusing the existing static invoice construction code. Accept peer and channel inputs lazily so recipients without a configured static invoice server return before collecting an unnecessary channel snapshot. This keeps the later channel-change behavior focused on deciding when to refresh, while avoiding reads of transitional channel state when no refresh can be produced. No new refresh behavior is activated in this commit. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex --- lightning/src/ln/channelmanager.rs | 11 ++++ lightning/src/offers/flow.rs | 95 +++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2d7370bb15e..289ad367237 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5905,6 +5905,17 @@ impl< } } + fn force_refresh_async_receive_static_invoices(&self) { + let router = &self.router; + + // Only collect peers and usable channels when async receiving is configured. This avoids reading + // channels during state transitions when there is no static invoice to refresh. + self.flow.force_refresh_async_receive_static_invoices( + || (self.get_peers_for_blinded_path(), self.list_usable_channels()), + router, + ); + } + #[cfg(test)] pub(crate) fn test_check_refresh_async_receive_offers(&self) { self.check_refresh_async_receive_offer_cache(false); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index ade684e5be1..6a49224ee1f 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1349,12 +1349,31 @@ impl OffersMessageFlow { self.check_refresh_async_offers(peers.clone(), timer_tick_occurred)?; if timer_tick_occurred { - self.check_refresh_static_invoices(peers, usable_channels, router); + self.check_refresh_static_invoices(peers, usable_channels, router, false); } Ok(()) } + /// Enqueues static invoice updates for cached async receive offers after local channel changes. + pub fn force_refresh_async_receive_static_invoices( + &self, get_refresh_inputs: F, router: R, + ) where + F: FnOnce() -> (Vec, Vec), + { + // A forced refresh is useful only for an async recipient already configured with a server. + let cache = self.async_receive_offer_cache.lock().unwrap(); + if cache.paths_to_static_invoice_server().is_empty() { + return; + } + core::mem::drop(cache); + + // Channel details may be in a short-lived transitional state when this refresh is requested. + // Collect them only after confirming that async receiving needs the snapshot. + let (peers, usable_channels) = get_refresh_inputs(); + self.check_refresh_static_invoices(peers, usable_channels, router, true); + } + fn check_refresh_async_offers( &self, peers: Vec, timer_tick_occurred: bool, ) -> Result<(), ()> { @@ -1408,41 +1427,59 @@ impl OffersMessageFlow { /// server, based on the offers provided by the cache. fn check_refresh_static_invoices( &self, peers: Vec, usable_channels: Vec, router: R, + force_refresh: bool, ) { let mut serve_static_invoice_msgs = Vec::new(); { let duration_since_epoch = self.duration_since_epoch(); let cache = self.async_receive_offer_cache.lock().unwrap(); - for offer_and_metadata in cache.offers_needing_invoice_refresh(duration_since_epoch) { - let (offer, offer_nonce, update_static_invoice_path) = offer_and_metadata; - - let (invoice, forward_invreq_path) = match self.create_static_invoice_for_server( - offer, - offer_nonce, - peers.clone(), - usable_channels.clone(), - &router, - ) { - Ok((invoice, path)) => (invoice, path), - Err(()) => continue, - }; - let reply_path_context = { - MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted { - invoice_created_at: invoice.created_at(), - offer_id: offer.id(), - }) - }; + // Both timer-driven and forced refreshes build the same update message. Keep the + // construction in one place so the only difference is which cached offers are selected. + macro_rules! build_refresh_message { + ($offer: expr, $offer_nonce: expr, $update_static_invoice_path: expr) => {{ + let (invoice, forward_invreq_path) = match self + .create_static_invoice_for_server( + $offer, + $offer_nonce, + peers.clone(), + usable_channels.clone(), + &router, + ) { + Ok((invoice, path)) => (invoice, path), + Err(()) => continue, + }; + + let reply_path_context = MessageContext::AsyncPayments( + AsyncPaymentsContext::StaticInvoicePersisted { + invoice_created_at: invoice.created_at(), + offer_id: $offer.id(), + }, + ); - let serve_invoice_message = ServeStaticInvoice { - invoice, - forward_invoice_request_path: forward_invreq_path, - }; - serve_static_invoice_msgs.push(( - serve_invoice_message, - update_static_invoice_path.clone(), - reply_path_context, - )); + let serve_invoice_message = ServeStaticInvoice { + invoice, + forward_invoice_request_path: forward_invreq_path, + }; + serve_static_invoice_msgs.push(( + serve_invoice_message, + $update_static_invoice_path.clone(), + reply_path_context, + )); + }}; + } + + if force_refresh { + for offer_and_metadata in cache.offers_needing_forced_invoice_refresh() { + let (offer, offer_nonce, update_static_invoice_path) = offer_and_metadata; + build_refresh_message!(offer, offer_nonce, update_static_invoice_path); + } + } else { + for offer_and_metadata in cache.offers_needing_invoice_refresh(duration_since_epoch) + { + let (offer, offer_nonce, update_static_invoice_path) = offer_and_metadata; + build_refresh_message!(offer, offer_nonce, update_static_invoice_path); + } } } From 10b19886d9871e9639c654497f1620163b748a3d Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 27 Jun 2026 16:57:34 +0530 Subject: [PATCH 3/4] [feat] Refresh async static invoices after channel changes Static invoices contain blinded payment paths built from the recipient's current usable channels and counterparty forwarding parameters. These paths can become stale when channels close, complete a splice, or receive changed routing parameters. Mark those path-affecting changes as requiring a forced async static invoice refresh, then process the refresh after channel locks are released. The deferred flag avoids rebuilding invoices while holding peer or channel locks, since rebuilding needs a fresh usable-channel snapshot. Only applied ChannelUpdates for local channels trigger routing-related refreshes. This lets a newly opened channel provide its forwarding parameters before its payment path is rebuilt. Commitment feerate updates remain excluded because they do not affect blinded payment paths. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex --- lightning/src/ln/channelmanager.rs | 70 ++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 289ad367237..41f0447ff9a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3006,6 +3006,8 @@ pub struct ChannelManager< funding_batch_states: Mutex>>, background_events_processed_since_startup: AtomicBool, + /// Set when a channel change may have made cached async receive static invoices stale. + async_receive_static_invoice_refresh_pending: AtomicBool, event_persist_notifier: Notifier, needs_persist_flag: AtomicBool, @@ -3766,6 +3768,7 @@ impl< pending_background_events: Mutex::new(Vec::new()), total_consistency_lock: RwLock::new(()), background_events_processed_since_startup: AtomicBool::new(false), + async_receive_static_invoice_refresh_pending: AtomicBool::new(false), event_persist_notifier: Notifier::new(), needs_persist_flag: AtomicBool::new(false), funding_batch_states: Mutex::new(BTreeMap::new()), @@ -4564,6 +4567,8 @@ impl< )); } } + self.mark_async_receive_static_invoice_refresh_pending(); + for (err, counterparty_node_id) in shutdown_results.drain(..) { let _ = self.handle_error(err, counterparty_node_id); } @@ -4693,6 +4698,7 @@ impl< log_error!(logger, "Closing channel: {}", err_internal.err.err); self.finish_close_channel(shutdown_res); + self.process_pending_async_receive_static_invoice_refresh(); if let Some((update, node_id_1, node_id_2)) = update_option { let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); @@ -5916,6 +5922,19 @@ impl< ); } + fn mark_async_receive_static_invoice_refresh_pending(&self) { + self.async_receive_static_invoice_refresh_pending.store(true, Ordering::Release); + } + + fn process_pending_async_receive_static_invoice_refresh(&self) { + // Channel state transitions often happen while a peer's channel lock is held. Defer the + // actual refresh until after those locks are released, because rebuilding static invoices + // needs a fresh snapshot of usable channels. + if self.async_receive_static_invoice_refresh_pending.swap(false, Ordering::AcqRel) { + self.force_refresh_async_receive_static_invoices(); + } + } + #[cfg(test)] pub(crate) fn test_check_refresh_async_receive_offers(&self) { self.check_refresh_async_receive_offer_cache(false); @@ -9141,6 +9160,7 @@ impl< .remove_stale_payments(duration_since_epoch, &self.pending_events); self.check_refresh_async_receive_offer_cache(true); + self.process_pending_async_receive_static_invoice_refresh(); if self.check_free_holding_cells() { // While we try to ensure we clear holding cells immediately, its possible we miss @@ -13761,6 +13781,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, None, )); + self.mark_async_receive_static_invoice_refresh_pending(); splice_promotion.discarded_funding.into_iter().for_each(|funding_info| { let event = Event::DiscardFunding { channel_id: chan.context.channel_id(), @@ -16464,6 +16485,7 @@ impl< funding_txo: Some(funding_txo.into_bitcoin_outpoint()), channel_type: funded_channel.funding.get_channel_type().clone(), }, None)); + self.mark_async_receive_static_invoice_refresh_pending(); discarded_funding.into_iter().for_each(|funding_info| { let event = Event::DiscardFunding { channel_id: funded_channel.context.channel_id(), @@ -16884,16 +16906,19 @@ impl< #[rustfmt::skip] fn handle_splice_locked(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceLocked) { - let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { - let res = self.internal_splice_locked(&counterparty_node_id, msg); - let persist = match &res { - Err(e) if e.closes_channel() => NotifyOption::DoPersist, - Err(_) => NotifyOption::SkipPersistHandleEvents, - Ok(()) => NotifyOption::DoPersist, - }; - let _ = self.handle_error(res, counterparty_node_id); - persist - }); + { + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_splice_locked(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::DoPersist, + }; + let _ = self.handle_error(res, counterparty_node_id); + persist + }); + } + self.process_pending_async_receive_static_invoice_refresh(); } fn handle_shutdown(&self, counterparty_node_id: PublicKey, msg: &msgs::Shutdown) { @@ -17028,14 +17053,22 @@ impl< } fn handle_channel_update(&self, counterparty_node_id: PublicKey, msg: &msgs::ChannelUpdate) { - PersistenceNotifierGuard::optionally_notify(self, || { - let res = self.internal_channel_update(&counterparty_node_id, msg); - if let Ok(persist) = self.handle_error(res, counterparty_node_id) { - persist - } else { - NotifyOption::DoPersist - } - }); + { + PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_channel_update(&counterparty_node_id, msg); + if let Ok(persist) = self.handle_error(res, counterparty_node_id) { + if persist == NotifyOption::DoPersist { + // Static invoices encode the counterparty's forwarding parameters. Refresh + // them when an update changes those parameters for a local channel. + self.mark_async_receive_static_invoice_refresh_pending(); + } + persist + } else { + NotifyOption::DoPersist + } + }); + } + self.process_pending_async_receive_static_invoice_refresh(); } fn handle_channel_reestablish( @@ -20524,6 +20557,7 @@ impl< pending_background_events: Mutex::new(pending_background_events), total_consistency_lock: RwLock::new(()), background_events_processed_since_startup: AtomicBool::new(false), + async_receive_static_invoice_refresh_pending: AtomicBool::new(false), event_persist_notifier: Notifier::new(), needs_persist_flag: AtomicBool::new(false), From f3c61ede8a2be072e03aa4ef6a4bc0d53ad6d8dd Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 27 Jun 2026 17:08:43 +0530 Subject: [PATCH 4/4] [test] Cover async invoice refresh after channel changes Used async receive offers may already be published, so changes to their payment paths must update server-side static invoices without waiting for the normal age threshold. Cover channel-opening and counterparty forwarding-update flows. Verify that a newly usable channel adds another payment path and that a changed forwarding fee is encoded in the replacement invoice. Both flows confirm that the server receives the replacement invoice for the same offer slot. Ignore concurrent OfferPathsRequest messages so assertions remain focused on ServeStaticInvoice updates. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex --- lightning/src/ln/async_payments_tests.rs | 180 ++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 6e8f38f847a..2158430808c 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -60,7 +60,7 @@ use crate::sign::NodeSigner; use crate::sync::Mutex; use crate::types::features::Bolt12InvoiceFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; -use crate::util::config::{HTLCInterceptionFlags, UserConfig}; +use crate::util::config::{ChannelConfigUpdate, HTLCInterceptionFlags, UserConfig}; use crate::util::ser::Writeable; use bitcoin::constants::ChainHash; use bitcoin::network::Network; @@ -416,6 +416,55 @@ fn extract_static_invoice_om<'a>( (peer_id, om, static_invoice.unwrap()) } +/// Extracts the next static invoice update while ignoring unrelated offer-path requests. +fn extract_serve_static_invoice_om<'a>( + recipient: &'a Node, next_hop_nodes: &[&'a Node], +) -> (PublicKey, msgs::OnionMessage, StaticInvoice) { + let mut static_invoice = None; + let mut expected_msg_type = |peeled_onion: &_| match peeled_onion { + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(msg), _, _) => { + static_invoice = Some(msg.invoice.clone()); + true + }, + _ => false, + }; + let expected_msg_type_to_ignore = |peeled_onion: &_| { + matches!( + peeled_onion, + &PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + ) + }; + let (peer_id, om) = extract_expected_om( + recipient, + next_hop_nodes, + expected_msg_type, + expected_msg_type_to_ignore, + ) + .pop() + .unwrap(); + (peer_id, om, static_invoice.unwrap()) +} + +/// Delivers a static invoice update and checks that the server persists it in the expected slot. +fn expect_static_invoice_persist_event( + server: &Node, recipient: &Node, serve_static_invoice_om: &msgs::OnionMessage, + expected_invoice: &StaticInvoice, expected_invoice_slot: u16, expected_recipient_id: &[u8], +) { + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), serve_static_invoice_om); + let mut events = server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events.pop().unwrap() { + Event::PersistStaticInvoice { invoice, invoice_slot, recipient_id, .. } => { + assert_eq!(&invoice, expected_invoice); + assert_eq!(invoice_slot, expected_invoice_slot); + assert_eq!(recipient_id, expected_recipient_id); + }, + _ => panic!(), + } +} + fn extract_held_htlc_available_oms<'a>( payer: &'a Node, next_hop_nodes: &[&'a Node], ) -> Vec<(PublicKey, msgs::OnionMessage)> { @@ -2507,6 +2556,135 @@ fn refresh_static_invoices_for_used_offers() { assert_eq!(res.0, Some(PaidBolt12Invoice::StaticInvoice(updated_invoice))); } +/// Checks that a used async receive offer gets a fresh server-side static invoice when a new +/// channel becomes usable. Used offers may already be published, so they should not wait for the +/// normal invoice refresh threshold after local payment paths change. +#[test] +fn refresh_static_invoices_for_used_offers_when_channel_opens() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); + + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let server = &nodes[1]; + let recipient = &nodes[2]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + expect_offer_paths_requests(recipient, &[&nodes[0], server]); + + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + let original_invoice = flow_res.invoice; + assert_eq!(original_invoice.payment_paths().len(), 1); + + // Mark the offer as used so the cache treats it as potentially published by the application. + let _offer = recipient.node.get_async_receive_offer().unwrap(); + + // Keep onion delivery direct so the test only checks that opening a channel refreshes the + // invoice after its forwarding information is available. + server.message_router.peers_override.lock().unwrap().push(recipient.node.get_our_node_id()); + recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id()); + + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let (peer_node_id, serve_static_invoice_om, updated_invoice) = + extract_serve_static_invoice_om(recipient, &[server]); + assert_eq!(peer_node_id, server.node.get_our_node_id()); + assert_ne!(original_invoice, updated_invoice); + assert_eq!(updated_invoice.payment_paths().len(), 2); + + expect_static_invoice_persist_event( + server, + recipient, + &serve_static_invoice_om, + &updated_invoice, + flow_res.invoice_slot, + &recipient_id, + ); +} + +/// Checks that changed forwarding parameters refresh the static invoice for a used offer without +/// waiting for the normal invoice refresh threshold. +#[test] +fn refresh_static_invoices_for_used_offers_when_forwarding_fees_change() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); + + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let server = &nodes[1]; + let recipient = &nodes[2]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + expect_offer_paths_requests(recipient, &[&nodes[0], server]); + + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + let original_invoice = flow_res.invoice; + let _offer = recipient.node.get_async_receive_offer().unwrap(); + + // Keep onion delivery direct so the test only checks the forwarding update trigger. + server.message_router.peers_override.lock().unwrap().push(recipient.node.get_our_node_id()); + recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id()); + + let channel = server + .node + .list_channels() + .into_iter() + .find(|channel| channel.counterparty.node_id == recipient.node.get_our_node_id()) + .unwrap(); + let updated_fee_base_msat = channel.config.unwrap().forwarding_fee_base_msat + 10; + let config_update = ChannelConfigUpdate { + forwarding_fee_base_msat: Some(updated_fee_base_msat), + ..ChannelConfigUpdate::default() + }; + server + .node + .update_partial_channel_config( + &recipient.node.get_our_node_id(), + &[channel.channel_id], + &config_update, + ) + .unwrap(); + let channel_update = get_event_msg!( + server, + MessageSendEvent::SendChannelUpdate, + recipient.node.get_our_node_id() + ); + recipient.node.handle_channel_update(server.node.get_our_node_id(), &channel_update); + + let (peer_node_id, serve_static_invoice_om, updated_invoice) = + extract_serve_static_invoice_om(recipient, &[server]); + assert_eq!(peer_node_id, server.node.get_our_node_id()); + assert_ne!(original_invoice, updated_invoice); + assert_eq!(updated_invoice.payment_paths().len(), 1); + assert_eq!(updated_invoice.payment_paths()[0].payinfo.fee_base_msat, updated_fee_base_msat); + + expect_static_invoice_persist_event( + server, + recipient, + &serve_static_invoice_om, + &updated_invoice, + flow_res.invoice_slot, + &recipient_id, + ); +} + #[cfg_attr(feature = "std", ignore)] #[test] fn ignore_expired_static_invoice() {