From ee026d43574c49e41aa0355c6fda49c72b45523e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 2 Mar 2023 14:15:19 -0600 Subject: [PATCH 01/14] Re-order AnchorsZeroFeeHtlcTx in module docs/tests Move AnchorsZeroFeeHtlcTx after Wumbo to keep order by feature bit. Also, update setting order and comment in tests. --- lightning/src/ln/features.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index d10c3a71927..08df63e7033 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -41,6 +41,10 @@ //! (see [BOLT-4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#basic-multi-part-payments) for more information). //! - `Wumbo` - requires/supports that a node create large channels. Called `option_support_large_channel` in the spec. //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#the-open_channel-message) for more information). +//! - `AnchorsZeroFeeHtlcTx` - requires/supports that commitment transactions include anchor outputs +//! and HTLC transactions are pre-signed with zero fee (see +//! [BOLT-3](https://github.com/lightning/bolts/blob/master/03-transactions.md) for more +//! information). //! - `ShutdownAnySegwit` - requires/supports that future segwit versions are allowed in `shutdown` //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md) for more information). //! - `OnionMessages` - requires/supports forwarding onion messages @@ -60,10 +64,6 @@ //! for more info). //! - `Keysend` - send funds to a node without an invoice //! (see the [`Keysend` feature assignment proposal](https://github.com/lightning/bolts/issues/605#issuecomment-606679798) for more information). -//! - `AnchorsZeroFeeHtlcTx` - requires/supports that commitment transactions include anchor outputs -//! and HTLC transactions are pre-signed with zero fee (see -//! [BOLT-3](https://github.com/lightning/bolts/blob/master/03-transactions.md) for more -//! information). //! //! LDK knows about the following features, but does not support them: //! - `AnchorsNonzeroFeeHtlcTx` - the initial version of anchor outputs, which was later found to be @@ -1068,7 +1068,7 @@ mod tests { // Check that the flags are as expected: // - option_data_loss_protect (req) // - var_onion_optin (req) | static_remote_key (req) | payment_secret(req) - // - basic_mpp | wumbo | anchors_zero_fee_htlc_tx + // - basic_mpp | wumbo | option_anchors_zero_fee_htlc_tx // - opt_shutdown_anysegwit // - onion_messages // - option_channel_type | option_scid_alias From ffb0d8329824d5ee86107307d1cac37ea9b57290 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 2 Mar 2023 14:57:07 -0600 Subject: [PATCH 02/14] Add RouteBlinding feature flag The RouteBlinding feature flag is signals support for relaying payments over blinded paths. It is used for paying BOLT 12 invoices, which are required to included at least one blinded path. --- lightning/src/ln/features.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index 08df63e7033..df5c0abf25f 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -45,6 +45,8 @@ //! and HTLC transactions are pre-signed with zero fee (see //! [BOLT-3](https://github.com/lightning/bolts/blob/master/03-transactions.md) for more //! information). +//! - `RouteBlinding` - requires/supports that a node can relay payments over blinded paths +//! (see [BOLT-4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#route-blinding) for more information). //! - `ShutdownAnySegwit` - requires/supports that future segwit versions are allowed in `shutdown` //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md) for more information). //! - `OnionMessages` - requires/supports forwarding onion messages @@ -143,7 +145,7 @@ mod sealed { // Byte 2 BasicMPP | Wumbo | AnchorsNonzeroFeeHtlcTx | AnchorsZeroFeeHtlcTx, // Byte 3 - ShutdownAnySegwit | Taproot, + RouteBlinding | ShutdownAnySegwit | Taproot, // Byte 4 OnionMessages, // Byte 5 @@ -159,7 +161,7 @@ mod sealed { // Byte 2 BasicMPP | Wumbo | AnchorsNonzeroFeeHtlcTx | AnchorsZeroFeeHtlcTx, // Byte 3 - ShutdownAnySegwit | Taproot, + RouteBlinding | ShutdownAnySegwit | Taproot, // Byte 4 OnionMessages, // Byte 5 @@ -391,6 +393,9 @@ mod sealed { define_feature!(23, AnchorsZeroFeeHtlcTx, [InitContext, NodeContext, ChannelTypeContext], "Feature flags for `option_anchors_zero_fee_htlc_tx`.", set_anchors_zero_fee_htlc_tx_optional, set_anchors_zero_fee_htlc_tx_required, supports_anchors_zero_fee_htlc_tx, requires_anchors_zero_fee_htlc_tx); + define_feature!(25, RouteBlinding, [InitContext, NodeContext], + "Feature flags for `option_route_blinding`.", set_route_blinding_optional, + set_route_blinding_required, supports_route_blinding, requires_route_blinding); define_feature!(27, ShutdownAnySegwit, [InitContext, NodeContext], "Feature flags for `opt_shutdown_anysegwit`.", set_shutdown_any_segwit_optional, set_shutdown_any_segwit_required, supports_shutdown_anysegwit, requires_shutdown_anysegwit); @@ -1053,6 +1058,7 @@ mod tests { init_features.set_basic_mpp_optional(); init_features.set_wumbo_optional(); init_features.set_anchors_zero_fee_htlc_tx_optional(); + init_features.set_route_blinding_optional(); init_features.set_shutdown_any_segwit_optional(); init_features.set_onion_messages_optional(); init_features.set_channel_type_optional(); @@ -1069,7 +1075,7 @@ mod tests { // - option_data_loss_protect (req) // - var_onion_optin (req) | static_remote_key (req) | payment_secret(req) // - basic_mpp | wumbo | option_anchors_zero_fee_htlc_tx - // - opt_shutdown_anysegwit + // - option_route_blinding | opt_shutdown_anysegwit // - onion_messages // - option_channel_type | option_scid_alias // - option_zeroconf @@ -1077,7 +1083,7 @@ mod tests { assert_eq!(node_features.flags[0], 0b00000001); assert_eq!(node_features.flags[1], 0b01010001); assert_eq!(node_features.flags[2], 0b10001010); - assert_eq!(node_features.flags[3], 0b00001000); + assert_eq!(node_features.flags[3], 0b00001010); assert_eq!(node_features.flags[4], 0b10000000); assert_eq!(node_features.flags[5], 0b10100000); assert_eq!(node_features.flags[6], 0b00001000); From 97049daac211b65cf6113960059fedaad12397b4 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 7 Dec 2023 15:48:43 -0600 Subject: [PATCH 03/14] Add create_blinded_paths to MessageRouter The MessageRouter trait is used to find an OnionMessagePath to a Destination (e.g., to a BlindedPath). Expand the interface with a create_blinded_paths method for creating such paths to a recipient. Provide a default implementation creating two-hop blinded paths where the recipient's peers serve as introduction nodes. --- fuzz/src/onion_message.rs | 14 ++++- .../src/onion_message/functional_tests.rs | 13 ++++- lightning/src/onion_message/messenger.rs | 55 ++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 1051083d36b..d2d60cfcf64 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -1,17 +1,18 @@ // Imports that need to be added manually use bitcoin::bech32::u5; use bitcoin::blockdata::script::ScriptBuf; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, self}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::RecoverableSignature; use bitcoin::secp256k1::schnorr; -use lightning::sign::{Recipient, KeyMaterial, EntropySource, NodeSigner, SignerProvider}; +use lightning::blinded_path::BlindedPath; use lightning::ln::features::InitFeatures; use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler}; use lightning::ln::script::ShutdownScript; use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::offers::invoice_request::UnsignedInvoiceRequest; +use lightning::sign::{Recipient, KeyMaterial, EntropySource, NodeSigner, SignerProvider}; use lightning::util::test_channel_signer::TestChannelSigner; use lightning::util::logger::Logger; use lightning::util::ser::{Readable, Writeable, Writer}; @@ -82,6 +83,15 @@ impl MessageRouter for TestMessageRouter { first_node_addresses: None, }) } + + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _peers: Vec, _entropy_source: &ES, + _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } } struct TestOffersMessageHandler {} diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 9323be5fff2..b0031a6c3f4 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -13,14 +13,14 @@ use crate::blinded_path::BlindedPath; use crate::events::{Event, EventsProvider}; use crate::ln::features::InitFeatures; use crate::ln::msgs::{self, DecodeError, OnionMessageHandler, SocketAddress}; -use crate::sign::{NodeSigner, Recipient}; +use crate::sign::{EntropySource, NodeSigner, Recipient}; use crate::util::ser::{FixedLengthReader, LengthReadable, Writeable, Writer}; use crate::util::test_utils; use super::{CustomOnionMessageHandler, Destination, MessageRouter, OffersMessage, OffersMessageHandler, OnionMessageContents, OnionMessagePath, OnionMessenger, PendingOnionMessage, SendError}; use bitcoin::network::constants::Network; use bitcoin::hashes::hex::FromHex; -use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey, self}; use crate::io; use crate::io_extras::read_to_end; @@ -55,6 +55,15 @@ impl MessageRouter for TestMessageRouter { Some(vec![SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: 1000 }]), }) } + + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _peers: Vec, _entropy_source: &ES, + _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } } struct TestOffersMessageHandler {} diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 8a44eb2a5be..c0db6096b99 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -64,9 +64,9 @@ pub(super) const MAX_TIMER_TICKS: usize = 2; /// # extern crate bitcoin; /// # use bitcoin::hashes::_export::_core::time::Duration; /// # use bitcoin::hashes::hex::FromHex; -/// # use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; +/// # use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey, self}; /// # use lightning::blinded_path::BlindedPath; -/// # use lightning::sign::KeysManager; +/// # use lightning::sign::{EntropySource, KeysManager}; /// # use lightning::ln::peer_handler::IgnoringMessageHandler; /// # use lightning::onion_message::{OnionMessageContents, Destination, MessageRouter, OnionMessagePath, OnionMessenger}; /// # use lightning::util::logger::{Logger, Record}; @@ -90,6 +90,11 @@ pub(super) const MAX_TIMER_TICKS: usize = 2; /// # first_node_addresses: None, /// # }) /// # } +/// # fn create_blinded_paths( +/// # &self, _recipient: PublicKey, _peers: Vec, _entropy_source: &ES, _secp_ctx: &Secp256k1 +/// # ) -> Result, ()> { +/// # unreachable!() +/// # } /// # } /// # let seed = [42u8; 32]; /// # let time = Duration::from_secs(123456); @@ -270,6 +275,15 @@ pub trait MessageRouter { fn find_path( &self, sender: PublicKey, peers: Vec, destination: Destination ) -> Result; + + /// Creates [`BlindedPath`]s to the `recipient` node. The nodes in `peers` are assumed to be + /// direct peers with the `recipient`. + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, entropy_source: &ES, + secp_ctx: &Secp256k1 + ) -> Result, ()>; } /// A [`MessageRouter`] that can only route to a directly connected [`Destination`]. @@ -321,6 +335,43 @@ where } } } + + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, entropy_source: &ES, + secp_ctx: &Secp256k1 + ) -> Result, ()> { + // Limit the number of blinded paths that are computed. + const MAX_PATHS: usize = 3; + + // Ensure peers have at least three channels so that it is more difficult to infer the + // recipient's node_id. + const MIN_PEER_CHANNELS: usize = 3; + + let network_graph = self.network_graph.deref().read_only(); + let paths = peers.into_iter() + // Limit to peers with announced channels + .filter(|pubkey| + network_graph + .node(&NodeId::from_pubkey(&pubkey)) + .map(|info| &info.channels[..]) + .map(|channels| channels.len() >= MIN_PEER_CHANNELS) + .unwrap_or(false) + ) + .map(|pubkey| vec![pubkey, recipient]) + .map(|node_pks| BlindedPath::new_for_message(&node_pks, entropy_source, secp_ctx)) + .take(MAX_PATHS) + .collect::, _>>(); + + match paths { + Ok(paths) if !paths.is_empty() => Ok(paths), + _ => { + BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx) + .map(|path| vec![path]) + }, + } + } } /// A path for sending an [`OnionMessage`]. From bedc2c64fcfe5fa5f85ded630e9ed2eb3c3651eb Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 7 Dec 2023 15:53:15 -0600 Subject: [PATCH 04/14] Require any Router also implements MessageRouter ChannelManager is parameterized by a Router in order to find routes when sending and retrying payments. For the offers flow, it needs to be able to construct blinded paths (e.g., in the offer and in reply paths). Instead of adding yet another parameter to ChannelManager, require that any Router also implements MessageRouter. Implement this for DefaultRouter by delegating to a DefaultMessageRouter. --- fuzz/src/chanmon_consistency.rs | 21 +++++++++++++++++- fuzz/src/full_stack.rs | 21 +++++++++++++++++- lightning/src/routing/router.rs | 38 ++++++++++++++++++++++++++------ lightning/src/util/test_utils.rs | 21 +++++++++++++++++- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index f654908771f..2c4552d4374 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -30,6 +30,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{BlockHash, WPubkeyHash}; +use lightning::blinded_path::BlindedPath; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, chainmonitor, channelmonitor, Confirm, Watch}; use lightning::chain::channelmonitor::{ChannelMonitor, MonitorEvent}; @@ -46,6 +47,7 @@ use lightning::ln::script::ShutdownScript; use lightning::ln::functional_test_utils::*; use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::offers::invoice_request::UnsignedInvoiceRequest; +use lightning::onion_message::{Destination, MessageRouter, OnionMessagePath}; use lightning::util::test_channel_signer::{TestChannelSigner, EnforcementState}; use lightning::util::errors::APIError; use lightning::util::logger::Logger; @@ -56,7 +58,7 @@ use lightning::routing::router::{InFlightHtlcs, Path, Route, RouteHop, RoutePara use crate::utils::test_logger::{self, Output}; use crate::utils::test_persister::TestPersister; -use bitcoin::secp256k1::{Message, PublicKey, SecretKey, Scalar, Secp256k1}; +use bitcoin::secp256k1::{Message, PublicKey, SecretKey, Scalar, Secp256k1, self}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::schnorr; @@ -101,6 +103,23 @@ impl Router for FuzzRouter { } } +impl MessageRouter for FuzzRouter { + fn find_path( + &self, _sender: PublicKey, _peers: Vec, _destination: Destination + ) -> Result { + unreachable!() + } + + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _peers: Vec, _entropy_source: &ES, + _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } +} + pub struct TestBroadcaster {} impl BroadcasterInterface for TestBroadcaster { fn broadcast_transactions(&self, _txs: &[&Transaction]) { } diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 725f83af984..9eb39b119af 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -28,6 +28,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{Txid, BlockHash, WPubkeyHash}; +use lightning::blinded_path::BlindedPath; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen}; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; @@ -43,6 +44,7 @@ use lightning::ln::script::ShutdownScript; use lightning::ln::functional_test_utils::*; use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::offers::invoice_request::UnsignedInvoiceRequest; +use lightning::onion_message::{Destination, MessageRouter, OnionMessagePath}; use lightning::routing::gossip::{P2PGossipSync, NetworkGraph}; use lightning::routing::utxo::UtxoLookup; use lightning::routing::router::{InFlightHtlcs, PaymentParameters, Route, RouteParameters, Router}; @@ -55,7 +57,7 @@ use lightning::util::ser::{ReadableArgs, Writeable}; use crate::utils::test_logger; use crate::utils::test_persister::TestPersister; -use bitcoin::secp256k1::{Message, PublicKey, SecretKey, Scalar, Secp256k1}; +use bitcoin::secp256k1::{Message, PublicKey, SecretKey, Scalar, Secp256k1, self}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::schnorr; @@ -144,6 +146,23 @@ impl Router for FuzzRouter { } } +impl MessageRouter for FuzzRouter { + fn find_path( + &self, _sender: PublicKey, _peers: Vec, _destination: Destination + ) -> Result { + unreachable!() + } + + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _peers: Vec, _entropy_source: &ES, + _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } +} + struct TestBroadcaster { txn_broadcasted: Mutex>, } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 7bd1cebc532..42eeb6cb136 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -9,7 +9,7 @@ //! The router finds paths within a [`NetworkGraph`] for a payment. -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{PublicKey, Secp256k1, self}; use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; @@ -19,8 +19,10 @@ use crate::ln::channelmanager::{ChannelDetails, PaymentId}; use crate::ln::features::{Bolt11InvoiceFeatures, Bolt12InvoiceFeatures, ChannelFeatures, NodeFeatures}; use crate::ln::msgs::{DecodeError, ErrorAction, LightningError, MAX_VALUE_MSAT}; use crate::offers::invoice::{BlindedPayInfo, Bolt12Invoice}; +use crate::onion_message::{DefaultMessageRouter, Destination, MessageRouter, OnionMessagePath}; use crate::routing::gossip::{DirectedChannelInfo, EffectiveCapacity, ReadOnlyNetworkGraph, NetworkGraph, NodeId, RoutingFees}; use crate::routing::scoring::{ChannelUsage, LockableScore, ScoreLookUp}; +use crate::sign::EntropySource; use crate::util::ser::{Writeable, Readable, ReadableArgs, Writer}; use crate::util::logger::{Level, Logger}; use crate::util::chacha20::ChaCha20; @@ -33,7 +35,7 @@ use core::{cmp, fmt}; use core::ops::Deref; /// A [`Router`] implemented using [`find_route`]. -pub struct DefaultRouter>, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> where +pub struct DefaultRouter> + Clone, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> where L::Target: Logger, S::Target: for <'a> LockableScore<'a, ScoreLookUp = Sc>, { @@ -41,21 +43,23 @@ pub struct DefaultRouter>, L: Deref, S: Deref, logger: L, random_seed_bytes: Mutex<[u8; 32]>, scorer: S, - score_params: SP + score_params: SP, + message_router: DefaultMessageRouter, } -impl>, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> DefaultRouter where +impl> + Clone, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> DefaultRouter where L::Target: Logger, S::Target: for <'a> LockableScore<'a, ScoreLookUp = Sc>, { /// Creates a new router. pub fn new(network_graph: G, logger: L, random_seed_bytes: [u8; 32], scorer: S, score_params: SP) -> Self { let random_seed_bytes = Mutex::new(random_seed_bytes); - Self { network_graph, logger, random_seed_bytes, scorer, score_params } + let message_router = DefaultMessageRouter::new(network_graph.clone()); + Self { network_graph, logger, random_seed_bytes, scorer, score_params, message_router } } } -impl< G: Deref>, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> Router for DefaultRouter where +impl> + Clone, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> Router for DefaultRouter where L::Target: Logger, S::Target: for <'a> LockableScore<'a, ScoreLookUp = Sc>, { @@ -80,8 +84,28 @@ impl< G: Deref>, L: Deref, S: Deref, SP: Sized, Sc: Sco } } +impl< G: Deref> + Clone, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> MessageRouter for DefaultRouter where + L::Target: Logger, + S::Target: for <'a> LockableScore<'a, ScoreLookUp = Sc>, +{ + fn find_path( + &self, sender: PublicKey, peers: Vec, destination: Destination + ) -> Result { + self.message_router.find_path(sender, peers, destination) + } + + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, entropy_source: &ES, + secp_ctx: &Secp256k1 + ) -> Result, ()> { + self.message_router.create_blinded_paths(recipient, peers, entropy_source, secp_ctx) + } +} + /// A trait defining behavior for routing a payment. -pub trait Router { +pub trait Router: MessageRouter { /// Finds a [`Route`] for a payment between the given `payer` and a payee. /// /// The `payee` and the payment's value are given in [`RouteParameters::payment_params`] diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 805806dc346..3954088fc16 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -7,6 +7,7 @@ // You may not use this file except in accordance with one or both of these // licenses. +use crate::blinded_path::BlindedPath; use crate::chain; use crate::chain::WatchedOutput; use crate::chain::chaininterface; @@ -30,6 +31,7 @@ use crate::ln::msgs::LightningError; use crate::ln::script::ShutdownScript; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::offers::invoice_request::UnsignedInvoiceRequest; +use crate::onion_message::{Destination, MessageRouter, OnionMessagePath}; use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId, RoutingFees}; use crate::routing::utxo::{UtxoLookup, UtxoLookupError, UtxoResult}; use crate::routing::router::{find_route, InFlightHtlcs, Path, Route, RouteParameters, RouteHintHop, Router, ScorerAccountingForInFlightHtlcs}; @@ -51,7 +53,7 @@ use bitcoin::network::constants::Network; use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::sighash::{SighashCache, EcdsaSighashType}; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, self}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::schnorr; @@ -191,6 +193,23 @@ impl<'a> Router for TestRouter<'a> { } } +impl<'a> MessageRouter for TestRouter<'a> { + fn find_path( + &self, _sender: PublicKey, _peers: Vec, _destination: Destination + ) -> Result { + unreachable!() + } + + fn create_blinded_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _peers: Vec, _entropy_source: &ES, + _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } +} + impl<'a> Drop for TestRouter<'a> { fn drop(&mut self) { #[cfg(feature = "std")] { From c558ccd6a92fa9034929769f55e65bf9c1336abd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 8 Dec 2023 15:02:22 -0600 Subject: [PATCH 05/14] Fix create_one_hop_blinded_payment_path docs --- lightning/src/ln/channelmanager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9536a9366e1..9017f3ffd98 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7867,8 +7867,8 @@ where BlindedPath::one_hop_for_message(self.get_our_node_id(), entropy_source, secp_ctx).unwrap() } - /// Creates a one-hop blinded path with [`ChannelManager::get_our_node_id`] as the introduction - /// node. + /// Creates a one-hop blinded payment path with [`ChannelManager::get_our_node_id`] as the + /// introduction node. fn create_one_hop_blinded_payment_path( &self, payment_secret: PaymentSecret ) -> (BlindedPayInfo, BlindedPath) { From dcd8d583465c2fbc9d651050c093a82b2141b12d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 15 Dec 2023 15:37:18 -0600 Subject: [PATCH 06/14] Use CLTV_FAR_FAR_AWAY in PaymentConstraints When finding a route through a blinded path, a random CLTV offset may be added to the path in order to preserve privacy. This needs to be accounted for in the blinded path's PaymentConstraints. Add CLTV_FAR_FAR_AWAY to the max_cltv_expiry constraint to allow for such offsets. --- lightning/src/ln/channelmanager.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9017f3ffd98..dccc53ae813 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7876,7 +7876,8 @@ where let secp_ctx = &self.secp_ctx; let payee_node_id = self.get_our_node_id(); - let max_cltv_expiry = self.best_block.read().unwrap().height() + LATENCY_GRACE_PERIOD_BLOCKS; + let max_cltv_expiry = self.best_block.read().unwrap().height() + CLTV_FAR_FAR_AWAY + + LATENCY_GRACE_PERIOD_BLOCKS; let payee_tlvs = ReceiveTlvs { payment_secret, payment_constraints: PaymentConstraints { From 606304aa32f4d767f2049a5fdff9e5236e3aa5e7 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 3 Mar 2023 09:38:45 -0600 Subject: [PATCH 07/14] Multi-hop blinded paths in ChannelManager When constructing blinded paths for Offer and Refund, delegate to MessageRouter::create_blinded_paths which may produce multi-hop blinded paths. Fallback to one-hop blinded paths if the MessageRouter fails or returns no paths. Likewise, do the same for InvoiceRequest and Bolt12Invoice reply paths. --- lightning/src/ln/channelmanager.rs | 80 ++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index dccc53ae813..e9c6a197ea1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -65,7 +65,7 @@ use crate::offers::merkle::SignError; use crate::offers::offer::{DerivedMetadata, Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; -use crate::onion_message::{Destination, OffersMessage, OffersMessageHandler, PendingOnionMessage, new_pending_onion_message}; +use crate::onion_message::{Destination, MessageRouter, OffersMessage, OffersMessageHandler, PendingOnionMessage, new_pending_onion_message}; use crate::sign::{EntropySource, KeysManager, NodeSigner, Recipient, SignerProvider}; use crate::sign::ecdsa::WriteableEcdsaChannelSigner; use crate::util::config::{UserConfig, ChannelConfig, ChannelConfigUpdate}; @@ -7483,32 +7483,43 @@ where /// /// # Privacy /// - /// Uses a one-hop [`BlindedPath`] for the offer with [`ChannelManager::get_our_node_id`] as the - /// introduction node and a derived signing pubkey for recipient privacy. As such, currently, - /// the node must be announced. Otherwise, there is no way to find a path to the introduction - /// node in order to send the [`InvoiceRequest`]. + /// Uses [`MessageRouter::create_blinded_paths`] to construct a [`BlindedPath`] for the offer. + /// However, if one is not found, uses a one-hop [`BlindedPath`] with + /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, + /// the node must be announced, otherwise, there is no way to find a path to the introduction in + /// order to send the [`InvoiceRequest`]. + /// + /// Also, uses a derived signing pubkey in the offer for recipient privacy. /// /// # Limitations /// /// Requires a direct connection to the introduction node in the responding [`InvoiceRequest`]'s /// reply path. /// + /// # Errors + /// + /// Errors if the parameterized [`Router`] is unable to create a blinded path for the offer. + /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. /// /// [`Offer`]: crate::offers::offer::Offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest pub fn create_offer_builder( &self, description: String - ) -> OfferBuilder { + ) -> Result, Bolt12SemanticError> { let node_id = self.get_our_node_id(); let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; let secp_ctx = &self.secp_ctx; - let path = self.create_one_hop_blinded_path(); - OfferBuilder::deriving_signing_pubkey(description, node_id, expanded_key, entropy, secp_ctx) + let path = self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; + let builder = OfferBuilder::deriving_signing_pubkey( + description, node_id, expanded_key, entropy, secp_ctx + ) .chain_hash(self.chain_hash) - .path(path) + .path(path); + + Ok(builder) } /// Creates a [`RefundBuilder`] such that the [`Refund`] it builds is recognized by the @@ -7533,10 +7544,13 @@ where /// /// # Privacy /// - /// Uses a one-hop [`BlindedPath`] for the refund with [`ChannelManager::get_our_node_id`] as - /// the introduction node and a derived payer id for payer privacy. As such, currently, the - /// node must be announced. Otherwise, there is no way to find a path to the introduction node - /// in order to send the [`Bolt12Invoice`]. + /// Uses [`MessageRouter::create_blinded_paths`] to construct a [`BlindedPath`] for the refund. + /// However, if one is not found, uses a one-hop [`BlindedPath`] with + /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, + /// the node must be announced, otherwise, there is no way to find a path to the introduction in + /// order to send the [`Bolt12Invoice`]. + /// + /// Also, uses a derived payer id in the refund for payer privacy. /// /// # Limitations /// @@ -7545,8 +7559,10 @@ where /// /// # Errors /// - /// Errors if a duplicate `payment_id` is provided given the caveats in the aforementioned link - /// or if `amount_msats` is invalid. + /// Errors if: + /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, + /// - `amount_msats` is invalid, or + /// - the parameterized [`Router`] is unable to create a blinded path for the refund. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. /// @@ -7561,8 +7577,8 @@ where let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; let secp_ctx = &self.secp_ctx; - let path = self.create_one_hop_blinded_path(); + let path = self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = RefundBuilder::deriving_payer_id( description, node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id )? @@ -7620,8 +7636,11 @@ where /// /// # Errors /// - /// Errors if a duplicate `payment_id` is provided given the caveats in the aforementioned link - /// or if the provided parameters are invalid for the offer. + /// Errors if: + /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, + /// - the provided parameters are invalid for the offer, + /// - the parameterized [`Router`] is unable to create a blinded reply path for the invoice + /// request. /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`InvoiceRequest::quantity`]: crate::offers::invoice_request::InvoiceRequest::quantity @@ -7654,9 +7673,8 @@ where None => builder, Some(payer_note) => builder.payer_note(payer_note), }; - let invoice_request = builder.build_and_sign()?; - let reply_path = self.create_one_hop_blinded_path(); + let reply_path = self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; let expiration = StaleExpiration::TimerTicks(1); self.pending_outbound_payments @@ -7732,7 +7750,8 @@ where payment_paths, payment_hash, created_at, expanded_key, entropy )?; let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; - let reply_path = self.create_one_hop_blinded_path(); + let reply_path = self.create_blinded_path() + .map_err(|_| Bolt12SemanticError::MissingPaths)?; let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); if refund.paths().is_empty() { @@ -7859,12 +7878,23 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } - /// Creates a one-hop blinded path with [`ChannelManager::get_our_node_id`] as the introduction - /// node. - fn create_one_hop_blinded_path(&self) -> BlindedPath { + /// Creates a blinded path by delegating to [`MessageRouter::create_blinded_paths`]. + /// + /// Errors if the `MessageRouter` errors or returns an empty `Vec`. + fn create_blinded_path(&self) -> Result { + let recipient = self.get_our_node_id(); let entropy_source = self.entropy_source.deref(); let secp_ctx = &self.secp_ctx; - BlindedPath::one_hop_for_message(self.get_our_node_id(), entropy_source, secp_ctx).unwrap() + + let peers = self.per_peer_state.read().unwrap() + .iter() + .filter(|(_, peer)| peer.lock().unwrap().latest_features.supports_onion_messages()) + .map(|(node_id, _)| *node_id) + .collect::>(); + + self.router + .create_blinded_paths(recipient, peers, entropy_source, secp_ctx) + .and_then(|paths| paths.into_iter().next().ok_or(())) } /// Creates a one-hop blinded payment path with [`ChannelManager::get_our_node_id`] as the From 4da08623a64ef103cfcd665a18c2598125ff7556 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 15 Dec 2023 08:45:38 -0600 Subject: [PATCH 08/14] Fix broken doc link in create_refund_builder --- lightning/src/ln/channelmanager.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e9c6a197ea1..397ffa5866b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7569,6 +7569,7 @@ where /// [`Refund`]: crate::offers::refund::Refund /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths + /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments pub fn create_refund_builder( &self, description: String, amount_msats: u64, absolute_expiry: Duration, payment_id: PaymentId, retry_strategy: Retry, max_total_routing_fee_msat: Option From edb589203001d4c07e9bbbbb1816363f75917715 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 8 Dec 2023 12:01:58 -0600 Subject: [PATCH 09/14] CounterpartyForwardingInfo to PaymentRelay mapping --- lightning/src/blinded_path/payment.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 7b604fbdcb1..f4df1e379d9 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -8,6 +8,7 @@ use crate::blinded_path::BlindedHop; use crate::blinded_path::utils; use crate::io; use crate::ln::PaymentSecret; +use crate::ln::channelmanager::CounterpartyForwardingInfo; use crate::ln::features::BlindedHopFeatures; use crate::ln::msgs::DecodeError; use crate::offers::invoice::BlindedPayInfo; @@ -96,6 +97,15 @@ pub struct PaymentConstraints { pub htlc_minimum_msat: u64, } +impl From for PaymentRelay { + fn from(info: CounterpartyForwardingInfo) -> Self { + let CounterpartyForwardingInfo { + fee_base_msat, fee_proportional_millionths, cltv_expiry_delta + } = info; + Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat } + } +} + impl Writeable for ForwardTlvs { fn write(&self, w: &mut W) -> Result<(), io::Error> { encode_tlv_stream!(w, { From 62f866965436fff1a8e98ee655a8a6dcbb8716c1 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 8 Dec 2023 12:03:06 -0600 Subject: [PATCH 10/14] Add create_blinded_payment_paths to Router The Router trait is used to find a Route for paying a node. Expand the interface with a create_blinded_payment paths method for creating such paths to a recipient node. Provide an implementation for DefaultRouter that creates two-hop blinded paths where the recipient's peers serve as the introduction nodes. --- fuzz/src/chanmon_consistency.rs | 12 ++++- fuzz/src/full_stack.rs | 12 ++++- lightning/src/routing/router.rs | 88 +++++++++++++++++++++++++++++++- lightning/src/util/test_utils.rs | 16 ++++-- 4 files changed, 122 insertions(+), 6 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 2c4552d4374..7a32434b86f 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -31,6 +31,7 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{BlockHash, WPubkeyHash}; use lightning::blinded_path::BlindedPath; +use lightning::blinded_path::payment::ReceiveTlvs; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, chainmonitor, channelmonitor, Confirm, Watch}; use lightning::chain::channelmonitor::{ChannelMonitor, MonitorEvent}; @@ -45,7 +46,7 @@ use lightning::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; use lightning::ln::msgs::{self, CommitmentUpdate, ChannelMessageHandler, DecodeError, UpdateAddHTLC, Init}; use lightning::ln::script::ShutdownScript; use lightning::ln::functional_test_utils::*; -use lightning::offers::invoice::UnsignedBolt12Invoice; +use lightning::offers::invoice::{BlindedPayInfo, UnsignedBolt12Invoice}; use lightning::offers::invoice_request::UnsignedInvoiceRequest; use lightning::onion_message::{Destination, MessageRouter, OnionMessagePath}; use lightning::util::test_channel_signer::{TestChannelSigner, EnforcementState}; @@ -101,6 +102,15 @@ impl Router for FuzzRouter { action: msgs::ErrorAction::IgnoreError }) } + + fn create_blinded_payment_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _first_hops: Vec, _tlvs: ReceiveTlvs, + _amount_msats: u64, _entropy_source: &ES, _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } } impl MessageRouter for FuzzRouter { diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 9eb39b119af..a2ce98cf4d2 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -29,6 +29,7 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{Txid, BlockHash, WPubkeyHash}; use lightning::blinded_path::BlindedPath; +use lightning::blinded_path::payment::ReceiveTlvs; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen}; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; @@ -42,7 +43,7 @@ use lightning::ln::peer_handler::{MessageHandler,PeerManager,SocketDescriptor,Ig use lightning::ln::msgs::{self, DecodeError}; use lightning::ln::script::ShutdownScript; use lightning::ln::functional_test_utils::*; -use lightning::offers::invoice::UnsignedBolt12Invoice; +use lightning::offers::invoice::{BlindedPayInfo, UnsignedBolt12Invoice}; use lightning::offers::invoice_request::UnsignedInvoiceRequest; use lightning::onion_message::{Destination, MessageRouter, OnionMessagePath}; use lightning::routing::gossip::{P2PGossipSync, NetworkGraph}; @@ -144,6 +145,15 @@ impl Router for FuzzRouter { action: msgs::ErrorAction::IgnoreError }) } + + fn create_blinded_payment_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _first_hops: Vec, _tlvs: ReceiveTlvs, + _amount_msats: u64, _entropy_source: &ES, _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } } impl MessageRouter for FuzzRouter { diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 42eeb6cb136..08c57266cb5 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -14,9 +14,10 @@ use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; use crate::blinded_path::{BlindedHop, BlindedPath}; +use crate::blinded_path::payment::{ForwardNode, ForwardTlvs, PaymentConstraints, PaymentRelay, ReceiveTlvs}; use crate::ln::PaymentHash; use crate::ln::channelmanager::{ChannelDetails, PaymentId}; -use crate::ln::features::{Bolt11InvoiceFeatures, Bolt12InvoiceFeatures, ChannelFeatures, NodeFeatures}; +use crate::ln::features::{BlindedHopFeatures, Bolt11InvoiceFeatures, Bolt12InvoiceFeatures, ChannelFeatures, NodeFeatures}; use crate::ln::msgs::{DecodeError, ErrorAction, LightningError, MAX_VALUE_MSAT}; use crate::offers::invoice::{BlindedPayInfo, Bolt12Invoice}; use crate::onion_message::{DefaultMessageRouter, Destination, MessageRouter, OnionMessagePath}; @@ -82,6 +83,81 @@ impl> + Clone, L: Deref, S: Deref, SP: Sized, &random_seed_bytes ) } + + fn create_blinded_payment_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, + amount_msats: u64, entropy_source: &ES, secp_ctx: &Secp256k1 + ) -> Result, ()> { + // Limit the number of blinded paths that are computed. + const MAX_PAYMENT_PATHS: usize = 3; + + // Ensure peers have at least three channels so that it is more difficult to infer the + // recipient's node_id. + const MIN_PEER_CHANNELS: usize = 3; + + let network_graph = self.network_graph.deref().read_only(); + let paths = first_hops.into_iter() + .filter(|details| details.counterparty.features.supports_route_blinding()) + .filter(|details| amount_msats <= details.inbound_capacity_msat) + .filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0)) + .filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(0)) + .filter(|details| network_graph + .node(&NodeId::from_pubkey(&details.counterparty.node_id)) + .map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS) + .unwrap_or(false) + ) + .filter_map(|details| { + let short_channel_id = match details.get_inbound_payment_scid() { + Some(short_channel_id) => short_channel_id, + None => return None, + }; + let payment_relay: PaymentRelay = match details.counterparty.forwarding_info { + Some(forwarding_info) => forwarding_info.into(), + None => return None, + }; + + // Avoid exposing esoteric CLTV expiry deltas + let cltv_expiry_delta = match payment_relay.cltv_expiry_delta { + 0..=40 => 40u32, + 41..=80 => 80u32, + 81..=144 => 144u32, + 145..=216 => 216u32, + _ => return None, + }; + + let payment_constraints = PaymentConstraints { + max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta, + htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0), + }; + Some(ForwardNode { + tlvs: ForwardTlvs { + short_channel_id, + payment_relay, + payment_constraints, + features: BlindedHopFeatures::empty(), + }, + node_id: details.counterparty.node_id, + htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(0), + }) + }) + .map(|forward_node| { + BlindedPath::new_for_payment( + &[forward_node], recipient, tlvs.clone(), u64::MAX, entropy_source, secp_ctx + ) + }) + .take(MAX_PAYMENT_PATHS) + .collect::, _>>(); + + match paths { + Ok(paths) if !paths.is_empty() => Ok(paths), + _ => { + BlindedPath::one_hop_for_payment(recipient, tlvs, entropy_source, secp_ctx) + .map(|path| vec![path]) + }, + } + } } impl< G: Deref> + Clone, L: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> MessageRouter for DefaultRouter where @@ -129,6 +205,16 @@ pub trait Router: MessageRouter { ) -> Result { self.find_route(payer, route_params, first_hops, inflight_htlcs) } + + /// Creates [`BlindedPath`]s for payment to the `recipient` node. The channels in `first_hops` + /// are assumed to be with the `recipient`'s peers. The payment secret and any constraints are + /// given in `tlvs`. + fn create_blinded_payment_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, + amount_msats: u64, entropy_source: &ES, secp_ctx: &Secp256k1 + ) -> Result, ()>; } /// [`ScoreLookUp`] implementation that factors in in-flight HTLC liquidity. diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 3954088fc16..ba56edf3584 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -8,6 +8,7 @@ // licenses. use crate::blinded_path::BlindedPath; +use crate::blinded_path::payment::ReceiveTlvs; use crate::chain; use crate::chain::WatchedOutput; use crate::chain::chaininterface; @@ -23,13 +24,13 @@ use crate::sign; use crate::events; use crate::events::bump_transaction::{WalletSource, Utxo}; use crate::ln::ChannelId; -use crate::ln::channelmanager; +use crate::ln::channelmanager::{ChannelDetails, self}; use crate::ln::chan_utils::CommitmentTransaction; use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures}; use crate::ln::{msgs, wire}; use crate::ln::msgs::LightningError; use crate::ln::script::ShutdownScript; -use crate::offers::invoice::UnsignedBolt12Invoice; +use crate::offers::invoice::{BlindedPayInfo, UnsignedBolt12Invoice}; use crate::offers::invoice_request::UnsignedInvoiceRequest; use crate::onion_message::{Destination, MessageRouter, OnionMessagePath}; use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId, RoutingFees}; @@ -121,7 +122,7 @@ impl<'a> TestRouter<'a> { impl<'a> Router for TestRouter<'a> { fn find_route( - &self, payer: &PublicKey, params: &RouteParameters, first_hops: Option<&[&channelmanager::ChannelDetails]>, + &self, payer: &PublicKey, params: &RouteParameters, first_hops: Option<&[&ChannelDetails]>, inflight_htlcs: InFlightHtlcs ) -> Result { if let Some((find_route_query, find_route_res)) = self.next_routes.lock().unwrap().pop_front() { @@ -191,6 +192,15 @@ impl<'a> Router for TestRouter<'a> { &[42; 32] ) } + + fn create_blinded_payment_paths< + ES: EntropySource + ?Sized, T: secp256k1::Signing + secp256k1::Verification + >( + &self, _recipient: PublicKey, _first_hops: Vec, _tlvs: ReceiveTlvs, + _amount_msats: u64, _entropy_source: &ES, _secp_ctx: &Secp256k1 + ) -> Result, ()> { + unreachable!() + } } impl<'a> MessageRouter for TestRouter<'a> { From 827833c933013b52fde8326715cfe803c9299fb6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 8 Dec 2023 15:54:21 -0600 Subject: [PATCH 11/14] Multi-hop blinded payment paths in ChannelManager When constructing blinded payment paths for Bolt12Invoice, delegate to Router::create_blinded_payment_paths which may produce multi-hop blinded paths. Fallback to one-hop blinded paths if the Router fails or returns no paths. --- lightning/src/ln/channelmanager.rs | 58 +++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 397ffa5866b..af37399e212 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -7724,6 +7724,11 @@ where /// node meeting the aforementioned criteria, but there's no guarantee that they will be /// received and no retries will be made. /// + /// # Errors + /// + /// Errors if the parameterized [`Router`] is unable to create a blinded payment path or reply + /// path for the invoice. + /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub fn request_refund_payment(&self, refund: &Refund) -> Result<(), Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; @@ -7735,9 +7740,9 @@ where match self.create_inbound_payment(Some(amount_msats), relative_expiry, None) { Ok((payment_hash, payment_secret)) => { - let payment_paths = vec![ - self.create_one_hop_blinded_payment_path(payment_secret), - ]; + let payment_paths = self.create_blinded_payment_paths(amount_msats, payment_secret) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + #[cfg(not(feature = "no-std"))] let builder = refund.respond_using_derived_keys( payment_paths, payment_hash, expanded_key, entropy @@ -7898,14 +7903,15 @@ where .and_then(|paths| paths.into_iter().next().ok_or(())) } - /// Creates a one-hop blinded payment path with [`ChannelManager::get_our_node_id`] as the - /// introduction node. - fn create_one_hop_blinded_payment_path( - &self, payment_secret: PaymentSecret - ) -> (BlindedPayInfo, BlindedPath) { + /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to + /// [`Router::create_blinded_payment_paths`]. + fn create_blinded_payment_paths( + &self, amount_msats: u64, payment_secret: PaymentSecret + ) -> Result, ()> { let entropy_source = self.entropy_source.deref(); let secp_ctx = &self.secp_ctx; + let first_hops = self.list_usable_channels(); let payee_node_id = self.get_our_node_id(); let max_cltv_expiry = self.best_block.read().unwrap().height() + CLTV_FAR_FAR_AWAY + LATENCY_GRACE_PERIOD_BLOCKS; @@ -7916,10 +7922,9 @@ where htlc_minimum_msat: 1, }, }; - // TODO: Err for overflow? - BlindedPath::one_hop_for_payment( - payee_node_id, payee_tlvs, entropy_source, secp_ctx - ).unwrap() + self.router.create_blinded_payment_paths( + payee_node_id, first_hops, payee_tlvs, amount_msats, entropy_source, secp_ctx + ) } /// Gets a fake short channel id for use in receiving [phantom node payments]. These fake scids @@ -9159,7 +9164,7 @@ where let amount_msats = match InvoiceBuilder::::amount_msats( &invoice_request ) { - Ok(amount_msats) => Some(amount_msats), + Ok(amount_msats) => amount_msats, Err(error) => return Some(OffersMessage::InvoiceError(error.into())), }; let invoice_request = match invoice_request.verify(expanded_key, secp_ctx) { @@ -9171,11 +9176,17 @@ where }; let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - match self.create_inbound_payment(amount_msats, relative_expiry, None) { + match self.create_inbound_payment(Some(amount_msats), relative_expiry, None) { Ok((payment_hash, payment_secret)) if invoice_request.keys.is_some() => { - let payment_paths = vec![ - self.create_one_hop_blinded_payment_path(payment_secret), - ]; + let payment_paths = match self.create_blinded_payment_paths( + amount_msats, payment_secret + ) { + Ok(payment_paths) => payment_paths, + Err(()) => { + let error = Bolt12SemanticError::MissingPaths; + return Some(OffersMessage::InvoiceError(error.into())); + }, + }; #[cfg(not(feature = "no-std"))] let builder = invoice_request.respond_using_derived_keys( payment_paths, payment_hash @@ -9194,9 +9205,16 @@ where } }, Ok((payment_hash, payment_secret)) => { - let payment_paths = vec![ - self.create_one_hop_blinded_payment_path(payment_secret), - ]; + let payment_paths = match self.create_blinded_payment_paths( + amount_msats, payment_secret + ) { + Ok(payment_paths) => payment_paths, + Err(()) => { + let error = Bolt12SemanticError::MissingPaths; + return Some(OffersMessage::InvoiceError(error.into())); + }, + }; + #[cfg(not(feature = "no-std"))] let builder = invoice_request.respond_with(payment_paths, payment_hash); #[cfg(feature = "no-std")] From 16ee24086cda7a87daf14ad94491215da75a6f9b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 14 Dec 2023 17:28:11 -0600 Subject: [PATCH 12/14] DRY up OffersMessage::InvoiceRequest handling --- lightning/src/ln/channelmanager.rs | 124 ++++++++++++++--------------- 1 file changed, 58 insertions(+), 66 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index af37399e212..560a7e58eda 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9174,77 +9174,69 @@ where return Some(OffersMessage::InvoiceError(error.into())); }, }; - let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - match self.create_inbound_payment(Some(amount_msats), relative_expiry, None) { - Ok((payment_hash, payment_secret)) if invoice_request.keys.is_some() => { - let payment_paths = match self.create_blinded_payment_paths( - amount_msats, payment_secret - ) { - Ok(payment_paths) => payment_paths, - Err(()) => { - let error = Bolt12SemanticError::MissingPaths; - return Some(OffersMessage::InvoiceError(error.into())); - }, - }; - #[cfg(not(feature = "no-std"))] - let builder = invoice_request.respond_using_derived_keys( - payment_paths, payment_hash - ); - #[cfg(feature = "no-std")] - let created_at = Duration::from_secs( - self.highest_seen_timestamp.load(Ordering::Acquire) as u64 - ); - #[cfg(feature = "no-std")] - let builder = invoice_request.respond_using_derived_keys_no_std( - payment_paths, payment_hash, created_at - ); - match builder.and_then(|b| b.allow_mpp().build_and_sign(secp_ctx)) { - Ok(invoice) => Some(OffersMessage::Invoice(invoice)), - Err(error) => Some(OffersMessage::InvoiceError(error.into())), - } + let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + let (payment_hash, payment_secret) = match self.create_inbound_payment( + Some(amount_msats), relative_expiry, None + ) { + Ok((payment_hash, payment_secret)) => (payment_hash, payment_secret), + Err(()) => { + let error = Bolt12SemanticError::InvalidAmount; + return Some(OffersMessage::InvoiceError(error.into())); }, - Ok((payment_hash, payment_secret)) => { - let payment_paths = match self.create_blinded_payment_paths( - amount_msats, payment_secret - ) { - Ok(payment_paths) => payment_paths, - Err(()) => { - let error = Bolt12SemanticError::MissingPaths; - return Some(OffersMessage::InvoiceError(error.into())); - }, - }; + }; - #[cfg(not(feature = "no-std"))] - let builder = invoice_request.respond_with(payment_paths, payment_hash); - #[cfg(feature = "no-std")] - let created_at = Duration::from_secs( - self.highest_seen_timestamp.load(Ordering::Acquire) as u64 - ); - #[cfg(feature = "no-std")] - let builder = invoice_request.respond_with_no_std( - payment_paths, payment_hash, created_at - ); - let response = builder.and_then(|builder| builder.allow_mpp().build()) - .map_err(|e| OffersMessage::InvoiceError(e.into())) - .and_then(|invoice| - match invoice.sign(|invoice| self.node_signer.sign_bolt12_invoice(invoice)) { - Ok(invoice) => Ok(OffersMessage::Invoice(invoice)), - Err(SignError::Signing(())) => Err(OffersMessage::InvoiceError( - InvoiceError::from_string("Failed signing invoice".to_string()) - )), - Err(SignError::Verification(_)) => Err(OffersMessage::InvoiceError( - InvoiceError::from_string("Failed invoice signature verification".to_string()) - )), - }); - match response { - Ok(invoice) => Some(invoice), - Err(error) => Some(error), - } - }, + let payment_paths = match self.create_blinded_payment_paths( + amount_msats, payment_secret + ) { + Ok(payment_paths) => payment_paths, Err(()) => { - Some(OffersMessage::InvoiceError(Bolt12SemanticError::InvalidAmount.into())) + let error = Bolt12SemanticError::MissingPaths; + return Some(OffersMessage::InvoiceError(error.into())); }, + }; + + #[cfg(feature = "no-std")] + let created_at = Duration::from_secs( + self.highest_seen_timestamp.load(Ordering::Acquire) as u64 + ); + + if invoice_request.keys.is_some() { + #[cfg(not(feature = "no-std"))] + let builder = invoice_request.respond_using_derived_keys( + payment_paths, payment_hash + ); + #[cfg(feature = "no-std")] + let builder = invoice_request.respond_using_derived_keys_no_std( + payment_paths, payment_hash, created_at + ); + match builder.and_then(|b| b.allow_mpp().build_and_sign(secp_ctx)) { + Ok(invoice) => Some(OffersMessage::Invoice(invoice)), + Err(error) => Some(OffersMessage::InvoiceError(error.into())), + } + } else { + #[cfg(not(feature = "no-std"))] + let builder = invoice_request.respond_with(payment_paths, payment_hash); + #[cfg(feature = "no-std")] + let builder = invoice_request.respond_with_no_std( + payment_paths, payment_hash, created_at + ); + let response = builder.and_then(|builder| builder.allow_mpp().build()) + .map_err(|e| OffersMessage::InvoiceError(e.into())) + .and_then(|invoice| + match invoice.sign(|invoice| self.node_signer.sign_bolt12_invoice(invoice)) { + Ok(invoice) => Ok(OffersMessage::Invoice(invoice)), + Err(SignError::Signing(())) => Err(OffersMessage::InvoiceError( + InvoiceError::from_string("Failed signing invoice".to_string()) + )), + Err(SignError::Verification(_)) => Err(OffersMessage::InvoiceError( + InvoiceError::from_string("Failed invoice signature verification".to_string()) + )), + }); + match response { + Ok(invoice) => Some(invoice), + Err(error) => Some(error), + } } }, OffersMessage::Invoice(invoice) => { From 164c8553e594269a4c42821f04f41df4b41892c8 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 14 Dec 2023 21:19:57 -0600 Subject: [PATCH 13/14] Use one-hop blinded paths only for announced nodes To avoid exposing a node's identity in a blinded path, only create one-hop blinded paths if the node has been announced, and thus has public channels. Otherwise, there is no way to route a payment to the node, exposing its identity needlessly. --- lightning/src/onion_message/messenger.rs | 14 +++++++++----- lightning/src/routing/router.rs | 8 ++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index c0db6096b99..7743270e257 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -350,16 +350,16 @@ where const MIN_PEER_CHANNELS: usize = 3; let network_graph = self.network_graph.deref().read_only(); - let paths = peers.into_iter() + let paths = peers.iter() // Limit to peers with announced channels .filter(|pubkey| network_graph - .node(&NodeId::from_pubkey(&pubkey)) + .node(&NodeId::from_pubkey(pubkey)) .map(|info| &info.channels[..]) .map(|channels| channels.len() >= MIN_PEER_CHANNELS) .unwrap_or(false) ) - .map(|pubkey| vec![pubkey, recipient]) + .map(|pubkey| vec![*pubkey, recipient]) .map(|node_pks| BlindedPath::new_for_message(&node_pks, entropy_source, secp_ctx)) .take(MAX_PATHS) .collect::, _>>(); @@ -367,8 +367,12 @@ where match paths { Ok(paths) if !paths.is_empty() => Ok(paths), _ => { - BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx) - .map(|path| vec![path]) + if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) { + BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx) + .map(|path| vec![path]) + } else { + Err(()) + } }, } } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 08c57266cb5..86c42841027 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -153,8 +153,12 @@ impl> + Clone, L: Deref, S: Deref, SP: Sized, match paths { Ok(paths) if !paths.is_empty() => Ok(paths), _ => { - BlindedPath::one_hop_for_payment(recipient, tlvs, entropy_source, secp_ctx) - .map(|path| vec![path]) + if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) { + BlindedPath::one_hop_for_payment(recipient, tlvs, entropy_source, secp_ctx) + .map(|path| vec![path]) + } else { + Err(()) + } }, } } From 37319a6193b6dbeb1570a29dc3ed667148095471 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 15 Dec 2023 15:11:51 -0600 Subject: [PATCH 14/14] Fix build warning --- lightning/src/routing/scoring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index 12ffdecec42..6ace8681ac0 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -3442,7 +3442,7 @@ mod tests { Some(([0; 32], [0; 32]))); assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms), None); - let mut usage = ChannelUsage { + let usage = ChannelUsage { amount_msat: 100, inflight_htlc_msat: 1024, effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },