diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 99dd4fa667c..795841d4f7e 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -35,6 +35,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use core::mem; use core::ops::Deref; +use core::time::Duration; /// A blinded path to be used for sending or receiving a message, hiding the identity of the /// recipient. @@ -342,6 +343,43 @@ pub enum OffersContext { /// [`Offer`]: crate::offers::offer::Offer nonce: Nonce, }, + /// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient. + /// + /// This variant is received by the static invoice server when handling an [`InvoiceRequest`] on + /// behalf of said async recipient. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + StaticInvoiceRequested { + /// An identifier for the async recipient for whom we as a static invoice server are serving + /// [`StaticInvoice`]s. Used paired with the + /// [`OffersContext::StaticInvoiceRequested::invoice_id`] when looking up a corresponding + /// [`StaticInvoice`] to return to the payer if the recipient is offline. This id was previously + /// provided via [`AsyncPaymentsContext::ServeStaticInvoice::recipient_id`]. + /// + /// Also useful for rate limiting the number of [`InvoiceRequest`]s we will respond to on + /// recipient's behalf. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + recipient_id: Vec, + + /// A random unique identifier for a specific [`StaticInvoice`] that the recipient previously + /// requested be served on their behalf. Useful when paired with the + /// [`OffersContext::StaticInvoiceRequested::recipient_id`] to pull that specific invoice from + /// the database when payers send an [`InvoiceRequest`]. This id was previously + /// provided via [`AsyncPaymentsContext::ServeStaticInvoice::invoice_id`]. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + invoice_id: u128, + + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: Duration, + }, /// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an /// [`InvoiceRequest`]. /// @@ -405,6 +443,24 @@ pub enum OffersContext { /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage #[derive(Clone, Debug)] pub enum AsyncPaymentsContext { + /// Context used by a [`BlindedMessagePath`] provided out-of-band to an async recipient, where the + /// context is provided back to the static invoice server in corresponding [`OfferPathsRequest`]s. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + OfferPathsRequest { + /// An identifier for the async recipient that is requesting blinded paths to include in their + /// [`Offer::paths`]. This ID will be surfaced when the async recipient eventually sends a + /// corresponding [`ServeStaticInvoice`] message, and can be used to rate limit the recipient. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + recipient_id: Vec, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: core::time::Duration, + }, /// Context used by a reply path to an [`OfferPathsRequest`], provided back to us as an async /// recipient in corresponding [`OfferPaths`] messages from the static invoice server. /// @@ -420,6 +476,38 @@ pub enum AsyncPaymentsContext { /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths path_absolute_expiry: core::time::Duration, }, + /// Context used by a reply path to an [`OfferPaths`] message, provided back to us as the static + /// invoice server in corresponding [`ServeStaticInvoice`] messages. + /// + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + ServeStaticInvoice { + /// An identifier for the async recipient that is requesting that a [`StaticInvoice`] be served + /// on their behalf. + /// + /// Useful for retrieving the invoice when payers send an [`InvoiceRequest`] to us as the static + /// invoice server. Also useful to rate limit the invoices being persisted on behalf of a + /// particular recipient. This id will be provided back to us as the static invoice server via + /// [`OffersContext::StaticInvoiceRequested::recipient_id`] + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + recipient_id: Vec, + /// A random unique identifier for the specific [`StaticInvoice`] that the recipient is + /// requesting be served on their behalf. Useful when surfaced alongside the above + /// `recipient_id` when payers send an [`InvoiceRequest`], to pull the specific static invoice + /// from the database. This id will be provided back to us as the static invoice server via + /// [`OffersContext::StaticInvoiceRequested::invoice_id`] + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + invoice_id: u128, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: core::time::Duration, + }, /// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in /// corresponding [`StaticInvoicePersisted`] messages. /// @@ -508,6 +596,11 @@ impl_writeable_tlv_based_enum!(OffersContext, (1, nonce, required), (2, hmac, required) }, + (3, StaticInvoiceRequested) => { + (0, recipient_id, required), + (2, invoice_id, required), + (4, path_absolute_expiry, required), + }, ); impl_writeable_tlv_based_enum!(AsyncPaymentsContext, @@ -528,6 +621,15 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (0, offer_id, required), (2, path_absolute_expiry, required), }, + (4, OfferPathsRequest) => { + (0, recipient_id, required), + (2, path_absolute_expiry, required), + }, + (5, ServeStaticInvoice) => { + (0, recipient_id, required), + (2, invoice_id, required), + (4, path_absolute_expiry, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index d01af737c32..6b9a52c2ccc 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1582,6 +1582,75 @@ pub enum Event { /// onion messages. peer_node_id: PublicKey, }, + /// As a static invoice server, we received a [`StaticInvoice`] from an async recipient that wants + /// us to serve the invoice to payers on their behalf when they are offline. This event will only + /// be generated if we previously created paths using + /// [`ChannelManager::blinded_paths_for_async_recipient`] and the recipient was configured with + /// them via [`ChannelManager::set_paths_to_static_invoice_server`]. + /// + /// [`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 + #[cfg(async_payments)] + PersistStaticInvoice { + /// The invoice that should be persisted and later provided to payers when handling a future + /// [`Event::StaticInvoiceRequested`]. + invoice: StaticInvoice, + /// Useful for the recipient to replace a specific invoice stored by us as the static invoice + /// server. + /// + /// When this invoice is persisted, this slot number should be included so if we receive another + /// [`Event::PersistStaticInvoice`] containing the same slot number we can swap the existing + /// invoice out for the new one. + invoice_slot: u16, + /// An identifier for the recipient, originally provided to + /// [`ChannelManager::blinded_paths_for_async_recipient`]. + /// + /// When an [`Event::StaticInvoiceRequested`] comes in for the invoice, this id will be surfaced + /// and can be used alongside the `invoice_id` to retrieve the invoice from the database. + recipient_id: Vec, + /// A unique identifier for the invoice. When an [`Event::StaticInvoiceRequested`] comes in for + /// the invoice, this id will be surfaced and can be used alongside the `recipient_id` to + /// retrieve the invoice from the database. + invoice_id: u128, + /// Once the [`StaticInvoice`], `invoice_slot` and `invoice_id` are persisted, + /// [`ChannelManager::static_invoice_persisted`] should be called with this responder to confirm + /// to the recipient that their [`Offer`] is ready to be used for async payments. + /// + /// [`ChannelManager::static_invoice_persisted`]: crate::ln::channelmanager::ChannelManager::static_invoice_persisted + /// [`Offer`]: crate::offers::offer::Offer + invoice_persisted_path: Responder, + }, + /// As a static invoice server, we received an [`InvoiceRequest`] on behalf of an often-offline + /// recipient for whom we are serving [`StaticInvoice`]s. + /// + /// This event will only be generated if we previously created paths using + /// [`ChannelManager::blinded_paths_for_async_recipient`] and the recipient was configured with + /// them via [`ChannelManager::set_paths_to_static_invoice_server`]. + /// + /// If we previously persisted a [`StaticInvoice`] from an [`Event::PersistStaticInvoice`] that + /// matches the below `recipient_id` and `invoice_id`, that invoice should be retrieved now + /// and forwarded to the payer via [`ChannelManager::send_static_invoice`]. + /// + /// [`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 + #[cfg(async_payments)] + StaticInvoiceRequested { + /// An identifier for the recipient previously surfaced in + /// [`Event::PersistStaticInvoice::recipient_id`]. Useful when paired with the `invoice_id` to + /// retrieve the [`StaticInvoice`] requested by the payer. + recipient_id: Vec, + /// A random unique identifier for the invoice being requested, previously surfaced in + /// [`Event::PersistStaticInvoice::invoice_id`]. Useful when paired with the `recipient_id` to + /// retrieve the [`StaticInvoice`] requested by the payer. + invoice_id: u128, + /// 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. + /// + /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + reply_path: Responder, + }, } impl Writeable for Event { @@ -2012,6 +2081,17 @@ impl Writeable for Event { (8, former_temporary_channel_id, required), }); }, + #[cfg(async_payments)] + &Event::PersistStaticInvoice { .. } => { + 45u8.write(writer)?; + // No need to write these events because we can just restart the static invoice negotiation + // on startup. + }, + #[cfg(async_payments)] + &Event::StaticInvoiceRequested { .. } => { + 47u8.write(writer)?; + // Never write StaticInvoiceRequested events as buffered onion messages aren't serialized. + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2583,6 +2663,12 @@ impl MaybeReadable for Event { former_temporary_channel_id: former_temporary_channel_id.0.unwrap(), })) }, + // Note that we do not write a length-prefixed TLV for PersistStaticInvoice events. + #[cfg(async_payments)] + 45u8 => Ok(None), + // Note that we do not write a length-prefixed TLV for StaticInvoiceRequested events. + #[cfg(async_payments)] + 47u8 => Ok(None), // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index a956f2ebae2..c9d799fa241 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -11,7 +11,9 @@ use crate::blinded_path::message::{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}; -use crate::events::{Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason}; +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::functional_test_utils::*; @@ -23,12 +25,22 @@ use crate::ln::offers_tests; use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::ln::outbound_payment::PendingOutboundPayment; use crate::ln::outbound_payment::Retry; +use crate::offers::async_receive_offer_cache::{ + TEST_MAX_CACHED_OFFERS_TARGET, TEST_MAX_UPDATE_ATTEMPTS, + TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS, +}; +use crate::offers::flow::{ + TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY, TEST_OFFERS_MESSAGE_REQUEST_LIMIT, + TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY, +}; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::offer::Offer; use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler}; -use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; +use crate::onion_message::messenger::{ + Destination, MessageRouter, MessageSendInstructions, PeeledOnion, +}; use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::prelude::*; @@ -37,6 +49,7 @@ use crate::sign::NodeSigner; use crate::sync::Mutex; use crate::types::features::Bolt12InvoiceFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use crate::util::ser::Writeable; use bitcoin::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1; @@ -45,6 +58,136 @@ use bitcoin::secp256k1::Secp256k1; use core::convert::Infallible; use core::time::Duration; +struct StaticInvoiceServerFlowResult { + invoice: StaticInvoice, + + // Returning messages that were sent along the way allows us to test handling duplicate messages. + offer_paths_request: msgs::OnionMessage, + static_invoice_persisted_message: msgs::OnionMessage, +} + +// Go through the flow of interactively building a `StaticInvoice`, returning the +// AsyncPaymentsMessage::ServeStaticInvoice that has yet to be provided to the server node. +// Assumes that the sender and recipient are only peers with each other. +// +// Returns (offer_paths_req, serve_static_invoice) +fn invoice_flow_up_to_send_serve_static_invoice( + server: &Node, recipient: &Node, +) -> (msgs::OnionMessage, msgs::OnionMessage) { + // First provide an OfferPathsRequest from the recipient to the server. + recipient.node.timer_tick_occurred(); + let offer_paths_req = loop { + let msg = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + // Ignore any messages that are updating the static invoice stored with the server here + if matches!( + server.onion_messenger.peel_onion_message(&msg).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + ) { + break msg; + } + }; + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + + // Check that the right number of requests were queued and that they were only queued for the + // server node. + let mut pending_oms = recipient.onion_messenger.release_pending_msgs(); + let mut offer_paths_req_msgs = pending_oms.remove(&server.node.get_our_node_id()).unwrap(); + assert!(offer_paths_req_msgs.len() <= TEST_OFFERS_MESSAGE_REQUEST_LIMIT); + for (_, msgs) in pending_oms { + assert!(msgs.is_empty()); + } + + // The server responds with OfferPaths. + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + + // Only one OfferPaths response should be queued. + let mut pending_oms = server.onion_messenger.release_pending_msgs(); + for (_, msgs) in pending_oms { + assert!(msgs.is_empty()); + } + + // After receiving the offer paths, the recipient constructs the static invoice and sends + // ServeStaticInvoice to the server. + let serve_static_invoice_om = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + (offer_paths_req, serve_static_invoice_om) +} + +// Go through the flow of interactively building a `StaticInvoice` and storing it with the static +// invoice server, returning the invoice and messages that were exchanged along the way at the end. +fn pass_static_invoice_server_messages( + server: &Node, recipient: &Node, recipient_id: Vec, +) -> StaticInvoiceServerFlowResult { + // Force the server and recipient to send OMs directly to each other for testing simplicity. + 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 num_cached_offers_before_flow = recipient.node.flow.test_get_async_receive_offers().len(); + + let (offer_paths_req, serve_static_invoice_om) = + invoice_flow_up_to_send_serve_static_invoice(server, recipient); + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice_om); + + // Upon handling the ServeStaticInvoice message, the server's node surfaces an event indicating + // that the static invoice should be persisted. + let mut events = server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (static_invoice, ack_path) = match events.pop().unwrap() { + Event::PersistStaticInvoice { + invoice, + invoice_persisted_path, + recipient_id: ev_id, + invoice_slot: _, // TODO: do something with this? + invoice_id: _, // TODO: do something with this? + } => { + assert_eq!(recipient_id, ev_id); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + assert_eq!( + recipient.node.flow.test_get_async_receive_offers().len(), + num_cached_offers_before_flow + ); + + // Once the static invoice is persisted, the server needs to call `static_invoice_persisted` with + // the reply path to the ServeStaticInvoice message, to tell the recipient that their offer is + // ready to be used for async payments. + server.node.static_invoice_persisted(ack_path); + let invoice_persisted_om = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); + assert_eq!( + recipient.node.flow.test_get_async_receive_offers().len(), + num_cached_offers_before_flow + 1 + ); + + // Remove the peer restriction added above. + server.message_router.peers_override.lock().unwrap().clear(); + recipient.message_router.peers_override.lock().unwrap().clear(); + + StaticInvoiceServerFlowResult { + offer_paths_request: offer_paths_req, + static_invoice_persisted_message: invoice_persisted_om, + invoice: static_invoice, + } +} + // Goes through the async receive onion message flow, returning the final release_held_htlc OM. // // Assumes the held_htlc_available message will be sent: @@ -53,28 +196,30 @@ use core::time::Duration; // 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: &Node, recipient_id: Vec, ) -> (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(); - // Don't forward the invreq since we don't support retrieving the static invoice from the - // recipient's LSP yet, instead manually construct the response. let invreq_om = sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); - let invreq_reply_path = - offers_tests::extract_invoice_request(always_online_recipient_counterparty, &invreq_om).1; - always_online_recipient_counterparty .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path), - }, - ) + .handle_onion_message(sender_node_id, &invreq_om); + + 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_id: _, reply_path } => { + assert_eq!(recipient_id, ev_id); + reply_path + }, + _ => panic!(), + }; + + always_online_recipient_counterparty + .node + .send_static_invoice(static_invoice, reply_path) .unwrap(); let static_invoice_om = always_online_recipient_counterparty .onion_messenger @@ -95,10 +240,9 @@ fn pass_async_payments_oms( .onion_messenger .handle_onion_message(always_online_node_id, &held_htlc_available_om_1_2); - ( - held_htlc_available_om_1_2, - recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(), - ) + let release_held_htlc = + recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + (held_htlc_available_om_1_2, release_held_htlc) } fn create_static_invoice( @@ -128,6 +272,25 @@ fn create_static_invoice( (offer, static_invoice) } +fn extract_payment_hash(event: &MessageSendEvent) -> PaymentHash { + match event { + MessageSendEvent::UpdateHTLCs { ref updates, .. } => { + updates.update_add_htlcs[0].payment_hash + }, + _ => panic!(), + } +} + +fn extract_payment_preimage(event: &Event) -> PaymentPreimage { + match event { + Event::PaymentClaimable { + purpose: PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. }, + .. + } => payment_preimage.unwrap(), + _ => panic!(), + } +} + #[test] fn invalid_keysend_payment_secret() { let chanmon_cfgs = create_chanmon_cfgs(3); @@ -215,6 +378,7 @@ fn static_invoice_unknown_required_features() { 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); + // Manually construct a static invoice so we can set unknown required features. let blinded_paths_to_always_online_node = nodes[1] .message_router .create_blinded_paths( @@ -237,6 +401,8 @@ fn static_invoice_unknown_required_features() { .build_and_sign(&secp_ctx) .unwrap(); + // Initiate payment to the offer corresponding to the manually-constructed invoice that has + // unknown required features. let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -245,8 +411,8 @@ fn static_invoice_unknown_required_features() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); - // Don't forward the invreq since we don't support retrieving the static invoice from the - // recipient's LSP yet, instead manually construct the response. + // Don't forward the invreq since the invoice was created outside of the normal flow, instead + // manually construct the response. let invreq_om = nodes[0] .onion_messenger .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) @@ -264,6 +430,8 @@ fn static_invoice_unknown_required_features() { ) .unwrap(); + // Check that paying the static invoice fails as expected with + // `PaymentFailureReason::UnknownRequiredFeatures`. let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -287,7 +455,6 @@ fn static_invoice_unknown_required_features() { fn ignore_unexpected_static_invoice() { // Test that we'll ignore unexpected static invoices, invoices that don't match our invoice // request, and duplicate invoices. - let secp_ctx = Secp256k1::new(); 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]); @@ -295,9 +462,21 @@ fn ignore_unexpected_static_invoice() { 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 recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + // Initiate payment to the sender's intended offer. - let (offer, valid_static_invoice) = - create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + let valid_static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + + // 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 amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -306,30 +485,24 @@ fn ignore_unexpected_static_invoice() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); - // Don't forward the invreq since we don't support retrieving the static invoice from the - // recipient's LSP yet, instead manually construct the responses below. let invreq_om = nodes[0] .onion_messenger .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) .unwrap(); - let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); - // 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 = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx).1; + 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_id: _, reply_path } => { + assert_eq!(recipient_id, ev_id); + reply_path + }, + _ => panic!(), + }; - // Check that we'll ignore the unexpected static invoice. - nodes[1] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - unexpected_static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path.clone()), - }, - ) - .unwrap(); + // Check that the sender will ignore the unexpected static invoice. + nodes[1].node.send_static_invoice(unexpected_static_invoice, reply_path.clone()).unwrap(); let unexpected_static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -343,17 +516,7 @@ 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] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - valid_static_invoice.clone(), - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path.clone()), - }, - ) - .unwrap(); + nodes[1].node.send_static_invoice(valid_static_invoice.clone(), reply_path.clone()).unwrap(); let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -368,17 +531,7 @@ fn ignore_unexpected_static_invoice() { .all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_)))); // Receiving a duplicate invoice will have no effect. - nodes[1] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - valid_static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path), - }, - ) - .unwrap(); + nodes[1].node.send_static_invoice(valid_static_invoice, reply_path).unwrap(); let dup_static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -393,29 +546,29 @@ fn ignore_unexpected_static_invoice() { #[test] fn async_receive_flow_success() { // Test that an always-online sender can successfully pay an async receiver. - let secp_ctx = Secp256k1::new(); + 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); - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); - let relative_expiry = Duration::from_secs(1000); - let (offer, static_invoice) = - create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; assert!(static_invoice.invoice_features().supports_basic_mpp()); - assert_eq!(static_invoice.relative_expiry(), relative_expiry); - + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -423,8 +576,14 @@ fn async_receive_flow_success() { .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.clone(), &nodes[0], &nodes[1], &nodes[2]).1; + let release_held_htlc_om = pass_async_payments_oms( + static_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + ) + .1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); @@ -433,6 +592,7 @@ fn async_receive_flow_success() { let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); check_added_monitors!(nodes[0], 1); // Receiving a duplicate release_htlc message doesn't result in duplicate payment. @@ -442,12 +602,11 @@ fn async_receive_flow_success() { assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); - do_pass_along_path(args); + let args = PassAlongPathArgs::new(&nodes[0], 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(&nodes[0], route, keysend_preimage)); - assert!(res.is_some()); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } @@ -455,7 +614,6 @@ fn async_receive_flow_success() { #[test] fn expired_static_invoice_fail() { // Test that if we receive an expired static invoice we'll fail the payment. - let secp_ctx = Secp256k1::new(); 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]); @@ -463,10 +621,14 @@ fn expired_static_invoice_fail() { 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); - const INVOICE_EXPIRY_SECS: u32 = 10; - let relative_expiry = Duration::from_secs(INVOICE_EXPIRY_SECS as u64); - let (offer, static_invoice) = - create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -480,20 +642,16 @@ fn expired_static_invoice_fail() { .onion_messenger .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) .unwrap(); - let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; - // TODO: update to not manually send here when we add support for being the recipient's - // always-online counterparty - nodes[1] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path), - }, - ) - .unwrap(); + nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); + + 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, + _ => panic!(), + }; + + nodes[1].node.send_static_invoice(static_invoice.clone(), reply_path).unwrap(); let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -502,7 +660,7 @@ fn expired_static_invoice_fail() { // Wait until the static invoice expires before providing it to the sender. let block = create_dummy_block( nodes[0].best_block_hash(), - nodes[0].node.duration_since_epoch().as_secs() as u32 + INVOICE_EXPIRY_SECS + 1, + (static_invoice.created_at() + static_invoice.relative_expiry()).as_secs() as u32 + 1u32, Vec::new(), ); connect_block(&nodes[0], &block); @@ -519,17 +677,18 @@ fn expired_static_invoice_fail() { }, _ => panic!(), } - // The sender doesn't reply with InvoiceError right now because the always-online node doesn't - // currently provide them with a reply path to do so. + // TODO: the sender doesn't reply with InvoiceError right now because the always-online node + // doesn't currently provide them with a reply path to do so. } #[test] fn async_receive_mpp() { - let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &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( 4, &node_cfgs, @@ -554,11 +713,15 @@ fn async_receive_mpp() { connect_blocks(&nodes[2], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[2].best_block_info().1); connect_blocks(&nodes[3], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[3].best_block_info().1); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[3].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()).invoice; + let offer = nodes[3].node.get_async_receive_offer().unwrap(); - // In other tests we hardcode the sender's random bytes so we can predict the keysend preimage to - // check later in the test, but that doesn't work for MPP because it causes the session_privs for - // the different MPP parts to not be unique. let amt_msat = 15_000_000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -567,7 +730,7 @@ fn async_receive_mpp() { .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]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3], recipient_id).1; nodes[0] .onion_messenger .handle_onion_message(nodes[3].node.get_our_node_id(), &release_held_htlc_om_3_0); @@ -593,8 +756,8 @@ fn async_receive_mpp() { let args = PassAlongPathArgs::new(&nodes[0], expected_route[1], amt_msat, payment_hash, ev); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = match claimable_ev { - crate::events::Event::PaymentClaimable { - purpose: crate::events::PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. }, + Event::PaymentClaimable { + purpose: PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. }, .. } => payment_preimage.unwrap(), _ => panic!(), @@ -613,6 +776,7 @@ fn amount_doesnt_match_invreq() { let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; // Make one blinded path's fees slightly higher so they are tried in a deterministic order. @@ -642,13 +806,14 @@ fn amount_doesnt_match_invreq() { connect_blocks(&nodes[2], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[2].best_block_info().1); connect_blocks(&nodes[3], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[3].best_block_info().1); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[3].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()).invoice; + let offer = nodes[3].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -658,7 +823,7 @@ fn amount_doesnt_match_invreq() { .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]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3], recipient_id).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. @@ -696,10 +861,10 @@ fn amount_doesnt_match_invreq() { let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); assert!(matches!( ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + let payment_hash = extract_payment_hash(&ev); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); do_pass_along_path(args); @@ -725,9 +890,9 @@ fn amount_doesnt_match_invreq() { ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[2], &nodes[3]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); - do_pass_along_path(args); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = extract_payment_preimage(&claimable_ev); claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); } @@ -830,15 +995,25 @@ fn invalid_async_receive_with_retry( let secp_ctx = Secp256k1::new(); 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); + 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 recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Set the random bytes so we can predict the offer nonce. + let hardcoded_random_bytes = [42; 32]; + *nodes[2].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + // Ensure all nodes start at the same height. connect_blocks(&nodes[0], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[0].best_block_info().1); connect_blocks(&nodes[1], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[1].best_block_info().1); @@ -875,18 +1050,9 @@ fn invalid_async_receive_with_retry( } nodes[2].router.expect_blinded_payment_paths(static_invoice_paths); - let static_invoice = nodes[2] - .node - .create_static_invoice_builder(&offer, offer_nonce, None) - .unwrap() - .build_and_sign(&secp_ctx) - .unwrap(); - - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let params = RouteParametersConfig::default(); nodes[0] @@ -894,7 +1060,7 @@ fn invalid_async_receive_with_retry( .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]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2], recipient_id).1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om_2_0); @@ -906,10 +1072,10 @@ fn invalid_async_receive_with_retry( let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); assert!(matches!( ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + let payment_hash = extract_payment_hash(&ev); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); do_pass_along_path(args); // Fail the HTLC backwards to enable us to more easily modify the now-Retryable outbound to test @@ -935,7 +1101,6 @@ fn invalid_async_receive_with_retry( check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); do_pass_along_path(args); @@ -949,18 +1114,17 @@ fn invalid_async_receive_with_retry( let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); - do_pass_along_path(args); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = extract_payment_preimage(&claimable_ev); claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); } -#[cfg(not(feature = "std"))] +#[cfg_attr(feature = "std", ignore)] #[test] fn expired_static_invoice_message_path() { // Test that if we receive a held_htlc_available message over an expired blinded path, we'll // ignore it. - let secp_ctx = Secp256k1::new(); 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]); @@ -968,13 +1132,14 @@ fn expired_static_invoice_message_path() { 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); - const INVOICE_EXPIRY_SECS: u32 = 10; - let (offer, static_invoice) = create_static_invoice( - &nodes[1], - &nodes[2], - Some(Duration::from_secs(INVOICE_EXPIRY_SECS as u64)), - &secp_ctx, - ); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -985,13 +1150,18 @@ fn expired_static_invoice_message_path() { .unwrap(); // While the invoice is unexpired, respond with release_held_htlc. - let (held_htlc_available_om, _release_held_htlc_om) = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]); + let (held_htlc_available_om, _release_held_htlc_om) = pass_async_payments_oms( + static_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + ); // After the invoice is expired, ignore inbound held_htlc_available messages over the path. let path_absolute_expiry = crate::ln::inbound_payment::calculate_absolute_expiry( nodes[2].node.duration_since_epoch().as_secs(), - INVOICE_EXPIRY_SECS, + static_invoice.relative_expiry().as_secs() as u32, ); let block = create_dummy_block( nodes[2].best_block_hash(), @@ -1016,14 +1186,21 @@ fn expired_static_invoice_payment_path() { let secp_ctx = Secp256k1::new(); 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 recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + // Make sure all nodes are at the same block height in preparation for CLTV timeout things. let node_max_height = nodes.iter().map(|node| node.blocks.lock().unwrap().len()).max().unwrap() as u32; @@ -1031,12 +1208,6 @@ fn expired_static_invoice_payment_path() { connect_blocks(&nodes[1], node_max_height - nodes[1].best_block_info().1); connect_blocks(&nodes[2], node_max_height - nodes[2].best_block_info().1); - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); - // Hardcode the blinded payment path returned by the router so we can expire it via mining blocks. let (_, static_invoice_expired_paths) = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); @@ -1080,7 +1251,10 @@ fn expired_static_invoice_payment_path() { ); connect_blocks(&nodes[2], final_max_cltv_expiry - nodes[2].best_block_info().1); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -1089,7 +1263,7 @@ fn expired_static_invoice_payment_path() { .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]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2], recipient_id).1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); @@ -1097,11 +1271,11 @@ fn expired_static_invoice_payment_path() { let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); do_pass_along_path(args); @@ -1112,3 +1286,632 @@ fn expired_static_invoice_payment_path() { 1, ); } + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_offer_paths_request() { + // Ignore an incoming `OfferPathsRequest` if it is sent over a blinded path that is expired. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + const OFFER_PATHS_REQ_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60); + let recipient_id = vec![42; 32]; + let inv_server_paths = server + .node + .blinded_paths_for_async_recipient(recipient_id, Some(OFFER_PATHS_REQ_RELATIVE_EXPIRY)) + .unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Retrieve the offer paths request, and check that before the path that the recipient was + // configured with expires the server will respond to it, and after the config path expires they + // won't. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&offer_paths_req).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + )); + recipient.onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + + // Prior to the config path expiry the server will respond with offer_paths: + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + recipient.onion_messenger.peel_onion_message(&offer_paths).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPaths(_), _, _) + )); + server.onion_messenger.release_pending_msgs(); // Ignore redundant offer_paths + + // After the config path expiry the offer paths request will be ignored: + let configured_path_absolute_expiry = + (server.node.duration_since_epoch() + OFFER_PATHS_REQ_RELATIVE_EXPIRY).as_secs() as u32; + let block = create_dummy_block( + server.best_block_hash(), + configured_path_absolute_expiry + 1u32, + Vec::new(), + ); + connect_block(&server, &block); + connect_block(&recipient, &block); + + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + assert!(server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .is_none()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_offer_paths_message() { + // If the recipient receives an offer_paths message over an expired reply path, it should be ignored. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // First retrieve the offer_paths_request and corresponding offer_paths response from the server. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + recipient.onion_messenger.peel_onion_message(&offer_paths).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPaths(_), _, _) + )); + + // Prior to expiry of the offer_paths_request reply path, the recipient will respond to + // offer_paths with serve_static_invoice. + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + let serve_static_invoice = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&serve_static_invoice).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) + )); + + // Manually advance time for the recipient so they will perceive the offer_paths message as being + // sent over an expired reply path, and not respond with serve_static_invoice. + let offer_paths_request_reply_path_exp = + (recipient.node.duration_since_epoch() + TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY).as_secs(); + let block = create_dummy_block( + recipient.best_block_hash(), + offer_paths_request_reply_path_exp as u32 + 1u32, + Vec::new(), + ); + connect_block(&recipient, &block); + + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_serve_static_invoice_message() { + // If the server receives a serve_static_invoice message over an expired reply path, it should be + // ignored. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // First retrieve the serve_static_invoice message. + recipient.node.timer_tick_occurred(); + let serve_static_invoice = invoice_flow_up_to_send_serve_static_invoice(server, recipient).1; + + // Manually advance time for the server so they will perceive the serve_static_invoice message as being + // sent over an expired reply path, and not respond with serve_static_invoice. + let block = create_dummy_block( + server.best_block_hash(), + (server.node.duration_since_epoch() + TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY).as_secs() + as u32 + 1u32, + Vec::new(), + ); + connect_block(&server, &block); + + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice); + assert!(server.node.get_and_clear_pending_events().is_empty()); + assert!(server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .is_none()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_static_invoice_persisted_message() { + // If the recipient receives a static_invoice_persisted message over an expired reply path, it + // should be ignored. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Exchange messages until we can extract the final static_invoice_persisted OM. + recipient.node.timer_tick_occurred(); + let serve_static_invoice = invoice_flow_up_to_send_serve_static_invoice(server, recipient).1; + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice); + let mut events = server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let ack_path = match events.pop().unwrap() { + Event::PersistStaticInvoice { invoice_persisted_path, .. } => invoice_persisted_path, + _ => panic!(), + }; + + server.node.static_invoice_persisted(ack_path); + let invoice_persisted = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + recipient.onion_messenger.peel_onion_message(&invoice_persisted).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::StaticInvoicePersisted(_), _, _) + )); + + let block = create_dummy_block( + recipient.best_block_hash(), + (recipient.node.duration_since_epoch() + TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY).as_secs() + as u32 + 1u32, + Vec::new(), + ); + connect_block(&server, &block); + connect_block(&recipient, &block); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted); + assert!(recipient.node.get_async_receive_offer().is_err()); +} + +#[test] +fn limit_offer_paths_requests() { + // Limit the number of offer_paths_requests sent to the server if they aren't responding. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Up to TEST_MAX_UPDATE_ATTEMPTS offer_paths_requests are allowed to be sent out before the async + // recipient should give up. + for _ in 0..TEST_MAX_UPDATE_ATTEMPTS { + recipient.node.test_check_refresh_async_receive_offers(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&offer_paths_req).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + )); + recipient.onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + } + + // After the recipient runs out of attempts to request offer paths, they will give up until the + // next timer tick. + recipient.node.test_check_refresh_async_receive_offers(); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // On the next timer tick, more offer paths requests should be allowed to go through. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&offer_paths_req).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + )); +} + +#[test] +fn limit_serve_static_invoice_requests() { + // If we have enough async receive offers cached already, the recipient should stop sending out + // offer_paths_requests. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let persister; + let chain_monitor; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let payee_node_deserialized; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_id = + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.channel_id; + let server = &nodes[0]; + let recipient = &nodes[1]; + + 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(); + + // Build the target number of offers interactively with the static invoice server. + let mut offer_paths_req = None; + for _ in 0..TEST_MAX_CACHED_OFFERS_TARGET { + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + offer_paths_req = Some(flow_res.offer_paths_request); + + // Trigger a cache refresh + recipient.node.timer_tick_occurred(); + } + assert_eq!( + recipient.node.flow.test_get_async_receive_offers().len(), + TEST_MAX_CACHED_OFFERS_TARGET + ); + + // Force allowing more offer paths request attempts so we can check that the recipient will not + // attempt to build any further offers. + recipient.node.timer_tick_occurred(); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // If the recipient now receives new offer_paths, they should not attempt to build new offers as + // they already have enough. + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req.unwrap()); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // Check that round trip serialization of the ChannelManager will result in identical stored + // offers. + let cached_offers_pre_ser = recipient.node.flow.test_get_async_receive_offers(); + let config = test_default_channel_config(); + let serialized_monitor = get_monitor!(recipient, chan_id).encode(); + reload_node!( + nodes[1], + config, + recipient.node.encode(), + &[&serialized_monitor], + persister, + chain_monitor, + payee_node_deserialized + ); + let recipient = &nodes[1]; + let cached_offers_post_ser = recipient.node.flow.test_get_async_receive_offers(); + assert_eq!(cached_offers_pre_ser, cached_offers_post_ser); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn refresh_static_invoices() { + // Check that an invoice for a particular offer stored with the server will be updated once per + // timer tick. + 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(); + + // Set up the recipient to have one offer and an invoice with the static invoice server. + let original_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + // Mark the offer as used so we'll update the invoice on timer tick. + let _offer = recipient.node.get_async_receive_offer().unwrap(); + + // Force the server and recipient to send OMs directly to each other for testing simplicity. + 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()); + + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // Check that we'll refresh the invoice on the next timer tick. + recipient.node.timer_tick_occurred(); + let serve_static_invoice_om = loop { + let msg = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + match server.onion_messenger.peel_onion_message(&msg).unwrap() { + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) => { + break msg; + }, + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) => {}, + _ => panic!("Unexpected message"), + } + }; + 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); + let (updated_invoice, ack_path) = match events.pop().unwrap() { + Event::PersistStaticInvoice { + invoice, + invoice_persisted_path, + recipient_id: ev_id, + .. + } => { + assert_eq!(recipient_id, ev_id); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + assert_ne!(original_invoice, updated_invoice); + server.node.static_invoice_persisted(ack_path); + let invoice_persisted_om = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); + assert_eq!(recipient.node.flow.test_get_async_receive_offers().len(), 1); + + // Remove the peer restriction added above. + server.message_router.peers_override.lock().unwrap().clear(); + recipient.message_router.peers_override.lock().unwrap().clear(); + + // Complete a payment to the new invoice. + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let params = RouteParametersConfig::default(); + nodes[0] + .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( + updated_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + ) + .1; + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors!(nodes[0], 1); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], 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(&nodes[0], route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(updated_invoice))); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_static_invoice() { + // If a server receives an expired static invoice to persist, they should ignore it and not + // generate an event. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let (_, serve_static_invoice_om) = + invoice_flow_up_to_send_serve_static_invoice(server, recipient); + + // Advance time for the server so that by the time it receives the serve_static_invoice message, + // the invoice within has expired. + let block = create_dummy_block( + server.best_block_hash(), + server.node.duration_since_epoch().as_secs() as u32 + + TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY.as_secs() as u32 + + 1, + Vec::new(), + ); + connect_block(server, &block); + + 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!(events.is_empty()); +} + +#[test] +fn ignore_offer_paths_expiry_too_soon() { + // Recipents should ignore received offer_paths that expire too soon. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Get a legit offer_paths message from the server. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.release_pending_msgs(); + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + + // Get the blinded path use when manually sending the modified offer_paths message to the + // recipient. + let offer_paths_req_reply_path = + match server.onion_messenger.peel_onion_message(&offer_paths_req) { + Ok(PeeledOnion::AsyncPayments( + AsyncPaymentsMessage::OfferPathsRequest(_), + _, + reply_path, + )) => reply_path.unwrap(), + _ => panic!(), + }; + + // Modify the offer_paths message from the server to indicate that the offer paths expire too + // soon. + let (mut offer_paths_unwrapped, ctx) = match recipient + .onion_messenger + .peel_onion_message(&offer_paths) + { + Ok(PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPaths(msg), ctx, _)) => (msg, ctx), + _ => panic!(), + }; + let too_soon_expiry_secs = recipient + .node + .duration_since_epoch() + .as_secs() + .saturating_add(TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS - 1); + offer_paths_unwrapped.paths_absolute_expiry = Some(too_soon_expiry_secs); + + // Deliver the expired paths to the recipient and make sure they don't construct a + // serve_static_invoice message in response. + server + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::AsyncPayments( + AsyncPaymentsMessage::OfferPaths(offer_paths_unwrapped), + ), + MessageSendInstructions::WithReplyPath { + destination: Destination::BlindedPath(offer_paths_req_reply_path), + // This context isn't used because the recipient doesn't reply to the message + context: MessageContext::AsyncPayments(ctx), + }, + ) + .unwrap(); + let offer_paths_expiry_too_soon = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &offer_paths_expiry_too_soon); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); +} + +#[test] +fn ignore_duplicate_offer() { + // Test that if an async receiver gets notified that the invoice for an offer was persisted twice, + // they won't cache the offer twice. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None, None]); + + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[0].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[1].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[0], &nodes[1], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; + assert!(static_invoice.invoice_features().supports_basic_mpp()); + assert!(nodes[1].node.get_async_receive_offer().is_ok()); + + // Check that the recipient will ignore duplicate offers received. + nodes[1].onion_messenger.handle_onion_message( + nodes[1].node.get_our_node_id(), + &invoice_flow_res.static_invoice_persisted_message, + ); + assert_eq!(nodes[1].node.flow.test_get_async_receive_offers().len(), 1); +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8a904a90e64..e5a58109f5c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -87,7 +87,7 @@ use crate::ln::outbound_payment::{ }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; -use crate::offers::flow::OffersMessageFlow; +use crate::offers::flow::{InvreqResponseInstructions, OffersMessageFlow}; use crate::offers::invoice::{ Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, }; @@ -5272,6 +5272,26 @@ where } } + #[cfg(all(test, async_payments))] + pub(crate) fn test_check_refresh_async_receive_offers(&self) { + self.check_refresh_async_receive_offer_cache(false); + } + + /// Should be called after handling an [`Event::PersistStaticInvoice`], where the `Responder` + /// comes from [`Event::PersistStaticInvoice::invoice_persisted_path`]. + #[cfg(async_payments)] + pub fn static_invoice_persisted(&self, invoice_persisted_path: Responder) { + self.flow.static_invoice_persisted(invoice_persisted_path); + } + + /// Forwards a [`StaticInvoice`] in response to an [`Event::StaticInvoiceRequested`]. + #[cfg(async_payments)] + pub fn send_static_invoice( + &self, invoice: StaticInvoice, responder: Responder, + ) -> Result<(), Bolt12SemanticError> { + self.flow.enqueue_static_invoice(invoice, responder) + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId, @@ -11499,6 +11519,34 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, expanded_key) } + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// ## Usage + /// 1. Static invoice server calls [`Self::blinded_paths_for_async_recipient`] + /// 2. Static invoice server communicates the resulting paths out-of-band to the async recipient, + /// who calls [`Self::set_paths_to_static_invoice_server`] to configure themselves with these + /// paths + /// 3. Async recipient automatically sends [`OfferPathsRequest`]s over the configured paths, and + /// uses the resulting paths from the server's [`OfferPaths`] response to build their async + /// receive offer + /// + /// If `relative_expiry` is unset, the [`BlindedMessagePath`]s will never expire. + /// + /// Returns the paths that the recipient should be configured with via + /// [`Self::set_paths_to_static_invoice_server`]. + /// + /// The provided `recipient_id` must uniquely identify the recipient, and will be surfaced later + /// when the recipient provides us with a static invoice to persist and serve to payers on their + /// behalf. + #[cfg(async_payments)] + pub fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, relative_expiry: Option, + ) -> Result, ()> { + let peers = self.get_peers_for_blinded_path(); + self.flow.blinded_paths_for_async_recipient(recipient_id, relative_expiry, peers) + } + #[cfg(any(test, async_payments))] pub(super) fn duration_since_epoch(&self) -> Duration { #[cfg(not(feature = "std"))] @@ -13320,7 +13368,17 @@ where }; let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) { - Ok(invoice_request) => invoice_request, + Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request, + Ok(InvreqResponseInstructions::SendStaticInvoice { + recipient_id: _recipient_id, invoice_id: _invoice_id + }) => { + #[cfg(async_payments)] + self.pending_events.lock().unwrap().push_back((Event::StaticInvoiceRequested { + recipient_id: _recipient_id, invoice_id: _invoice_id, reply_path: responder + }, None)); + + return None + }, Err(_) => return None, }; @@ -13454,6 +13512,19 @@ where &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, _responder: Option, ) -> Option<(OfferPaths, ResponseInstruction)> { + #[cfg(async_payments)] + { + let peers = self.get_peers_for_blinded_path(); + let entropy = &*self.entropy_source; + let (message, reply_path_context) = + match self.flow.handle_offer_paths_request(_context, peers, entropy) { + Some(msg) => msg, + None => return None, + }; + _responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context))) + } + + #[cfg(not(async_payments))] None } @@ -13494,6 +13565,31 @@ where &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, _responder: Option, ) { + #[cfg(async_payments)] + { + let responder = match _responder { + Some(resp) => resp, + None => return, + }; + + let (recipient_id, invoice_id) = + match self.flow.verify_serve_static_invoice_message(&_message, _context) { + Ok(nonce) => nonce, + Err(()) => return, + }; + + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + Event::PersistStaticInvoice { + invoice: _message.invoice, + invoice_slot: _message.invoice_slot, + recipient_id, + invoice_id, + invoice_persisted_path: responder, + }, + None, + )); + } } fn handle_static_invoice_persisted( diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 245479e1df8..4761420d9dc 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1331,6 +1331,7 @@ macro_rules! reload_node { _reload_node(&$node, $new_config, &chanman_encoded, $monitors_encoded); $node.node = &$new_channelmanager; $node.onion_messenger.set_offers_handler(&$new_channelmanager); + $node.onion_messenger.set_async_payments_handler(&$new_channelmanager); }; ($node: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => { reload_node!( diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index dd10fb099c2..f0d7a7ff4fe 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -194,6 +194,14 @@ const OFFER_REFRESH_THRESHOLD: Duration = Duration::from_secs(2 * 60 * 60); #[cfg(async_payments)] const MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = 3 * 30 * 24 * 60 * 60; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MAX_CACHED_OFFERS_TARGET: usize = MAX_CACHED_OFFERS_TARGET; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MAX_UPDATE_ATTEMPTS: u8 = MAX_UPDATE_ATTEMPTS; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = + MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS; + #[cfg(async_payments)] impl AsyncReceiveOfferCache { /// Retrieve a cached [`Offer`] for receiving async payments as an often-offline recipient, as @@ -461,6 +469,21 @@ impl AsyncReceiveOfferCache { false } + + #[cfg(test)] + pub(super) fn test_get_payable_offers(&self) -> Vec { + self.offers_with_idx() + .filter_map(|(_, offer)| { + if matches!(offer.status, OfferStatus::Ready { .. }) + || matches!(offer.status, OfferStatus::Used) + { + Some(offer.offer.clone()) + } else { + None + } + }) + .collect() + } } impl Writeable for AsyncReceiveOfferCache { diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 0990251c311..fbf3779dac5 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -68,6 +68,7 @@ use { crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}, crate::onion_message::async_payments::{ HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, + StaticInvoicePersisted, }, crate::onion_message::messenger::Responder, }; @@ -241,15 +242,51 @@ where /// even if multiple invoices are received. const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10; +#[cfg(all(async_payments, test))] +pub(crate) const TEST_OFFERS_MESSAGE_REQUEST_LIMIT: usize = OFFERS_MESSAGE_REQUEST_LIMIT; + /// The default relative expiry for reply paths where a quick response is expected and the reply /// path is single-use. #[cfg(async_payments)] -const TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); +const TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60); + +#[cfg(all(async_payments, test))] +pub(crate) const TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = TEMP_REPLY_PATH_RELATIVE_EXPIRY; + +// Default to async receive offers and the paths used to update them lasting one year. +#[cfg(async_payments)] +const DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = Duration::from_secs(365 * 24 * 60 * 60); + +#[cfg(all(async_payments, test))] +pub(crate) const TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = + DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY; impl OffersMessageFlow where MR::Target: MessageRouter, { + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// If `relative_expiry` is unset, the [`BlindedMessagePath`]s will never expire. + /// + /// Returns the paths that the recipient should be configured with via + /// [`Self::set_paths_to_static_invoice_server`]. + #[cfg(async_payments)] + pub(crate) fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, relative_expiry: Option, + peers: Vec, + ) -> Result, ()> { + let path_absolute_expiry = + relative_expiry.unwrap_or(Duration::MAX).saturating_add(self.duration_since_epoch()); + + let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPathsRequest { + recipient_id, + path_absolute_expiry, + }); + self.create_blinded_paths(peers, context) + } + /// Creates a collection of blinded paths by delegating to [`MessageRouter`] based on /// the path's intended lifetime. /// @@ -388,6 +425,26 @@ fn enqueue_onion_message_with_reply_paths( }); } +/// Instructions for how to respond to an `InvoiceRequest`. +pub enum InvreqResponseInstructions { + /// We are the recipient of this payment, and a [`Bolt12Invoice`] should be sent in response to + /// the invoice request since it is now verified. + 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_id` and calling + /// `OffersMessageFlow::enqueue_static_invoice`. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + SendStaticInvoice { + /// An identifier for the async recipient for whom we are serving [`StaticInvoice`]s. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + recipient_id: Vec, + /// An identifier for the specific invoice being requested by the payer. + invoice_id: u128, + }, +} + impl OffersMessageFlow where MR::Target: MessageRouter, @@ -405,13 +462,28 @@ where /// - The verification process (via recipient context data or metadata) fails. pub fn verify_invoice_request( &self, invoice_request: InvoiceRequest, context: Option, - ) -> Result { + ) -> Result { let secp_ctx = &self.secp_ctx; let expanded_key = &self.inbound_payment_key; let nonce = match context { None if invoice_request.metadata().is_some() => None, Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), + #[cfg(async_payments)] + Some(OffersContext::StaticInvoiceRequested { + recipient_id, + invoice_id, + path_absolute_expiry, + }) => { + if path_absolute_expiry < self.duration_since_epoch() { + return Err(()); + } + + return Ok(InvreqResponseInstructions::SendStaticInvoice { + recipient_id, + invoice_id, + }); + }, _ => return Err(()), }; @@ -422,7 +494,7 @@ where None => invoice_request.verify_using_metadata(expanded_key, secp_ctx), }?; - Ok(invoice_request) + Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) } /// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer metadata, @@ -1032,6 +1104,26 @@ where Ok(()) } + /// Forwards a [`StaticInvoice`] over the provided `responder`. + #[cfg(async_payments)] + pub(crate) fn enqueue_static_invoice( + &self, invoice: StaticInvoice, responder: Responder, + ) -> Result<(), Bolt12SemanticError> { + let duration_since_epoch = self.duration_since_epoch(); + if invoice.is_expired_no_std(duration_since_epoch) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + if invoice.is_offer_expired_no_std(duration_since_epoch) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); + let message = OffersMessage::StaticInvoice(invoice); + pending_offers_messages.push((message, responder.respond().into_instructions())); + + Ok(()) + } + /// Enqueues `held_htlc_available` onion messages to be sent to the payee via the reply paths /// contained within the provided [`StaticInvoice`]. /// @@ -1143,6 +1235,11 @@ where cache.get_async_receive_offer(self.duration_since_epoch()) } + #[cfg(all(test, async_payments))] + pub(crate) fn test_get_async_receive_offers(&self) -> Vec { + self.async_receive_offer_cache.lock().unwrap().test_get_payable_offers() + } + /// Sends out [`OfferPathsRequest`] and [`ServeStaticInvoice`] onion messages if we are an /// often-offline recipient and are configured to interactively build offers and static invoices /// with a static invoice server. @@ -1287,6 +1384,69 @@ where } } + /// Handles an incoming [`OfferPathsRequest`] onion message from an often-offline recipient who + /// wants us (the static invoice server) to serve [`StaticInvoice`]s to payers on their behalf. + /// Sends out [`OfferPaths`] onion messages in response. + #[cfg(async_payments)] + pub(crate) fn handle_offer_paths_request( + &self, context: AsyncPaymentsContext, peers: Vec, entropy_source: ES, + ) -> Option<(OfferPaths, MessageContext)> + where + ES::Target: EntropySource, + { + let duration_since_epoch = self.duration_since_epoch(); + + let recipient_id = match context { + AsyncPaymentsContext::OfferPathsRequest { recipient_id, path_absolute_expiry } => { + if duration_since_epoch > path_absolute_expiry { + return None; + } + recipient_id + }, + _ => return None, + }; + + let mut random_bytes = [0u8; 16]; + random_bytes.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]); + let invoice_id = u128::from_be_bytes(random_bytes); + + // Create the blinded paths that will be included in the async recipient's offer. + let (offer_paths, paths_expiry) = { + let path_absolute_expiry = + duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY); + let context = OffersContext::StaticInvoiceRequested { + recipient_id: recipient_id.clone(), + path_absolute_expiry, + invoice_id, + }; + match self.create_blinded_paths_using_absolute_expiry( + context, + Some(path_absolute_expiry), + peers, + ) { + Ok(paths) => (paths, path_absolute_expiry), + Err(()) => return None, + } + }; + + // Create a reply path so that the recipient can respond to our offer_paths message with the + // static invoice that they create. This path will also be used by the recipient to update said + // invoice. + let reply_path_context = { + let path_absolute_expiry = + duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY); + MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice { + recipient_id, + invoice_id, + path_absolute_expiry, + }) + }; + + let offer_paths_om = + OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry.as_secs()) }; + return Some((offer_paths_om, reply_path_context)); + } + /// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out /// [`ServeStaticInvoice`] onion messages in response if we've built a new async receive offer and /// need the corresponding [`StaticInvoice`] to be persisted by the static invoice server. @@ -1437,6 +1597,49 @@ where Ok((invoice, forward_invoice_request_path)) } + /// Verifies an incoming [`ServeStaticInvoice`] onion message from an often-offline recipient who + /// wants us as a static invoice server to serve the [`ServeStaticInvoice::invoice`] to payers on + /// their behalf. + /// + /// On success, returns (recipient_id, invoice_id) for use in persisting and later retrieving + /// the static invoice from the database. + /// + /// [`ServeStaticInvoice::invoice`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice + #[cfg(async_payments)] + pub fn verify_serve_static_invoice_message( + &self, message: &ServeStaticInvoice, context: AsyncPaymentsContext, + ) -> Result<(Vec, u128), ()> { + if message.invoice.is_expired_no_std(self.duration_since_epoch()) { + return Err(()); + } + match context { + AsyncPaymentsContext::ServeStaticInvoice { + recipient_id, + invoice_id, + path_absolute_expiry, + } => { + if self.duration_since_epoch() > path_absolute_expiry { + return Err(()); + } + + return Ok((recipient_id, invoice_id)); + }, + _ => return Err(()), + }; + } + + /// Indicates that a [`ServeStaticInvoice::invoice`] has been persisted and is ready to be served + /// to payers on behalf of an often-offline recipient. This method must be called after persisting + /// a [`StaticInvoice`] to confirm to the recipient that their corresponding [`Offer`] is ready to + /// receive async payments. + #[cfg(async_payments)] + pub fn static_invoice_persisted(&self, responder: Responder) { + let mut pending_async_payments_messages = + self.pending_async_payments_messages.lock().unwrap(); + let message = AsyncPaymentsMessage::StaticInvoicePersisted(StaticInvoicePersisted {}); + pending_async_payments_messages.push((message, responder.respond().into_instructions())); + } + /// Handles an incoming [`StaticInvoicePersisted`] onion message from the static invoice server. /// Returns a bool indicating whether the async receive offer cache needs to be re-persisted. /// diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index eacc2a8914b..7aa3df4ba39 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1223,6 +1223,10 @@ impl InvoiceContents { is_expired(self.created_at(), self.relative_expiry()) } + fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.created_at().saturating_add(self.relative_expiry()) < duration_since_epoch + } + fn payment_hash(&self) -> PaymentHash { self.fields().payment_hash } diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index af3c2a6155e..1ac6e40b896 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -131,6 +131,11 @@ macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice $contents.is_expired() } + /// Whether the invoice has expired given the current time as duration since the Unix epoch. + pub fn is_expired_no_std(&$self, duration_since_epoch: Duration) -> bool { + $contents.is_expired_no_std(duration_since_epoch) + } + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to /// least-preferred. pub fn fallbacks(&$self) -> Vec
{ diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 08170fda867..8fa5790161e 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -395,6 +395,18 @@ impl StaticInvoice { self.signature } + /// Whether the [`Offer`] that this invoice is based on is expired. + #[cfg(feature = "std")] + pub fn is_offer_expired(&self) -> bool { + self.contents.is_expired() + } + + /// Whether the [`Offer`] that this invoice is based on is expired, given the current time as + /// duration since the Unix epoch. + pub fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.contents.is_offer_expired_no_std(duration_since_epoch) + } + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag pub(crate) fn is_from_same_offer(&self, invreq: &InvoiceRequest) -> bool { let invoice_offer_tlv_stream = @@ -411,7 +423,6 @@ impl InvoiceContents { self.offer.is_expired() } - #[cfg(not(feature = "std"))] fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { self.offer.is_expired_no_std(duration_since_epoch) } @@ -528,6 +539,10 @@ impl InvoiceContents { is_expired(self.created_at(), self.relative_expiry()) } + fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.created_at().saturating_add(self.relative_expiry()) < duration_since_epoch + } + fn fallbacks(&self) -> Vec
{ let chain = self.chain(); self.fallbacks diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 3b2566119de..f5762b04310 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1324,6 +1324,11 @@ where self.offers_handler = offers_handler; } + #[cfg(any(test, feature = "_test_utils"))] + pub fn set_async_payments_handler(&mut self, async_payments_handler: APH) { + self.async_payments_handler = async_payments_handler; + } + /// Sends an [`OnionMessage`] based on its [`MessageSendInstructions`]. pub fn send_onion_message( &self, contents: T, instructions: MessageSendInstructions, @@ -1538,7 +1543,7 @@ where } #[cfg(test)] - pub(super) fn release_pending_msgs(&self) -> HashMap> { + pub(crate) fn release_pending_msgs(&self) -> HashMap> { let mut message_recipients = self.message_recipients.lock().unwrap(); let mut msgs = new_hash_map(); // We don't want to disconnect the peers by removing them entirely from the original map, so we diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index fecdb830fe0..e0869bf4364 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -317,13 +317,17 @@ pub struct TestMessageRouter<'a> { &'a TestLogger, &'a TestKeysInterface, >, + pub peers_override: Mutex>, } impl<'a> TestMessageRouter<'a> { pub fn new( network_graph: Arc>, entropy_source: &'a TestKeysInterface, ) -> Self { - Self { inner: DefaultMessageRouter::new(network_graph, entropy_source) } + Self { + inner: DefaultMessageRouter::new(network_graph, entropy_source), + peers_override: Mutex::new(Vec::new()), + } } } @@ -331,6 +335,13 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { fn find_path( &self, sender: PublicKey, peers: Vec, destination: Destination, ) -> Result { + let mut peers = peers; + { + let peers_override = self.peers_override.lock().unwrap(); + if !peers_override.is_empty() { + peers = peers_override.clone(); + } + } self.inner.find_path(sender, peers, destination) } @@ -338,6 +349,13 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { &self, recipient: PublicKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { + let mut peers = peers; + { + let peers_override = self.peers_override.lock().unwrap(); + if !peers_override.is_empty() { + peers = peers_override.clone(); + } + } self.inner.create_blinded_paths(recipient, context, peers, secp_ctx) } @@ -345,6 +363,17 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { &self, recipient: PublicKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { + let mut peers = peers; + { + let peers_override = self.peers_override.lock().unwrap(); + if !peers_override.is_empty() { + peers = peers_override + .clone() + .iter() + .map(|pk| MessageForwardNode { node_id: *pk, short_channel_id: None }) + .collect(); + } + } self.inner.create_compact_blinded_paths(recipient, context, peers, secp_ctx) } }