From e8f5f906e72d973dc548360b5998b4f6f75a53fa Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 1 Jul 2025 16:32:19 +0530 Subject: [PATCH 01/11] Fixup: Remove stale documentation In earlier versions of LDK, [`Router`] implemented [`MessageRouter`], but the two have since been decoupled. The removed documentation is outdated and no longer accurate, so it has been cleaned up in this commit. --- lightning/src/ln/channelmanager.rs | 12 ++++-------- lightning/src/offers/flow.rs | 3 +-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e2dcb890fd4..c3ba755c03f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11467,8 +11467,7 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the offer based on the given /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications as well as those of the parameterized [`Router`], which implements - /// [`MessageRouter`]. + /// privacy implications. /// /// Also, uses a derived signing pubkey in the offer for recipient privacy. /// @@ -11519,8 +11518,7 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the refund based on the given /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications as well as those of the parameterized [`Router`], which implements - /// [`MessageRouter`]. + /// privacy implications. /// /// Also, uses a derived payer id in the refund for payer privacy. /// @@ -11716,8 +11714,7 @@ where /// # Privacy /// /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] - /// to construct a [`BlindedMessagePath`] for the reply path. For further privacy implications, see the - /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. + /// to construct a [`BlindedMessagePath`] for the reply path. /// /// # Limitations /// @@ -11896,8 +11893,7 @@ where /// # Privacy /// /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] - /// to construct a [`BlindedMessagePath`] for the reply path. For further privacy implications, see the - /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. + /// to construct a [`BlindedMessagePath`] for the reply path. /// /// # Limitations /// diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index a1b83260dfd..ee441e55d79 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -575,8 +575,7 @@ where /// /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the offer based on the given /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications, as well as those of the parameterized [`Router`], which implements - /// [`MessageRouter`]. + /// privacy implications. /// /// Also uses a derived signing pubkey in the offer for recipient privacy. /// From 5bc12d4edc678a12198c8ed878445422fd2ce00b Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 19 Jun 2025 23:50:09 +0530 Subject: [PATCH 02/11] Fix: Ignore SCIDs when creating full-length blinded paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `compact_paths` flag was only used to determine whether to use a compact introduction node when creating compact blinded paths. With the upcoming change to accept `MessageForwardNode` in `create_blinded_paths`, there's a risk of SCIDs being passed (and used) even when the user intends to create a full-length blinded path. This patch updates the logic in `create_blinded_paths_from_iter` to ignore SCIDs unless `compact_paths` is explicitly true—preserving correct behavior for full-length blinded paths. And while we are at it, we also update a minor article fix for the DefaultMessageRouter, and Router documentation. --- lightning/src/onion_message/messenger.rs | 6 +++++- lightning/src/routing/router.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index f73d189203b..4fe0caac6d7 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -541,7 +541,7 @@ pub trait MessageRouter { /// # Privacy /// /// Creating [`BlindedMessagePath`]s may affect privacy since, if a suitable path cannot be found, -/// it will create a one-hop path using the recipient as the introduction node if it is a announced +/// it will create a one-hop path using the recipient as the introduction node if it is an announced /// node. Otherwise, there is no way to find a path to the introduction node in order to send a /// message, and thus an `Err` is returned. pub struct DefaultMessageRouter>, L: Deref, ES: Deref> @@ -584,6 +584,10 @@ where let has_one_peer = peers.len() == 1; let mut peer_info = peers + .map(|peer| MessageForwardNode { + short_channel_id: if compact_paths { peer.short_channel_id } else { None }, + ..peer + }) // Limit to peers with announced channels unless the recipient is unannounced. .filter_map(|peer| { network_graph diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 5ad5c3e7786..d56d5747e09 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -53,7 +53,7 @@ pub use lightning_types::routing::{RouteHint, RouteHintHop}; /// # Privacy /// /// Creating [`BlindedPaymentPath`]s may affect privacy since, if a suitable path cannot be found, -/// it will create a one-hop path using the recipient as the introduction node if it is a announced +/// it will create a one-hop path using the recipient as the introduction node if it is an announced /// node. Otherwise, there is no way to find a path to the introduction node in order to send a /// payment, and thus an `Err` is returned. pub struct DefaultRouter< From d6099c330a0b4c5e0b6ca88733feaf08e0e2eca7 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 27 Mar 2025 14:27:09 +0530 Subject: [PATCH 03/11] Update `create_blinded_paths` to accept `Vec` To prepare for supporting both standard and compact blinded paths, this commit updates the `create_blinded_paths` function to take a `Vec` as input. This change ensures the function has all the information it needs to handle both types of blinded path creation. This refactor that sets the stage for upcoming enhancements. --- fuzz/src/chanmon_consistency.rs | 4 +- fuzz/src/full_stack.rs | 4 +- fuzz/src/onion_message.rs | 4 +- lightning-dns-resolver/src/lib.rs | 6 ++- lightning/src/offers/flow.rs | 1 - lightning/src/onion_message/messenger.rs | 63 ++++++------------------ lightning/src/util/test_utils.rs | 8 ++- 7 files changed, 30 insertions(+), 60 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ede579bd7d1..022dbdf4c14 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -33,7 +33,7 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hashes::Hash as TraitImport; use bitcoin::WPubkeyHash; -use lightning::blinded_path::message::{BlindedMessagePath, MessageContext}; +use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode}; use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; use lightning::chain; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; @@ -144,7 +144,7 @@ impl MessageRouter for FuzzRouter { fn create_blinded_paths( &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, - _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 970726e7005..49506bb8543 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -30,7 +30,7 @@ use bitcoin::hashes::Hash as _; use bitcoin::hex::FromHex; use bitcoin::WPubkeyHash; -use lightning::blinded_path::message::{BlindedMessagePath, MessageContext}; +use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode}; use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; use lightning::chain; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; @@ -175,7 +175,7 @@ impl MessageRouter for FuzzRouter { fn create_blinded_paths( &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, - _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 85fb5a9f513..5bed7e42d5f 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -6,7 +6,7 @@ use bitcoin::secp256k1::schnorr; use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1, SecretKey}; use lightning::blinded_path::message::{ - AsyncPaymentsContext, BlindedMessagePath, MessageContext, OffersContext, + AsyncPaymentsContext, BlindedMessagePath, MessageContext, MessageForwardNode, OffersContext, }; use lightning::blinded_path::EmptyNodeIdLookUp; use lightning::ln::inbound_payment::ExpandedKey; @@ -107,7 +107,7 @@ impl MessageRouter for TestMessageRouter { fn create_blinded_paths( &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, - _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index e3ebf992528..fcbf1bf05a4 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -159,7 +159,9 @@ mod test { use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; use bitcoin::Block; - use lightning::blinded_path::message::{BlindedMessagePath, MessageContext}; + use lightning::blinded_path::message::{ + BlindedMessagePath, MessageContext, MessageForwardNode, + }; use lightning::blinded_path::NodeIdLookUp; use lightning::events::{Event, PaymentPurpose}; use lightning::ln::channelmanager::{PaymentId, Retry}; @@ -228,7 +230,7 @@ mod test { fn create_blinded_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, _peers: Vec, secp_ctx: &Secp256k1, + context: MessageContext, _peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { let keys = KeysManager::new(&[0; 32], 42, 43); Ok(vec![BlindedMessagePath::one_hop( diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index ee441e55d79..f0b4e35708f 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -334,7 +334,6 @@ where let receive_key = self.get_receive_auth_key(); let secp_ctx = &self.secp_ctx; - let peers = peers.into_iter().map(|node| node.node_id).collect(); self.message_router .create_blinded_paths(recipient, receive_key, context, peers, secp_ctx) .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 4fe0caac6d7..58c0c832597 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -219,7 +219,7 @@ where /// # } /// # fn create_blinded_paths( /// # &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, -/// # _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1 +/// # _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1 /// # ) -> Result, ()> { /// # unreachable!() /// # } @@ -508,7 +508,7 @@ pub trait MessageRouter { /// be direct peers with the `recipient`. fn create_blinded_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()>; /// Creates compact [`BlindedMessagePath`]s to the `recipient` node. The nodes in `peers` are @@ -528,10 +528,6 @@ pub trait MessageRouter { &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { - let peers = peers - .into_iter() - .map(|MessageForwardNode { node_id, short_channel_id: _ }| node_id) - .collect(); self.create_blinded_paths(recipient, local_node_receive_key, context, peers, secp_ctx) } } @@ -563,7 +559,7 @@ where Self { network_graph, entropy_source } } - fn create_blinded_paths_from_iter< + pub(crate) fn create_blinded_paths_from_iter< I: ExactSizeIterator, T: secp256k1::Signing + secp256k1::Verification, >( @@ -692,42 +688,6 @@ where } } } - - pub(crate) fn create_blinded_paths( - network_graph: &G, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, entropy_source: &ES, - secp_ctx: &Secp256k1, - ) -> Result, ()> { - let peers = - peers.into_iter().map(|node_id| MessageForwardNode { node_id, short_channel_id: None }); - Self::create_blinded_paths_from_iter( - network_graph, - recipient, - local_node_receive_key, - context, - peers.into_iter(), - entropy_source, - secp_ctx, - false, - ) - } - - pub(crate) fn create_compact_blinded_paths( - network_graph: &G, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, entropy_source: &ES, - secp_ctx: &Secp256k1, - ) -> Result, ()> { - Self::create_blinded_paths_from_iter( - network_graph, - recipient, - local_node_receive_key, - context, - peers.into_iter(), - entropy_source, - secp_ctx, - true, - ) - } } impl>, L: Deref, ES: Deref> MessageRouter @@ -744,16 +704,17 @@ where fn create_blinded_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { - Self::create_blinded_paths( + Self::create_blinded_paths_from_iter( &self.network_graph, recipient, local_node_receive_key, context, - peers, + peers.into_iter(), &self.entropy_source, secp_ctx, + false, ) } @@ -761,14 +722,15 @@ where &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { - Self::create_compact_blinded_paths( + Self::create_blinded_paths_from_iter( &self.network_graph, recipient, local_node_receive_key, context, - peers, + peers.into_iter(), &self.entropy_source, secp_ctx, + true, ) } } @@ -1498,7 +1460,10 @@ where message_recipients .iter() .filter(|(_, peer)| matches!(peer, OnionMessageRecipient::ConnectedPeer(_))) - .map(|(node_id, _)| *node_id) + .map(|(node_id, _)| MessageForwardNode { + node_id: *node_id, + short_channel_id: None, + }) .collect::>() }; diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index c04af39d481..c85077ef14d 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -347,13 +347,17 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { fn create_blinded_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, + 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(); + let peer_override_nodes: Vec<_> = peers_override + .iter() + .map(|pk| MessageForwardNode { node_id: *pk, short_channel_id: None }) + .collect(); + peers = peer_override_nodes; } } self.inner.create_blinded_paths(recipient, local_node_receive_key, context, peers, secp_ctx) From d78d640a4fd28464423daa77f63535dcc12f25bb Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 27 Mar 2025 14:40:00 +0530 Subject: [PATCH 04/11] Introduce `NodeIdMessageRouter` and `NullMessageRouter` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To make the purpose of each `MessageRouter` implementation unambiguous, this commit sets a direction where the type of `MessageRouter` used deterministically defines the kind of blinded paths created. As a step toward this goal, two new default routers are introduced: - `NodeIdMessageRouter` – creates full-length blinded paths using the peer's node ID. - `NullMessageRouter` – intentionally creates no blinded paths. --- lightning/src/onion_message/messenger.rs | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 58c0c832597..ccb58360e70 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -735,6 +735,120 @@ where } } +/// This message router is similar to [`DefaultMessageRouter`], but it always creates +/// full-length blinded paths, using the peer's [`NodeId`]. +/// +/// This message router can only route to a directly connected [`Destination`]. +/// +/// # Privacy +/// +/// Creating [`BlindedMessagePath`]s may affect privacy since, if a suitable path cannot be found, +/// it will create a one-hop path using the recipient as the introduction node if it is an announced +/// node. Otherwise, there is no way to find a path to the introduction node in order to send a +/// message, and thus an `Err` is returned. +pub struct NodeIdMessageRouter>, L: Deref, ES: Deref> +where + L::Target: Logger, + ES::Target: EntropySource, +{ + network_graph: G, + entropy_source: ES, +} + +impl>, L: Deref, ES: Deref> NodeIdMessageRouter +where + L::Target: Logger, + ES::Target: EntropySource, +{ + /// Creates a [`NodeIdMessageRouter`] using the given [`NetworkGraph`]. + pub fn new(network_graph: G, entropy_source: ES) -> Self { + Self { network_graph, entropy_source } + } +} + +impl>, L: Deref, ES: Deref> MessageRouter + for NodeIdMessageRouter +where + L::Target: Logger, + ES::Target: EntropySource, +{ + fn find_path( + &self, sender: PublicKey, peers: Vec, destination: Destination, + ) -> Result { + DefaultMessageRouter::::find_path(&self.network_graph, sender, peers, destination) + } + + fn create_blinded_paths( + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + DefaultMessageRouter::create_blinded_paths_from_iter( + &self.network_graph, + recipient, + local_node_receive_key, + context, + peers.into_iter(), + &self.entropy_source, + secp_ctx, + false, + ) + } + + fn create_compact_blinded_paths( + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + DefaultMessageRouter::create_blinded_paths_from_iter( + &self.network_graph, + recipient, + local_node_receive_key, + context, + peers.into_iter(), + &self.entropy_source, + secp_ctx, + false, + ) + } +} + +/// A special [`MessageRouter`] that performs no routing and does not create blinded paths. +/// Its purpose is to enable the creation of [`Offer`]s and [`Refund`]s without blinded paths, +/// where the user's `node_id` is used directly as the [`Destination`]. +/// +/// # Note +/// [`NullMessageRouter`] **must not** be used as the type parameter for [`ChannelManager`], +/// since [`ChannelManager`] requires a functioning [`MessageRouter`] to create blinded paths, +/// which are necessary for constructing reply paths in onion message communication. +/// However, [`NullMessageRouter`] *can* still be passed as an argument to [`ChannelManager`] +/// methods that accept a router when blinded paths are not needed. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// [`Refund`]: crate::offers::refund::Refund +/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager +pub struct NullMessageRouter {} + +impl MessageRouter for NullMessageRouter { + fn find_path( + &self, _sender: PublicKey, _peers: Vec, _destination: Destination, + ) -> Result { + Err(()) + } + + fn create_blinded_paths( + &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, + ) -> Result, ()> { + Ok(vec![]) + } + + fn create_compact_blinded_paths( + &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, + ) -> Result, ()> { + Ok(vec![]) + } +} + /// A path for sending an [`OnionMessage`]. #[derive(Clone)] pub struct OnionMessagePath { From 26e4fe66d5ae9d46105ce8769b000fe72f9582bb Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 18 Jun 2025 18:47:01 +0530 Subject: [PATCH 05/11] Refactor: Update TestMessageRouter to enum To allow choosing different message router types for testing nodes, convert `TestMessageRouter` to an enum with variants `DefaultMessageRouter` and `NodeIdMessageRouter`. This provides better flexibility when testing various scenarios. --- lightning/src/ln/channelmanager.rs | 2 +- lightning/src/ln/functional_test_utils.rs | 94 +++++++++++++++-------- lightning/src/ln/functional_tests.rs | 2 +- lightning/src/util/test_utils.rs | 91 ++++++++++++++++++---- 4 files changed, 140 insertions(+), 49 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c3ba755c03f..e6980dfd04a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -18209,7 +18209,7 @@ pub mod bench { let scorer = RwLock::new(test_utils::TestScorer::new()); let entropy = test_utils::TestKeysInterface::new(&[0u8; 32], network); let router = test_utils::TestRouter::new(Arc::new(NetworkGraph::new(network, &logger_a)), &logger_a, &scorer); - let message_router = test_utils::TestMessageRouter::new(Arc::new(NetworkGraph::new(network, &logger_a)), &entropy); + let message_router = test_utils::TestMessageRouter::new_default(Arc::new(NetworkGraph::new(network, &logger_a)), &entropy); let mut config: UserConfig = Default::default(); config.channel_config.max_dust_htlc_exposure = MaxDustHTLCExposure::FeeRateMultiplier(5_000_000 / 253); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index a46e7c0a381..d5a8dc4fee6 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -47,7 +47,7 @@ use crate::util::scid_utils; use crate::util::ser::{ReadableArgs, Writeable}; use crate::util::test_channel_signer::SignerOp; use crate::util::test_channel_signer::TestChannelSigner; -use crate::util::test_utils; +use crate::util::test_utils::{self, TestLogger}; use crate::util::test_utils::{TestChainMonitor, TestKeysInterface, TestScorer}; use bitcoin::amount::Amount; @@ -877,7 +877,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { &self.logger, &scorer, ), - message_router: &test_utils::TestMessageRouter::new( + message_router: &test_utils::TestMessageRouter::new_default( network_graph, self.keys_manager, ), @@ -4155,49 +4155,47 @@ pub fn create_chanmon_cfgs_with_keys( chan_mon_cfgs } -pub fn create_node_cfgs<'a>( - node_count: usize, chanmon_cfgs: &'a Vec, -) -> Vec> { - create_node_cfgs_with_persisters( - node_count, - chanmon_cfgs, - chanmon_cfgs.iter().map(|c| &c.persister).collect(), - ) -} - -pub fn create_node_cfgs_with_persisters<'a>( +fn create_node_cfgs_internal<'a, F>( node_count: usize, chanmon_cfgs: &'a Vec, - persisters: Vec<&'a impl test_utils::SyncPersist>, -) -> Vec> { + persisters: Vec<&'a impl test_utils::SyncPersist>, message_router_constructor: F, +) -> Vec> +where + F: Fn( + Arc>, + &'a TestKeysInterface, + ) -> test_utils::TestMessageRouter<'a>, +{ let mut nodes = Vec::new(); for i in 0..node_count { + let cfg = &chanmon_cfgs[i]; + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &cfg.logger)); let chain_monitor = test_utils::TestChainMonitor::new( - Some(&chanmon_cfgs[i].chain_source), - &chanmon_cfgs[i].tx_broadcaster, - &chanmon_cfgs[i].logger, - &chanmon_cfgs[i].fee_estimator, + Some(&cfg.chain_source), + &cfg.tx_broadcaster, + &cfg.logger, + &cfg.fee_estimator, persisters[i], - &chanmon_cfgs[i].keys_manager, + &cfg.keys_manager, ); - let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &chanmon_cfgs[i].logger)); + let seed = [i as u8; 32]; nodes.push(NodeCfg { - chain_source: &chanmon_cfgs[i].chain_source, - logger: &chanmon_cfgs[i].logger, - tx_broadcaster: &chanmon_cfgs[i].tx_broadcaster, - fee_estimator: &chanmon_cfgs[i].fee_estimator, + chain_source: &cfg.chain_source, + logger: &cfg.logger, + tx_broadcaster: &cfg.tx_broadcaster, + fee_estimator: &cfg.fee_estimator, router: test_utils::TestRouter::new( Arc::clone(&network_graph), - &chanmon_cfgs[i].logger, - &chanmon_cfgs[i].scorer, + &cfg.logger, + &cfg.scorer, ), - message_router: test_utils::TestMessageRouter::new( + message_router: message_router_constructor( Arc::clone(&network_graph), - &chanmon_cfgs[i].keys_manager, + &cfg.keys_manager, ), chain_monitor, - keys_manager: &chanmon_cfgs[i].keys_manager, + keys_manager: &cfg.keys_manager, node_seed: seed, network_graph, override_init_features: Rc::new(RefCell::new(None)), @@ -4207,6 +4205,42 @@ pub fn create_node_cfgs_with_persisters<'a>( nodes } +pub fn create_node_cfgs<'a>( + node_count: usize, chanmon_cfgs: &'a Vec, +) -> Vec> { + let persisters = chanmon_cfgs.iter().map(|c| &c.persister).collect(); + create_node_cfgs_internal( + node_count, + chanmon_cfgs, + persisters, + test_utils::TestMessageRouter::new_default, + ) +} + +pub fn create_node_cfgs_with_persisters<'a>( + node_count: usize, chanmon_cfgs: &'a Vec, + persisters: Vec<&'a impl test_utils::SyncPersist>, +) -> Vec> { + create_node_cfgs_internal( + node_count, + chanmon_cfgs, + persisters, + test_utils::TestMessageRouter::new_default, + ) +} + +pub fn create_node_cfgs_with_node_id_message_router<'a>( + node_count: usize, chanmon_cfgs: &'a Vec, +) -> Vec> { + let persisters = chanmon_cfgs.iter().map(|c| &c.persister).collect(); + create_node_cfgs_internal( + node_count, + chanmon_cfgs, + persisters, + test_utils::TestMessageRouter::new_node_id_router, + ) +} + pub fn test_default_channel_config() -> UserConfig { let mut default_config = UserConfig::default(); // Set cltv_expiry_delta slightly lower to keep the final CLTV values inside one byte in our diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 8e411ae78b9..07ad982cb7b 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -5151,7 +5151,7 @@ pub fn test_key_derivation_params() { let router = test_utils::TestRouter::new(Arc::clone(&network_graph), &chanmon_cfgs[0].logger, &scorer); let message_router = - test_utils::TestMessageRouter::new(Arc::clone(&network_graph), &keys_manager); + test_utils::TestMessageRouter::new_default(Arc::clone(&network_graph), &keys_manager); let node = NodeCfg { chain_source: &chanmon_cfgs[0].chain_source, logger: &chanmon_cfgs[0].logger, diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index c85077ef14d..c56154e8d9f 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -34,7 +34,7 @@ use crate::ln::types::ChannelId; use crate::ln::{msgs, wire}; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::onion_message::messenger::{ - DefaultMessageRouter, Destination, MessageRouter, OnionMessagePath, + DefaultMessageRouter, Destination, MessageRouter, NodeIdMessageRouter, OnionMessagePath, }; use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId, RoutingFees}; use crate::routing::router::{ @@ -311,21 +311,49 @@ impl<'a> Drop for TestRouter<'a> { } } +pub enum TestMessageRouterInternal<'a> { + Default( + DefaultMessageRouter< + Arc>, + &'a TestLogger, + &'a TestKeysInterface, + >, + ), + NodeId( + NodeIdMessageRouter< + Arc>, + &'a TestLogger, + &'a TestKeysInterface, + >, + ), +} + pub struct TestMessageRouter<'a> { - inner: DefaultMessageRouter< - Arc>, - &'a TestLogger, - &'a TestKeysInterface, - >, + pub inner: TestMessageRouterInternal<'a>, pub peers_override: Mutex>, } impl<'a> TestMessageRouter<'a> { - pub fn new( + pub fn new_default( network_graph: Arc>, entropy_source: &'a TestKeysInterface, ) -> Self { Self { - inner: DefaultMessageRouter::new(network_graph, entropy_source), + inner: TestMessageRouterInternal::Default(DefaultMessageRouter::new( + network_graph, + entropy_source, + )), + peers_override: Mutex::new(Vec::new()), + } + } + + pub fn new_node_id_router( + network_graph: Arc>, entropy_source: &'a TestKeysInterface, + ) -> Self { + Self { + inner: TestMessageRouterInternal::NodeId(NodeIdMessageRouter::new( + network_graph, + entropy_source, + )), peers_override: Mutex::new(Vec::new()), } } @@ -342,7 +370,12 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { peers = peers_override.clone(); } } - self.inner.find_path(sender, peers, destination) + match &self.inner { + TestMessageRouterInternal::Default(inner) => { + inner.find_path(sender, peers, destination) + }, + TestMessageRouterInternal::NodeId(inner) => inner.find_path(sender, peers, destination), + } } fn create_blinded_paths( @@ -360,7 +393,22 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { peers = peer_override_nodes; } } - self.inner.create_blinded_paths(recipient, local_node_receive_key, context, peers, secp_ctx) + match &self.inner { + TestMessageRouterInternal::Default(inner) => inner.create_blinded_paths( + recipient, + local_node_receive_key, + context, + peers, + secp_ctx, + ), + TestMessageRouterInternal::NodeId(inner) => inner.create_blinded_paths( + recipient, + local_node_receive_key, + context, + peers, + secp_ctx, + ), + } } fn create_compact_blinded_paths( @@ -378,13 +426,22 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { .collect(); } } - self.inner.create_compact_blinded_paths( - recipient, - local_node_receive_key, - context, - peers, - secp_ctx, - ) + match &self.inner { + TestMessageRouterInternal::Default(inner) => inner.create_compact_blinded_paths( + recipient, + local_node_receive_key, + context, + peers, + secp_ctx, + ), + TestMessageRouterInternal::NodeId(inner) => inner.create_compact_blinded_paths( + recipient, + local_node_receive_key, + context, + peers, + secp_ctx, + ), + } } } From 1a57e63914f420b3178d163ebf95dea8c903d813 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 13 Jun 2025 17:08:11 +0530 Subject: [PATCH 06/11] Update `DefaultMessageRouter` to always create compact blinded paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reasoning: This change aligns `DefaultMessageRouter`'s default behavior with the most common and practical usage: generating compact blinded paths for BOLT12 offers. While configurability is important, most users won't read all documentation and will rely on defaults that "just work." In line with this PR's principle— "One `MessageRouter`, one type of `BlindedPath`"—the previous default would silently produce full blinded paths, even when a compact one would have been more appropriate. In typical setups (e.g., a user connected to a single LSP), the SCID alias remains stable for the channel's lifetime. Compact paths are not only sufficient in this case, but also result in smaller, more efficient offers. And if the alias becomes invalid, a pubkey-based path wouldn't help either—so compact remains the better default. In brief: This commit makes the default behavior match what users actually want. Thanks to [@TheBlueMatt](https://github.com/TheBlueMatt) for the original reasoning. **Discussion link:** [#3246 (pull request review)](https://github.com/lightningdevkit/rust-lightning/pull/3246#pullrequestreview-2898070836) --- lightning-dns-resolver/src/lib.rs | 2 +- lightning/src/ln/offers_tests.rs | 71 +++++++++++++----------- lightning/src/onion_message/messenger.rs | 18 +++++- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index fcbf1bf05a4..60f2af1ffe6 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -467,7 +467,7 @@ mod test { #[tokio::test] async fn end_to_end_test() { let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_cfgs = create_node_cfgs_with_node_id_message_router(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index ad0a8eea2aa..8bc8d902af1 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -149,6 +149,16 @@ fn resolve_introduction_node<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, path: &Blinded .unwrap() } +fn check_compact_path_introduction_node<'a, 'b, 'c>( + path: &BlindedMessagePath, + lookup_node: &Node<'a, 'b, 'c>, + expected_introduction_node: PublicKey, +) -> bool { + let introduction_node_id = resolve_introduction_node(lookup_node, path); + introduction_node_id == expected_introduction_node + && matches!(path.introduction_node(), IntroductionNode::DirectedShortChannelId(..)) +} + fn route_bolt12_payment<'a, 'b, 'c>( node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], invoice: &Bolt12Invoice ) { @@ -406,7 +416,7 @@ fn creates_short_lived_offer() { #[test] fn creates_long_lived_offer() { let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_cfgs = create_node_cfgs_with_node_id_message_router(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -470,7 +480,7 @@ fn creates_short_lived_refund() { #[test] fn creates_long_lived_refund() { let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_cfgs = create_node_cfgs_with_node_id_message_router(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -539,7 +549,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&path, alice, bob_id)); } let payment_id = PaymentId([1; 32]); @@ -569,7 +579,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&reply_path, bob, charlie_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); charlie.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -588,10 +598,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { // to Alice when she's handling the message. Therefore, either Bob or Charlie could // serve as the introduction node for the reply path back to Alice. assert!( - matches!( - reply_path.introduction_node(), - &IntroductionNode::NodeId(node_id) if node_id == bob_id || node_id == charlie_id, - ) + check_compact_path_introduction_node(&reply_path, david, bob_id) || + check_compact_path_introduction_node(&reply_path, david, charlie_id) ); route_bolt12_payment(david, &[charlie, bob, alice], &invoice); @@ -650,7 +658,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { assert_ne!(refund.payer_signing_pubkey(), david_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&path, david, charlie_id)); } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -674,8 +682,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); } - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); - + assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); @@ -708,7 +715,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); + assert!(check_compact_path_introduction_node(&path, bob, alice_id)); } let payment_id = PaymentId([1; 32]); @@ -730,7 +737,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -742,7 +749,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id)); + assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id)); route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); @@ -779,7 +786,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { assert_ne!(refund.payer_signing_pubkey(), bob_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&path, alice, bob_id)); } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -798,7 +805,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id)); + assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id)); route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); @@ -956,7 +963,7 @@ fn send_invoice_requests_with_distinct_reply_path() { assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&path, alice, bob_id)); } let payment_id = PaymentId([1; 32]); @@ -975,7 +982,7 @@ fn send_invoice_requests_with_distinct_reply_path() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (_, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&reply_path, alice, charlie_id)); // Send, extract and verify the second Invoice Request message let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -985,7 +992,7 @@ fn send_invoice_requests_with_distinct_reply_path() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (_, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id())); + assert!(check_compact_path_introduction_node(&reply_path, alice, nodes[6].node.get_our_node_id())); } /// This test checks that when multiple potential introduction nodes are available for the payee, @@ -1040,7 +1047,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), alice_id); for path in refund.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&path, alice, bob_id)); } expect_recent_payment!(alice, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1056,7 +1063,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (_, reply_path) = extract_invoice(alice, &onion_message); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&reply_path, alice, charlie_id)); // Send, extract and verify the second Invoice Request message let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1065,7 +1072,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (_, reply_path) = extract_invoice(alice, &onion_message); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id())); + assert!(check_compact_path_introduction_node(&reply_path, alice, nodes[6].node.get_our_node_id())); } /// Verifies that the invoice request message can be retried if it fails to reach the @@ -1091,7 +1098,7 @@ fn creates_and_pays_for_offer_with_retry() { assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); + assert!(check_compact_path_introduction_node(&path, bob, alice_id)); } let payment_id = PaymentId([1; 32]); bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), RouteParametersConfig::default()).unwrap(); @@ -1119,7 +1126,7 @@ fn creates_and_pays_for_offer_with_retry() { }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -1391,7 +1398,7 @@ fn fails_authentication_when_handling_invoice_request() { assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&path, alice, bob_id)); } let invalid_path = alice.node @@ -1400,7 +1407,7 @@ fn fails_authentication_when_handling_invoice_request() { .build().unwrap() .paths().first().unwrap() .clone(); - assert_eq!(invalid_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&invalid_path, alice, bob_id)); // Send the invoice request directly to Alice instead of using a blinded path. let payment_id = PaymentId([1; 32]); @@ -1421,7 +1428,7 @@ fn fails_authentication_when_handling_invoice_request() { let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id)); assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None); @@ -1451,7 +1458,7 @@ fn fails_authentication_when_handling_invoice_request() { let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id)); assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None); } @@ -1502,7 +1509,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + assert!(check_compact_path_introduction_node(&path, alice, bob_id)); } // Initiate an invoice request, but abandon tracking it. @@ -1553,7 +1560,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); charlie.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -1610,7 +1617,7 @@ fn fails_authentication_when_handling_invoice_for_refund() { assert_ne!(refund.payer_signing_pubkey(), david_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&path, david, charlie_id)); } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1644,7 +1651,7 @@ fn fails_authentication_when_handling_invoice_for_refund() { assert_ne!(refund.payer_signing_pubkey(), david_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert!(check_compact_path_introduction_node(&path, david, charlie_id)); } let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index ccb58360e70..4834cfc1ec0 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -534,6 +534,22 @@ pub trait MessageRouter { /// A [`MessageRouter`] that can only route to a directly connected [`Destination`]. /// +/// [`DefaultMessageRouter`] constructs compact [`BlindedMessagePath`]s on a best-effort basis. +/// That is, if appropriate SCID information is available for the intermediate peers, it will +/// default to creating compact paths. +/// +/// # Compact Blinded Paths +/// +/// Compact blinded paths use short channel IDs (SCIDs) instead of pubkeys, resulting in smaller +/// serialization. This is particularly useful when encoding data into space-constrained formats +/// such as QR codes. The SCID is communicated via a [`MessageForwardNode`], but may be `None` +/// to allow for graceful degradation. +/// +/// **Note:** +/// If any SCID in the blinded path becomes invalid, the entire compact blinded path may fail to route. +/// For the immediate hop, this can happen if the corresponding channel is closed. +/// For other intermediate hops, it can happen if the channel is closed or modified (e.g., due to splicing). +/// /// # Privacy /// /// Creating [`BlindedMessagePath`]s may affect privacy since, if a suitable path cannot be found, @@ -714,7 +730,7 @@ where peers.into_iter(), &self.entropy_source, secp_ctx, - false, + true, ) } From ddb1167f1f9a9c840cc7afeb6be197996098af36 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 20 Jun 2025 18:54:42 +0530 Subject: [PATCH 07/11] Update `create_offer_builder` to use `create_blinded_paths` To simplify blinded path creation and uphold the principle of "One `MessageRouter`, one `BlindedPath` type," this commit updates `create_offer_builder` to use the `create_blinded_paths` method of the `MessageRouter`. Now, when `create_offer_builder` is called, the offer will be created using the `MessageRouter` implementation that the `ChannelManager` or `OffersMessageFlow` is parameterized with. If a user wishes to create an offer with a different type of blinded path, they can explicitly use `create_offer_builder_using_router`, which allows passing a custom `MessageRouter`. The reasoning behind this change is to give users clearer, more deterministic control over the type of blinded path used in the offer. It also improves user awareness, ensuring that creating a non-default blinded path becomes an *intentional choice*. --- lightning-dns-resolver/src/lib.rs | 2 +- lightning/src/ln/channelmanager.rs | 43 +++++-- .../src/ln/max_payment_path_len_tests.rs | 2 +- lightning/src/ln/offers_tests.rs | 70 +++++------ lightning/src/offers/flow.rs | 116 ++++++++++-------- lightning/src/onion_message/messenger.rs | 4 +- 6 files changed, 134 insertions(+), 103 deletions(-) diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 60f2af1ffe6..14024b62982 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -493,7 +493,7 @@ mod test { let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); - let bs_offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + let bs_offer = nodes[1].node.create_offer_builder().unwrap().build().unwrap(); let resolvers = vec![Destination::Node(resolver_id)]; let retry = Retry::Attempts(0); let amt = 42_000; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e6980dfd04a..696ebc88f53 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2157,9 +2157,8 @@ where /// # /// # fn example(channel_manager: T) -> Result<(), Bolt12SemanticError> { /// # let channel_manager = channel_manager.get_cm(); -/// # let absolute_expiry = None; /// let offer = channel_manager -/// .create_offer_builder(absolute_expiry)? +/// .create_offer_builder()? /// # ; /// # // Needed for compiling for c_bindings /// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); @@ -11465,9 +11464,8 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// /// # Privacy /// - /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the offer based on the given - /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications. + /// Uses the [`MessageRouter`] provided to the [`ChannelManager`] at construction to build a + /// [`BlindedMessagePath`] for the offer. See those docs for privacy implications. /// /// Also, uses a derived signing pubkey in the offer for recipient privacy. /// @@ -11477,17 +11475,40 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// /// # Errors /// - /// Errors if the parameterized [`Router`] is unable to create a blinded path for the offer. + /// Errors if the parameterized [`MessageRouter`] is unable to create a blinded path for the offer. /// /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath /// [`Offer`]: crate::offers::offer::Offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - pub fn create_offer_builder( - &$self, absolute_expiry: Option - ) -> Result<$builder, Bolt12SemanticError> { - let entropy = &*$self.entropy_source; + pub fn create_offer_builder(&$self) -> Result<$builder, Bolt12SemanticError> { + let builder = $self.flow.create_offer_builder( + &*$self.entropy_source, $self.get_peers_for_blinded_path() + )?; - let builder = $self.flow.create_offer_builder(entropy, absolute_expiry, $self.get_peers_for_blinded_path())?; + Ok(builder.into()) + } + + /// Same as [`Self::create_offer_builder`], but allows specifying a custom [`MessageRouter`] + /// instead of using the [`MessageRouter`] provided to the [`ChannelManager`] at construction. + /// + /// This gives users full control over how the [`BlindedMessagePath`] is constructed, + /// including the option to omit it entirely. + /// + /// See [`Self::create_offer_builder`] for details on offer construction, privacy, and limitations. + /// + /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub fn create_offer_builder_using_router( + &$self, + router: ME, + ) -> Result<$builder, Bolt12SemanticError> + where + ME::Target: MessageRouter, + { + let builder = $self.flow.create_offer_builder_using_router( + router, &*$self.entropy_source, $self.get_peers_for_blinded_path() + )?; Ok(builder.into()) } diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index ff5053644d8..4efa105e0ad 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -517,7 +517,7 @@ fn bolt12_invoice_too_large_blinded_paths() { ), ]); - let offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + let offer = nodes[1].node.create_offer_builder().unwrap().build().unwrap(); let payment_id = PaymentId([1; 32]); let route_config = RouteParametersConfig::default(); nodes[0] diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 8bc8d902af1..a85a9704d14 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -60,7 +60,7 @@ use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::{Destination, PeeledOnion, MessageSendInstructions}; +use crate::onion_message::messenger::{Destination, MessageSendInstructions, NodeIdMessageRouter, PeeledOnion}; use crate::onion_message::offers::OffersMessage; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; @@ -307,7 +307,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone()); let offer = bob.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); @@ -323,7 +323,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone()); let offer = bob.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); @@ -374,7 +374,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = bob.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); @@ -399,11 +399,9 @@ fn creates_short_lived_offer() { let alice_id = alice.node.get_our_node_id(); let bob = &nodes[1]; - let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let offer = alice.node - .create_offer_builder(Some(absolute_expiry)).unwrap() + .create_offer_builder().unwrap() .build().unwrap(); - assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); assert!(!offer.paths().is_empty()); for path in offer.paths() { let introduction_node_id = resolve_introduction_node(bob, &path); @@ -416,7 +414,7 @@ fn creates_short_lived_offer() { #[test] fn creates_long_lived_offer() { let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs_with_node_id_message_router(2, &chanmon_cfgs); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -425,22 +423,11 @@ fn creates_long_lived_offer() { let alice = &nodes[0]; let alice_id = alice.node.get_our_node_id(); - let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY - + Duration::from_secs(1); + let router = NodeIdMessageRouter::new(alice.network_graph, alice.keys_manager); let offer = alice.node - .create_offer_builder(Some(absolute_expiry)) + .create_offer_builder_using_router(&router) .unwrap() .build().unwrap(); - assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); - assert!(!offer.paths().is_empty()); - for path in offer.paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); - } - - let offer = alice.node - .create_offer_builder(None).unwrap() - .build().unwrap(); - assert_eq!(offer.absolute_expiry(), None); assert!(!offer.paths().is_empty()); for path in offer.paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); @@ -542,7 +529,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder() .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -709,7 +696,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); @@ -832,7 +819,7 @@ fn pays_for_offer_without_blinded_paths() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .clear_paths() .amount_msats(10_000_000) .build().unwrap(); @@ -956,7 +943,7 @@ fn send_invoice_requests_with_distinct_reply_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder() .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1092,7 +1079,7 @@ fn creates_and_pays_for_offer_with_retry() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); @@ -1168,7 +1155,7 @@ fn pays_bolt12_invoice_asynchronously() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1260,7 +1247,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); @@ -1390,7 +1377,7 @@ fn fails_authentication_when_handling_invoice_request() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder() .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1402,7 +1389,7 @@ fn fails_authentication_when_handling_invoice_request() { } let invalid_path = alice.node - .create_offer_builder(None) + .create_offer_builder() .unwrap() .build().unwrap() .paths().first().unwrap() @@ -1502,7 +1489,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder() .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1698,8 +1685,7 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { disconnect_peers(alice, &[bob, charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); - let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; - match alice.node.create_offer_builder(Some(absolute_expiry)) { + match alice.node.create_offer_builder() { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -1708,9 +1694,11 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { args.send_channel_ready = (true, true); reconnect_nodes(args); + let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let offer = alice.node - .create_offer_builder(Some(absolute_expiry)).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) + .absolute_expiry(absolute_expiry) .build().unwrap(); let payment_id = PaymentId([1; 32]); @@ -1813,7 +1801,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { let bob = &nodes[1]; let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .clear_chains() .chain(Network::Signet) .build().unwrap(); @@ -1872,7 +1860,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1906,7 +1894,7 @@ fn fails_creating_invoice_request_with_duplicate_payment_id() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1992,7 +1980,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_offer() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -2201,7 +2189,7 @@ fn fails_paying_invoice_with_unknown_required_features() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -2280,7 +2268,7 @@ fn rejects_keysend_to_non_static_invoice_path() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); // First pay the offer and save the payment preimage and invoice. - let offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + let offer = nodes[1].node.create_offer_builder().unwrap().build().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0].node.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), RouteParametersConfig::default()).unwrap(); @@ -2360,7 +2348,7 @@ fn no_double_pay_with_stale_channelmanager() { let amt_msat = nodes[0].node.list_usable_channels()[0].next_outbound_htlc_limit_msat + 1; // Force MPP let offer = nodes[1].node - .create_offer_builder(None).unwrap() + .create_offer_builder().unwrap() .clear_paths() .amount_msats(amt_msat) .build().unwrap(); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index f0b4e35708f..59f0638e46e 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -565,6 +565,37 @@ where } } + fn create_offer_builder_intern( + &self, entropy_source: ES, make_paths: PF, + ) -> Result<(OfferBuilder, Nonce), Bolt12SemanticError> + where + ES::Target: EntropySource, + PF: FnOnce( + PublicKey, + MessageContext, + &secp256k1::Secp256k1, + ) -> Result, + I: IntoIterator, + { + let node_id = self.get_our_node_id(); + let expanded_key = &self.inbound_payment_key; + let entropy = entropy_source; + let secp_ctx = &self.secp_ctx; + + let nonce = Nonce::from_entropy_source(entropy); + let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce }); + + let mut builder = + OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) + .chain_hash(self.chain_hash); + + for path in make_paths(node_id, context, secp_ctx)? { + builder = builder.path(path) + } + + Ok((builder.into(), nonce)) + } + /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the /// [`OffersMessageFlow`], and any corresponding [`InvoiceRequest`] can be verified using /// [`Self::verify_invoice_request`]. The offer will expire at `absolute_expiry` if `Some`, @@ -572,17 +603,16 @@ where /// /// # Privacy /// - /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the offer based on the given - /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications. + /// Uses the [`OffersMessageFlow`]'s [`MessageRouter`] to construct a [`BlindedMessagePath`] + /// for the offer. See those docs for privacy implications. /// /// Also uses a derived signing pubkey in the offer for recipient privacy. /// /// # Limitations /// /// If [`DefaultMessageRouter`] is used to parameterize the [`OffersMessageFlow`], a direct - /// connection to the introduction node in the responding [`InvoiceRequest`]'s reply path is required. - /// See the [`DefaultMessageRouter`] documentation for more details. + /// connection to the introduction node in the responding [`InvoiceRequest`]'s reply path is + /// required. See the [`DefaultMessageRouter`] documentation for more details. /// /// # Errors /// @@ -590,35 +620,41 @@ where /// /// [`DefaultMessageRouter`]: crate::onion_message::messenger::DefaultMessageRouter pub fn create_offer_builder( - &self, entropy_source: ES, absolute_expiry: Option, - peers: Vec, + &self, entropy_source: ES, peers: Vec, ) -> Result, Bolt12SemanticError> where ES::Target: EntropySource, { - let node_id = self.get_our_node_id(); - let expanded_key = &self.inbound_payment_key; - let entropy = &*entropy_source; - let secp_ctx = &self.secp_ctx; - - let nonce = Nonce::from_entropy_source(entropy); - let context = OffersContext::InvoiceRequest { nonce }; - - let path = self - .create_blinded_paths_using_absolute_expiry(context, absolute_expiry, peers) - .and_then(|paths| paths.into_iter().next().ok_or(())) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) - .chain_hash(self.chain_hash) - .path(path); - - let builder = match absolute_expiry { - None => builder, - Some(absolute_expiry) => builder.absolute_expiry(absolute_expiry), - }; + self.create_offer_builder_intern(&*entropy_source, |_, context, _| { + self.create_blinded_paths(peers, context) + .map(|paths| paths.into_iter().take(1)) + .map_err(|_| Bolt12SemanticError::MissingPaths) + }) + .map(|(builder, _)| builder) + } - Ok(builder) + /// Same as [`Self::create_offer_builder`], but allows specifying a custom [`MessageRouter`] + /// instead of using the one provided via the [`OffersMessageFlow`] parameterization. + /// + /// This gives users full control over how the [`BlindedMessagePath`] is constructed, + /// including the option to omit it entirely. + /// + /// See [`Self::create_offer_builder`] for details on offer construction, privacy, and limitations. + pub fn create_offer_builder_using_router( + &self, router: ME, entropy_source: ES, peers: Vec, + ) -> Result, Bolt12SemanticError> + where + ME::Target: MessageRouter, + ES::Target: EntropySource, + { + let receive_key = self.get_receive_auth_key(); + self.create_offer_builder_intern(&*entropy_source, |node_id, context, secp_ctx| { + router + .create_blinded_paths(node_id, receive_key, context, peers, secp_ctx) + .map(|paths| paths.into_iter().take(1)) + .map_err(|_| Bolt12SemanticError::MissingPaths) + }) + .map(|(builder, _)| builder) } /// Create an offer for receiving async payments as an often-offline recipient. @@ -636,25 +672,9 @@ where where ES::Target: EntropySource, { - if message_paths_to_always_online_node.is_empty() { - return Err(Bolt12SemanticError::MissingPaths); - } - - let node_id = self.get_our_node_id(); - let expanded_key = &self.inbound_payment_key; - let entropy = &*entropy_source; - let secp_ctx = &self.secp_ctx; - - let nonce = Nonce::from_entropy_source(entropy); - let mut builder = - OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) - .chain_hash(self.chain_hash); - - for path in message_paths_to_always_online_node { - builder = builder.path(path); - } - - Ok((builder.into(), nonce)) + self.create_offer_builder_intern(&*entropy_source, |_, _, _| { + Ok(message_paths_to_always_online_node) + }) } /// Creates a [`RefundBuilder`] such that the [`Refund`] it builds is recognized by the diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 4834cfc1ec0..07aa314fd25 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -836,11 +836,13 @@ where /// since [`ChannelManager`] requires a functioning [`MessageRouter`] to create blinded paths, /// which are necessary for constructing reply paths in onion message communication. /// However, [`NullMessageRouter`] *can* still be passed as an argument to [`ChannelManager`] -/// methods that accept a router when blinded paths are not needed. +/// methods that accepts a [`MessageRouter`], such as [`ChannelManager::create_offer_builder_using_router`], +/// when blinded paths are not needed. /// /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager +/// [`ChannelManager::create_offer_builder_using_router`]: crate::ln::channelmanager::ChannelManager::create_offer_builder_using_router pub struct NullMessageRouter {} impl MessageRouter for NullMessageRouter { From d6292d1e8fce916874e3e69c8a50e59121acfc8c Mon Sep 17 00:00:00 2001 From: shaavan Date: Sun, 22 Jun 2025 00:18:24 +0530 Subject: [PATCH 08/11] Update `create_refund_builder` to use `create_blinded_paths` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change mirrors the previous update to `create_offer_builder`, applying the **“One `MessageRouter`, one `BlindedPath` type”** principle to refund creation. Now, `create_refund_builder` uses the `create_blinded_paths` method of the `MessageRouter` associated with the `ChannelManager` or `OffersMessageFlow`. For non-default path behavior, users can call `create_refund_builder_using_router` and pass a custom `MessageRouter`. See previous commit for detailed reasoning. --- lightning/src/ln/channelmanager.rs | 49 +++++++++++ lightning/src/ln/offers_tests.rs | 6 +- lightning/src/offers/flow.rs | 130 ++++++++++++++++++++++------- 3 files changed, 153 insertions(+), 32 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 696ebc88f53..0dc8dcbaaef 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11577,6 +11577,55 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { Ok(builder.into()) } + + /// Same as [`Self::create_refund_builder`], but allows specifying a custom [`MessageRouter`] + /// instead of using the one provided during [`ChannelManager`] construction for + /// [`BlindedMessagePath`] creation. + /// + /// This gives users full control over how the [`BlindedMessagePath`] is constructed for the + /// refund, including the option to omit it entirely. This is useful for testing or when + /// alternative privacy strategies are needed. + /// + /// See [`Self::create_refund_builder`] for: + /// - refund recognition by [`ChannelManager`] via [`Bolt12Invoice`] handling, + /// - `payment_id` rules and expiration behavior, + /// - invoice revocation and refund failure handling, + /// - defaulting behavior for `max_total_routing_fee_msat`, + /// - and detailed payment and privacy semantics. + /// + /// # Errors + /// + /// In addition to the errors in [`Self::create_refund_builder`], this returns an error if + /// the provided [`MessageRouter`] fails to construct a valid [`BlindedMessagePath`] for the refund. + /// + /// [`Refund`]: crate::offers::refund::Refund + /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + pub fn create_refund_builder_using_router( + &$self, router: ME, amount_msats: u64, absolute_expiry: Duration, payment_id: PaymentId, + retry_strategy: Retry, route_params_config: RouteParametersConfig + ) -> Result<$builder, Bolt12SemanticError> + where + ME::Target: MessageRouter, + { + let entropy = &*$self.entropy_source; + + let builder = $self.flow.create_refund_builder_using_router( + router, entropy, amount_msats, absolute_expiry, + payment_id, $self.get_peers_for_blinded_path() + )?; + + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop($self); + + let expiration = StaleExpiration::AbsoluteTimeout(absolute_expiry); + $self.pending_outbound_payments + .add_new_awaiting_invoice( + payment_id, expiration, retry_strategy, route_params_config, None, + ) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?; + + Ok(builder.into()) + } } } impl< diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index a85a9704d14..a90bcb24038 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -467,7 +467,7 @@ fn creates_short_lived_refund() { #[test] fn creates_long_lived_refund() { let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs_with_node_id_message_router(2, &chanmon_cfgs); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -479,8 +479,10 @@ fn creates_long_lived_refund() { let absolute_expiry = bob.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + Duration::from_secs(1); let payment_id = PaymentId([1; 32]); + + let router = NodeIdMessageRouter::new(bob.network_graph, bob.keys_manager); let refund = bob.node - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), RouteParametersConfig::default()) + .create_refund_builder_using_router(&router, 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), RouteParametersConfig::default()) .unwrap() .build().unwrap(); assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 59f0638e46e..c11dea60975 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -677,28 +677,67 @@ where }) } + fn create_refund_builder_intern( + &self, entropy_source: ES, make_paths: PF, amount_msats: u64, absolute_expiry: Duration, + payment_id: PaymentId, + ) -> Result, Bolt12SemanticError> + where + ES::Target: EntropySource, + PF: FnOnce( + PublicKey, + MessageContext, + &secp256k1::Secp256k1, + ) -> Result, + I: IntoIterator, + { + let node_id = self.get_our_node_id(); + let expanded_key = &self.inbound_payment_key; + let entropy = &*entropy_source; + let secp_ctx = &self.secp_ctx; + + let nonce = Nonce::from_entropy_source(entropy); + let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce }); + + // Create the base builder with common properties + let mut builder = RefundBuilder::deriving_signing_pubkey( + node_id, + expanded_key, + nonce, + secp_ctx, + amount_msats, + payment_id, + )? + .chain_hash(self.chain_hash) + .absolute_expiry(absolute_expiry); + + for path in make_paths(node_id, context, secp_ctx)? { + builder = builder.path(path); + } + + Ok(builder.into()) + } + /// Creates a [`RefundBuilder`] such that the [`Refund`] it builds is recognized by the /// [`OffersMessageFlow`], and any corresponding [`Bolt12Invoice`] received for the refund /// can be verified using [`Self::verify_bolt12_invoice`]. /// + /// # Privacy + /// + /// Uses the [`OffersMessageFlow`]'s [`MessageRouter`] to construct a [`BlindedMessagePath`] + /// for the offer. See those docs for privacy implications. + /// /// The builder will have the provided expiration set. Any changes to the expiration on the /// returned builder will not be honored by [`OffersMessageFlow`]. For non-`std`, the highest seen /// block time minus two hours is used for the current time when determining if the refund has /// expired. /// - /// To refund can be revoked by the user prior to receiving the invoice. + /// The refund can be revoked by the user prior to receiving the invoice. /// If abandoned, or if an invoice is not received before expiration, the payment will fail /// with an [`Event::PaymentFailed`]. /// /// If `max_total_routing_fee_msat` is not specified, the default from /// [`RouteParameters::from_payment_params_and_value`] is applied. /// - /// # Privacy - /// - /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the refund based on the given - /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications. - /// /// Also uses a derived payer id in the refund for payer privacy. /// /// # Errors @@ -717,32 +756,63 @@ where where ES::Target: EntropySource, { - let node_id = self.get_our_node_id(); - let expanded_key = &self.inbound_payment_key; - let entropy = &*entropy_source; - let secp_ctx = &self.secp_ctx; - - let nonce = Nonce::from_entropy_source(entropy); - let context = OffersContext::OutboundPayment { payment_id, nonce }; - - let path = self - .create_blinded_paths_using_absolute_expiry(context, Some(absolute_expiry), peers) - .and_then(|paths| paths.into_iter().next().ok_or(())) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - let builder = RefundBuilder::deriving_signing_pubkey( - node_id, - expanded_key, - nonce, - secp_ctx, + self.create_refund_builder_intern( + &*entropy_source, + |_, context, _| { + self.create_blinded_paths(peers, context) + .map(|paths| paths.into_iter().take(1)) + .map_err(|_| Bolt12SemanticError::MissingPaths) + }, amount_msats, + absolute_expiry, payment_id, - )? - .chain_hash(self.chain_hash) - .absolute_expiry(absolute_expiry) - .path(path); + ) + } - Ok(builder) + /// Same as [`Self::create_refund_builder`] but allows specifying a custom [`MessageRouter`] + /// instead of using the one provided via the [`OffersMessageFlow`] parameterization. + /// + /// This gives users full control over how the [`BlindedMessagePath`] is constructed, + /// including the option to omit it entirely. + /// + /// See [`Self::create_refund_builder`] for: + /// - how the resulting [`Refund`] is recognized by [`OffersMessageFlow`] and verified via [`Self::verify_bolt12_invoice`], + /// - refund expiration handling, + /// - rules around revocation and [`Event::PaymentFailed`] behavior, + /// - and defaulting logic for `max_total_routing_fee_msat`. + /// + /// # Errors + /// + /// In addition to the errors documented in [`Self::create_refund_builder`], this method will + /// return an error if the provided [`MessageRouter`] fails to construct a valid + /// [`BlindedMessagePath`] for the refund. + /// + /// [`Refund`]: crate::offers::refund::Refund + /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed + /// [`RouteParameters::from_payment_params_and_value`]: crate::routing::router::RouteParameters::from_payment_params_and_value + pub fn create_refund_builder_using_router( + &self, router: ME, entropy_source: ES, amount_msats: u64, absolute_expiry: Duration, + payment_id: PaymentId, peers: Vec, + ) -> Result, Bolt12SemanticError> + where + ME::Target: MessageRouter, + ES::Target: EntropySource, + { + let receive_key = self.get_receive_auth_key(); + self.create_refund_builder_intern( + &*entropy_source, + |node_id, context, secp_ctx| { + router + .create_blinded_paths(node_id, receive_key, context, peers, secp_ctx) + .map(|paths| paths.into_iter().take(1)) + .map_err(|_| Bolt12SemanticError::MissingPaths) + }, + amount_msats, + absolute_expiry, + payment_id, + ) } /// Creates an [`InvoiceRequestBuilder`] such that the [`InvoiceRequest`] it builds is recognized From 512e825ce85f19929682107198f0e4d16e55ced7 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 24 Jul 2025 17:22:45 +0530 Subject: [PATCH 09/11] Update handle_offer_paths_request to use `create_blinded_paths` --- lightning/src/offers/flow.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index c11dea60975..5b9550e9ae9 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1455,16 +1455,13 @@ where let (offer_paths, paths_expiry) = { let path_absolute_expiry = duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY); - let context = OffersContext::StaticInvoiceRequested { + let context = MessageContext::Offers(OffersContext::StaticInvoiceRequested { recipient_id: recipient_id.clone(), path_absolute_expiry, invoice_id, - }; - match self.create_blinded_paths_using_absolute_expiry( - context, - Some(path_absolute_expiry), - peers, - ) { + }); + + match self.create_blinded_paths(peers, context) { Ok(paths) => (paths, path_absolute_expiry), Err(()) => return None, } From 80e7385537dc453438b20bfffbd63048df4b7fa9 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 24 Jul 2025 17:20:35 +0530 Subject: [PATCH 10/11] Cleanup: Remove redundant create_blinded_paths variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit completes the series implementing the principle: **“One `MessageRouter`, one `BlindedPath` type.”** As the final step, it removes now-redundant variations of the blinded path creation functions, streamlining the API and simplifying the blinded path creation process. --- lightning/src/ln/channelmanager.rs | 4 +- lightning/src/offers/flow.rs | 45 +----------------- lightning/src/onion_message/messenger.rs | 59 ------------------------ lightning/src/util/test_utils.rs | 33 ------------- 4 files changed, 3 insertions(+), 138 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 0dc8dcbaaef..f8a3859cbe6 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2969,9 +2969,7 @@ const MAX_NO_CHANNEL_PEERS: usize = 250; /// short-lived, while anything with a greater expiration is considered long-lived. /// /// Using [`ChannelManager::create_offer_builder`] or [`ChannelManager::create_refund_builder`], -/// will included a [`BlindedMessagePath`] created using: -/// - [`MessageRouter::create_compact_blinded_paths`] when short-lived, and -/// - [`MessageRouter::create_blinded_paths`] when long-lived. +/// will include a [`BlindedMessagePath`] created using [`MessageRouter::create_blinded_paths`]. /// /// Using compact [`BlindedMessagePath`]s may provide better privacy as the [`MessageRouter`] could select /// more hops. However, since they use short channel ids instead of pubkeys, they are more likely to diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 5b9550e9ae9..b7c1ea1adf8 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -32,7 +32,7 @@ use crate::prelude::*; use crate::chain::BestBlock; use crate::ln::channel_state::ChannelDetails; -use crate::ln::channelmanager::{PaymentId, CLTV_FAR_FAR_AWAY, MAX_SHORT_LIVED_RELATIVE_EXPIRY}; +use crate::ln::channelmanager::{PaymentId, CLTV_FAR_FAR_AWAY}; use crate::ln::inbound_payment; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; use crate::offers::invoice::{ @@ -193,6 +193,7 @@ where self.receive_auth_key } + #[cfg(async_payments)] fn duration_since_epoch(&self) -> Duration { #[cfg(not(feature = "std"))] let now = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); @@ -303,26 +304,6 @@ where self.create_blinded_paths(peers, context) } - /// Creates a collection of blinded paths by delegating to [`MessageRouter`] based on - /// the path's intended lifetime. - /// - /// Whether or not the path is compact depends on whether the path is short-lived or long-lived, - /// respectively, based on the given `absolute_expiry` as seconds since the Unix epoch. See - /// [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. - fn create_blinded_paths_using_absolute_expiry( - &self, context: OffersContext, absolute_expiry: Option, - peers: Vec, - ) -> Result, ()> { - let now = self.duration_since_epoch(); - let max_short_lived_absolute_expiry = now.saturating_add(MAX_SHORT_LIVED_RELATIVE_EXPIRY); - - if absolute_expiry.unwrap_or(Duration::MAX) <= max_short_lived_absolute_expiry { - self.create_compact_blinded_paths(peers, context) - } else { - self.create_blinded_paths(peers, MessageContext::Offers(context)) - } - } - /// Creates a collection of blinded paths by delegating to /// [`MessageRouter::create_blinded_paths`]. /// @@ -339,28 +320,6 @@ where .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) } - /// Creates a collection of blinded paths by delegating to - /// [`MessageRouter::create_compact_blinded_paths`]. - /// - /// Errors if the `MessageRouter` errors. - fn create_compact_blinded_paths( - &self, peers: Vec, context: OffersContext, - ) -> Result, ()> { - let recipient = self.get_our_node_id(); - let receive_key = self.get_receive_auth_key(); - let secp_ctx = &self.secp_ctx; - - self.message_router - .create_compact_blinded_paths( - recipient, - receive_key, - MessageContext::Offers(context), - peers, - secp_ctx, - ) - .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) - } - /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to /// [`Router::create_blinded_payment_paths`]. fn create_blinded_payment_paths( diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 07aa314fd25..38c8cd304d9 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -510,26 +510,6 @@ pub trait MessageRouter { &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()>; - - /// Creates compact [`BlindedMessagePath`]s to the `recipient` node. The nodes in `peers` are - /// assumed to be direct peers with the `recipient`. - /// - /// Compact blinded paths use short channel ids instead of pubkeys for a smaller serialization, - /// which is beneficial when a QR code is used to transport the data. The SCID is passed using - /// a [`MessageForwardNode`] but may be `None` for graceful degradation. - /// - /// Implementations using additional intermediate nodes are responsible for using a - /// [`MessageForwardNode`] with `Some` short channel id, if possible. Similarly, implementations - /// should call [`BlindedMessagePath::use_compact_introduction_node`]. - /// - /// The provided implementation simply delegates to [`MessageRouter::create_blinded_paths`], - /// ignoring the short channel ids. - fn create_compact_blinded_paths( - &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, - ) -> Result, ()> { - self.create_blinded_paths(recipient, local_node_receive_key, context, peers, secp_ctx) - } } /// A [`MessageRouter`] that can only route to a directly connected [`Destination`]. @@ -733,22 +713,6 @@ where true, ) } - - fn create_compact_blinded_paths( - &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, - ) -> Result, ()> { - Self::create_blinded_paths_from_iter( - &self.network_graph, - recipient, - local_node_receive_key, - context, - peers.into_iter(), - &self.entropy_source, - secp_ctx, - true, - ) - } } /// This message router is similar to [`DefaultMessageRouter`], but it always creates @@ -809,22 +773,6 @@ where false, ) } - - fn create_compact_blinded_paths( - &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, - ) -> Result, ()> { - DefaultMessageRouter::create_blinded_paths_from_iter( - &self.network_graph, - recipient, - local_node_receive_key, - context, - peers.into_iter(), - &self.entropy_source, - secp_ctx, - false, - ) - } } /// A special [`MessageRouter`] that performs no routing and does not create blinded paths. @@ -858,13 +806,6 @@ impl MessageRouter for NullMessageRouter { ) -> Result, ()> { Ok(vec![]) } - - fn create_compact_blinded_paths( - &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, - _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, - ) -> Result, ()> { - Ok(vec![]) - } } /// A path for sending an [`OnionMessage`]. diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index c56154e8d9f..6fc64817599 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -410,39 +410,6 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { ), } } - - fn create_compact_blinded_paths( - &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - 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(); - } - } - match &self.inner { - TestMessageRouterInternal::Default(inner) => inner.create_compact_blinded_paths( - recipient, - local_node_receive_key, - context, - peers, - secp_ctx, - ), - TestMessageRouterInternal::NodeId(inner) => inner.create_compact_blinded_paths( - recipient, - local_node_receive_key, - context, - peers, - secp_ctx, - ), - } - } } pub struct OnlyReadsKeysInterface {} From 596cf479543cecf0df2f986f3cf736c5b681d411 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 3 Oct 2024 17:34:08 +0530 Subject: [PATCH 11/11] Test: Add coverage for Offers and Refunds without blinded paths Introduced tests to validate the behavior of Offers and Refunds created without blinded paths, using `NullMessageRouter`. --- lightning/src/ln/offers_tests.rs | 51 +++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index a90bcb24038..7072aa3439b 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -60,7 +60,7 @@ use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::{Destination, MessageSendInstructions, NodeIdMessageRouter, PeeledOnion}; +use crate::onion_message::messenger::{Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion}; use crate::onion_message::offers::OffersMessage; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; @@ -265,6 +265,55 @@ fn extract_invoice_error<'a, 'b, 'c>( } } +/// Checks that an offer can be created with no blinded paths. +#[test] +fn create_offer_with_no_blinded_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + + let router = NullMessageRouter {}; + let offer = alice.node + .create_offer_builder_using_router(&router).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + assert_eq!(offer.issuer_signing_pubkey(), Some(alice_id)); + assert!(offer.paths().is_empty()); +} + +/// Checks that a refund can be created with no blinded paths. +#[test] +fn create_refund_with_no_blinded_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + + let absolute_expiry = Duration::from_secs(u64::MAX); + let payment_id = PaymentId([1; 32]); + + let router = NullMessageRouter {}; + let refund = alice.node + .create_refund_builder_using_router(&router, 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), RouteParametersConfig::default()) + .unwrap() + .build().unwrap(); + assert_eq!(refund.amount_msats(), 10_000_000); + assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); + assert_eq!(refund.payer_signing_pubkey(), alice_id); + assert!(refund.paths().is_empty()); +} + /// Checks that blinded paths without Tor-only nodes are preferred when constructing an offer. #[test] fn prefers_non_tor_nodes_in_blinded_paths() {