From 7d1745e7210975cc0fdc17b31072e33f0dfbf8e3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 12 Jun 2024 10:58:18 -0500 Subject: [PATCH 1/8] BlindedPath with unannounced introduction node When creating blinded paths for receiving onion messages, allow using the recipient's only peer as the introduction node when the recipient is unannounced. This allows for sending messages without going through an intermediary, which is useful for testing or when only connected to an LSP with an empty NetworkGraph. --- lightning/src/blinded_path/message.rs | 2 +- lightning/src/ln/offers_tests.rs | 67 ++++++++++++++----- .../src/onion_message/functional_tests.rs | 5 +- lightning/src/onion_message/messenger.rs | 9 ++- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 1f3f5a1fa38..369a12243f7 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -30,7 +30,7 @@ use core::mem; use core::ops::Deref; /// An intermediate node, and possibly a short channel id leading to the next node. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub struct ForwardNode { /// This node's pubkey. pub node_id: PublicKey, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index eca43afee8a..9f4f07df879 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -950,9 +950,12 @@ fn pays_bolt12_invoice_asynchronously() { ); } -/// Fails creating an offer when a blinded path cannot be created without exposing the node's id. +/// Checks that an offer can be created using an unannounced node as a blinded path's introduction +/// node. This is only preferred if there are no other options which may indicated either the offer +/// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but +/// the recipient doesn't have a network graph. #[test] -fn fails_creating_offer_without_blinded_paths() { +fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { 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]); @@ -960,15 +963,38 @@ fn fails_creating_offer_without_blinded_paths() { create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - match nodes[0].node.create_offer_builder(None) { - Ok(_) => panic!("Expected error"), - Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + let offer = alice.node + .create_offer_builder(None).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + assert_ne!(offer.signing_pubkey(), Some(alice_id)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); } + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + 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(alice_id)); } -/// Fails creating a refund when a blinded path cannot be created without exposing the node's id. +/// Checks that a refund can be created using an unannounced node as a blinded path's introduction +/// node. This is only preferred if there are no other options which may indicated either the refund +/// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but +/// the sender doesn't have a network graph. #[test] -fn fails_creating_refund_without_blinded_paths() { +fn creates_refund_with_blinded_path_using_unannounced_introduction_node() { 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]); @@ -976,17 +1002,23 @@ fn fails_creating_refund_without_blinded_paths() { create_unannounced_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 bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - - match nodes[0].node.create_refund_builder( - 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None - ) { - Ok(_) => panic!("Expected error"), - Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + let refund = bob.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + assert_ne!(refund.payer_id(), bob_id); + assert!(!refund.paths().is_empty()); + for path in refund.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); } - - assert!(nodes[0].node.list_recent_payments().is_empty()); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); } /// Fails creating or paying an offer when a blinded path cannot be created because no peers are @@ -1165,8 +1197,7 @@ fn fails_sending_invoice_with_unsupported_chain_for_refund() { } } -/// Fails creating an invoice request when a blinded reply path cannot be created without exposing -/// the node's id. +/// Fails creating an invoice request when a blinded reply path cannot be created. #[test] fn fails_creating_invoice_request_without_blinded_reply_path() { let chanmon_cfgs = create_chanmon_cfgs(6); @@ -1183,7 +1214,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); - disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); + disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); let offer = alice.node .create_offer_builder(None).unwrap() diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 08be1b2c502..ed601c047c2 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -492,8 +492,9 @@ fn async_response_with_reply_path_fails() { let path_id = Some([2; 32]); let reply_path = BlindedPath::new_for_message(&[], bob.node_id, &*bob.entropy_source, &secp_ctx).unwrap(); - // Alice tries to asynchronously respond to Bob, but fails because the nodes are unannounced. - // Therefore, the reply_path cannot be used for the response. + // Alice tries to asynchronously respond to Bob, but fails because the nodes are unannounced and + // disconnected. Thus, a reply path could no be created for the response. + disconnect_peers(alice, bob); let responder = Responder::new(reply_path, path_id); alice.custom_message_handler.expect_message_and_response(message.clone()); let response_instruction = alice.custom_message_handler.handle_custom_message(message, Some(responder)); diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 0fb72c52d27..d333eb2103c 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -489,7 +489,7 @@ where } fn create_blinded_paths_from_iter< - I: Iterator, + I: ExactSizeIterator, T: secp256k1::Signing + secp256k1::Verification >( &self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1, compact_paths: bool @@ -505,13 +505,18 @@ where let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); + let has_one_peer = peers.len() == 1; let mut peer_info = peers - // Limit to peers with announced channels + // Limit to peers with announced channels unless the recipient is unannounced. .filter_map(|peer| network_graph .node(&NodeId::from_pubkey(&peer.node_id)) .filter(|info| info.channels.len() >= MIN_PEER_CHANNELS) .map(|info| (peer, info.is_tor_only(), info.channels.len())) + // Allow messages directly with the only peer when unannounced. + .or_else(|| (!is_recipient_announced && has_one_peer) + .then(|| (peer, false, 0)) + ) ) // Exclude Tor-only nodes when the recipient is announced. .filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced)) From 3bf84204e3a4c6772d231fa3b85f90a79e5b4871 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 12 Jun 2024 16:54:19 -0500 Subject: [PATCH 2/8] Blinded payments to unannounced introduction node When creating blinded paths for receiving onion payments, allow using the recipient's only peer as the introduction node when the recipient is unannounced. This allows for sending payments without going through an intermediary, which is useful for testing or when only connected to an LSP with an empty NetworkGraph. --- lightning/src/ln/offers_tests.rs | 24 +++++++++++++++++++++++- lightning/src/routing/router.rs | 16 +++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 9f4f07df879..1922ef71be8 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -985,8 +985,18 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); alice.onion_messenger.handle_onion_message(&bob_id, &onion_message); - let (_, reply_path) = extract_invoice_request(alice, &onion_message); + let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + assert_ne!(invoice_request.payer_id(), bob_id); assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(alice_id)); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + + let invoice = extract_invoice(bob, &onion_message); + assert_ne!(invoice.signing_pubkey(), alice_id); + assert!(!invoice.payment_paths().is_empty()); + for (_, path) in invoice.payment_paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); + } } /// Checks that a refund can be created using an unannounced node as a blinded path's introduction @@ -1019,6 +1029,18 @@ fn creates_refund_with_blinded_path_using_unannounced_introduction_node() { assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + + let invoice = extract_invoice(bob, &onion_message); + assert_eq!(invoice, expected_invoice); + assert_ne!(invoice.signing_pubkey(), alice_id); + assert!(!invoice.payment_paths().is_empty()); + for (_, path) in invoice.payment_paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); + } } /// Fails creating or paying an offer when a blinded path cannot be created because no peers are diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 9a88d913896..19931d5c5d8 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -100,7 +100,20 @@ impl> + Clone, L: Deref, ES: Deref, S: Deref, // recipient's node_id. const MIN_PEER_CHANNELS: usize = 3; + let has_one_peer = first_hops + .first() + .map(|details| details.counterparty.node_id) + .map(|node_id| first_hops + .iter() + .skip(1) + .all(|details| details.counterparty.node_id == node_id) + ) + .unwrap_or(false); + let network_graph = self.network_graph.deref().read_only(); + let is_recipient_announced = + network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); + let paths = first_hops.into_iter() .filter(|details| details.counterparty.features.supports_route_blinding()) .filter(|details| amount_msats <= details.inbound_capacity_msat) @@ -109,7 +122,8 @@ impl> + Clone, L: Deref, ES: Deref, S: Deref, .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) + // Allow payments directly with the only peer when unannounced. + .unwrap_or(!is_recipient_announced && has_one_peer) ) .filter_map(|details| { let short_channel_id = match details.get_inbound_payment_scid() { From efc4647c6556ff2a41ef89994708277dac80875a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 14 Jun 2024 09:04:47 -0500 Subject: [PATCH 3/8] Don't modify path when advance_path_by_one errors When using advance_path_by_one when we are the introduction node, any error will result having the first hop of the input blinded path removed. Instead, only remove the first hop on success. Otherwise, the path will be invalid. --- lightning/src/blinded_path/message.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 369a12243f7..bdbb4be4541 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -106,6 +106,8 @@ pub(super) fn blinded_hops( // Advance the blinded onion message path by one hop, so make the second hop into the new // introduction node. +// +// Will only modify `path` when returning `Ok`. pub(crate) fn advance_path_by_one( path: &mut BlindedPath, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1 ) -> Result<(), ()> @@ -116,8 +118,8 @@ where { let control_tlvs_ss = node_signer.ecdh(Recipient::Node, &path.blinding_point, None)?; let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes()); - let encrypted_control_tlvs = path.blinded_hops.remove(0).encrypted_payload; - let mut s = Cursor::new(&encrypted_control_tlvs); + let encrypted_control_tlvs = &path.blinded_hops.get(0).ok_or(())?.encrypted_payload; + let mut s = Cursor::new(encrypted_control_tlvs); let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64); match ChaChaPolyReadAdapter::read(&mut reader, rho) { Ok(ChaChaPolyReadAdapter { @@ -139,6 +141,7 @@ where }; mem::swap(&mut path.blinding_point, &mut new_blinding_point); path.introduction_node = IntroductionNode::NodeId(next_node_id); + path.blinded_hops.remove(0); Ok(()) }, _ => Err(()) From b6fcd84a13785c6ad080f3a2bbfd9f174a58a12c Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 14 Jun 2024 16:57:41 -0500 Subject: [PATCH 4/8] Add advance_path_by_one for blinded payment paths Similar to blinded paths for onion messages, if given a blinded payment path where we are the introduction node, the path must be advanced by one in order to use it. --- lightning/src/blinded_path/payment.rs | 48 +++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 3b71f77c6d5..7df7d1c63ed 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -13,17 +13,24 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; -use crate::blinded_path::BlindedHop; +use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp}; use crate::blinded_path::utils; +use crate::crypto::streams::ChaChaPolyReadAdapter; use crate::io; +use crate::io::Cursor; use crate::ln::types::PaymentSecret; use crate::ln::channel_state::CounterpartyForwardingInfo; use crate::ln::features::BlindedHopFeatures; use crate::ln::msgs::DecodeError; +use crate::ln::onion_utils; use crate::offers::invoice::BlindedPayInfo; use crate::offers::invoice_request::InvoiceRequestFields; use crate::offers::offer::OfferId; -use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer}; +use crate::sign::{NodeSigner, Recipient}; +use crate::util::ser::{FixedLengthReader, LengthReadableArgs, HighZeroBytesDroppedBigSize, Readable, Writeable, Writer}; + +use core::mem; +use core::ops::Deref; #[allow(unused_imports)] use crate::prelude::*; @@ -274,6 +281,43 @@ pub(super) fn blinded_hops( utils::construct_blinded_hops(secp_ctx, pks, tlvs, session_priv) } +// Advance the blinded onion payment path by one hop, so make the second hop into the new +// introduction node. +// +// Will only modify `path` when returning `Ok`. +pub(crate) fn advance_path_by_one( + path: &mut BlindedPath, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1 +) -> Result<(), ()> +where + NS::Target: NodeSigner, + NL::Target: NodeIdLookUp, + T: secp256k1::Signing + secp256k1::Verification, +{ + let control_tlvs_ss = node_signer.ecdh(Recipient::Node, &path.blinding_point, None)?; + let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes()); + let encrypted_control_tlvs = &path.blinded_hops.get(0).ok_or(())?.encrypted_payload; + let mut s = Cursor::new(encrypted_control_tlvs); + let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64); + match ChaChaPolyReadAdapter::read(&mut reader, rho) { + Ok(ChaChaPolyReadAdapter { + readable: BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }) + }) => { + let next_node_id = match node_id_lookup.next_node_id(short_channel_id) { + Some(node_id) => node_id, + None => return Err(()), + }; + let mut new_blinding_point = onion_utils::next_hop_pubkey( + secp_ctx, path.blinding_point, control_tlvs_ss.as_ref() + ).map_err(|_| ())?; + mem::swap(&mut path.blinding_point, &mut new_blinding_point); + path.introduction_node = IntroductionNode::NodeId(next_node_id); + path.blinded_hops.remove(0); + Ok(()) + }, + _ => Err(()) + } +} + /// `None` if underflow occurs. pub(crate) fn amt_to_forward_msat(inbound_amt_msat: u64, payment_relay: &PaymentRelay) -> Option { let inbound_amt = inbound_amt_msat as u128; From 79122d69d4f34a6ac1b5e186b8a6ebc0550c9f1a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 14 Jun 2024 09:32:17 -0500 Subject: [PATCH 5/8] Rename variable --- lightning/src/ln/outbound_payment.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 0b8819aa0c6..ac9eb74823f 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -807,9 +807,11 @@ impl OutboundPayments { hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice), }; - let pay_params = PaymentParameters::from_bolt12_invoice(&invoice); + let payment_params = PaymentParameters::from_bolt12_invoice(&invoice); let amount_msat = invoice.amount_msats(); - let mut route_params = RouteParameters::from_payment_params_and_value(pay_params, amount_msat); + let mut route_params = RouteParameters::from_payment_params_and_value( + payment_params, amount_msat + ); if let Some(max_fee_msat) = max_total_routing_fee_msat { route_params.max_total_routing_fee_msat = Some(max_fee_msat); } From 55ba2aab41d423f3f043cff561c2640a21b36d39 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 13 Jun 2024 14:35:21 -0500 Subject: [PATCH 6/8] Re-order imports --- lightning/src/ln/outbound_payment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index ac9eb74823f..906f4c58c76 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -13,7 +13,6 @@ use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; -use crate::sign::{EntropySource, NodeSigner, Recipient}; use crate::events::{self, PaymentFailureReason}; use crate::ln::types::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel_state::ChannelDetails; @@ -22,6 +21,7 @@ use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::Bolt12Invoice; use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, Router}; +use crate::sign::{EntropySource, NodeSigner, Recipient}; use crate::util::errors::APIError; use crate::util::logger::Logger; use crate::util::time::Time; From 642913c586fc71b0e413532e7dedcd19cfd4815c Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 13 Jun 2024 17:15:19 -0500 Subject: [PATCH 7/8] Advance self blinded payment paths DefaultRouter will ignore blinded paths where the sender is the introduction node. Similar to message paths, advance such paths by one so that payments may be sent to them. --- lightning/src/ln/channelmanager.rs | 4 +-- lightning/src/ln/offers_tests.rs | 15 +++++++++ lightning/src/ln/outbound_payment.rs | 50 +++++++++++++++++++++++----- lightning/src/routing/router.rs | 7 ++++ 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 508d13a1582..9e573a705f7 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4031,8 +4031,8 @@ where self.pending_outbound_payments .send_payment_for_bolt12_invoice( invoice, payment_id, &self.router, self.list_usable_channels(), - || self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer, - best_block_height, &self.logger, &self.pending_events, + || self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer, &self, + &self.secp_ctx, best_block_height, &self.logger, &self.pending_events, |args| self.send_payment_along_path(args) ) } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 1922ef71be8..52375f723c7 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -986,10 +986,19 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { alice.onion_messenger.handle_onion_message(&bob_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_id: invoice_request.payer_id(), + quantity: None, + payer_note_truncated: None, + }, + }); assert_ne!(invoice_request.payer_id(), bob_id); assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(alice_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); let invoice = extract_invoice(bob, &onion_message); assert_ne!(invoice.signing_pubkey(), alice_id); @@ -997,6 +1006,12 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { for (_, path) in invoice.payment_paths() { assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); } + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + claim_bolt12_payment(bob, &[alice], payment_context); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } /// Checks that a refund can be created using an unannounced node as a blinded path's introduction diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 906f4c58c76..1915a4c5a14 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -13,6 +13,8 @@ use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; +use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; +use crate::blinded_path::payment::advance_path_by_one; use crate::events::{self, PaymentFailureReason}; use crate::ln::types::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel_state::ChannelDetails; @@ -775,10 +777,13 @@ impl OutboundPayments { } } - pub(super) fn send_payment_for_bolt12_invoice( + pub(super) fn send_payment_for_bolt12_invoice< + R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref + >( &self, invoice: &Bolt12Invoice, payment_id: PaymentId, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, - best_block_height: u32, logger: &L, + node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, + logger: &L, pending_events: &Mutex)>>, send_payment_along_path: SP, ) -> Result<(), Bolt12PaymentError> @@ -786,6 +791,7 @@ impl OutboundPayments { R::Target: Router, ES::Target: EntropySource, NS::Target: NodeSigner, + NL::Target: NodeIdLookUp, L::Target: Logger, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, @@ -807,7 +813,26 @@ impl OutboundPayments { hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice), }; - let payment_params = PaymentParameters::from_bolt12_invoice(&invoice); + let mut payment_params = PaymentParameters::from_bolt12_invoice(&invoice); + + // Advance any blinded path where the introduction node is our node. + if let Ok(our_node_id) = node_signer.get_node_id(Recipient::Node) { + for (_, path) in payment_params.payee.blinded_route_hints_mut().iter_mut() { + let introduction_node_id = match path.introduction_node { + IntroductionNode::NodeId(pubkey) => pubkey, + IntroductionNode::DirectedShortChannelId(direction, scid) => { + match node_id_lookup.next_node_id(scid) { + Some(next_node_id) => *direction.select_pubkey(&our_node_id, &next_node_id), + None => continue, + } + }, + }; + if introduction_node_id == our_node_id { + let _ = advance_path_by_one(path, node_signer, node_id_lookup, secp_ctx); + } + } + } + let amount_msat = invoice.amount_msats(); let mut route_params = RouteParameters::from_payment_params_and_value( payment_params, amount_msat @@ -1858,6 +1883,7 @@ mod tests { use core::time::Duration; + use crate::blinded_path::EmptyNodeIdLookUp; use crate::events::{Event, PathFailure, PaymentFailureReason}; use crate::ln::types::PaymentHash; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; @@ -2201,6 +2227,7 @@ mod tests { let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); + let secp_ctx = Secp256k1::new(); let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); let pending_events = Mutex::new(VecDeque::new()); @@ -2229,7 +2256,8 @@ mod tests { assert_eq!( outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, - &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + &&keys_manager, &EmptyNodeIdLookUp {}, &secp_ctx, 0, &&logger, &pending_events, + |_| panic!() ), Ok(()), ); @@ -2252,6 +2280,7 @@ mod tests { let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); + let secp_ctx = Secp256k1::new(); let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); let pending_events = Mutex::new(VecDeque::new()); @@ -2288,7 +2317,8 @@ mod tests { assert_eq!( outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, - &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + &&keys_manager, &EmptyNodeIdLookUp {}, &secp_ctx, 0, &&logger, &pending_events, + |_| panic!() ), Ok(()), ); @@ -2311,6 +2341,7 @@ mod tests { let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); + let secp_ctx = Secp256k1::new(); let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); let pending_events = Mutex::new(VecDeque::new()); @@ -2360,7 +2391,8 @@ mod tests { assert_eq!( outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, - &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + &&keys_manager, &EmptyNodeIdLookUp {}, &secp_ctx, 0, &&logger, &pending_events, + |_| panic!() ), Err(Bolt12PaymentError::UnexpectedInvoice), ); @@ -2377,7 +2409,8 @@ mod tests { assert_eq!( outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, - &&keys_manager, 0, &&logger, &pending_events, |_| Ok(()) + &&keys_manager, &EmptyNodeIdLookUp {}, &secp_ctx, 0, &&logger, &pending_events, + |_| Ok(()) ), Ok(()), ); @@ -2387,7 +2420,8 @@ mod tests { assert_eq!( outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, - &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + &&keys_manager, &EmptyNodeIdLookUp {}, &secp_ctx, 0, &&logger, &pending_events, + |_| panic!() ), Err(Bolt12PaymentError::DuplicateInvoice), ); diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 19931d5c5d8..036d6372af0 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -1045,6 +1045,13 @@ impl Payee { } } + pub(crate) fn blinded_route_hints_mut(&mut self) -> &mut [(BlindedPayInfo, BlindedPath)] { + match self { + Self::Blinded { route_hints, .. } => &mut route_hints[..], + Self::Clear { .. } => &mut [] + } + } + fn unblinded_route_hints(&self) -> &[RouteHint] { match self { Self::Blinded { .. } => &[], From c1eda4ba3d9189ea3831c705505c3e3f2edcdbbe Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 17 Jun 2024 14:41:32 -0500 Subject: [PATCH 8/8] Relax channel count check for unannounced nodes When creating blinded paths, introduction nodes are limited to peers with at least three channels to prevent easily guessing the recipient. Relax this check when the recipient is unannounced since they won't be in the NetworkGraph. --- lightning/src/onion_message/messenger.rs | 4 +++- lightning/src/routing/router.rs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index d333eb2103c..859c3f2b5b9 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -511,7 +511,9 @@ where .filter_map(|peer| network_graph .node(&NodeId::from_pubkey(&peer.node_id)) - .filter(|info| info.channels.len() >= MIN_PEER_CHANNELS) + .filter(|info| + !is_recipient_announced || info.channels.len() >= MIN_PEER_CHANNELS + ) .map(|info| (peer, info.is_tor_only(), info.channels.len())) // Allow messages directly with the only peer when unannounced. .or_else(|| (!is_recipient_announced && has_one_peer) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 036d6372af0..28aaba0513f 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -119,9 +119,10 @@ impl> + Clone, L: Deref, ES: Deref, S: Deref, .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(u64::MAX)) + // Limit to peers with announced channels unless the recipient is unannounced. .filter(|details| network_graph .node(&NodeId::from_pubkey(&details.counterparty.node_id)) - .map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS) + .map(|node| !is_recipient_announced || node.channels.len() >= MIN_PEER_CHANNELS) // Allow payments directly with the only peer when unannounced. .unwrap_or(!is_recipient_announced && has_one_peer) )