diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index a5782dacd42..e9b8b3d57d5 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -15,7 +15,8 @@ use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::script::ShutdownScript; use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::onion_message::async_payments::{ - AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc, + AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc, + ServeStaticInvoice, StaticInvoicePersisted, }; use lightning::onion_message::messenger::{ CustomOnionMessageHandler, Destination, MessageRouter, MessageSendInstructions, @@ -124,6 +125,30 @@ impl OffersMessageHandler for TestOffersMessageHandler { struct TestAsyncPaymentsMessageHandler {} impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, + responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + let responder = match responder { + Some(resp) => resp, + None => return None, + }; + Some((OfferPaths { paths: Vec::new(), paths_absolute_expiry: None }, responder.respond())) + } + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + None + } + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, + _responder: Option, + ) { + } + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) { + } fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, responder: Option, diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 164cfcfb1ad..5d5146aeea1 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -23,6 +23,8 @@ use crate::ln::channelmanager::PaymentId; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; use crate::offers::nonce::Nonce; +use crate::offers::offer::Offer; +use crate::onion_message::messenger::Responder; use crate::onion_message::packet::ControlTlvs; use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; use crate::sign::{EntropySource, NodeSigner, Recipient}; @@ -34,6 +36,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. @@ -341,6 +344,47 @@ 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. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + StaticInvoiceRequested { + /// An identifier for the async recipient for whom the static invoice server is serving + /// [`StaticInvoice`]s. Used to look up a corresponding [`StaticInvoice`] to return to the payer + /// if the recipient is offline. + /// + /// Also useful for the server to rate limit the number of [`InvoiceRequest`]s it will respond + /// to on recipient's behalf. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + recipient_id_nonce: Nonce, + + /// A nonce used for authenticating that a received [`InvoiceRequest`] is valid for a preceding + /// [`OfferPaths`] message sent by the static invoice server. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + nonce: Nonce, + + /// Authentication code for the [`InvoiceRequest`]. + /// + /// Prevents nodes from creating their own blinded path to the static invoice server and causing + /// them to unintentionally hit their database looking for a [`StaticInvoice`] to return. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + hmac: Hmac, + + /// 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`]. /// @@ -404,6 +448,148 @@ pub enum OffersContext { /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage #[derive(Clone, Debug)] pub enum AsyncPaymentsContext { + /// Context used by a [`BlindedMessagePath`] that an async recipient is configured with in + /// [`UserConfig::paths_to_static_invoice_server`], provided back to the static invoice server in + /// corresponding [`OfferPathsRequest`]s. + /// + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + /// [`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_nonce: Nonce, + /// Authentication code for the [`OfferPathsRequest`]. + /// + /// Prevents nodes from requesting offer paths from the static invoice server without having + /// been previously configured with a [`BlindedMessagePath`] that the server generated. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + hmac: Hmac, + /// 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. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + OfferPaths { + /// A nonce used for authenticating that an [`OfferPaths`] message is valid for a preceding + /// [`OfferPathsRequest`] that we sent as an async recipient. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + nonce: Nonce, + /// Authentication code for the [`OfferPaths`] message. + /// + /// Prevents nodes from creating their own blinded path that terminates at our async recipient + /// node and causing us to cache an unintended async receive offer. + /// + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + hmac: Hmac, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// As an async recipient we use this field to time out a static invoice server from sending us + /// offer paths if we are no longer configured to accept paths from them. + path_absolute_expiry: core::time::Duration, + }, + /// Context used by a reply path to an [`OfferPaths`] message, provided back to 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 as a key to retrieve the invoice when payers send an [`InvoiceRequest`] to the static + /// invoice server. Also useful to rate limit the invoices being persisted on behalf of a + /// particular recipient. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + recipient_id_nonce: Nonce, + /// A nonce used for authenticating that a [`ServeStaticInvoice`] message is valid for a preceding + /// [`OfferPaths`] message. + /// + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + nonce: Nonce, + /// Authentication code for the [`ServeStaticInvoice`] message. + /// + /// Prevents nodes from creating their own blinded path to the static invoice server and causing + /// them to persist an unintended [`StaticInvoice`]. + /// + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + hmac: Hmac, + /// 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. + /// + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + StaticInvoicePersisted { + /// The offer corresponding to the [`StaticInvoice`] that has been persisted. This invoice is + /// now ready to be provided by the static invoice server in response to [`InvoiceRequest`]s. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + offer: Offer, + /// A [`Nonce`] useful for updating the [`StaticInvoice`] that corresponds to the + /// [`AsyncPaymentsContext::StaticInvoicePersisted::offer`], since the offer may be much longer + /// lived than the invoice. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + offer_nonce: Nonce, + /// Useful to determine how far an offer is into its lifespan, to decide whether the offer is + /// expiring soon and we should start building a new one. + offer_created_at: core::time::Duration, + /// A [`Responder`] useful for updating the [`StaticInvoice`] that corresponds to the + /// [`AsyncPaymentsContext::StaticInvoicePersisted::offer`], since the offer may be much longer + /// lived than the invoice. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + update_static_invoice_path: Responder, + /// The time as duration since the Unix epoch at which the [`StaticInvoice`] expires, used to track + /// when we need to generate and persist a new invoice with the static invoice server. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + static_invoice_absolute_expiry: core::time::Duration, + /// A nonce used for authenticating that a [`StaticInvoicePersisted`] message is valid for a + /// preceding [`ServeStaticInvoice`] message. + /// + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + nonce: Nonce, + /// Authentication code for the [`StaticInvoicePersisted`] message. + /// + /// Prevents nodes from creating their own blinded path to us and causing us to cache an + /// unintended async receive offer. + /// + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + hmac: Hmac, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Prevents a static invoice server from causing an async recipient to cache an old offer if + /// the recipient is no longer configured to use that server. + path_absolute_expiry: core::time::Duration, + }, /// Context contained within the reply [`BlindedMessagePath`] we put in outbound /// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`] /// messages. @@ -473,6 +659,12 @@ impl_writeable_tlv_based_enum!(OffersContext, (1, nonce, required), (2, hmac, required) }, + (3, StaticInvoiceRequested) => { + (0, recipient_id_nonce, required), + (2, nonce, required), + (4, hmac, required), + (6, path_absolute_expiry, required), + }, ); impl_writeable_tlv_based_enum!(AsyncPaymentsContext, @@ -486,6 +678,32 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (2, hmac, required), (4, path_absolute_expiry, required), }, + (2, OfferPaths) => { + (0, nonce, required), + (2, hmac, required), + (4, path_absolute_expiry, required), + }, + (3, StaticInvoicePersisted) => { + (0, offer, required), + (2, offer_nonce, required), + (4, offer_created_at, required), + (6, update_static_invoice_path, required), + (8, static_invoice_absolute_expiry, required), + (10, nonce, required), + (12, hmac, required), + (14, path_absolute_expiry, required), + }, + (4, OfferPathsRequest) => { + (0, recipient_id_nonce, required), + (2, hmac, required), + (4, path_absolute_expiry, required), + }, + (5, ServeStaticInvoice) => { + (0, recipient_id_nonce, required), + (2, nonce, required), + (4, hmac, required), + (6, 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 c16305bcca0..dd0b47fb366 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -52,6 +52,9 @@ use bitcoin::{OutPoint, Transaction}; use core::ops::Deref; use core::time::Duration; +#[cfg(async_payments)] +use crate::offers::nonce::Nonce; + #[allow(unused_imports)] use crate::prelude::*; @@ -1572,6 +1575,60 @@ 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 configured the recipient with them + /// via [`UserConfig::paths_to_static_invoice_server`]. + /// + /// [`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::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, + /// An identifier for the recipient, originally surfaced in + /// [`ChannelManager::blinded_paths_for_async_recipient`]. When an + /// [`Event::StaticInvoiceRequested`] comes in for this invoice, this id will be surfaced so the + /// persisted invoice can be retrieved from the database. + recipient_id_nonce: Nonce, + /// Once the [`StaticInvoice`] is 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 configured the recipient with them + /// via [`UserConfig::paths_to_static_invoice_server`]. + /// + /// If we previously persisted a [`StaticInvoice`] from an [`Event::PersistStaticInvoice`] that + /// matches the contained [`Event::StaticInvoiceRequested::recipient_id_nonce`], 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 + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::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_nonce`]. Useful to retrieve the [`StaticInvoice`] + /// requested by the payer. + recipient_id_nonce: Nonce, + /// 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 { @@ -1996,6 +2053,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`. @@ -2560,6 +2628,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..28ad7c5747f 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,23 @@ 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_CACHE_SIZE, TEST_MAX_OFFERS, TEST_MAX_UPDATE_ATTEMPTS, + TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS, TEST_NUM_CACHED_OFFERS_TARGET, + TEST_OFFER_EXPIRES_SOON_THRESHOLD_PERCENT, TEST_PATHS_REQUESTS_RESET_INTERVAL, +}; +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::offers::static_invoice::{StaticInvoice, DEFAULT_RELATIVE_EXPIRY}; 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 +50,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 +59,173 @@ use bitcoin::secp256k1::Secp256k1; use core::convert::Infallible; use core::time::Duration; +// Reload the recipient node, now configured with blinded paths to reach the static invoice +// server. +macro_rules! reload_payee_with_async_receive_cfg { + ($server_node: expr, $payee_node: expr, $new_persister: ident, $new_chain_monitor: ident, + $payee_node_deserialized: ident, $chan_ids: expr + ) => {{ + reload_payee_with_async_receive_cfg!( + $server_node, + $payee_node, + $new_persister, + $new_chain_monitor, + $payee_node_deserialized, + $chan_ids, + None + ) + }}; + ($server_node: expr, $payee_node: expr, $new_persister: ident, $new_chain_monitor: ident, + $payee_node_deserialized: ident, $chan_ids: expr, $paths_relative_expiry: expr + ) => {{ + let (offer_paths_request_paths, recipient_id_nonce) = + $server_node.node.blinded_paths_for_async_recipient($paths_relative_expiry).unwrap(); + let mut async_payee_cfg = test_default_channel_config(); + async_payee_cfg.paths_to_static_invoice_server = offer_paths_request_paths; + + $server_node.node.peer_disconnected($payee_node.node.get_our_node_id()); + + let mut serialized_monitor_vecs = Vec::with_capacity($chan_ids.len()); + for chan_id in $chan_ids { + serialized_monitor_vecs.push(get_monitor!($payee_node, *chan_id).encode()); + } + let mut serialized_monitors = Vec::with_capacity($chan_ids.len()); + for vec in serialized_monitor_vecs.iter() { + serialized_monitors.push(&vec[..]); + } + + reload_node!( + $payee_node, + async_payee_cfg, + $payee_node.node.encode(), + &serialized_monitors[..], + $new_persister, + $new_chain_monitor, + $payee_node_deserialized + ); + + let mut reconnect_args = ReconnectArgs::new(&$server_node, &$payee_node); + reconnect_args.send_channel_ready = (true, true); + reconnect_nodes(reconnect_args); + + recipient_id_nonce + }}; +} + +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. + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + 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: Nonce, +) -> 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.get_cached_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_nonce } => { + assert_eq!(recipient_id, recipient_id_nonce); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + assert_eq!( + recipient.node.get_cached_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.get_cached_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 +234,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: Nonce, ) -> (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_nonce, reply_path } => { + assert_eq!(recipient_id, recipient_id_nonce); + 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 +278,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 +310,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 +416,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 +439,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 +449,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 +468,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,17 +493,30 @@ 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 new_persister; + let new_chain_monitor; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut 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 chan_id = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); // 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).invoice; + + let offer = nodes[2].node.get_cached_async_receive_offers().pop().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -306,30 +525,30 @@ 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); + + 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_nonce, reply_path } => { + assert_eq!(recipient_id, recipient_id_nonce); + reply_path + }, + _ => panic!(), + }; // 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; + nodes[2].node.timer_tick_occurred(); + let unexpected_static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id).invoice; // 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(); + 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 +562,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 +577,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 +592,44 @@ 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 new_persister; + let new_chain_monitor; + 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 payee_node_deserialized; - // 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 mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); - 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); + let static_invoice = invoice_flow_res.invoice; assert!(static_invoice.invoice_features().supports_basic_mpp()); - assert_eq!(static_invoice.relative_expiry(), relative_expiry); + // Check that the recipient will ignore duplicate offers received. + nodes[2].onion_messenger.handle_onion_message( + nodes[1].node.get_our_node_id(), + &invoice_flow_res.static_invoice_persisted_message, + ); + assert_eq!(nodes[2].node.get_cached_async_receive_offers().len(), 1); + + let offer = nodes[2].node.get_cached_async_receive_offers().pop().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -423,8 +637,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 +653,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 +663,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,18 +675,29 @@ 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 new_persister; + let new_chain_monitor; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); - 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 static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id).invoice; + let offer = nodes[2].node.get_cached_async_receive_offers().pop().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -480,20 +711,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 { recipient_id_nonce: _, 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 +729,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,15 +746,17 @@ 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 new_persister; + let new_chain_monitor; + 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( @@ -535,7 +764,8 @@ fn async_receive_mpp() { &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg.clone()), Some(allow_priv_chan_fwds_cfg), None], ); - let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut nodes = create_network(4, &node_cfgs, &node_chanmgrs); // Create this network topology: // n1 @@ -545,8 +775,10 @@ fn async_receive_mpp() { // n2 create_announced_chan_between_nodes(&nodes, 0, 1); create_announced_chan_between_nodes(&nodes, 0, 2); - create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + let chan_id_1_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0).0.channel_id; + let chan_id_2_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0).0.channel_id; // Ensure all nodes start at the same height. connect_blocks(&nodes[0], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[0].best_block_info().1); @@ -554,11 +786,23 @@ 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); + nodes[2].node.peer_disconnected(nodes[3].node.get_our_node_id()); + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[3], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_3, chan_id_2_3] + ); + let mut reconnect_args = ReconnectArgs::new(&nodes[2], &nodes[3]); + reconnect_args.send_channel_ready = (true, true); + reconnect_nodes(reconnect_args); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id).invoice; + let offer = nodes[3].node.get_cached_async_receive_offers().pop().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 +811,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 +837,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 +857,9 @@ 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 new_persister; + let new_chain_monitor; + 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. @@ -623,7 +870,8 @@ fn amount_doesnt_match_invreq() { &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), Some(higher_fee_chan_cfg), None], ); - let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut nodes = create_network(4, &node_cfgs, &node_chanmgrs); // Create this network topology so nodes[0] has a blinded route hint to retry over. // n1 @@ -633,8 +881,10 @@ fn amount_doesnt_match_invreq() { // n2 create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 1_000_000, 0); - create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + let chan_id_1_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0).0.channel_id; + let chan_id_2_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0).0.channel_id; // Ensure all nodes start at the same height. connect_blocks(&nodes[0], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[0].best_block_info().1); @@ -642,13 +892,22 @@ 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); + nodes[2].node.peer_disconnected(nodes[3].node.get_our_node_id()); + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[3], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_3, chan_id_2_3] + ); + let mut reconnect_args = ReconnectArgs::new(&nodes[2], &nodes[3]); + reconnect_args.send_channel_ready = (true, true); + reconnect_nodes(reconnect_args); - // 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).invoice; + let offer = nodes[3].node.get_cached_async_receive_offers().pop().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -658,7 +917,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 +955,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 +984,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,14 +1089,32 @@ 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 new_persister; + let new_chain_monitor; + 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 payee_node_deserialized; + let mut 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); + + // 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); @@ -875,18 +1152,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).invoice; + let offer = nodes[2].node.get_cached_async_receive_offers().pop().unwrap(); let params = RouteParametersConfig::default(); nodes[0] @@ -894,7 +1162,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 +1174,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 +1203,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,33 +1216,41 @@ 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 new_persister; + let new_chain_monitor; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut 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); - - 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] ); + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id).invoice; + let offer = nodes[2].node.get_cached_async_receive_offers().pop().unwrap(); + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -986,12 +1261,12 @@ fn expired_static_invoice_message_path() { // 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]); + pass_async_payments_oms(static_invoice, &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, + DEFAULT_RELATIVE_EXPIRY.as_secs() as u32, ); let block = create_dummy_block( nodes[2].best_block_hash(), @@ -1016,13 +1291,28 @@ 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 new_persister; + let new_chain_monitor; + 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 payee_node_deserialized; + + let mut 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); // Make sure all nodes are at the same block height in preparation for CLTV timeout things. let node_max_height = @@ -1031,12 +1321,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 +1364,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).invoice; + let offer = nodes[2].node.get_cached_async_receive_offers().pop().unwrap(); + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -1089,7 +1376,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 +1384,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 +1399,899 @@ fn expired_static_invoice_payment_path() { 1, ); } + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_offer_paths_request() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let new_persister; + let new_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; + + const OFFER_PATHS_REQ_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60); + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id], + Some(OFFER_PATHS_REQ_RELATIVE_EXPIRY) + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // 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. + 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 new_persister; + let new_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; + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // 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 new_persister; + let new_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; + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // 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 new_persister; + let new_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; + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // 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_cached_async_receive_offers().is_empty()); +} + +#[cfg_attr(feature = "std", ignore)] +#[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 new_persister; + let new_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; + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // 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 { + 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 + recipient.node.timer_tick_occurred(); + } + + // After the recipient runs out of attempts to request offer paths, they will give up for a time. + recipient.node.timer_tick_occurred(); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // After some time, more offer paths requests should be allowed to go through. + let block = create_dummy_block( + recipient.best_block_hash(), + (recipient.node.duration_since_epoch() + TEST_PATHS_REQUESTS_RESET_INTERVAL).as_secs() + as u32 + 1u32, + Vec::new(), + ); + connect_block(&recipient, &block); + + 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 new_persister_1; + let new_persister_2; + let new_chain_monitor_1; + let new_chain_monitor_2; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let payee_node_deserialized_1; + let payee_node_deserialized_2; + 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 recipient_id = reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister_1, + new_chain_monitor_1, + payee_node_deserialized_1, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // Build the target number of offers interactively with the static invoice server. + let mut offer_paths_req = None; + for _ in 0..TEST_NUM_CACHED_OFFERS_TARGET { + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id); + offer_paths_req = Some(flow_res.offer_paths_request); + + // Trigger a cache refresh + recipient.node.timer_tick_occurred(); + } + assert_eq!( + recipient.node.get_cached_async_receive_offers().len(), + TEST_NUM_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.flow.test_reset_more_offer_paths_request_attempts(); + + 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.get_cached_async_receive_offers(); + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister_2, + new_chain_monitor_2, + payee_node_deserialized_2, + &[chan_id] + ); + let recipient = &nodes[1]; + let cached_offers_post_ser = recipient.node.get_cached_async_receive_offers(); + assert_eq!(cached_offers_pre_ser, cached_offers_post_ser); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn refresh_expiring_static_invoices() { + // Check that if we have a longer-lived offer but the corresponding static invoice is expiring + // soon, we'll refresh the offer that is persisted with the static invoice server. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let new_persister; + let new_chain_monitor; + + 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 payee_node_deserialized; + + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + let recipient_id = reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); + let server = &nodes[1]; + let recipient = &nodes[2]; + + // Set up the recipient to have one offer and an invoice with the static invoice server. + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id); + + // Advance time for the recipient so they they view the static invoice corresponding to their + // offer as expired. + let block = create_dummy_block( + recipient.best_block_hash(), + (recipient.node.duration_since_epoch() + DEFAULT_RELATIVE_EXPIRY).as_secs() as u32 + 1u32, + Vec::new(), + ); + connect_block(&recipient, &block); + + // Exchange messages to ensure the recipient will update the static invoice that's persisted by + // the server. + + // 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()); + + 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_static_invoice, ack_path) = match events.pop().unwrap() { + Event::PersistStaticInvoice { invoice, invoice_persisted_path, recipient_id_nonce } => { + assert_eq!(recipient_id, recipient_id_nonce); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + 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.get_cached_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_cached_async_receive_offers().pop().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_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); + + 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_static_invoice))); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn limit_static_invoice_update_requests() { + // If a recipient tries to update the static invoice that is persisted with the server several + // times and the server is unresponsive, they should give up. + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let new_persister; + let new_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 recipient_id = reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // Set up the recipient to have one offer and an invoice with the static invoice server. + pass_static_invoice_server_messages(server, recipient, recipient_id); + + // Advance time for the recipient so they they view the static invoice corresponding to their + // offer as expired. + let block = create_dummy_block( + recipient.best_block_hash(), + (recipient.node.duration_since_epoch() + DEFAULT_RELATIVE_EXPIRY).as_secs() as u32 + 1u32, + Vec::new(), + ); + connect_block(&recipient, &block); + + // 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()); + + // Make sure the recipient will try MAX_UPDATE_ATTEMPTS to update their invoice. + for _ in 0..TEST_MAX_UPDATE_ATTEMPTS { + recipient.node.timer_tick_occurred(); + 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 + }, + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) => {}, + _ => panic!("Unexpected message"), + } + } + recipient.onion_messenger.release_pending_msgs(); // Clear all messages + } + + // After reaching the maximum number of update attempts, the recipient should give up updating + // their invoice. + recipient.node.timer_tick_occurred(); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + // Check that we won't return offers from our API where the invoice is expired. + assert!(recipient.node.get_cached_async_receive_offers().is_empty()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn limit_cached_offers() { + // While the cache size limit should never be hit in practice, check that the recipient will limit + // the number of offers stored. + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let new_persister; + let new_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 recipient_id = reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // Retrieve an offer_paths_request from the recipient. + recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id()); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + + // Send many paths requests to the server, who should respond to all of them, then send many + // static invoices to the server for persistence. Indicate to the recipient that all of these + // static invoices were persisted and make sure the recipient will cache many offers but will + // refuse to store any more when the cache is full. + let mut static_invoice_persisted_oms = Vec::new(); + for _ in 0..TEST_MAX_OFFERS + 1 { + 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(); + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + + // Advance time by for each new offer generated, so the recipient thinks each offer is + // newer than the last. + let block = create_dummy_block( + recipient.best_block_hash(), + recipient.node.duration_since_epoch().as_secs() as u32 + 1u32, + Vec::new(), + ); + connect_block(recipient, &block); + connect_block(server, &block); + + let serve_static_invoice_om = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + 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 (_static_invoice, ack_path) = match events.pop().unwrap() { + Event::PersistStaticInvoice { invoice, invoice_persisted_path, recipient_id_nonce } => { + assert_eq!(recipient_id, recipient_id_nonce); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + 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(); + static_invoice_persisted_oms.push(invoice_persisted_om); + } + for msg in static_invoice_persisted_oms { + let offers_pre_new_invoice_persist = recipient.node.get_cached_async_receive_offers(); + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &msg); + // Every new invoice persist should result in the recipient's list of offers being updated, + // since even if the cache is full they will swap out their soonest-expiring offer for the new + // one. + assert_ne!( + offers_pre_new_invoice_persist, + recipient.node.get_cached_async_receive_offers() + ); + } + assert!(recipient.node.get_cached_async_receive_offers().len() <= TEST_MAX_OFFERS); + assert!(recipient.node.get_cached_async_receive_offers().len() > TEST_MAX_OFFERS / 2); + assert!( + recipient.node.flow.writeable_async_receive_offer_cache().serialized_length() + <= TEST_MAX_CACHE_SIZE + ); +} + +#[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 new_persister; + let new_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; + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + 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 + + DEFAULT_RELATIVE_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 new_persister; + let new_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; + reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // Get a legit offer_paths message from the server. + 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(Duration::from_secs(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()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn request_more_offer_paths_when_offers_expire_soon() { + // Check that an async recipient will build new offers it has the target number of offers cached + // but doesn't have enough offers that don't expire soon. + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let new_persister; + let new_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 recipient_id = reload_payee_with_async_receive_cfg!( + nodes[0], + nodes[1], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id] + ); + let server = &nodes[0]; + let recipient = &nodes[1]; + + // First fill the recipient's offer cache with the target number of offers. + for _ in 0..TEST_NUM_CACHED_OFFERS_TARGET { + pass_static_invoice_server_messages(server, recipient, recipient_id); + recipient.node.timer_tick_occurred(); + } + + // If the recipient already has the target number of offers cached, they shouldn't request any + // more offer paths. + recipient.node.timer_tick_occurred(); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // Manually advance time for the recipient so they will perceive their offers as expiring soon. + let offers_expire_soon_relative_duration = (TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY + * TEST_OFFER_EXPIRES_SOON_THRESHOLD_PERCENT as u32) + / 100; + let block = create_dummy_block( + recipient.best_block_hash(), + (recipient.node.duration_since_epoch() + offers_expire_soon_relative_duration).as_secs() + as u32 + 1u32, + Vec::new(), + ); + connect_block(&recipient, &block); + + recipient.node.timer_tick_occurred(); + pass_static_invoice_server_messages(server, recipient, recipient_id); +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b018c6c74cd..5d6fcba8b3d 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -85,7 +85,8 @@ use crate::ln::outbound_payment::{ SendAlongPathArgs, StaleExpiration, }; use crate::ln::types::ChannelId; -use crate::offers::flow::OffersMessageFlow; +use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; +use crate::offers::flow::{InvreqResponseInstructions, OffersMessageFlow}; use crate::offers::invoice::{ Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, }; @@ -97,7 +98,8 @@ use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::Refund; use crate::offers::signer; use crate::onion_message::async_payments::{ - AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc, + AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, + OfferPathsRequest, ReleaseHeldHtlc, ServeStaticInvoice, StaticInvoicePersisted, }; use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{ @@ -5103,7 +5105,40 @@ where } #[cfg(async_payments)] + fn check_refresh_async_receive_offers(&self) { + let peers = self.get_peers_for_blinded_path(); + let channels = self.list_usable_channels(); + let entropy = &*self.entropy_source; + let router = &*self.router; + match self.flow.check_refresh_async_receive_offers(peers, channels, entropy, router) { + Err(()) => { + log_error!( + self.logger, + "Failed to create blinded paths when requesting async receive offer paths" + ); + }, + Ok(()) => {}, + } + } + + /// 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.serving_static_invoice(invoice_persisted_path); + } + + /// Forwards a [`StaticInvoice`] that was previously persisted by us from an + /// [`Event::PersistStaticInvoice`], 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) + } + #[rustfmt::skip] + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId ) -> Result<(), Bolt12PaymentError> { @@ -7047,6 +7082,9 @@ where duration_since_epoch, &self.pending_events ); + #[cfg(async_payments)] + self.check_refresh_async_receive_offers(); + // Technically we don't need to do this here, but if we have holding cell entries in a // channel that need freeing, it's better to do that here and block a background task // than block the message queueing pipeline. @@ -10586,9 +10624,23 @@ where #[cfg(c_bindings)] create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder); + /// Retrieve our cached [`Offer`]s for receiving async payments as an often-offline recipient. + /// Will only be set if [`UserConfig::paths_to_static_invoice_server`] is set and we succeeded in + /// interactively building a [`StaticInvoice`] with the static invoice server. + /// + /// Useful for posting offers to receive payments later, such as posting an offer on a website. + #[cfg(async_payments)] + pub fn get_cached_async_receive_offers(&self) -> Vec { + self.flow.get_cached_async_receive_offers() + } + /// Create an offer for receiving async payments as an often-offline recipient. /// - /// Because we may be offline when the payer attempts to request an invoice, you MUST: + /// Instead of using this method, it is preferable to set + /// [`UserConfig::paths_to_static_invoice_server`] and retrieve the automatically built offer via + /// [`Self::get_cached_async_receive_offers`]. + /// + /// If you want to build the [`StaticInvoice`] manually using this method instead, you MUST: /// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will /// serve the [`StaticInvoice`] created from this offer on our behalf. /// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this @@ -10605,6 +10657,10 @@ where /// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were /// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the /// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`]. + /// + /// Instead of using this method to manually build the invoice, it is preferable to set + /// [`UserConfig::paths_to_static_invoice_server`] and retrieve the automatically built offer via + /// [`Self::get_cached_async_receive_offers`]. #[cfg(async_payments)] #[rustfmt::skip] pub fn create_static_invoice_builder<'a>( @@ -10973,6 +11029,36 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_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 includes these paths in their [`UserConfig::paths_to_static_invoice_server`] + /// 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 expiry will default to + /// [`DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY`]. + /// + /// Returns the paths to be included in the recipient's + /// [`UserConfig::paths_to_static_invoice_server`] as well as a nonce that uniquely identifies the + /// recipient that has been configured with these paths. // TODO link to events that surface this nonce + /// + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + /// [`DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY`]: crate::onion_message::async_payments::DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY + #[cfg(async_payments)] + pub fn blinded_paths_for_async_recipient( + &self, relative_expiry: Option, + ) -> Result<(Vec, Nonce), ()> { + let peers = self.get_peers_for_blinded_path(); + let entropy = &*self.entropy_source; + self.flow.blinded_paths_for_async_recipient(peers, relative_expiry, entropy) + } + #[cfg(any(test, async_payments))] #[rustfmt::skip] pub(super) fn duration_since_epoch(&self) -> Duration { @@ -11482,6 +11568,13 @@ where return NotifyOption::SkipPersistHandleEvents; //TODO: Also re-broadcast announcement_signatures }); + + // While we usually refresh the AsyncReceiveOfferCache on a timer, we also want to start + // interactively building offers as soon as we can after startup. We can't start building offers + // until we have some peer connection(s) to send onion messages over, so as a minor optimization + // refresh the cache when a peer connects. + #[cfg(async_payments)] + self.check_refresh_async_receive_offers(); res } @@ -12676,7 +12769,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_nonce: _recipient_id_nonce + }) => { + #[cfg(async_payments)] + self.pending_events.lock().unwrap().push_back((Event::StaticInvoiceRequested { + recipient_id_nonce: _recipient_id_nonce, reply_path: responder + }, None)); + + return None + }, Err(_) => return None, }; @@ -12806,6 +12909,98 @@ where MR::Target: MessageRouter, L::Target: Logger, { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, + _responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + #[cfg(async_payments)] + { + let peers = self.get_peers_for_blinded_path(); + let (message, reply_path_context) = match self.flow.handle_offer_paths_request( + _context, + peers, + &*self.entropy_source, + ) { + Some(msg) => msg, + None => return None, + }; + _responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context))) + } + + #[cfg(not(async_payments))] + None + } + + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + #[cfg(async_payments)] + { + let responder = match _responder { + Some(responder) => responder, + None => return None, + }; + let (serve_static_invoice, reply_context) = match self.flow.handle_offer_paths( + _message, + _context, + responder.clone(), + self.get_peers_for_blinded_path(), + self.list_usable_channels(), + &*self.entropy_source, + &*self.router, + ) { + Some((msg, ctx)) => (msg, ctx), + None => return None, + }; + let response_instructions = responder.respond_with_reply_path(reply_context); + return Some((serve_static_invoice, response_instructions)); + } + + #[cfg(not(async_payments))] + return None; + } + + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, + _responder: Option, + ) { + #[cfg(async_payments)] + { + let responder = match _responder { + Some(resp) => resp, + None => return, + }; + + let recipient_id_nonce = + 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, + recipient_id_nonce, + invoice_persisted_path: responder, + }, + None, + )); + } + } + + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) { + #[cfg(async_payments)] + { + let should_persist = self.flow.handle_static_invoice_persisted(_context); + if should_persist { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + } + } + } + #[rustfmt::skip] fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, @@ -13658,6 +13853,7 @@ where (15, self.inbound_payment_id_secret, required), (17, in_flight_monitor_updates, option), (19, peer_storage_dir, optional_vec), + (21, self.flow.writeable_async_receive_offer_cache(), required), }); Ok(()) @@ -14222,6 +14418,7 @@ where let mut decode_update_add_htlcs: Option>> = None; let mut inbound_payment_id_secret = None; let mut peer_storage_dir: Option)>> = None; + let mut async_receive_offer_cache: AsyncReceiveOfferCache = AsyncReceiveOfferCache::new(); read_tlv_fields!(reader, { (1, pending_outbound_payments_no_retry, option), (2, pending_intercepted_htlcs, option), @@ -14239,6 +14436,7 @@ where (15, inbound_payment_id_secret, option), (17, in_flight_monitor_updates, option), (19, peer_storage_dir, optional_vec), + (21, async_receive_offer_cache, (default_value, async_receive_offer_cache)), }); let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map()); let peer_storage_dir: Vec<(PublicKey, Vec)> = peer_storage_dir.unwrap_or_else(Vec::new); @@ -14925,6 +15123,8 @@ where chain_hash, best_block, our_network_pubkey, highest_seen_timestamp, expanded_inbound_key, secp_ctx.clone(), args.message_router + ).with_async_payments_offers_cache( + async_receive_offer_cache, &args.default_config.paths_to_static_invoice_server[..] ); let channel_manager = ChannelManager { diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 3ff2a045d28..15130d0a64f 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1211,6 +1211,7 @@ macro_rules! reload_node { $new_channelmanager = _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!($node, $crate::util::config::UserConfig::default(), $chanman_encoded, $monitors_encoded, $persister, $new_chain_monitor, $new_channelmanager); diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 51146c1b6f1..a7d45b896a9 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -214,7 +214,7 @@ pub fn create_from_hash( } #[cfg(async_payments)] -pub(super) fn create_for_spontaneous_payment( +pub(crate) fn create_for_spontaneous_payment( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option, ) -> Result { diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 5c3bfd48d55..d09790ffbca 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -31,7 +31,7 @@ use crate::util::ser::{VecWriter, Writeable, Writer}; use crate::ln::peer_channel_encryptor::{PeerChannelEncryptor, NextNoiseStep, MessageBuf, MSG_BUF_ALLOC_SIZE}; use crate::ln::wire; use crate::ln::wire::{Encode, Type}; -use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc}; +use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, ReleaseHeldHtlc, StaticInvoicePersisted}; use crate::onion_message::dns_resolution::{DNSResolverMessageHandler, DNSResolverMessage, DNSSECProof, DNSSECQuery}; use crate::onion_message::messenger::{CustomOnionMessageHandler, Responder, ResponseInstruction, MessageSendInstructions}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; @@ -150,6 +150,23 @@ impl OffersMessageHandler for IgnoringMessageHandler { } } impl AsyncPaymentsMessageHandler for IgnoringMessageHandler { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + None + } + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + None + } + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, + _responder: Option, + ) {} + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) {} fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, _responder: Option, diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs new file mode 100644 index 00000000000..8a957b66c51 --- /dev/null +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -0,0 +1,443 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and methods for caching offers that we interactively build with a static invoice +//! server as an async recipient. The static invoice server will serve the resulting invoices to +//! payers on our behalf when we're offline. + +use crate::io; +use crate::io::Read; +use crate::ln::msgs::DecodeError; +use crate::offers::nonce::Nonce; +use crate::offers::offer::Offer; +use crate::onion_message::messenger::Responder; +use crate::prelude::*; +use crate::util::ser::{Readable, Writeable, Writer}; +use core::time::Duration; +#[cfg(async_payments)] +use { + crate::blinded_path::message::AsyncPaymentsContext, crate::offers::offer::OfferId, + crate::onion_message::async_payments::OfferPaths, +}; + +struct AsyncReceiveOffer { + offer: Offer, + /// We determine whether an offer is expiring "soon" based on how far the offer is into its total + /// lifespan, using this field. + offer_created_at: Duration, + + /// The below fields are used to generate and persist a new static invoice with the invoice + /// server, if the invoice is expiring prior to the corresponding offer. We support automatically + /// rotating the invoice for long-lived offers so users don't have to update the offer they've + /// posted on e.g. their website if fees change or the invoices' payment paths become otherwise + /// outdated. + offer_nonce: Nonce, + update_static_invoice_path: Responder, + static_invoice_absolute_expiry: Duration, + invoice_update_attempts: u8, +} + +impl_writeable_tlv_based!(AsyncReceiveOffer, { + (0, offer, required), + (2, offer_nonce, required), + (4, offer_created_at, required), + (6, update_static_invoice_path, required), + (8, static_invoice_absolute_expiry, required), + (10, invoice_update_attempts, (static_value, 0)), +}); + +/// If we are an often-offline recipient, we'll want to interactively build offers and static +/// invoices with an always-online node that will serve those static invoices to payers on our +/// behalf when we are offline. +/// +/// This struct is used to cache those interactively built offers, and should be passed into +/// [`OffersMessageFlow`] on startup as well as persisted whenever an offer or invoice is updated +/// with the static invoice server. +/// +/// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow +pub struct AsyncReceiveOfferCache { + offers: Vec, + /// Used to limit the number of times we request paths for our offer from the static invoice + /// server. + #[allow(unused)] // TODO: remove when we get rid of async payments cfg flag + offer_paths_request_attempts: u8, + /// Used to determine whether enough time has passed since our last request for offer paths that + /// more requests should be allowed to go out. + #[allow(unused)] // TODO: remove when we get rid of async payments cfg flag + last_offer_paths_request_timestamp: Duration, +} + +impl AsyncReceiveOfferCache { + /// Creates an empty [`AsyncReceiveOfferCache`] to be passed into [`OffersMessageFlow`]. + /// + /// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow + pub fn new() -> Self { + Self { + offers: Vec::new(), + offer_paths_request_attempts: 0, + last_offer_paths_request_timestamp: Duration::from_secs(0), + } + } +} + +// The target number of offers we want to have cached at any given time, to mitigate too much +// reuse of the same offer. +#[cfg(async_payments)] +const NUM_CACHED_OFFERS_TARGET: usize = 3; + +// Refuse to store offers if they will exceed the maximum cache size or the maximum number of +// offers. +#[cfg(async_payments)] +const MAX_CACHE_SIZE: usize = (1 << 10) * 70; // 70KiB +#[cfg(async_payments)] +const MAX_OFFERS: usize = 100; + +// The max number of times we'll attempt to request offer paths or attempt to refresh a static +// invoice before giving up. +#[cfg(async_payments)] +const MAX_UPDATE_ATTEMPTS: u8 = 3; + +// If we run out of attempts to request offer paths from the static invoice server, we'll stop +// sending requests for some time. After this amount of time has passed, more requests are allowed +// to be sent out. +#[cfg(async_payments)] +const PATHS_REQUESTS_RESET_INTERVAL: Duration = Duration::from_secs(3 * 60 * 60); + +// If an offer is 90% of the way through its lifespan, it's expiring soon. This allows us to be +// flexible for various offer lifespans, i.e. an offer that lasts 10 days expires soon after 9 days +// and an offer that lasts 10 years expires soon after 9 years. +#[cfg(async_payments)] +const OFFER_EXPIRES_SOON_THRESHOLD_PERCENT: u64 = 90; + +#[cfg(async_payments)] +const MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = 3 * 60 * 60; + +#[cfg(all(test, async_payments))] +pub(crate) const TEST_NUM_CACHED_OFFERS_TARGET: usize = NUM_CACHED_OFFERS_TARGET; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MAX_OFFERS: usize = MAX_OFFERS; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MAX_CACHE_SIZE: usize = MAX_CACHE_SIZE; +#[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_PATHS_REQUESTS_RESET_INTERVAL: Duration = PATHS_REQUESTS_RESET_INTERVAL; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_OFFER_EXPIRES_SOON_THRESHOLD_PERCENT: u64 = + OFFER_EXPIRES_SOON_THRESHOLD_PERCENT; +#[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 our cached [`Offer`]s for receiving async payments as an often-offline recipient. + pub fn offers(&self, duration_since_epoch: Duration) -> Vec { + const NEVER_EXPIRES: Duration = Duration::from_secs(u64::MAX); + + self.offers + .iter() + .filter_map(|offer| { + if offer.static_invoice_absolute_expiry < duration_since_epoch { + None + } else if offer.offer.absolute_expiry().unwrap_or(NEVER_EXPIRES) + < duration_since_epoch + { + None + } else { + Some(offer.offer.clone()) + } + }) + .collect() + } + + /// Remove expired offers from the cache, returning whether new offers are needed. + pub(super) fn prune_expired_offers(&mut self, duration_since_epoch: Duration) -> bool { + // Remove expired offers from the cache. + let mut offer_was_removed = false; + self.offers.retain(|offer| { + if offer.offer.is_expired_no_std(duration_since_epoch) { + offer_was_removed = true; + return false; + } + true + }); + + // If we just removed a newly expired offer, force allowing more paths request attempts. + if offer_was_removed { + self.reset_offer_paths_request_attempts(); + } else { + // If we haven't attempted to request new paths in a long time, allow more requests to go out + // if/when needed. + self.check_reset_offer_paths_request_attempts(duration_since_epoch); + } + + self.needs_new_offers(duration_since_epoch) + && self.offer_paths_request_attempts < MAX_UPDATE_ATTEMPTS + } + + /// Returns whether the new paths we've just received from the static invoice server should be used + /// to build a new offer. + pub(super) fn should_build_offer_with_paths( + &self, message: &OfferPaths, duration_since_epoch: Duration, + ) -> bool { + if !self.needs_new_offers(duration_since_epoch) { + return false; + } + + // Require the offer that would be built using these paths to last at least a few hours. + let min_offer_paths_absolute_expiry = + duration_since_epoch.as_secs().saturating_add(MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS); + let offer_paths_absolute_expiry = + message.paths_absolute_expiry.map(|exp| exp.as_secs()).unwrap_or(u64::MAX); + if offer_paths_absolute_expiry < min_offer_paths_absolute_expiry { + return false; + } + + // Check that we don't have any current offers that already contain these paths + self.offers.iter().all(|offer| offer.offer.paths() != message.paths) + } + + /// Returns a bool indicating whether new offers are needed in the cache. + fn needs_new_offers(&self, duration_since_epoch: Duration) -> bool { + // If we have fewer than NUM_CACHED_OFFERS_TARGET offers that aren't expiring soon, indicate + // that new offers should be interactively built. + let num_unexpiring_offers = self + .offers + .iter() + .filter(|offer| { + let offer_absolute_expiry = offer.offer.absolute_expiry().unwrap_or(Duration::MAX); + let offer_created_at = offer.offer_created_at; + let offer_lifespan = + offer_absolute_expiry.saturating_sub(offer_created_at).as_secs(); + let elapsed = duration_since_epoch.saturating_sub(offer_created_at).as_secs(); + + // If an offer is in the last 10% of its lifespan, it's expiring soon. + elapsed.saturating_mul(100) + < offer_lifespan.saturating_mul(OFFER_EXPIRES_SOON_THRESHOLD_PERCENT) + }) + .count(); + + num_unexpiring_offers < NUM_CACHED_OFFERS_TARGET + } + + // Indicates that onion messages requesting new offer paths have been sent to the static invoice + // server. Calling this method allows the cache to self-limit how many requests are sent, in case + // the server goes unresponsive. + pub(super) fn new_offers_requested(&mut self, duration_since_epoch: Duration) { + self.offer_paths_request_attempts += 1; + self.last_offer_paths_request_timestamp = duration_since_epoch; + } + + /// If we haven't sent an offer paths request in a long time, reset the limit to allow more + /// requests to be sent out if/when needed. + fn check_reset_offer_paths_request_attempts(&mut self, duration_since_epoch: Duration) { + let should_reset = + self.last_offer_paths_request_timestamp.saturating_add(PATHS_REQUESTS_RESET_INTERVAL) + < duration_since_epoch; + if should_reset { + self.reset_offer_paths_request_attempts(); + } + } + + fn reset_offer_paths_request_attempts(&mut self) { + self.offer_paths_request_attempts = 0; + self.last_offer_paths_request_timestamp = Duration::from_secs(0); + } + + #[cfg(test)] + pub(super) fn test_reset_offer_paths_request_attempts(&mut self) { + self.reset_offer_paths_request_attempts() + } + + /// Returns an iterator over the list of cached offers where the invoice is expiring soon and we + /// need to send an updated one to the static invoice server. + pub(super) fn offers_needing_invoice_refresh( + &self, duration_since_epoch: Duration, + ) -> impl Iterator { + self.offers.iter().filter_map(move |offer| { + const ONE_DAY: Duration = Duration::from_secs(24 * 60 * 60); + + if offer.offer.is_expired_no_std(duration_since_epoch) { + return None; + } + if offer.invoice_update_attempts >= MAX_UPDATE_ATTEMPTS { + return None; + } + + let time_until_invoice_expiry = + offer.static_invoice_absolute_expiry.saturating_sub(duration_since_epoch); + let time_until_offer_expiry = offer + .offer + .absolute_expiry() + .unwrap_or_else(|| Duration::from_secs(u64::MAX)) + .saturating_sub(duration_since_epoch); + + // Update the invoice if it expires in less than a day, as long as the offer has a longer + // expiry than that. + let needs_update = time_until_invoice_expiry < ONE_DAY + && time_until_offer_expiry > time_until_invoice_expiry; + if needs_update { + Some(( + &offer.offer, + offer.offer_nonce, + offer.offer_created_at, + &offer.update_static_invoice_path, + )) + } else { + None + } + }) + } + + /// Indicates that we've sent onion messages attempting to update the static invoice corresponding + /// to the provided offer_id. Calling this method allows the cache to self-limit how many invoice + /// update requests are sent. + /// + /// Errors if the offer corresponding to the provided offer_id could not be found. + pub(super) fn increment_invoice_update_attempts( + &mut self, offer_id: OfferId, + ) -> Result<(), ()> { + match self.offers.iter_mut().find(|offer| offer.offer.id() == offer_id) { + Some(offer) => { + offer.invoice_update_attempts += 1; + Ok(()) + }, + None => return Err(()), + } + } + + /// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice + /// server, which indicates that a new offer was persisted by the server and they are ready to + /// serve the corresponding static invoice to payers on our behalf. + /// + /// Returns a bool indicating whether an offer was added/updated and re-persistence of the cache + /// is needed. + pub(super) fn static_invoice_persisted( + &mut self, context: AsyncPaymentsContext, duration_since_epoch: Duration, + ) -> bool { + let ( + candidate_offer, + candidate_offer_nonce, + offer_created_at, + update_static_invoice_path, + static_invoice_absolute_expiry, + ) = match context { + AsyncPaymentsContext::StaticInvoicePersisted { + offer, + offer_nonce, + offer_created_at, + update_static_invoice_path, + static_invoice_absolute_expiry, + .. + } => ( + offer, + offer_nonce, + offer_created_at, + update_static_invoice_path, + static_invoice_absolute_expiry, + ), + _ => return false, + }; + + if candidate_offer.is_expired_no_std(duration_since_epoch) { + return false; + } + if static_invoice_absolute_expiry < duration_since_epoch { + return false; + } + + // If the candidate offer is known, either this is a duplicate message or we updated the + // corresponding static invoice that is stored with the server. + if let Some(existing_offer) = + self.offers.iter_mut().find(|cached_offer| cached_offer.offer == candidate_offer) + { + // The blinded path used to update the static invoice corresponding to an offer should never + // change because we reuse the same path every time we update. + debug_assert_eq!(existing_offer.update_static_invoice_path, update_static_invoice_path); + debug_assert_eq!(existing_offer.offer_nonce, candidate_offer_nonce); + + let needs_persist = + existing_offer.static_invoice_absolute_expiry != static_invoice_absolute_expiry; + + // Since this is the most recent update we've received from the static invoice server, assume + // that the invoice that was just persisted is the only invoice that the server has stored + // corresponding to this offer. + existing_offer.static_invoice_absolute_expiry = static_invoice_absolute_expiry; + existing_offer.invoice_update_attempts = 0; + + return needs_persist; + } + + let candidate_offer = AsyncReceiveOffer { + offer: candidate_offer, + offer_nonce: candidate_offer_nonce, + offer_created_at, + update_static_invoice_path, + static_invoice_absolute_expiry, + invoice_update_attempts: 0, + }; + + // If we have room in the cache, go ahead and add this new offer so we have more options. We + // should generally never get close to the cache limit because we limit the number of requests + // for offer persistence that are sent to begin with. + let candidate_cache_size = + self.serialized_length().saturating_add(candidate_offer.serialized_length()); + if self.offers.len() < MAX_OFFERS && candidate_cache_size <= MAX_CACHE_SIZE { + self.offers.push(candidate_offer); + return true; + } + + // Swap out our lowest expiring offer for this candidate offer if needed. Otherwise we'd be + // risking a situation where all of our existing offers expire soon but we still ignore this one + // even though it's fresh. + const NEVER_EXPIRES: Duration = Duration::from_secs(u64::MAX); + let (soonest_expiring_offer_idx, soonest_offer_expiry) = self + .offers + .iter() + .map(|offer| offer.offer.absolute_expiry().unwrap_or(NEVER_EXPIRES)) + .enumerate() + .min_by(|(_, offer_exp_a), (_, offer_exp_b)| offer_exp_a.cmp(offer_exp_b)) + .unwrap_or_else(|| { + debug_assert!(false); + (0, NEVER_EXPIRES) + }); + + if soonest_offer_expiry < candidate_offer.offer.absolute_expiry().unwrap_or(NEVER_EXPIRES) { + self.offers[soonest_expiring_offer_idx] = candidate_offer; + return true; + } + + false + } +} + +impl Writeable for AsyncReceiveOfferCache { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + write_tlv_fields!(w, { + (0, self.offers, required_vec), + // offer paths request retry info always resets on restart + }); + Ok(()) + } +} + +impl Readable for AsyncReceiveOfferCache { + fn read(r: &mut R) -> Result { + _init_and_read_len_prefixed_tlv_fields!(r, { + (0, offers, required_vec), + }); + let offers: Vec = offers; + Ok(Self { + offers, + offer_paths_request_attempts: 0, + last_offer_paths_request_timestamp: Duration::from_secs(0), + }) + } +} diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 38f674141b1..964fcc4c821 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -36,6 +36,7 @@ use crate::ln::channelmanager::{ Verification, {PaymentId, CLTV_FAR_FAR_AWAY, MAX_SHORT_LIVED_RELATIVE_EXPIRY}, }; use crate::ln::inbound_payment; +use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; use crate::offers::invoice::{ Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY, @@ -56,6 +57,7 @@ use crate::routing::router::Router; use crate::sign::{EntropySource, NodeSigner}; use crate::sync::{Mutex, RwLock}; use crate::types::payment::{PaymentHash, PaymentSecret}; +use crate::util::ser::Writeable; #[cfg(async_payments)] use { @@ -63,8 +65,15 @@ use { crate::blinded_path::payment::AsyncBolt12OfferContext, crate::offers::offer::Amount, crate::offers::signer, - crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}, - crate::onion_message::async_payments::HeldHtlcAvailable, + crate::offers::static_invoice::{ + StaticInvoice, StaticInvoiceBuilder, + DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, + }, + crate::onion_message::async_payments::{ + HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, + StaticInvoicePersisted, + }, + crate::onion_message::messenger::Responder, }; #[cfg(feature = "dnssec")] @@ -98,6 +107,10 @@ where pub(crate) pending_offers_messages: Mutex>, pending_async_payments_messages: Mutex>, + async_receive_offer_cache: Mutex, + /// Blinded paths used to request offer paths from the static invoice server, if we are an async + /// recipient. + paths_to_static_invoice_server: Vec, #[cfg(feature = "dnssec")] pub(crate) hrn_resolver: OMNameResolver, @@ -133,9 +146,25 @@ where hrn_resolver: OMNameResolver::new(current_timestamp, best_block.height), #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex::new(Vec::new()), + + async_receive_offer_cache: Mutex::new(AsyncReceiveOfferCache::new()), + paths_to_static_invoice_server: Vec::new(), } } + /// If we are an async recipient, on startup we'll interactively build offers and static invoices + /// with an always-online node that will serve static invoices on our behalf. Once the offer is + /// built and the static invoice is confirmed as persisted by the server, the underlying + /// [`AsyncReceiveOfferCache`] should be persisted so we remember the offers we've built. + pub(crate) fn with_async_payments_offers_cache( + mut self, async_receive_offer_cache: AsyncReceiveOfferCache, + paths_to_static_invoice_server: &[BlindedMessagePath], + ) -> Self { + self.async_receive_offer_cache = Mutex::new(async_receive_offer_cache); + self.paths_to_static_invoice_server = paths_to_static_invoice_server.to_vec(); + self + } + /// Gets the node_id held by this [`OffersMessageFlow`]` fn get_our_node_id(&self) -> PublicKey { self.our_network_pubkey @@ -195,10 +224,74 @@ 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(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 1 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. + /// + /// ## 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 includes these paths in their [`UserConfig::paths_to_static_invoice_server`] + /// 3. Async recipient automatically sends [`OfferPathsRequest`]s to the server over the + /// configured paths, and uses the paths from the server's [`OfferPaths`] response to build + /// their async receive offer + /// + /// If `relative_expiry` is unset, the [`BlindedMessagePath`]s expiry will default to + /// [`DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY`]. + /// + /// Returns the paths to be included in the recipient's + /// [`UserConfig::paths_to_static_invoice_server`] as well as a nonce that uniquely identifies the + /// recipient that has been configured with these paths. // TODO link to events that surface this nonce + /// + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + /// [`DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY`]: crate::onion_message::async_payments::DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY + #[cfg(async_payments)] + pub fn blinded_paths_for_async_recipient( + &self, peers: Vec, relative_expiry: Option, entropy: ES, + ) -> Result<(Vec, Nonce), ()> + where + ES::Target: EntropySource, + { + let expanded_key = &self.inbound_payment_key; + + let path_absolute_expiry = relative_expiry + .unwrap_or(Duration::from_secs(u64::MAX)) + .saturating_add(self.duration_since_epoch()); + + let recipient_id_nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_offer_paths_request_context(recipient_id_nonce, expanded_key); + + let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPathsRequest { + recipient_id_nonce, + hmac, + path_absolute_expiry, + }); + self.create_blinded_paths(peers, context).map(|paths| (paths, recipient_id_nonce)) + } + /// Creates a collection of blinded paths by delegating to [`MessageRouter`] based on /// the path's intended lifetime. /// @@ -337,6 +430,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_nonce` and calling + /// `OffersMessageFlow::enqueue_static_invoice`. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + // TODO: if the server stores multiple invoices on behalf of the recipient, how to narrow down + // which one is being requested? + SendStaticInvoice { + /// An identifier for the async recipient for whom we are serving [`StaticInvoice`]s. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + recipient_id_nonce: Nonce, + }, +} + impl OffersMessageFlow where MR::Target: MessageRouter, @@ -354,13 +467,31 @@ 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_nonce, + nonce, + hmac, + path_absolute_expiry, + }) => { + // TODO: vet invreq more? + if signer::verify_async_recipient_invreq_context(nonce, hmac, expanded_key).is_err() + { + return Err(()); + } + if path_absolute_expiry < self.duration_since_epoch() { + return Err(()); + } + + return Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id_nonce }); + }, _ => return Err(()), }; @@ -371,7 +502,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, @@ -981,6 +1112,28 @@ where Ok(()) } + /// Forwards a [`StaticInvoice`] that was previously persisted by us from an + /// [`Event::PersistStaticInvoice`], in response to an [`Event::StaticInvoiceRequested`]. + #[cfg(async_payments)] + pub 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); + // TODO include reply path for invoice error + 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`]. /// @@ -1082,4 +1235,487 @@ where ) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap()) } + + /// Retrieve our cached [`Offer`]s for receiving async payments as an often-offline recipient. + /// Will only be set if [`UserConfig::paths_to_static_invoice_server`] is set and we succeeded in + /// interactively building a [`StaticInvoice`] with the static invoice server. + #[cfg(async_payments)] + pub(crate) fn get_cached_async_receive_offers(&self) -> Vec { + self.async_receive_offer_cache.lock().unwrap().offers(self.duration_since_epoch()) + } + + /// 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. + /// + /// # Usage + /// + /// This method should be called on peer connection and every few minutes or so, to keep the + /// offers cache updated. + /// + /// Errors if we failed to create blinded reply paths when sending an [`OfferPathsRequest`] message. + #[cfg(async_payments)] + pub(crate) fn check_refresh_async_receive_offers( + &self, peers: Vec, usable_channels: Vec, entropy: ES, + router: R, + ) -> Result<(), ()> + where + ES::Target: EntropySource, + R::Target: Router, + { + // Terminate early if this node does not intend to receive async payments. + if self.paths_to_static_invoice_server.is_empty() { + return Ok(()); + } + + let expanded_key = &self.inbound_payment_key; + let duration_since_epoch = self.duration_since_epoch(); + + // Update the cache to remove expired offers, and check to see whether we need new offers to be + // interactively built with the static invoice server. + let needs_new_offers = self + .async_receive_offer_cache + .lock() + .unwrap() + .prune_expired_offers(duration_since_epoch); + + // If we need new offers, send out offer paths request messages to the static invoice server. + if needs_new_offers { + let nonce = Nonce::from_entropy_source(&*entropy); + let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths { + nonce, + hmac: signer::hmac_for_offer_paths_context(nonce, expanded_key), + path_absolute_expiry: duration_since_epoch + .saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY), + }); + let reply_paths = match self.create_blinded_paths(peers.clone(), context) { + Ok(paths) => paths, + Err(()) => { + return Err(()); + }, + }; + + // We can't fail past this point, so indicate to the cache that we've requested new offers. + self.async_receive_offer_cache + .lock() + .unwrap() + .new_offers_requested(duration_since_epoch); + + let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest {}); + enqueue_onion_message_with_reply_paths( + message, + &self.paths_to_static_invoice_server[..], + reply_paths, + &mut self.pending_async_payments_messages.lock().unwrap(), + ); + } + + self.check_refresh_static_invoices(peers, usable_channels, entropy, router); + + Ok(()) + } + + /// If a static invoice server has persisted an offer for us but the corresponding invoice is + /// expiring soon, we need to refresh that invoice. Here we enqueue the onion messages that will + /// be used to request invoice refresh, based on the offers provided by the cache. + #[cfg(async_payments)] + fn check_refresh_static_invoices( + &self, peers: Vec, usable_channels: Vec, entropy: ES, + router: R, + ) where + ES::Target: EntropySource, + R::Target: Router, + { + let duration_since_epoch = self.duration_since_epoch(); + + let mut serve_static_invoice_messages = Vec::new(); + { + let cache = self.async_receive_offer_cache.lock().unwrap(); + for offer_and_metadata in cache.offers_needing_invoice_refresh(duration_since_epoch) { + let (offer, offer_nonce, offer_created_at, update_static_invoice_path) = + offer_and_metadata; + let offer_id = offer.id(); + + let (serve_invoice_msg, reply_path_ctx) = match self + .create_serve_static_invoice_message( + offer.clone(), + offer_nonce, + offer_created_at, + peers.clone(), + usable_channels.clone(), + update_static_invoice_path.clone(), + &*entropy, + &*router, + ) { + Ok((msg, ctx)) => (msg, ctx), + Err(()) => continue, + }; + serve_static_invoice_messages.push(( + serve_invoice_msg, + update_static_invoice_path.clone(), + reply_path_ctx, + offer_id, + )); + } + } + + // Enqueue the new serve_static_invoice messages in a separate loop to avoid holding the offer + // cache lock and the pending_async_payments_messages lock at the same time. + for (serve_invoice_msg, serve_invoice_path, reply_path_ctx, offer_id) in + serve_static_invoice_messages + { + let context = MessageContext::AsyncPayments(reply_path_ctx); + let reply_paths = match self.create_blinded_paths(peers.clone(), context) { + Ok(paths) => paths, + Err(()) => continue, + }; + + { + // We can't fail past this point, so indicate to the cache that we've requested an invoice + // update. + let mut cache = self.async_receive_offer_cache.lock().unwrap(); + if cache.increment_invoice_update_attempts(offer_id).is_err() { + continue; + } + } + + let message = AsyncPaymentsMessage::ServeStaticInvoice(serve_invoice_msg); + enqueue_onion_message_with_reply_paths( + message, + &[serve_invoice_path.into_reply_path()], + reply_paths, + &mut self.pending_async_payments_messages.lock().unwrap(), + ); + } + } + + /// 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: ES, + ) -> Option<(OfferPaths, MessageContext)> + where + ES::Target: EntropySource, + { + let expanded_key = &self.inbound_payment_key; + let duration_since_epoch = self.duration_since_epoch(); + + // First verify the message context to make sure we created the blinded path that this message + // was received over. + let recipient_id_nonce = match context { + AsyncPaymentsContext::OfferPathsRequest { + recipient_id_nonce, + hmac, + path_absolute_expiry, + } => { + if let Err(()) = signer::verify_offer_paths_request_context( + recipient_id_nonce, + hmac, + expanded_key, + ) { + return None; + } + if duration_since_epoch > path_absolute_expiry { + return None; + } + recipient_id_nonce + }, + _ => return None, + }; + + // Next 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 nonce = Nonce::from_entropy_source(&*entropy); + let hmac = signer::hmac_for_async_recipient_invreq_context(nonce, expanded_key); + let context = OffersContext::StaticInvoiceRequested { + recipient_id_nonce, + nonce, + hmac, + path_absolute_expiry, + }; + match self.create_blinded_paths_using_absolute_expiry( + context, + Some(path_absolute_expiry), + peers, + ) { + Ok(paths) => (paths, path_absolute_expiry), + Err(()) => return None, + } + }; + + // Finally create a reply path so that the recipient can respond to our offer_paths message with + // the static invoice that they create, that corresponds to the offer containing our paths. + let reply_path_context = { + let nonce = Nonce::from_entropy_source(entropy); + let path_absolute_expiry = + duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY); + let hmac = signer::hmac_for_serve_static_invoice_context(nonce, expanded_key); + MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice { + nonce, + recipient_id_nonce, + hmac, + path_absolute_expiry, + }) + }; + + let offer_paths_om = + OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry) }; + 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 want to use the paths we've received + /// to build and cache an async receive offer. + /// + /// Returns `None` if we have enough offers cached already, verification of `message` fails, or we + /// fail to create blinded paths. + #[cfg(async_payments)] + pub(crate) fn handle_offer_paths( + &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Responder, + peers: Vec, usable_channels: Vec, entropy: ES, + router: R, + ) -> Option<(ServeStaticInvoice, MessageContext)> + where + ES::Target: EntropySource, + R::Target: Router, + { + let expanded_key = &self.inbound_payment_key; + let duration_since_epoch = self.duration_since_epoch(); + + match context { + AsyncPaymentsContext::OfferPaths { nonce, hmac, path_absolute_expiry } => { + if let Err(()) = signer::verify_offer_paths_context(nonce, hmac, expanded_key) { + return None; + } + if duration_since_epoch > path_absolute_expiry { + return None; + } + }, + _ => return None, + } + + { + // Only respond with `ServeStaticInvoice` if we actually need a new offer built. + let cache = self.async_receive_offer_cache.lock().unwrap(); + if !cache.should_build_offer_with_paths(&message, duration_since_epoch) { + return None; + } + } + + let (mut offer_builder, offer_nonce) = + match self.create_async_receive_offer_builder(&*entropy, message.paths) { + Ok((builder, nonce)) => (builder, nonce), + Err(_) => return None, // Only reachable if OfferPaths::paths is empty + }; + if let Some(paths_absolute_expiry) = message.paths_absolute_expiry { + offer_builder = offer_builder.absolute_expiry(paths_absolute_expiry); + } + let offer = match offer_builder.build() { + Ok(offer) => offer, + Err(_) => { + debug_assert!(false); + return None; + }, + }; + + let (serve_invoice_message, reply_path_context) = match self + .create_serve_static_invoice_message( + offer, + offer_nonce, + duration_since_epoch, + peers, + usable_channels, + responder, + &*entropy, + router, + ) { + Ok((msg, context)) => (msg, context), + Err(()) => return None, + }; + + let context = MessageContext::AsyncPayments(reply_path_context); + Some((serve_invoice_message, context)) + } + + /// Creates a [`ServeStaticInvoice`] onion message, including reply path context for the static + /// invoice server to respond with [`StaticInvoicePersisted`]. + /// + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + #[cfg(async_payments)] + fn create_serve_static_invoice_message( + &self, offer: Offer, offer_nonce: Nonce, offer_created_at: Duration, + peers: Vec, usable_channels: Vec, + update_static_invoice_path: Responder, entropy: ES, router: R, + ) -> Result<(ServeStaticInvoice, AsyncPaymentsContext), ()> + where + ES::Target: EntropySource, + R::Target: Router, + { + let expanded_key = &self.inbound_payment_key; + let duration_since_epoch = self.duration_since_epoch(); + let secp_ctx = &self.secp_ctx; + + let offer_relative_expiry = offer + .absolute_expiry() + .map(|exp| exp.saturating_sub(duration_since_epoch)) + .unwrap_or_else(|| Duration::from_secs(u64::MAX)); + + // We limit the static invoice lifetime to STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, meaning we'll + // need to refresh the static invoice using the reply path to the `OfferPaths` message if the + // offer expires later than that. + let static_invoice_relative_expiry = core::cmp::min( + offer_relative_expiry.as_secs(), + STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY.as_secs(), + ) as u32; + + let payment_secret = inbound_payment::create_for_spontaneous_payment( + expanded_key, + None, // The async receive offers we create are always amount-less + static_invoice_relative_expiry, + self.duration_since_epoch().as_secs(), + None, + )?; + + let invoice = self + .create_static_invoice_builder( + &router, + &*entropy, + &offer, + offer_nonce, + payment_secret, + static_invoice_relative_expiry, + usable_channels, + peers.clone(), + ) + .and_then(|builder| builder.build_and_sign(secp_ctx)) + .map_err(|_| ())?; + + let nonce = Nonce::from_entropy_source(&*entropy); + let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce }); + let forward_invoice_request_path = self + .create_blinded_paths(peers, context) + .and_then(|paths| paths.into_iter().next().ok_or(()))?; + + let reply_path_context = { + let nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_static_invoice_persisted_context(nonce, expanded_key); + let static_invoice_absolute_expiry = + invoice.created_at().saturating_add(invoice.relative_expiry()); + let path_absolute_expiry = + duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY); + AsyncPaymentsContext::StaticInvoicePersisted { + offer, + offer_nonce, + offer_created_at, + update_static_invoice_path, + static_invoice_absolute_expiry, + nonce, + hmac, + path_absolute_expiry, + } + }; + + Ok((ServeStaticInvoice { invoice, forward_invoice_request_path }, reply_path_context)) + } + + /// 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. + /// + /// If verification succeeds, the provided [`ServeStaticInvoice::invoice`] should be persisted + /// keyed by [`ServeStaticInvoice::recipient_id_nonce`]. The invoice should then be served in + /// response to incoming [`InvoiceRequest`]s that have a context of + /// [`OffersContext::StaticInvoiceRequested`], where the + /// [`OffersContext::StaticInvoiceRequested::recipient_id_nonce`] matches the `recipient_id_nonce` + /// from the original [`ServeStaticInvoice`] message. + /// + /// Once the invoice is persisted, [`Self::static_invoice_persisted`] must be called to confirm to + /// the recipient that their offer is ready to receive async payments. + /// + /// [`ServeStaticInvoice::invoice`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice + #[cfg(async_payments)] + pub(crate) fn verify_serve_static_invoice_message( + &self, message: &ServeStaticInvoice, context: AsyncPaymentsContext, + ) -> Result { + if message.invoice.is_expired_no_std(self.duration_since_epoch()) { + return Err(()); + } + let expanded_key = &self.inbound_payment_key; + match context { + AsyncPaymentsContext::ServeStaticInvoice { + recipient_id_nonce, + nonce, + hmac, + path_absolute_expiry, + } => { + signer::verify_serve_static_invoice_context(nonce, hmac, expanded_key)?; + if self.duration_since_epoch() > path_absolute_expiry { + return Err(()); + } + + return Ok(recipient_id_nonce); + }, + _ => 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(crate) fn serving_static_invoice(&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. + /// + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + #[cfg(async_payments)] + pub(crate) fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool { + let expanded_key = &self.inbound_payment_key; + let duration_since_epoch = self.duration_since_epoch(); + + if let AsyncPaymentsContext::StaticInvoicePersisted { + nonce, + hmac, + path_absolute_expiry, + .. + } = context + { + if let Err(()) = + signer::verify_static_invoice_persisted_context(nonce, hmac, expanded_key) + { + return false; + } + + if duration_since_epoch > path_absolute_expiry { + return false; + } + } else { + return false; + } + + let mut cache = self.async_receive_offer_cache.lock().unwrap(); + cache.static_invoice_persisted(context, duration_since_epoch) + } + + /// Get the `AsyncReceiveOfferCache` for persistence. + pub(crate) fn writeable_async_receive_offer_cache(&self) -> impl Writeable + '_ { + &self.async_receive_offer_cache + } + + #[cfg(all(test, async_payments))] + pub(crate) fn test_reset_more_offer_paths_request_attempts(&self) { + let mut cache = self.async_receive_offer_cache.lock().unwrap(); + cache.test_reset_offer_paths_request_attempts(); + } } diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 3615850a22e..6a9096338ff 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/mod.rs b/lightning/src/offers/mod.rs index cf078ed0e67..b603deecd60 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -16,6 +16,7 @@ pub mod offer; pub mod flow; +pub(crate) mod async_receive_offer_cache; pub mod invoice; pub mod invoice_error; mod invoice_macros; diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 329b90d2076..9ba94d9d007 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -55,6 +55,31 @@ const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16]; #[cfg(async_payments)] const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16]; +// HMAC input used in `AsyncPaymentsContext::OfferPaths` to authenticate inbound offer_paths onion +// messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_OFFER_PATHS_INPUT: &[u8; 16] = &[10; 16]; + +// HMAC input used in `AsyncPaymentsContext::StaticInvoicePersisted` to authenticate inbound +// static_invoice_persisted onion messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT: &[u8; 16] = &[11; 16]; + +/// HMAC input used in `AsyncPaymentsContext::OfferPathsRequest` to authenticate inbound +/// offer_paths_request onion messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_OFFER_PATHS_REQUEST_INPUT: &[u8; 16] = &[12; 16]; + +/// HMAC input used in `OffersContext::StaticInvoiceRequested` to authenticate inbound invoice +/// requests that are being serviced on behalf of async recipients. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_INVREQ: &[u8; 16] = &[13; 16]; + +/// HMAC input used in `AsyncPaymentsContext::ServeStaticInvoice` to authenticate inbound +/// serve_static_invoice onion messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_SERVE_STATIC_INVOICE_INPUT: &[u8; 16] = &[14; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -570,3 +595,123 @@ pub(crate) fn verify_held_htlc_available_context( Err(()) } } + +#[cfg(async_payments)] +pub(crate) fn hmac_for_offer_paths_request_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Paths Please"; // TODO + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_OFFER_PATHS_REQUEST_INPUT); + + Hmac::from_engine(hmac) +} + +#[cfg(async_payments)] +pub(crate) fn verify_offer_paths_request_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_offer_paths_request_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} + +#[cfg(async_payments)] +pub(crate) fn hmac_for_offer_paths_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer Paths~"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_OFFER_PATHS_INPUT); + + Hmac::from_engine(hmac) +} + +#[cfg(async_payments)] +pub(crate) fn verify_offer_paths_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_offer_paths_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} + +#[cfg(async_payments)] +pub(crate) fn hmac_for_serve_static_invoice_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Serve Inv~~~"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_SERVE_STATIC_INVOICE_INPUT); + + Hmac::from_engine(hmac) +} + +#[cfg(async_payments)] +pub(crate) fn verify_serve_static_invoice_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_serve_static_invoice_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} + +#[cfg(async_payments)] +pub(crate) fn hmac_for_static_invoice_persisted_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK InvPersisted"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT); + + Hmac::from_engine(hmac) +} + +#[cfg(async_payments)] +pub(crate) fn verify_static_invoice_persisted_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_static_invoice_persisted_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} + +#[cfg(async_payments)] +pub(crate) fn hmac_for_async_recipient_invreq_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Async Invreq"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_INVREQ); + + Hmac::from_engine(hmac) +} + +#[cfg(async_payments)] +pub(crate) fn verify_async_recipient_invreq_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_async_recipient_invreq_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} 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/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 7a473c90e8f..2dd9bbff284 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -9,15 +9,22 @@ //! Message handling for async payments. -use crate::blinded_path::message::AsyncPaymentsContext; +use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath}; use crate::io; use crate::ln::msgs::DecodeError; +use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; use crate::onion_message::packet::OnionMessageContents; use crate::prelude::*; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; +use core::time::Duration; + // TLV record types for the `onionmsg_tlv` TLV stream as defined in BOLT 4. +const OFFER_PATHS_REQ_TLV_TYPE: u64 = 65538; +const OFFER_PATHS_TLV_TYPE: u64 = 65540; +const SERVE_INVOICE_TLV_TYPE: u64 = 65542; +const INVOICE_PERSISTED_TLV_TYPE: u64 = 65544; const HELD_HTLC_AVAILABLE_TLV_TYPE: u64 = 72; const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74; @@ -25,6 +32,43 @@ const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74; /// /// [`OnionMessage`]: crate::ln::msgs::OnionMessage pub trait AsyncPaymentsMessageHandler { + /// Handle an [`OfferPathsRequest`] message. If we are a static invoice server and the message was + /// sent over paths that we previously provided to an async recipient via + /// [`UserConfig::paths_to_static_invoice_server`], an [`OfferPaths`] message should be returned. + /// + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + fn handle_offer_paths_request( + &self, message: OfferPathsRequest, context: AsyncPaymentsContext, + responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)>; + + /// Handle an [`OfferPaths`] message. If this is in response to an [`OfferPathsRequest`] that + /// we previously sent as an async recipient, we should build an [`Offer`] containing the + /// included [`OfferPaths::paths`] and a corresponding [`StaticInvoice`], and reply with + /// [`ServeStaticInvoice`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + fn handle_offer_paths( + &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)>; + + /// Handle a [`ServeStaticInvoice`] message. If this is in response to an [`OfferPaths`] message + /// we previously sent as a static invoice server, a [`StaticInvoicePersisted`] message should be + /// sent once the message is handled. + fn handle_serve_static_invoice( + &self, message: ServeStaticInvoice, context: AsyncPaymentsContext, + responder: Option, + ); + + /// Handle a [`StaticInvoicePersisted`] message. If this is in response to a + /// [`ServeStaticInvoice`] message we previously sent as an async recipient, then the offer we + /// generated on receipt of a previous [`OfferPaths`] message is now ready to be used for async + /// payments. + fn handle_static_invoice_persisted( + &self, message: StaticInvoicePersisted, context: AsyncPaymentsContext, + ); + /// Handle a [`HeldHtlcAvailable`] message. A [`ReleaseHeldHtlc`] should be returned to release /// the held funds. fn handle_held_htlc_available( @@ -50,6 +94,29 @@ pub trait AsyncPaymentsMessageHandler { /// [`OnionMessage`]: crate::ln::msgs::OnionMessage #[derive(Clone, Debug)] pub enum AsyncPaymentsMessage { + /// A request from an async recipient for [`BlindedMessagePath`]s, sent to a static invoice + /// server. + OfferPathsRequest(OfferPathsRequest), + + /// [`BlindedMessagePath`]s to be included in an async recipient's [`Offer::paths`], sent by a + /// static invoice server in response to an [`OfferPathsRequest`]. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + OfferPaths(OfferPaths), + + /// A request from an async recipient to a static invoice server that a [`StaticInvoice`] be + /// provided in response to [`InvoiceRequest`]s from payers. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + ServeStaticInvoice(ServeStaticInvoice), + + /// Confirmation from a static invoice server that a [`StaticInvoice`] was persisted and the + /// corresponding [`Offer`] is ready to be used to receive async payments. Sent to an async + /// recipient in response to a [`ServeStaticInvoice`] message. + /// + /// [`Offer`]: crate::offers::offer::Offer + StaticInvoicePersisted(StaticInvoicePersisted), + /// An HTLC is being held upstream for the often-offline recipient, to be released via /// [`ReleaseHeldHtlc`]. HeldHtlcAvailable(HeldHtlcAvailable), @@ -58,6 +125,57 @@ pub enum AsyncPaymentsMessage { ReleaseHeldHtlc(ReleaseHeldHtlc), } +/// A request from an async recipient for [`BlindedMessagePath`]s from a static invoice server. +/// These paths will be used in the async recipient's [`Offer::paths`], so payers can request +/// [`StaticInvoice`]s from the static invoice server. +/// +/// [`Offer::paths`]: crate::offers::offer::Offer::paths +#[derive(Clone, Debug)] +pub struct OfferPathsRequest {} + +/// [`BlindedMessagePath`]s to be included in an async recipient's [`Offer::paths`], sent by a +/// static invoice server in response to an [`OfferPathsRequest`]. +/// +/// [`Offer::paths`]: crate::offers::offer::Offer::paths +#[derive(Clone, Debug)] +pub struct OfferPaths { + /// The paths that should be included in the async recipient's [`Offer::paths`]. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + pub paths: Vec, + /// The time as duration since the Unix epoch at which the [`Self::paths`] expire. + pub paths_absolute_expiry: Option, +} + +/// A request from an async recipient to a static invoice server that a [`StaticInvoice`] be +/// provided in response to [`InvoiceRequest`]s from payers. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +#[derive(Clone, Debug)] +pub struct ServeStaticInvoice { + /// The invoice that should be served by the static invoice server. Once this invoice has been + /// persisted, the [`Responder`] accompanying this message should be used to send + /// [`StaticInvoicePersisted`] to the recipient to confirm that the offer corresponding to the + /// invoice is ready to receive async payments. + pub invoice: StaticInvoice, + /// If a static invoice server receives an [`InvoiceRequest`] for a [`StaticInvoice`], they should + /// also forward the [`InvoiceRequest`] to the async recipient so they can respond with a fresh + /// [`Bolt12Invoice`] if the recipient is online at the time. Use this path to forward the + /// [`InvoiceRequest`] to the async recipient. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + pub forward_invoice_request_path: BlindedMessagePath, +} + +/// Confirmation from a static invoice server that a [`StaticInvoice`] was persisted and the +/// corresponding [`Offer`] is ready to be used to receive async payments. Sent to an async +/// recipient in response to a [`ServeStaticInvoice`] message. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug)] +pub struct StaticInvoicePersisted {} + /// An HTLC destined for the recipient of this message is being held upstream. The reply path /// accompanying this onion message should be used to send a [`ReleaseHeldHtlc`] response, which /// will cause the upstream HTLC to be released. @@ -68,6 +186,34 @@ pub struct HeldHtlcAvailable {} #[derive(Clone, Debug)] pub struct ReleaseHeldHtlc {} +impl OnionMessageContents for OfferPaths { + fn tlv_type(&self) -> u64 { + OFFER_PATHS_TLV_TYPE + } + #[cfg(c_bindings)] + fn msg_type(&self) -> String { + "Offer Paths".to_string() + } + #[cfg(not(c_bindings))] + fn msg_type(&self) -> &'static str { + "Offer Paths" + } +} + +impl OnionMessageContents for ServeStaticInvoice { + fn tlv_type(&self) -> u64 { + SERVE_INVOICE_TLV_TYPE + } + #[cfg(c_bindings)] + fn msg_type(&self) -> String { + "Serve Static Invoice".to_string() + } + #[cfg(not(c_bindings))] + fn msg_type(&self) -> &'static str { + "Serve Static Invoice" + } +} + impl OnionMessageContents for ReleaseHeldHtlc { fn tlv_type(&self) -> u64 { RELEASE_HELD_HTLC_TLV_TYPE @@ -82,6 +228,20 @@ impl OnionMessageContents for ReleaseHeldHtlc { } } +impl_writeable_tlv_based!(OfferPathsRequest, {}); + +impl_writeable_tlv_based!(OfferPaths, { + (0, paths, required_vec), + (2, paths_absolute_expiry, option), +}); + +impl_writeable_tlv_based!(ServeStaticInvoice, { + (0, invoice, required), + (2, forward_invoice_request_path, required), +}); + +impl_writeable_tlv_based!(StaticInvoicePersisted, {}); + impl_writeable_tlv_based!(HeldHtlcAvailable, {}); impl_writeable_tlv_based!(ReleaseHeldHtlc, {}); @@ -90,7 +250,12 @@ impl AsyncPaymentsMessage { /// Returns whether `tlv_type` corresponds to a TLV record for async payment messages. pub fn is_known_type(tlv_type: u64) -> bool { match tlv_type { - HELD_HTLC_AVAILABLE_TLV_TYPE | RELEASE_HELD_HTLC_TLV_TYPE => true, + OFFER_PATHS_REQ_TLV_TYPE + | OFFER_PATHS_TLV_TYPE + | SERVE_INVOICE_TLV_TYPE + | INVOICE_PERSISTED_TLV_TYPE + | HELD_HTLC_AVAILABLE_TLV_TYPE + | RELEASE_HELD_HTLC_TLV_TYPE => true, _ => false, } } @@ -99,6 +264,10 @@ impl AsyncPaymentsMessage { impl OnionMessageContents for AsyncPaymentsMessage { fn tlv_type(&self) -> u64 { match self { + Self::OfferPathsRequest(_) => OFFER_PATHS_REQ_TLV_TYPE, + Self::OfferPaths(msg) => msg.tlv_type(), + Self::ServeStaticInvoice(msg) => msg.tlv_type(), + Self::StaticInvoicePersisted(_) => INVOICE_PERSISTED_TLV_TYPE, Self::HeldHtlcAvailable(_) => HELD_HTLC_AVAILABLE_TLV_TYPE, Self::ReleaseHeldHtlc(msg) => msg.tlv_type(), } @@ -106,6 +275,10 @@ impl OnionMessageContents for AsyncPaymentsMessage { #[cfg(c_bindings)] fn msg_type(&self) -> String { match &self { + Self::OfferPathsRequest(_) => "Offer Paths Request".to_string(), + Self::OfferPaths(msg) => msg.msg_type(), + Self::ServeStaticInvoice(msg) => msg.msg_type(), + Self::StaticInvoicePersisted(_) => "Static Invoice Persisted".to_string(), Self::HeldHtlcAvailable(_) => "Held HTLC Available".to_string(), Self::ReleaseHeldHtlc(msg) => msg.msg_type(), } @@ -113,6 +286,10 @@ impl OnionMessageContents for AsyncPaymentsMessage { #[cfg(not(c_bindings))] fn msg_type(&self) -> &'static str { match &self { + Self::OfferPathsRequest(_) => "Offer Paths Request", + Self::OfferPaths(msg) => msg.msg_type(), + Self::ServeStaticInvoice(msg) => msg.msg_type(), + Self::StaticInvoicePersisted(_) => "Static Invoice Persisted", Self::HeldHtlcAvailable(_) => "Held HTLC Available", Self::ReleaseHeldHtlc(msg) => msg.msg_type(), } @@ -122,6 +299,10 @@ impl OnionMessageContents for AsyncPaymentsMessage { impl Writeable for AsyncPaymentsMessage { fn write(&self, w: &mut W) -> Result<(), io::Error> { match self { + Self::OfferPathsRequest(message) => message.write(w), + Self::OfferPaths(message) => message.write(w), + Self::ServeStaticInvoice(message) => message.write(w), + Self::StaticInvoicePersisted(message) => message.write(w), Self::HeldHtlcAvailable(message) => message.write(w), Self::ReleaseHeldHtlc(message) => message.write(w), } @@ -131,6 +312,10 @@ impl Writeable for AsyncPaymentsMessage { impl ReadableArgs for AsyncPaymentsMessage { fn read(r: &mut R, tlv_type: u64) -> Result { match tlv_type { + OFFER_PATHS_REQ_TLV_TYPE => Ok(Self::OfferPathsRequest(Readable::read(r)?)), + OFFER_PATHS_TLV_TYPE => Ok(Self::OfferPaths(Readable::read(r)?)), + SERVE_INVOICE_TLV_TYPE => Ok(Self::ServeStaticInvoice(Readable::read(r)?)), + INVOICE_PERSISTED_TLV_TYPE => Ok(Self::StaticInvoicePersisted(Readable::read(r)?)), HELD_HTLC_AVAILABLE_TLV_TYPE => Ok(Self::HeldHtlcAvailable(Readable::read(r)?)), RELEASE_HELD_HTLC_TLV_TYPE => Ok(Self::ReleaseHeldHtlc(Readable::read(r)?)), _ => Err(DecodeError::InvalidValue), diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index b28819ee692..673696cfd11 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -9,7 +9,10 @@ //! Onion message testing and test utilities live here. -use super::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc}; +use super::async_payments::{ + AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc, + ServeStaticInvoice, StaticInvoicePersisted, +}; use super::dns_resolution::{ DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, }; @@ -91,6 +94,26 @@ impl OffersMessageHandler for TestOffersMessageHandler { struct TestAsyncPaymentsMessageHandler {} impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, + _responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + None + } + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + None + } + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, + _responder: Option, + ) { + } + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) { + } fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, _responder: Option, diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index fd98f78350e..f333ab97dfc 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -432,6 +432,12 @@ impl Responder { context: Some(context), } } + + /// Converts a [`Responder`] into its inner [`BlindedMessagePath`]. + #[cfg(async_payments)] + pub(crate) fn into_reply_path(self) -> BlindedMessagePath { + self.reply_path + } } /// Instructions for how and where to send the response to an onion message. @@ -1318,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, @@ -1532,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 @@ -1938,6 +1949,28 @@ where log_receive!(message, reply_path.is_some()); let responder = reply_path.map(Responder::new); match message { + AsyncPaymentsMessage::OfferPathsRequest(msg) => { + let response_instructions = self + .async_payments_handler + .handle_offer_paths_request(msg, context, responder); + if let Some((msg, instructions)) = response_instructions { + let _ = self.handle_onion_message_response(msg, instructions); + } + }, + AsyncPaymentsMessage::OfferPaths(msg) => { + let response_instructions = + self.async_payments_handler.handle_offer_paths(msg, context, responder); + if let Some((msg, instructions)) = response_instructions { + let _ = self.handle_onion_message_response(msg, instructions); + } + }, + AsyncPaymentsMessage::ServeStaticInvoice(msg) => { + self.async_payments_handler + .handle_serve_static_invoice(msg, context, responder); + }, + AsyncPaymentsMessage::StaticInvoicePersisted(msg) => { + self.async_payments_handler.handle_static_invoice_persisted(msg, context); + }, AsyncPaymentsMessage::HeldHtlcAvailable(msg) => { let response_instructions = self .async_payments_handler diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index 672783a127f..ecc70f2b393 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -10,8 +10,10 @@ //! Various user-configurable channel limits and settings which ChannelManager //! applies for you. +use crate::blinded_path::message::BlindedMessagePath; use crate::ln::channel::MAX_FUNDING_SATOSHIS_NO_WUMBO; use crate::ln::channelmanager::{BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT}; +use crate::prelude::*; #[cfg(fuzzing)] use crate::util::ser::Readable; @@ -884,6 +886,14 @@ pub struct UserConfig { /// /// Default value: `false` pub enable_dual_funded_channels: bool, + /// [`BlindedMessagePath`]s to reach an always-online node that will serve [`StaticInvoice`]s on + /// our behalf. Useful if we are an often-offline recipient that wants to receive async payments. + /// Payers will send [`InvoiceRequest`]s over these paths, and receive a [`StaticInvoice`] in + /// response from the always-online node. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub paths_to_static_invoice_server: Vec, } impl Default for UserConfig { @@ -898,6 +908,7 @@ impl Default for UserConfig { accept_intercept_htlcs: false, manually_handle_bolt12_invoices: false, enable_dual_funded_channels: false, + paths_to_static_invoice_server: Vec::new(), } } } @@ -918,6 +929,7 @@ impl Readable for UserConfig { accept_intercept_htlcs: Readable::read(reader)?, manually_handle_bolt12_invoices: Readable::read(reader)?, enable_dual_funded_channels: Readable::read(reader)?, + paths_to_static_invoice_server: Vec::new(), }) } } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 737a558946e..3bb47bfdb25 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1080,6 +1080,7 @@ impl_for_vec!(crate::chain::channelmonitor::ChannelMonitorUpdate); impl_for_vec!(crate::ln::channelmanager::MonitorUpdateCompletionAction); impl_for_vec!(crate::ln::channelmanager::PaymentClaimDetails); impl_for_vec!(crate::ln::msgs::SocketAddress); +impl_for_vec!(crate::blinded_path::message::BlindedMessagePath); impl_for_vec!((A, B), A, B); impl_for_vec!(SerialId); impl_for_vec!(InteractiveTxInput); diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 00b85cc1ef8..b7c82c970ac 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -319,13 +319,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()), + } } } @@ -333,6 +337,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) } @@ -340,6 +351,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) } @@ -347,6 +365,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) } }