From 10295e191b492fc19696a3e3394b0d83efcb1cb2 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/src/ln/msgs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d3a9732b46426cbed48115366ad2c48d584f65a3 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 5edc82a4584085bc1809150c6dafe07f19c9f1da 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 | 27 +- lightning/src/ln/async_payments_tests.rs | 462 ++++++++++++++++++++--- lightning/src/ln/channelmanager.rs | 19 +- lightning/src/offers/flow.rs | 26 ++ lightning/src/offers/invoice_request.rs | 9 +- 5 files changed, 478 insertions(+), 65 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index d36143dce9f..fd08d3ca9db 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,15 @@ 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. As a static invoice + /// server, if we receive an invoice request on behalf of an async recipient, a static + /// invoice will be provided to the payer. However, we'll also forward the invoice + /// request to this path to the async recipient in case it is online so that the + /// recipient can provide a new invoice. This path should be persisted and later + /// provided to [`ChannelManager::send_response_static_invoice_request`]. + /// + /// [`ChannelManager::send_response_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::send_response_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 +1696,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::send_response_static_invoice_request`]. + /// The invoice request path previously persisted from [`Event::PersistStaticInvoice`] should + /// also be provided in [`ChannelManager::send_response_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::send_response_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::send_response_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 +1714,15 @@ 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::send_response_static_invoice_request`] along with the invoice. /// - /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + /// [`ChannelManager::send_response_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::send_response_static_invoice_request reply_path: Responder, + /// The invoice request that will be forwarded to the async recipient to give it a + /// chance to provide an invoice in case it is online. It should be provided to [`ChannelManager::send_response_static_invoice_request`]. + /// + /// [`ChannelManager::send_response_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::send_response_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..03329c6eac5 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) + .send_response_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 + .send_response_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 + .send_response_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 + .send_response_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,150 @@ 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 + .send_response_static_invoice_request( + static_invoice.clone(), + reply_path, + invoice_request, + invoice_flow_res.invoice_request_path, + ) + .unwrap(); + + // After calling `send_response_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 a 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))); + + // 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!(), + } +} + #[test] fn async_receive_flow_success() { // Test that an always-online sender can successfully pay an async receiver. @@ -659,6 +851,7 @@ fn async_receive_flow_success() { &nodes[1], &nodes[2], recipient_id, + invoice_flow_res.invoice_request_path, ) .1; nodes[0] @@ -704,8 +897,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 +918,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 + .send_response_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 +984,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 +1003,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 + .send_response_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 +1081,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 +1093,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 +1183,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 +1195,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 +1432,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 +1442,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 +1528,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 +1548,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 +1645,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 +1657,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 +2064,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 +2078,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 +2106,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 +2429,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 +2443,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 +2470,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 + .send_response_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..4dcb9ff5a08 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5292,10 +5292,18 @@ 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, + /// When handling an [`Event::StaticInvoiceRequested`], this should be called to forward the + /// [`InvoiceRequest`] over the `invoice_request_path` to the async recipient if it is online + /// and it will forward the [`StaticInvoice`] to the responder. + pub fn send_response_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 +14463,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 +14645,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..cbf072911a9 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -406,6 +406,9 @@ 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 it is + /// online to respond. + invoice_request: InvoiceRequest, }, } @@ -445,6 +448,7 @@ where return Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, + invoice_request, }); }, _ => return Err(()), @@ -1117,6 +1121,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 the recipient to give it a chance to provide an + /// invoice if it 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, invoice_request_path: 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(invoice_request_path), + 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..8956fc6cafa 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -581,13 +581,20 @@ impl AsRef for UnsignedInvoiceRequest { /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] pub struct InvoiceRequest { pub(super) bytes: Vec, pub(super) contents: InvoiceRequestContents, signature: Signature, } +impl PartialEq for InvoiceRequest { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) + } +} + +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.