From d1d51b3ae0d966b6d90b6f1f3aac603b0a83f2cb Mon Sep 17 00:00:00 2001 From: elnosh Date: Thu, 28 Aug 2025 08:25:24 -0500 Subject: [PATCH 1/3] Update OnionMessage with link to BOLTs --- lightning-types/src/features.rs | 3 +-- lightning/src/ln/msgs.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lightning-types/src/features.rs b/lightning-types/src/features.rs index aca4bb6e5a9..77436a23444 100644 --- a/lightning-types/src/features.rs +++ b/lightning-types/src/features.rs @@ -54,8 +54,7 @@ //! - `SimpleClose` - requires/supports simplified closing negotiation //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#closing-negotiation-closing_complete-and-closing_sig) for more information). //! - `OnionMessages` - requires/supports forwarding onion messages -//! (see [BOLT-7](https://github.com/lightning/bolts/pull/759/files) for more information). -// TODO: update link +//! (see [BOLT-4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#onion-messages) for more information). //! - `ChannelType` - node supports the channel_type field in open/accept //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md) for more information). //! - `SCIDPrivacy` - supply channel aliases for routing diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 6d025293b9e..9650b625284 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -767,9 +767,9 @@ pub struct UpdateAddHTLC { pub blinding_point: Option, } -/// An onion message to be sent to or received from a peer. +/// An [`onion message`] to be sent to or received from a peer. /// -// TODO: update with link to OM when they are merged into the BOLTs +/// [`onion message`]: https://github.com/lightning/bolts/blob/master/04-onion-routing.md#onion-messages #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct OnionMessage { /// Used in decrypting the onion packet's payload. From d810708ed76af060368918a5c17c1f51936ecd4b Mon Sep 17 00:00:00 2001 From: elnosh Date: Fri, 29 Aug 2025 15:25:21 -0500 Subject: [PATCH 2/3] Use correct nonce in InvoiceRequest context Previously it would generate a new nonce for this context instead of using the offer nonce. This would make it so that verification would fail later when receiving a invoice request. --- lightning/src/offers/flow.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 38f472b2f5b..734482845ee 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1557,8 +1557,7 @@ where .and_then(|builder| builder.build_and_sign(secp_ctx)) .map_err(|_| ())?; - let nonce = Nonce::from_entropy_source(&*entropy); - let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce }); + let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce: offer_nonce }); let forward_invoice_request_path = self .create_blinded_paths(peers, context) .and_then(|paths| paths.into_iter().next().ok_or(()))?; From f46a3ffdfa7c6f0924423405b293c08cfdc7a84e Mon Sep 17 00:00:00 2001 From: elnosh Date: Fri, 29 Aug 2025 16:42:31 -0500 Subject: [PATCH 3/3] Forward invoice requests to async recipient As a static invoice server, if we receive an invoice request on behalf of an often-offline recipient we will reply to the sender with the static invoice previously provided by the async recipient. Here, in addition to doing that we'll forward the invoice request received to the async recipient to give it a chance to reply with a fresh invoice in case it is online. --- lightning/src/events/mod.rs | 26 +- lightning/src/ln/async_payments_tests.rs | 540 ++++++++++++++++++++--- lightning/src/ln/channelmanager.rs | 20 +- lightning/src/offers/flow.rs | 29 +- lightning/src/offers/invoice_request.rs | 9 + 5 files changed, 559 insertions(+), 65 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index d36143dce9f..cb4d40f4876 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -18,7 +18,7 @@ pub mod bump_transaction; pub use bump_transaction::BumpTransactionEvent; -use crate::blinded_path::message::OffersContext; +use crate::blinded_path::message::{BlindedMessagePath, OffersContext}; use crate::blinded_path::payment::{ Bolt12OfferContext, Bolt12RefundContext, PaymentContext, PaymentContextRef, }; @@ -28,6 +28,7 @@ use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; use crate::ln::types::ChannelId; use crate::ln::{msgs, LocalHTLCFailureReason}; use crate::offers::invoice::Bolt12Invoice; +use crate::offers::invoice_request::InvoiceRequest; use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::messenger::Responder; use crate::routing::gossip::NetworkUpdate; @@ -1654,6 +1655,13 @@ pub enum Event { /// The invoice that should be persisted and later provided to payers when handling a future /// [`Event::StaticInvoiceRequested`]. invoice: StaticInvoice, + /// The path to where invoice requests will be forwarded. If we receive an invoice + /// request, we'll forward it to the async recipient over this path in case the + /// recipient is online to provide a new invoice. This path should be persisted and + /// later provided to [`ChannelManager::respond_to_static_invoice_request`]. + /// + /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request + invoice_request_path: BlindedMessagePath, /// Useful for the recipient to replace a specific invoice stored by us as the static invoice /// server. /// @@ -1686,12 +1694,14 @@ pub enum Event { /// /// If we previously persisted a [`StaticInvoice`] from an [`Event::PersistStaticInvoice`] that /// matches the below `recipient_id` and `invoice_slot`, that invoice should be retrieved now - /// and forwarded to the payer via [`ChannelManager::send_static_invoice`]. + /// and forwarded to the payer via [`ChannelManager::respond_to_static_invoice_request`]. + /// The invoice request path previously persisted from [`Event::PersistStaticInvoice`] should + /// also be provided in [`ChannelManager::respond_to_static_invoice_request`]. /// /// [`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient /// [`ChannelManager::set_paths_to_static_invoice_server`]: crate::ln::channelmanager::ChannelManager::set_paths_to_static_invoice_server /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request StaticInvoiceRequested { /// An identifier for the recipient previously surfaced in /// [`Event::PersistStaticInvoice::recipient_id`]. Useful when paired with the `invoice_slot` to @@ -1702,10 +1712,16 @@ pub enum Event { /// retrieve the [`StaticInvoice`] requested by the payer. invoice_slot: u16, /// The path over which the [`StaticInvoice`] will be sent to the payer, which should be - /// provided to [`ChannelManager::send_static_invoice`] along with the invoice. + /// provided to [`ChannelManager::respond_to_static_invoice_request`] along with the invoice. /// - /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request reply_path: Responder, + /// The invoice request that will be forwarded to the async recipient to give the + /// recipient a chance to provide an invoice in case it is online. It should be + /// provided to [`ChannelManager::respond_to_static_invoice_request`]. + /// + /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request + invoice_request: InvoiceRequest, }, /// Indicates that a channel funding transaction constructed interactively is ready to be /// signed. This event will only be triggered if at least one input was contributed. diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index e617f6fbf1f..1fa1b77f706 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -7,7 +7,7 @@ // You may not use this file except in accordance with one or both of these // licenses. -use crate::blinded_path::message::{MessageContext, OffersContext}; +use crate::blinded_path::message::{BlindedMessagePath, MessageContext, OffersContext}; use crate::blinded_path::payment::PaymentContext; use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs}; use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS}; @@ -15,7 +15,7 @@ use crate::events::{ Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose, }; use crate::ln::blinded_payment_tests::{fail_blinded_htlc_backwards, get_blinded_route_parameters}; -use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; +use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, RecipientOnionFields}; use crate::ln::functional_test_utils::*; use crate::ln::inbound_payment; use crate::ln::msgs; @@ -66,6 +66,7 @@ use core::time::Duration; struct StaticInvoiceServerFlowResult { invoice: StaticInvoice, invoice_slot: u16, + invoice_request_path: BlindedMessagePath, // Returning messages that were sent along the way allows us to test handling duplicate messages. offer_paths_request: msgs::OnionMessage, @@ -147,15 +148,16 @@ fn pass_static_invoice_server_messages( // that the static invoice should be persisted. let mut events = server.node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); - let (invoice, invoice_slot, ack_path) = match events.pop().unwrap() { + let (invoice, invoice_slot, ack_path, invoice_request_path) = match events.pop().unwrap() { Event::PersistStaticInvoice { invoice, invoice_persisted_path, recipient_id: ev_id, invoice_slot, + invoice_request_path, } => { assert_eq!(recipient_id, ev_id); - (invoice, invoice_slot, invoice_persisted_path) + (invoice, invoice_slot, invoice_persisted_path, invoice_request_path) }, _ => panic!(), }; @@ -179,6 +181,7 @@ fn pass_static_invoice_server_messages( StaticInvoiceServerFlowResult { offer_paths_request: offer_paths_req, static_invoice_persisted_message: invoice_persisted_om, + invoice_request_path, invoice, invoice_slot, } @@ -192,7 +195,7 @@ fn pass_static_invoice_server_messages( // Returns: (held_htlc_available_om, release_held_htlc_om) fn pass_async_payments_oms( static_invoice: StaticInvoice, sender: &Node, always_online_recipient_counterparty: &Node, - recipient: &Node, recipient_id: Vec, + recipient: &Node, recipient_id: Vec, invoice_request_path: BlindedMessagePath, ) -> (msgs::OnionMessage, msgs::OnionMessage) { let sender_node_id = sender.node.get_our_node_id(); let always_online_node_id = always_online_recipient_counterparty.node.get_our_node_id(); @@ -205,17 +208,32 @@ fn pass_async_payments_oms( let mut events = always_online_recipient_counterparty.node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); - let reply_path = match events.pop().unwrap() { - Event::StaticInvoiceRequested { recipient_id: ev_id, invoice_slot: _, reply_path } => { + let (reply_path, invoice_request) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { + recipient_id: ev_id, + invoice_slot: _, + reply_path, + invoice_request, + } => { assert_eq!(recipient_id, ev_id); - reply_path + (reply_path, invoice_request) }, _ => panic!(), }; always_online_recipient_counterparty .node - .send_static_invoice(static_invoice, reply_path) + .respond_to_static_invoice_request( + static_invoice, + reply_path, + invoice_request, + invoice_request_path, + ) + .unwrap(); + + let _invreq_om = always_online_recipient_counterparty + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) .unwrap(); let static_invoice_om = always_online_recipient_counterparty .onion_messenger @@ -550,8 +568,9 @@ fn ignore_unexpected_static_invoice() { // Create a static invoice to be sent over the reply path containing the original payment_id, but // the static invoice corresponds to a different offer than was originally paid. - let unexpected_static_invoice = - pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let unexpected_static_invoice = invoice_flow_res.invoice; let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -569,16 +588,29 @@ fn ignore_unexpected_static_invoice() { let mut events = nodes[1].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); - let reply_path = match events.pop().unwrap() { - Event::StaticInvoiceRequested { recipient_id: ev_id, invoice_slot: _, reply_path } => { + let (reply_path, invoice_request) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { + recipient_id: ev_id, + invoice_slot: _, + reply_path, + invoice_request, + } => { assert_eq!(recipient_id, ev_id); - reply_path + (reply_path, invoice_request) }, _ => panic!(), }; // Check that the sender will ignore the unexpected static invoice. - nodes[1].node.send_static_invoice(unexpected_static_invoice, reply_path.clone()).unwrap(); + nodes[1] + .node + .respond_to_static_invoice_request( + unexpected_static_invoice, + reply_path.clone(), + invoice_request.clone(), + invoice_flow_res.invoice_request_path.clone(), + ) + .unwrap(); let unexpected_static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -592,7 +624,15 @@ fn ignore_unexpected_static_invoice() { // A valid static invoice corresponding to the correct offer will succeed and cause us to send a // held_htlc_available onion message. - nodes[1].node.send_static_invoice(valid_static_invoice.clone(), reply_path.clone()).unwrap(); + nodes[1] + .node + .respond_to_static_invoice_request( + valid_static_invoice.clone(), + reply_path.clone(), + invoice_request.clone(), + invoice_flow_res.invoice_request_path.clone(), + ) + .unwrap(); let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -607,7 +647,15 @@ fn ignore_unexpected_static_invoice() { .all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_)))); // Receiving a duplicate invoice will have no effect. - nodes[1].node.send_static_invoice(valid_static_invoice, reply_path).unwrap(); + nodes[1] + .node + .respond_to_static_invoice_request( + valid_static_invoice, + reply_path, + invoice_request, + invoice_flow_res.invoice_request_path, + ) + .unwrap(); let dup_static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -619,6 +667,228 @@ fn ignore_unexpected_static_invoice() { assert!(async_pmts_msgs.is_empty()); } +#[test] +fn ignore_duplicate_invoice() { + // When a sender tries to pay an async recipient it could potentially end up receiving two + // invoices: one static invoice that it received from always-online node and a fresh invoice + // received from async recipient in case it was online to reply to request. Test that it + // will only pay one of the two invoices. + 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 sender = &nodes[0]; + let always_online_node = &nodes[1]; + let async_recipient = &nodes[2]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = always_online_node + .node + .blinded_paths_for_async_recipient(recipient_id.clone(), None) + .unwrap(); + async_recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + expect_offer_paths_requests(async_recipient, &[sender, always_online_node]); + + let invoice_flow_res = pass_static_invoice_server_messages( + always_online_node, + async_recipient, + recipient_id.clone(), + ); + let static_invoice = invoice_flow_res.invoice; + assert!(static_invoice.invoice_features().supports_basic_mpp()); + let offer = async_recipient.node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let params = RouteParametersConfig::default(); + sender + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) + .unwrap(); + + let sender_node_id = sender.node.get_our_node_id(); + let always_online_node_id = always_online_node.node.get_our_node_id(); + let async_recipient_id = async_recipient.node.get_our_node_id(); + + let invreq_om = + sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); + always_online_node.onion_messenger.handle_onion_message(sender_node_id, &invreq_om); + + let mut events = always_online_node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (reply_path, invoice_request) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { + recipient_id: ev_id, + invoice_slot: _, + reply_path, + invoice_request, + } => { + assert_eq!(recipient_id, ev_id); + (reply_path, invoice_request) + }, + _ => panic!(), + }; + + always_online_node + .node + .respond_to_static_invoice_request( + static_invoice.clone(), + reply_path, + invoice_request, + invoice_flow_res.invoice_request_path.clone(), + ) + .unwrap(); + + // After calling `respond_to_static_invoice_request` the next two messages should be the + // invoice request to the intended for the async recipient and the static invoice to the + // payer. + let invreq_om = + always_online_node.onion_messenger.next_onion_message_for_peer(async_recipient_id).unwrap(); + let peeled_msg = async_recipient.onion_messenger.peel_onion_message(&invreq_om).unwrap(); + assert!(matches!(peeled_msg, PeeledOnion::Offers(OffersMessage::InvoiceRequest(_), _, _))); + + let static_invoice_om = + always_online_node.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + let peeled_msg = sender.onion_messenger.peel_onion_message(&static_invoice_om).unwrap(); + assert!(matches!(peeled_msg, PeeledOnion::Offers(OffersMessage::StaticInvoice(_), _, _))); + + // Handling the `invoice_request` from the async recipient we should get back an invoice. + async_recipient.onion_messenger.handle_onion_message(always_online_node_id, &invreq_om); + let invoice_om = + async_recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + + // First pay the static invoice. + sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om); + + let held_htlc_available_om_0_1 = + sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); + always_online_node + .onion_messenger + .handle_onion_message(sender_node_id, &held_htlc_available_om_0_1); + let held_htlc_available_om_1_2 = + always_online_node.onion_messenger.next_onion_message_for_peer(async_recipient_id).unwrap(); + async_recipient + .onion_messenger + .handle_onion_message(always_online_node_id, &held_htlc_available_om_1_2); + + let release_held_htlc_om = + async_recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + sender.onion_messenger.handle_onion_message(async_recipient_id, &release_held_htlc_om); + + let mut events = sender.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&always_online_node_id, &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors!(sender, 1); + + let route: &[&[&Node]] = &[&[always_online_node, async_recipient]]; + let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = extract_payment_preimage(&claimable_ev); + let (res, _) = + claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice.clone()))); + + // After paying the static invoice, check that regular invoice received from async recipient is ignored. + match sender.onion_messenger.peel_onion_message(&invoice_om) { + Ok(PeeledOnion::Offers(OffersMessage::Invoice(invoice), context, _)) => { + assert!(matches!( + sender.node.send_payment_for_bolt12_invoice(&invoice, context.as_ref()), + Err(Bolt12PaymentError::DuplicateInvoice) + )) + }, + _ => panic!(), + } + + // Now handle case where the sender pays regular invoice and ignores static invoice. + let payment_id = PaymentId([2; 32]); + sender + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) + .unwrap(); + + let invreq_om = + sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); + always_online_node.onion_messenger.handle_onion_message(sender_node_id, &invreq_om); + + let mut events = always_online_node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (reply_path, invoice_request) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { + recipient_id: ev_id, + invoice_slot: _, + reply_path, + invoice_request, + } => { + assert_eq!(recipient_id, ev_id); + (reply_path, invoice_request) + }, + _ => panic!(), + }; + + always_online_node + .node + .respond_to_static_invoice_request( + static_invoice.clone(), + reply_path, + invoice_request, + invoice_flow_res.invoice_request_path, + ) + .unwrap(); + + let invreq_om = + always_online_node.onion_messenger.next_onion_message_for_peer(async_recipient_id).unwrap(); + let peeled_msg = async_recipient.onion_messenger.peel_onion_message(&invreq_om).unwrap(); + assert!(matches!(peeled_msg, PeeledOnion::Offers(OffersMessage::InvoiceRequest(_), _, _))); + + let static_invoice_om = + always_online_node.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + let peeled_msg = sender.onion_messenger.peel_onion_message(&static_invoice_om).unwrap(); + assert!(matches!(peeled_msg, PeeledOnion::Offers(OffersMessage::StaticInvoice(_), _, _))); + + async_recipient.onion_messenger.handle_onion_message(always_online_node_id, &invreq_om); + let invoice_om = + async_recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + + let invoice = match sender.onion_messenger.peel_onion_message(&invoice_om) { + Ok(PeeledOnion::Offers(OffersMessage::Invoice(invoice), _, _)) => invoice, + _ => panic!(), + }; + + sender.onion_messenger.handle_onion_message(async_recipient_id, &invoice_om); + + let mut events = sender.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&always_online_node_id, &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors!(sender, 1); + + let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) + .without_clearing_recipient_events(); + do_pass_along_path(args); + + let payment_preimage = match get_event!(async_recipient, Event::PaymentClaimable) { + Event::PaymentClaimable { purpose, .. } => purpose.preimage().unwrap(), + _ => panic!("No Event::PaymentClaimable"), + }; + + // After paying invoice, check that static invoice is ignored. + let res = claim_payment(sender, route[0], payment_preimage); + assert_eq!(res, Some(PaidBolt12Invoice::Bolt12Invoice(invoice))); + + sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(sender.node); + assert!(async_pmts_msgs.is_empty()); + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); +} + #[test] fn async_receive_flow_success() { // Test that an always-online sender can successfully pay an async receiver. @@ -659,6 +929,7 @@ fn async_receive_flow_success() { &nodes[1], &nodes[2], recipient_id, + invoice_flow_res.invoice_request_path, ) .1; nodes[0] @@ -704,8 +975,9 @@ fn expired_static_invoice_fail() { nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]); - let static_invoice = - pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; @@ -724,12 +996,22 @@ fn expired_static_invoice_fail() { let mut events = nodes[1].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); - let reply_path = match events.pop().unwrap() { - Event::StaticInvoiceRequested { reply_path, .. } => reply_path, + let (reply_path, invoice_request) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { reply_path, invoice_request, .. } => { + (reply_path, invoice_request) + }, _ => panic!(), }; - nodes[1].node.send_static_invoice(static_invoice.clone(), reply_path).unwrap(); + nodes[1] + .node + .respond_to_static_invoice_request( + static_invoice.clone(), + reply_path, + invoice_request, + invoice_flow_res.invoice_request_path, + ) + .unwrap(); let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -780,8 +1062,9 @@ fn timeout_unreleased_payment() { recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]); - let static_invoice = - pass_static_invoice_server_messages(server, recipient, recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; let offer = recipient.node.get_async_receive_offer().unwrap(); let amt_msat = 5000; @@ -798,12 +1081,22 @@ fn timeout_unreleased_payment() { let mut events = server.node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); - let reply_path = match events.pop().unwrap() { - Event::StaticInvoiceRequested { reply_path, .. } => reply_path, + let (reply_path, invoice_request) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { reply_path, invoice_request, .. } => { + (reply_path, invoice_request) + }, _ => panic!(), }; - server.node.send_static_invoice(static_invoice.clone(), reply_path).unwrap(); + server + .node + .respond_to_static_invoice_request( + static_invoice.clone(), + reply_path, + invoice_request, + invoice_flow_res.invoice_request_path, + ) + .unwrap(); let static_invoice_om = server.onion_messenger.next_onion_message_for_peer(sender.node.get_our_node_id()).unwrap(); @@ -866,8 +1159,9 @@ fn async_receive_mpp() { nodes[3].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); expect_offer_paths_requests(&nodes[3], &[&nodes[0], &nodes[1], &nodes[2]]); - let static_invoice = - pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; let offer = nodes[3].node.get_async_receive_offer().unwrap(); let amt_msat = 15_000_000; @@ -877,8 +1171,15 @@ fn async_receive_mpp() { .node .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), params) .unwrap(); - let release_held_htlc_om_3_0 = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3], recipient_id).1; + let release_held_htlc_om_3_0 = pass_async_payments_oms( + static_invoice, + &nodes[0], + &nodes[1], + &nodes[3], + recipient_id, + invoice_flow_res.invoice_request_path, + ) + .1; nodes[0] .onion_messenger .handle_onion_message(nodes[3].node.get_our_node_id(), &release_held_htlc_om_3_0); @@ -960,8 +1261,9 @@ fn amount_doesnt_match_invreq() { nodes[3].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); expect_offer_paths_requests(&nodes[3], &[&nodes[0], &nodes[1], &nodes[2]]); - let static_invoice = - pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; let offer = nodes[3].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; @@ -971,8 +1273,15 @@ fn amount_doesnt_match_invreq() { .node .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), params) .unwrap(); - let release_held_htlc_om_3_0 = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3], recipient_id).1; + let release_held_htlc_om_3_0 = pass_async_payments_oms( + static_invoice, + &nodes[0], + &nodes[1], + &nodes[3], + recipient_id, + invoice_flow_res.invoice_request_path, + ) + .1; // Replace the invoice request contained within outbound_payments before sending so the invreq // amount doesn't match the onion amount when the HTLC gets to the recipient. @@ -1201,8 +1510,9 @@ fn invalid_async_receive_with_retry( } nodes[2].router.expect_blinded_payment_paths(static_invoice_paths); - let static_invoice = - pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; let offer = nodes[2].node.get_async_receive_offer().unwrap(); let params = RouteParametersConfig::default(); @@ -1210,8 +1520,15 @@ fn invalid_async_receive_with_retry( .node .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(2), params) .unwrap(); - let release_held_htlc_om_2_0 = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2], recipient_id).1; + let release_held_htlc_om_2_0 = pass_async_payments_oms( + static_invoice, + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + invoice_flow_res.invoice_request_path, + ) + .1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om_2_0); @@ -1289,8 +1606,9 @@ fn expired_static_invoice_message_path() { nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]); - let static_invoice = - pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; @@ -1308,6 +1626,7 @@ fn expired_static_invoice_message_path() { &nodes[1], &nodes[2], recipient_id, + invoice_flow_res.invoice_request_path, ); // After the invoice is expired, ignore inbound held_htlc_available messages over the path. @@ -1404,8 +1723,9 @@ fn expired_static_invoice_payment_path() { ); connect_blocks(&nodes[2], final_max_cltv_expiry - nodes[2].best_block_info().1); - let static_invoice = - pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; @@ -1415,8 +1735,15 @@ fn expired_static_invoice_payment_path() { .node .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); - let release_held_htlc_om = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2], recipient_id).1; + let release_held_htlc_om = pass_async_payments_oms( + static_invoice, + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + invoice_flow_res.invoice_request_path, + ) + .1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); @@ -1815,12 +2142,13 @@ fn refresh_static_invoices_for_used_offers() { .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); - let (updated_invoice, ack_path) = match events.pop().unwrap() { + let (updated_invoice, ack_path, invoice_request_path) = match events.pop().unwrap() { Event::PersistStaticInvoice { invoice, invoice_slot, invoice_persisted_path, recipient_id: ev_id, + invoice_request_path, } => { assert_ne!(original_invoice, invoice); assert_eq!(recipient_id, ev_id); @@ -1828,7 +2156,7 @@ fn refresh_static_invoices_for_used_offers() { // When we update the invoice corresponding to a specific offer, the invoice_slot stays the // same. assert_eq!(invoice_slot, flow_res.invoice_slot); - (invoice, invoice_persisted_path) + (invoice, invoice_persisted_path, invoice_request_path) }, _ => panic!(), }; @@ -1856,8 +2184,15 @@ fn refresh_static_invoices_for_used_offers() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); - let release_held_htlc_om = - pass_async_payments_oms(updated_invoice.clone(), sender, server, recipient, recipient_id).1; + let release_held_htlc_om = pass_async_payments_oms( + updated_invoice.clone(), + sender, + server, + recipient, + recipient_id, + invoice_request_path, + ) + .1; sender .onion_messenger .handle_onion_message(recipient.node.get_our_node_id(), &release_held_htlc_om); @@ -2172,9 +2507,9 @@ fn invoice_server_is_not_channel_peer() { invoice_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(&nodes[2], &[&nodes[0], &nodes[1], &nodes[3]]); - let invoice = - pass_static_invoice_server_messages(invoice_server, recipient, recipient_id.clone()) - .invoice; + let flow_res = + pass_static_invoice_server_messages(invoice_server, recipient, recipient_id.clone()); + let invoice = flow_res.invoice; let offer = recipient.node.get_async_receive_offer().unwrap(); let amt_msat = 5000; @@ -2186,8 +2521,15 @@ fn invoice_server_is_not_channel_peer() { .unwrap(); // Do the held_htlc_available --> release_held_htlc dance. - let release_held_htlc_om = - pass_async_payments_oms(invoice.clone(), sender, invoice_server, recipient, recipient_id).1; + let release_held_htlc_om = pass_async_payments_oms( + invoice.clone(), + sender, + invoice_server, + recipient, + recipient_id, + flow_res.invoice_request_path, + ) + .1; sender .onion_messenger .handle_onion_message(recipient.node.get_our_node_id(), &release_held_htlc_om); @@ -2206,3 +2548,93 @@ fn invoice_server_is_not_channel_peer() { let res = claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); assert_eq!(res.0, Some(PaidBolt12Invoice::StaticInvoice(invoice))); } + +#[test] +fn invoice_request_forwarded_to_async_recipient() { + // Test that when an always-online node receives a static invoice request on behalf of an async + // recipient it forwards the invoice request to the async recipient and also sends back the + // static invoice to the payer. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, 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 sender = &nodes[0]; + let always_online_node = &nodes[1]; + let async_recipient = &nodes[2]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = always_online_node + .node + .blinded_paths_for_async_recipient(recipient_id.clone(), None) + .unwrap(); + async_recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]); + + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; + + let offer = async_recipient.node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let params = RouteParametersConfig::default(); + sender + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) + .unwrap(); + + let sender_node_id = sender.node.get_our_node_id(); + + // `invoice_request` message intended for the always-online node that receives requests on + // behalf of async recipient. + let invreq_om = sender + .onion_messenger + .next_onion_message_for_peer(always_online_node.node.get_our_node_id()) + .unwrap(); + + always_online_node.onion_messenger.handle_onion_message(sender_node_id, &invreq_om); + + let mut events = always_online_node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (reply_path, invoice_request) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { + recipient_id: ev_id, + invoice_slot: _, + reply_path, + invoice_request, + } => { + assert_eq!(recipient_id, ev_id); + (reply_path, invoice_request) + }, + _ => panic!(), + }; + + always_online_node + .node + .respond_to_static_invoice_request( + static_invoice, + reply_path, + invoice_request, + invoice_flow_res.invoice_request_path, + ) + .unwrap(); + + // Check that the next onion messages are the invoice request that will be forwarded to the async + // recipient and the static invoice to the payer. + let invreq_om = always_online_node + .onion_messenger + .next_onion_message_for_peer(async_recipient.node.get_our_node_id()) + .unwrap(); + + let static_invoice_om = + always_online_node.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + + let peeled_msg = async_recipient.onion_messenger.peel_onion_message(&invreq_om).unwrap(); + assert!(matches!(peeled_msg, PeeledOnion::Offers(OffersMessage::InvoiceRequest(_), _, _))); + + let peeled_msg = sender.onion_messenger.peel_onion_message(&static_invoice_om).unwrap(); + assert!(matches!(peeled_msg, PeeledOnion::Offers(OffersMessage::StaticInvoice(_), _, _))); +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index cfef0540a97..2b28f7104ad 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5292,10 +5292,19 @@ where self.flow.static_invoice_persisted(invoice_persisted_path); } - /// Forwards a [`StaticInvoice`] in response to an [`Event::StaticInvoiceRequested`]. - pub fn send_static_invoice( - &self, invoice: StaticInvoice, responder: Responder, + /// Forwards a [`StaticInvoice`] to a payer in response to an + /// [`Event::StaticInvoiceRequested`]. Also forwards the payer's [`InvoiceRequest`] to the + /// async recipient, in case the recipient is online to provide the payer with a fresh + /// [`Bolt12Invoice`]. + pub fn respond_to_static_invoice_request( + &self, invoice: StaticInvoice, responder: Responder, invoice_request: InvoiceRequest, + invoice_request_path: BlindedMessagePath, ) -> Result<(), Bolt12SemanticError> { + self.flow.enqueue_invoice_request_to_forward( + invoice_request, + invoice_request_path, + responder.clone(), + ); self.flow.enqueue_static_invoice(invoice, responder) } @@ -14455,9 +14464,9 @@ where let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) { Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request, - Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot }) => { + Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, invoice_request }) => { self.pending_events.lock().unwrap().push_back((Event::StaticInvoiceRequested { - recipient_id, invoice_slot, reply_path: responder + recipient_id, invoice_slot, reply_path: responder, invoice_request, }, None)); return None @@ -14637,6 +14646,7 @@ where pending_events.push_back(( Event::PersistStaticInvoice { invoice: message.invoice, + invoice_request_path: message.forward_invoice_request_path, invoice_slot, recipient_id, invoice_persisted_path: responder, diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 734482845ee..81197361625 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -396,7 +396,7 @@ pub enum InvreqResponseInstructions { SendInvoice(VerifiedInvoiceRequest), /// We are a static invoice server and should respond to this invoice request by retrieving the /// [`StaticInvoice`] corresponding to the `recipient_id` and `invoice_slot` and calling - /// `OffersMessageFlow::enqueue_static_invoice`. + /// [`OffersMessageFlow::enqueue_static_invoice`]. /// /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice SendStaticInvoice { @@ -406,6 +406,10 @@ pub enum InvreqResponseInstructions { recipient_id: Vec, /// The slot number for the specific invoice being requested by the payer. invoice_slot: u16, + /// The invoice request that should be forwarded to the async recipient in case the + /// recipient is online to respond. Should be forwarded by calling + /// [`OffersMessageFlow::enqueue_invoice_request_to_forward`]. + invoice_request: InvoiceRequest, }, } @@ -445,6 +449,7 @@ where return Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, + invoice_request, }); }, _ => return Err(()), @@ -1117,6 +1122,28 @@ where Ok(()) } + /// Forwards an [`InvoiceRequest`] to the specified [`BlindedMessagePath`]. If we receive an + /// invoice request as a static invoice server on behalf of an often-offline recipient this + /// can be used to forward the request to give the recipient a chance to provide an + /// invoice if the recipient is online. The reply_path [`Responder`] provided is the path to + /// the sender where the recipient can send the invoice. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath + /// [`Responder`]: crate::onion_message::messenger::Responder + pub fn enqueue_invoice_request_to_forward( + &self, invoice_request: InvoiceRequest, destination: BlindedMessagePath, + reply_path: Responder, + ) { + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); + let message = OffersMessage::InvoiceRequest(invoice_request); + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::BlindedPath(destination), + reply_path: reply_path.into_blinded_path(), + }; + pending_offers_messages.push((message, instructions)); + } + /// Enqueues `held_htlc_available` onion messages to be sent to the payee via the reply paths /// contained within the provided [`StaticInvoice`]. /// diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 27f32bcc1d2..d5d3c4d75a8 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -588,6 +588,15 @@ pub struct InvoiceRequest { signature: Signature, } +#[cfg(not(test))] +impl PartialEq for InvoiceRequest { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) && self.signature.eq(&other.signature) + } +} + +impl Eq for InvoiceRequest {} + /// An [`InvoiceRequest`] that has been verified by [`InvoiceRequest::verify_using_metadata`] or /// [`InvoiceRequest::verify_using_recipient_data`] and exposes different ways to respond depending /// on whether the signing keys were derived.