diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index f5b1d53fc8a..765557db8df 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -236,6 +236,7 @@ mod test { recipient, local_node_receive_key, context, + false, &keys, secp_ctx, )]) @@ -345,6 +346,7 @@ mod test { payer_id, receive_key, query_context, + false, &*payer_keys, &secp_ctx, ); diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index ed55ca5dc9b..bbcea4c3663 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -54,21 +54,38 @@ impl Readable for BlindedMessagePath { impl BlindedMessagePath { /// Create a one-hop blinded path for a message. + /// + /// `compact_padding` selects between space-inefficient padding that better hides contents and + /// a space-constrained padding that does very little to hide the contents, especially for the + /// last hop. It should only be set when the blinded path needs to be as compact as possible. pub fn one_hop( recipient_node_id: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1, + context: MessageContext, compact_padding: bool, entropy_source: ES, + secp_ctx: &Secp256k1, ) -> Self where ES::Target: EntropySource, { - Self::new(&[], recipient_node_id, local_node_receive_key, context, entropy_source, secp_ctx) + Self::new( + &[], + recipient_node_id, + local_node_receive_key, + context, + compact_padding, + entropy_source, + secp_ctx, + ) } /// Create a path for an onion message, to be forwarded along `node_pks`. + /// + /// `compact_padding` selects between space-inefficient padding that better hides contents and + /// a space-constrained padding that does very little to hide the contents, especially for the + /// last hop. It should only be set when the blinded path needs to be as compact as possible. pub fn new( intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, - local_node_receive_key: ReceiveAuthKey, context: MessageContext, entropy_source: ES, - secp_ctx: &Secp256k1, + local_node_receive_key: ReceiveAuthKey, context: MessageContext, compact_padding: bool, + entropy_source: ES, secp_ctx: &Secp256k1, ) -> Self where ES::Target: EntropySource, @@ -79,6 +96,7 @@ impl BlindedMessagePath { 0, local_node_receive_key, context, + compact_padding, entropy_source, secp_ctx, ) @@ -86,12 +104,16 @@ impl BlindedMessagePath { /// Same as [`BlindedMessagePath::new`], but allows specifying a number of dummy hops. /// - /// Note: - /// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path. + /// + /// `compact_padding` selects between space-inefficient padding that better hides contents and + /// a space-constrained padding that does very little to hide the contents, especially for the + /// last hop. It should only be set when the blinded path needs to be as compact as possible. + /// + /// Note: At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path. pub fn new_with_dummy_hops( intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, context: MessageContext, - entropy_source: ES, secp_ctx: &Secp256k1, + compact_padding: bool, entropy_source: ES, secp_ctx: &Secp256k1, ) -> Self where ES::Target: EntropySource, @@ -114,6 +136,7 @@ impl BlindedMessagePath { context, &blinding_secret, local_node_receive_key, + compact_padding, ), }) } @@ -416,28 +439,45 @@ pub enum OffersContext { /// Useful to timeout async recipients that are no longer supported as clients. path_absolute_expiry: Duration, }, - /// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an - /// [`InvoiceRequest`]. + /// Context used by a [`BlindedMessagePath`] within a [`Refund`]. /// /// This variant is intended to be received when handling a [`Bolt12Invoice`] or an /// [`InvoiceError`]. /// /// [`Refund`]: crate::offers::refund::Refund - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError - OutboundPayment { - /// Payment ID used when creating a [`Refund`] or [`InvoiceRequest`]. + OutboundPaymentInRefund { + /// Payment ID used when creating a [`Refund`]. /// /// [`Refund`]: crate::offers::refund::Refund - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest payment_id: PaymentId, - /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid [`Refund`] or - /// [`InvoiceRequest`] and for deriving their signing keys. + /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid [`Refund`] and + /// for deriving its signing keys. /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`Refund`]: crate::offers::refund::Refund + nonce: Nonce, + }, + /// Context used by a [`BlindedMessagePath`] as a reply path for an [`InvoiceRequest`]. + /// + /// This variant is intended to be received when handling a [`Bolt12Invoice`] or an + /// [`InvoiceError`]. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError + OutboundPaymentInInvReq { + /// Payment ID used when creating an [`InvoiceRequest`]. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + payment_id: PaymentId, + + /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid + /// [`InvoiceRequest`] and for deriving its signing keys. + /// + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest nonce: Nonce, }, @@ -619,7 +659,7 @@ impl_writeable_tlv_based_enum!(OffersContext, (0, InvoiceRequest) => { (0, nonce, required), }, - (1, OutboundPayment) => { + (1, OutboundPaymentInRefund) => { (0, payment_id, required), (1, nonce, required), }, @@ -631,6 +671,10 @@ impl_writeable_tlv_based_enum!(OffersContext, (2, invoice_slot, required), (4, path_absolute_expiry, required), }, + (4, OutboundPaymentInInvReq) => { + (0, payment_id, required), + (1, nonce, required), + }, ); impl_writeable_tlv_based_enum!(AsyncPaymentsContext, @@ -693,7 +737,7 @@ pub const MAX_DUMMY_HOPS_COUNT: usize = 10; pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, dummy_hop_count: usize, context: MessageContext, - session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, + session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, compact_padding: bool, ) -> Vec { let dummy_count = cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT); let pks = intermediate_nodes @@ -703,9 +747,8 @@ pub(super) fn blinded_hops( core::iter::repeat((recipient_node_id, Some(local_node_receive_key))).take(dummy_count), ) .chain(core::iter::once((recipient_node_id, Some(local_node_receive_key)))); - let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some()); - let tlvs = pks + let intermediate_tlvs = pks .clone() .skip(1) // The first node's TLVs contains the next node's pubkey .zip(intermediate_nodes.iter().map(|node| node.short_channel_id)) @@ -716,18 +759,42 @@ pub(super) fn blinded_hops( .map(|next_hop| { ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None }) }) - .chain((0..dummy_count).map(|_| ControlTlvs::Dummy)) - .chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); - - if is_compact { - let path = pks.zip(tlvs); - utils::construct_blinded_hops(secp_ctx, path, session_priv) + .chain((0..dummy_count).map(|_| ControlTlvs::Dummy)); + + let max_intermediate_len = + intermediate_tlvs.clone().map(|tlvs| tlvs.serialized_length()).max().unwrap_or(0); + let have_intermediate_one_byte_smaller = + intermediate_tlvs.clone().any(|tlvs| tlvs.serialized_length() == max_intermediate_len - 1); + + let round_off = if compact_padding { + // We can only pad by a minimum of two bytes. Thus, if there are any intermediate hops that + // need to be padded by exactly one byte, we have to instead pad everything by two. + if have_intermediate_one_byte_smaller { + max_intermediate_len + 2 + } else { + max_intermediate_len + } } else { - let path = - pks.zip(tlvs.map(|tlv| BlindedPathWithPadding { - tlvs: tlv, - round_off: MESSAGE_PADDING_ROUND_OFF, - })); - utils::construct_blinded_hops(secp_ctx, path, session_priv) - } + MESSAGE_PADDING_ROUND_OFF + }; + + let tlvs = intermediate_tlvs + .map(|tlvs| { + let res = BlindedPathWithPadding { tlvs, round_off }; + if compact_padding { + debug_assert_eq!(res.serialized_length(), max_intermediate_len); + } else { + // We don't currently ever push extra stuff to intermediate hops, so simply assert that + // the fully-padded hops are always `MESSAGE_PADDING_ROUND_OFF` long. + debug_assert_eq!(res.serialized_length(), MESSAGE_PADDING_ROUND_OFF); + } + res + }) + .chain(core::iter::once(BlindedPathWithPadding { + tlvs: ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }), + round_off: if compact_padding { 0 } else { MESSAGE_PADDING_ROUND_OFF }, + })); + + let path = pks.zip(tlvs); + utils::construct_blinded_hops(secp_ctx, path, session_priv) } diff --git a/lightning/src/blinded_path/utils.rs b/lightning/src/blinded_path/utils.rs index 8894f37ad33..339b4337eb3 100644 --- a/lightning/src/blinded_path/utils.rs +++ b/lightning/src/blinded_path/utils.rs @@ -256,9 +256,12 @@ impl Writeable for BlindedPathWithPadding { let tlv_length = self.tlvs.serialized_length(); let total_length = tlv_length + TLV_OVERHEAD; - let padding_length = total_length.div_ceil(self.round_off) * self.round_off - total_length; - - let padding = Some(BlindedPathPadding::new(padding_length)); + let padding = if self.round_off == 0 || tlv_length % self.round_off == 0 { + None + } else { + let length = total_length.div_ceil(self.round_off) * self.round_off - total_length; + Some(BlindedPathPadding::new(length)) + }; encode_tlv_stream!(writer, { (1, padding, option), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 644920557d2..3fcb55a00e4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5593,29 +5593,12 @@ where pub fn send_payment_for_bolt12_invoice( &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, ) -> Result<(), Bolt12PaymentError> { - match self.verify_bolt12_invoice(invoice, context) { + match self.flow.verify_bolt12_invoice(invoice, context) { Ok(payment_id) => self.send_payment_for_verified_bolt12_invoice(invoice, payment_id), Err(()) => Err(Bolt12PaymentError::UnexpectedInvoice), } } - fn verify_bolt12_invoice( - &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, - ) -> Result { - let secp_ctx = &self.secp_ctx; - let expanded_key = &self.inbound_payment_key; - - match context { - None if invoice.is_for_refund_without_paths() => { - invoice.verify_using_metadata(expanded_key, secp_ctx) - }, - Some(&OffersContext::OutboundPayment { payment_id, nonce, .. }) => { - invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) - }, - _ => Err(()), - } - } - fn send_payment_for_verified_bolt12_invoice( &self, invoice: &Bolt12Invoice, payment_id: PaymentId, ) -> Result<(), Bolt12PaymentError> { @@ -15366,7 +15349,7 @@ where }, OffersMessage::StaticInvoice(invoice) => { let payment_id = match context { - Some(OffersContext::OutboundPayment { payment_id, .. }) => payment_id, + Some(OffersContext::OutboundPaymentInInvReq { payment_id, .. }) => payment_id, _ => return None }; let res = self.initiate_async_payment(&invoice, payment_id); @@ -15382,7 +15365,8 @@ where log_trace!(logger, "Received invoice_error: {}", invoice_error); match context { - Some(OffersContext::OutboundPayment { payment_id, .. }) => { + Some(OffersContext::OutboundPaymentInInvReq { payment_id, .. }) + |Some(OffersContext::OutboundPaymentInRefund { payment_id, .. }) => { self.abandon_payment_with_reason( payment_id, PaymentFailureReason::InvoiceRequestRejected, ); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 3a6965c6646..1dcc9ffcbf1 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -60,7 +60,7 @@ use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer}; use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH}; +use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH, QR_CODED_PADDED_PATH_LENGTH}; use crate::onion_message::offers::OffersMessage; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; @@ -163,6 +163,17 @@ fn check_compact_path_introduction_node<'a, 'b, 'c>( && matches!(path.introduction_node(), IntroductionNode::DirectedShortChannelId(..)) } +fn check_padded_path_length<'a, 'b, 'c>( + path: &BlindedMessagePath, + lookup_node: &Node<'a, 'b, 'c>, + expected_introduction_node: PublicKey, + expected_path_length: usize, +) -> bool { + let introduction_node_id = resolve_introduction_node(lookup_node, path); + introduction_node_id == expected_introduction_node + && path.blinded_hops().len() == expected_path_length +} + fn route_bolt12_payment<'a, 'b, 'c>( node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], invoice: &Bolt12Invoice ) { @@ -455,7 +466,7 @@ fn check_dummy_hop_pattern_in_offer() { let bob_id = bob.node.get_our_node_id(); // Case 1: DefaultMessageRouter → uses compact blinded paths (via SCIDs) - // Expected: No dummy hops; each path contains only the recipient. + // Expected: Padded to QR_CODED_PADDED_PATH_LENGTH for QR code size optimization let default_router = DefaultMessageRouter::new(alice.network_graph, alice.keys_manager); let compact_offer = alice.node @@ -467,8 +478,8 @@ fn check_dummy_hop_pattern_in_offer() { for path in compact_offer.paths() { assert_eq!( - path.blinded_hops().len(), 1, - "Compact paths must include only the recipient" + path.blinded_hops().len(), QR_CODED_PADDED_PATH_LENGTH, + "Compact offer paths are padded to QR_CODED_PADDED_PATH_LENGTH" ); } @@ -480,10 +491,10 @@ fn check_dummy_hop_pattern_in_offer() { assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); - assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); + assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH)); // Case 2: NodeIdMessageRouter → uses node ID-based blinded paths - // Expected: 0 to MAX_DUMMY_HOPS_COUNT dummy hops, followed by recipient. + // Expected: Also padded to QR_CODED_PADDED_PATH_LENGTH for QR code size optimization let node_id_router = NodeIdMessageRouter::new(alice.network_graph, alice.keys_manager); let padded_offer = alice.node @@ -492,7 +503,7 @@ fn check_dummy_hop_pattern_in_offer() { .build().unwrap(); assert!(!padded_offer.paths().is_empty()); - assert!(padded_offer.paths().iter().all(|path| path.blinded_hops().len() == PADDED_PATH_LENGTH)); + assert!(padded_offer.paths().iter().all(|path| path.blinded_hops().len() == QR_CODED_PADDED_PATH_LENGTH)); let payment_id = PaymentId([2; 32]); bob.node.pay_for_offer(&padded_offer, None, payment_id, Default::default()).unwrap(); @@ -502,7 +513,7 @@ fn check_dummy_hop_pattern_in_offer() { assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); - assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); + assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH)); } /// Checks that blinded paths are compact for short-lived offers. @@ -687,7 +698,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert!(check_compact_path_introduction_node(&reply_path, bob, charlie_id)); + assert!(check_padded_path_length(&reply_path, bob, charlie_id, PADDED_PATH_LENGTH)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); charlie.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -706,8 +717,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { // to Alice when she's handling the message. Therefore, either Bob or Charlie could // serve as the introduction node for the reply path back to Alice. assert!( - check_compact_path_introduction_node(&reply_path, david, bob_id) || - check_compact_path_introduction_node(&reply_path, david, charlie_id) + check_padded_path_length(&reply_path, david, bob_id, PADDED_PATH_LENGTH) || + check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH) ); route_bolt12_payment(david, &[charlie, bob, alice], &invoice); @@ -790,7 +801,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); } - assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); + assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH)); route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); @@ -845,7 +856,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); - assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); + assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -857,7 +868,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } - assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id)); + assert!(check_padded_path_length(&reply_path, bob, alice_id, PADDED_PATH_LENGTH)); route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); @@ -913,7 +924,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } - assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id)); + assert!(check_padded_path_length(&reply_path, bob, alice_id, PADDED_PATH_LENGTH)); route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); @@ -1059,6 +1070,7 @@ fn send_invoice_requests_with_distinct_reply_path() { let bob_id = bob.node.get_our_node_id(); let charlie_id = charlie.node.get_our_node_id(); let david_id = david.node.get_our_node_id(); + let frank_id = nodes[6].node.get_our_node_id(); disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5], &nodes[6]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); @@ -1089,7 +1101,7 @@ fn send_invoice_requests_with_distinct_reply_path() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (_, reply_path) = extract_invoice_request(alice, &onion_message); - assert!(check_compact_path_introduction_node(&reply_path, alice, charlie_id)); + assert!(check_padded_path_length(&reply_path, alice, charlie_id, PADDED_PATH_LENGTH)); // Send, extract and verify the second Invoice Request message let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1099,7 +1111,7 @@ fn send_invoice_requests_with_distinct_reply_path() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (_, reply_path) = extract_invoice_request(alice, &onion_message); - assert!(check_compact_path_introduction_node(&reply_path, alice, nodes[6].node.get_our_node_id())); + assert!(check_padded_path_length(&reply_path, alice, frank_id, PADDED_PATH_LENGTH)); } /// This test checks that when multiple potential introduction nodes are available for the payee, @@ -1170,7 +1182,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (_, reply_path) = extract_invoice(alice, &onion_message); - assert!(check_compact_path_introduction_node(&reply_path, alice, charlie_id)); + assert!(check_padded_path_length(&reply_path, alice, charlie_id, PADDED_PATH_LENGTH)); // Send, extract and verify the second Invoice Request message let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1179,7 +1191,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (_, reply_path) = extract_invoice(alice, &onion_message); - assert!(check_compact_path_introduction_node(&reply_path, alice, nodes[6].node.get_our_node_id())); + assert!(check_padded_path_length(&reply_path, alice, nodes[6].node.get_our_node_id(), PADDED_PATH_LENGTH)); } /// Verifies that the invoice request message can be retried if it fails to reach the @@ -1233,7 +1245,7 @@ fn creates_and_pays_for_offer_with_retry() { }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); - assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); + assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -1534,7 +1546,7 @@ fn fails_authentication_when_handling_invoice_request() { let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id)); + assert!(check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH)); assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None); @@ -1563,7 +1575,7 @@ fn fails_authentication_when_handling_invoice_request() { let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id)); + assert!(check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH)); assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None); } @@ -1663,7 +1675,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); - assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id)); + assert!(check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); charlie.onion_messenger.handle_onion_message(alice_id, &onion_message); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 88f0cc5079c..0d51122e0c8 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -495,11 +495,12 @@ where Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) } - /// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer metadata, - /// returning the corresponding [`PaymentId`] if successful. + /// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer + /// metadata, returning the corresponding [`PaymentId`] if successful. /// - /// - If an [`OffersContext::OutboundPayment`] with a `nonce` is provided, verification is performed - /// using this to form the payer metadata. + /// - If an [`OffersContext::OutboundPaymentInInvReq`] or + /// [`OffersContext::OutboundPaymentInRefund`] with a `nonce` is provided, verification is + /// performed using this to form the payer metadata. /// - If no context is provided and the invoice corresponds to a [`Refund`] without blinded paths, /// verification is performed using the [`Bolt12Invoice::payer_metadata`]. /// - If neither condition is met, verification fails. @@ -513,8 +514,19 @@ where None if invoice.is_for_refund_without_paths() => { invoice.verify_using_metadata(expanded_key, secp_ctx) }, - Some(&OffersContext::OutboundPayment { payment_id, nonce, .. }) => { - invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + Some(&OffersContext::OutboundPaymentInInvReq { payment_id, nonce, .. }) => { + if invoice.is_for_offer() { + invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + } else { + Err(()) + } + }, + Some(&OffersContext::OutboundPaymentInRefund { payment_id, nonce, .. }) => { + if invoice.is_for_refund() { + invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + } else { + Err(()) + } }, _ => Err(()), } @@ -680,7 +692,8 @@ where let secp_ctx = &self.secp_ctx; let nonce = Nonce::from_entropy_source(entropy); - let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce }); + let context = + MessageContext::Offers(OffersContext::OutboundPaymentInRefund { payment_id, nonce }); // Create the base builder with common properties let mut builder = RefundBuilder::deriving_signing_pubkey( @@ -1116,7 +1129,8 @@ where &self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce, peers: Vec, ) -> Result<(), Bolt12SemanticError> { - let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce }); + let context = + MessageContext::Offers(OffersContext::OutboundPaymentInInvReq { payment_id, nonce }); let reply_paths = self .create_blinded_paths(peers, context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -1305,6 +1319,7 @@ where num_dummy_hops, self.receive_auth_key, context, + false, &*entropy, &self.secp_ctx, ) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 6dfd6eac508..8d83225f117 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -778,6 +778,19 @@ struct InvoiceFields { } macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { + /// Whether the invoice was created in response to a [`Refund`]. + pub fn is_for_refund(&$self) -> bool { + $contents.is_for_refund() + } + + /// Whether the invoice was created in response to an [`InvoiceRequest`] created from an + /// [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + pub fn is_for_offer(&$self) -> bool { + $contents.is_for_offer() + } + /// The chains that may be used when paying a requested invoice. /// /// From [`Offer::chains`]; `None` if the invoice was created in response to a [`Refund`]. @@ -1093,6 +1106,20 @@ impl InvoiceContents { } } + fn is_for_refund(&self) -> bool { + match self { + InvoiceContents::ForRefund { .. } => true, + InvoiceContents::ForOffer { .. } => false, + } + } + + fn is_for_offer(&self) -> bool { + match self { + InvoiceContents::ForRefund { .. } => false, + InvoiceContents::ForOffer { .. } => true, + } + } + fn offer_chains(&self) -> Option> { match self { InvoiceContents::ForOffer { invoice_request, .. } => { diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 605a81a4f95..ffdb2f64037 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -436,8 +436,9 @@ fn one_blinded_hop() { let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); + let node_id = nodes[1].node_id; let blinded_path = - BlindedMessagePath::new(&[], nodes[1].node_id, receive_key, context, entropy, &secp_ctx); + BlindedMessagePath::new(&[], node_id, receive_key, context, false, entropy, &secp_ctx); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); @@ -450,18 +451,15 @@ fn blinded_path_with_dummy_hops() { let nodes = create_nodes(2); let test_msg = TestCustomMessage::Pong; - let secp_ctx = Secp256k1::new(); - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[1].entropy_source; - let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new_with_dummy_hops( &[], nodes[1].node_id, TEST_DUMMY_HOP_COUNT, - receive_key, - context, - entropy, - &secp_ctx, + nodes[1].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[1].entropy_source, + &Secp256k1::new(), ); // Ensure that dummy hops are added to the blinded path. assert_eq!(blinded_path.blinded_hops().len(), 6); @@ -477,19 +475,16 @@ fn two_unblinded_two_blinded() { let nodes = create_nodes(5); let test_msg = TestCustomMessage::Pong; - let secp_ctx = Secp256k1::new(); let intermediate_nodes = [MessageForwardNode { node_id: nodes[3].node_id, short_channel_id: None }]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[4].entropy_source; - let receive_key = nodes[4].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[4].node_id, - receive_key, - context, - entropy, - &secp_ctx, + nodes[4].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[4].entropy_source, + &Secp256k1::new(), ); let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id], @@ -507,21 +502,18 @@ fn three_blinded_hops() { let nodes = create_nodes(4); let test_msg = TestCustomMessage::Pong; - let secp_ctx = Secp256k1::new(); let intermediate_nodes = [ MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, ]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[3].entropy_source; - let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[3].node_id, - receive_key, - context, - entropy, - &secp_ctx, + nodes[3].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[3].entropy_source, + &Secp256k1::new(), ); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -548,8 +540,9 @@ fn async_response_over_one_blinded_hop() { let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); + let node_id = nodes[1].node_id; let reply_path = - BlindedMessagePath::new(&[], nodes[1].node_id, receive_key, context, entropy, &secp_ctx); + BlindedMessagePath::new(&[], node_id, receive_key, context, false, entropy, &secp_ctx); // 4. Create a responder using the reply path for Alice. let responder = Some(Responder::new(reply_path)); @@ -590,7 +583,7 @@ fn async_response_with_reply_path_succeeds() { let entropy = &*bob.entropy_source; let receive_key = bob.messenger.node_signer.get_receive_auth_key(); let reply_path = - BlindedMessagePath::new(&[], bob.node_id, receive_key, context, entropy, &secp_ctx); + BlindedMessagePath::new(&[], bob.node_id, receive_key, context, false, entropy, &secp_ctx); // Alice asynchronously responds to Bob, expecting a response back from him. let responder = Responder::new(reply_path); @@ -632,7 +625,7 @@ fn async_response_with_reply_path_fails() { let entropy = &*bob.entropy_source; let receive_key = bob.messenger.node_signer.get_receive_auth_key(); let reply_path = - BlindedMessagePath::new(&[], bob.node_id, receive_key, context, entropy, &secp_ctx); + BlindedMessagePath::new(&[], bob.node_id, receive_key, context, false, entropy, &secp_ctx); // 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. @@ -668,28 +661,26 @@ fn too_big_packet_error() { #[test] fn test_blinded_path_padding_for_full_length_path() { - // Check that for a full blinded path, all encrypted payload are padded to rounded-off length. + // Check that for a full blinded path without compact padding, all encrypted payload are padded + // to rounded-off length. let nodes = create_nodes(4); let test_msg = TestCustomMessage::Pong; - let secp_ctx = Secp256k1::new(); let intermediate_nodes = [ MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, ]; - // Update the context to create a larger final receive TLVs, ensuring that - // the hop sizes vary before padding. - let context = MessageContext::Custom(vec![0u8; 42]); - let entropy = &*nodes[3].entropy_source; - let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); + // Build with a larger context to create a larger final receive TLVs, ensuring that the hop + // sizes vary before padding. let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, nodes[3].node_id, TEST_DUMMY_HOP_COUNT, - receive_key, - context, - entropy, - &secp_ctx, + nodes[3].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(vec![0u8; 42]), + false, + &*nodes[3].entropy_source, + &Secp256k1::new(), ); assert!(is_padded(&blinded_path.blinded_hops(), MESSAGE_PADDING_ROUND_OFF)); @@ -703,32 +694,70 @@ fn test_blinded_path_padding_for_full_length_path() { } #[test] -fn test_blinded_path_no_padding_for_compact_path() { - // Check that for a compact blinded path, no padding is applied. +fn test_blinded_path_compact_padding() { + // Check that for a blinded path with compact padding, no extra padding is applied. let nodes = create_nodes(4); - let secp_ctx = Secp256k1::new(); - // Include some short_channel_id, so that MessageRouter uses this to create compact blinded paths. + let intermediate_nodes = [ + MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, + MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, + ]; + // Build with a larger context to create a larger final receive TLVs, ensuring that the hop + // sizes vary before padding. + let blinded_path = BlindedMessagePath::new_with_dummy_hops( + &intermediate_nodes, + nodes[3].node_id, + TEST_DUMMY_HOP_COUNT, + nodes[3].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(vec![0u8; 42]), + true, + &*nodes[3].entropy_source, + &Secp256k1::new(), + ); + + let hops = blinded_path.blinded_hops(); + assert!(!is_padded(&hops, MESSAGE_PADDING_ROUND_OFF)); + assert_eq!(hops.len(), TEST_DUMMY_HOP_COUNT + 3); + for hop in hops.iter().take(TEST_DUMMY_HOP_COUNT + 2) { + assert_eq!(hops[0].encrypted_payload.len(), hop.encrypted_payload.len()); + } + // Check the actual encrypted payload lengths, which may change in the future but serves to + // ensure that this and test_compact_blinded_path_compact_padding, below, differ. + assert_eq!(hops[0].encrypted_payload.len(), 51); +} + +#[test] +fn test_compact_blinded_path_compact_padding() { + // Check that for a blinded path with compact padding, no extra padding is applied. + let nodes = create_nodes(4); + + // Include some short_channel_id, so that MessageRouter uses this to create compact blinded paths let intermediate_nodes = [ MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: Some(24) }, MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: Some(25) }, ]; - // Update the context to create a larger final receive TLVs, ensuring that - // the hop sizes vary before padding. - let context = MessageContext::Custom(vec![0u8; 42]); - let entropy = &*nodes[3].entropy_source; - let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); + // Build with a larger context to create a larger final receive TLVs, ensuring that the hop + // sizes vary before padding. let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, nodes[3].node_id, TEST_DUMMY_HOP_COUNT, - receive_key, - context, - entropy, - &secp_ctx, + nodes[3].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(vec![0u8; 42]), + true, + &*nodes[3].entropy_source, + &Secp256k1::new(), ); - assert!(!is_padded(&blinded_path.blinded_hops(), MESSAGE_PADDING_ROUND_OFF)); + let hops = blinded_path.blinded_hops(); + assert!(!is_padded(&hops, MESSAGE_PADDING_ROUND_OFF)); + assert_eq!(hops.len(), TEST_DUMMY_HOP_COUNT + 3); + for hop in hops.iter().take(TEST_DUMMY_HOP_COUNT + 2) { + assert_eq!(hops[0].encrypted_payload.len(), hop.encrypted_payload.len()); + } + // Check the actual encrypted payload lengths, which may change in the future but serves to + // ensure that this and test_blinded_path_compact_padding, above, differ. + assert_eq!(hops[0].encrypted_payload.len(), 26); } #[test] @@ -743,15 +772,13 @@ fn we_are_intro_node() { MessageForwardNode { node_id: nodes[0].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, ]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[2].entropy_source; - let receive_key = nodes[2].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[2].node_id, - receive_key, - context, - entropy, + nodes[2].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[2].entropy_source, &secp_ctx, ); let destination = Destination::BlindedPath(blinded_path); @@ -764,15 +791,13 @@ fn we_are_intro_node() { // Try with a two-hop blinded path where we are the introduction node. let intermediate_nodes = [MessageForwardNode { node_id: nodes[0].node_id, short_channel_id: None }]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[1].entropy_source; - let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[1].node_id, - receive_key, - context, - entropy, + nodes[1].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[1].entropy_source, &secp_ctx, ); let destination = Destination::BlindedPath(blinded_path); @@ -790,19 +815,16 @@ fn invalid_blinded_path_error() { let nodes = create_nodes(3); let test_msg = TestCustomMessage::Pong; - let secp_ctx = Secp256k1::new(); let intermediate_nodes = [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[2].entropy_source; - let receive_key = nodes[2].messenger.node_signer.get_receive_auth_key(); let mut blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[2].node_id, - receive_key, - context, - entropy, - &secp_ctx, + nodes[2].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[2].entropy_source, + &Secp256k1::new(), ); blinded_path.clear_blinded_hops(); let destination = Destination::BlindedPath(blinded_path); @@ -828,15 +850,13 @@ fn reply_path() { MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, ]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[0].entropy_source; - let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); let reply_path = BlindedMessagePath::new( &intermediate_nodes, nodes[0].node_id, - receive_key, - context, - entropy, + nodes[0].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[0].entropy_source, &secp_ctx, ); nodes[0] @@ -855,15 +875,13 @@ fn reply_path() { MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, ]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[3].entropy_source; - let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[3].node_id, - receive_key, - context, - entropy, + nodes[3].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[3].entropy_source, &secp_ctx, ); let destination = Destination::BlindedPath(blinded_path); @@ -871,15 +889,13 @@ fn reply_path() { MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, ]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[0].entropy_source; - let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); let reply_path = BlindedMessagePath::new( &intermediate_nodes, nodes[0].node_id, - receive_key, - context, - entropy, + nodes[0].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[0].entropy_source, &secp_ctx, ); let instructions = MessageSendInstructions::WithSpecifiedReplyPath { destination, reply_path }; @@ -975,15 +991,13 @@ fn requests_peer_connection_for_buffered_messages() { let intermediate_nodes = [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[0].entropy_source; - let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[2].node_id, - receive_key, - context, - entropy, + nodes[0].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[0].entropy_source, &secp_ctx, ); let destination = Destination::BlindedPath(blinded_path); @@ -1046,15 +1060,13 @@ fn drops_buffered_messages_waiting_for_peer_connection() { let intermediate_nodes = [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[0].entropy_source; - let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[2].node_id, - receive_key, - context, - entropy, + nodes[0].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[0].entropy_source, &secp_ctx, ); let destination = Destination::BlindedPath(blinded_path); @@ -1107,19 +1119,16 @@ fn intercept_offline_peer_oms() { } let message = TestCustomMessage::Pong; - let secp_ctx = Secp256k1::new(); let intermediate_nodes = [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; - let context = MessageContext::Custom(Vec::new()); - let entropy = &*nodes[2].entropy_source; - let receive_key = nodes[2].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[2].node_id, - receive_key, - context, - entropy, - &secp_ctx, + nodes[2].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[2].entropy_source, + &Secp256k1::new(), ); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 9a2c06bb72f..f1ea1777943 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -272,7 +272,7 @@ where /// ]; /// let context = MessageContext::Custom(Vec::new()); /// let receive_key = keys_manager.get_receive_auth_key(); -/// let blinded_path = BlindedMessagePath::new(&hops, your_node_id, receive_key, context, &keys_manager, &secp_ctx); +/// let blinded_path = BlindedMessagePath::new(&hops, your_node_id, receive_key, context, false, &keys_manager, &secp_ctx); /// /// // Send a custom onion message to a blinded path. /// let destination = Destination::BlindedPath(blinded_path); @@ -524,9 +524,11 @@ pub trait MessageRouter { /// A [`MessageRouter`] that can only route to a directly connected [`Destination`]. /// -/// [`DefaultMessageRouter`] constructs compact [`BlindedMessagePath`]s on a best-effort basis. -/// That is, if appropriate SCID information is available for the intermediate peers, it will -/// default to creating compact paths. +/// [`DefaultMessageRouter`] tries to construct compact and padded [`BlindedMessagePath`]s based on +/// the [`MessageContext`] given to [`MessageRouter::create_blinded_paths`]. That is, if the +/// provided context implies the path may be used in a BOLT 12 object which might appear in a QR +/// code, it reduces the amount of padding and prefers building compact paths when short channel +/// IDs (SCIDs) are available for intermediate peers. /// /// # Compact Blinded Paths /// @@ -545,7 +547,8 @@ pub trait MessageRouter { /// Creating [`BlindedMessagePath`]s may affect privacy since, if a suitable path cannot be found, /// it will create a one-hop path using the recipient as the introduction node if it is an announced /// node. Otherwise, there is no way to find a path to the introduction node in order to send a -/// message, and thus an `Err` is returned. +/// message, and thus an `Err` is returned. The impact of this may be somewhat muted when +/// additional padding is added to the blinded path, but this protection is not complete. pub struct DefaultMessageRouter>, L: Deref, ES: Deref> where L::Target: Logger, @@ -555,14 +558,17 @@ where entropy_source: ES, } -// Target total length (in hops) for non-compact blinded paths. -// We pad with dummy hops until the path reaches this length, -// obscuring the recipient's true position. +// Target total length (in hops) for blinded paths used outside of QR codes. // -// Compact paths are optimized for minimal size, so we avoid -// adding dummy hops to them. +// We pad with dummy hops until the path reaches this length (including the recipient). pub(crate) const PADDED_PATH_LENGTH: usize = 4; +// Target total length (in hops) for blinded paths which are included in objects which may appear +// in a QR code. +// +// We pad with dummy hops until the path reaches this length (including the recipient). +pub(crate) const QR_CODED_PADDED_PATH_LENGTH: usize = 2; + impl>, L: Deref, ES: Deref> DefaultMessageRouter where L::Target: Logger, @@ -574,12 +580,12 @@ where } pub(crate) fn create_blinded_paths_from_iter< - I: ExactSizeIterator, + I: ExactSizeIterator + Clone, T: secp256k1::Signing + secp256k1::Verification, >( network_graph: &G, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, peers: I, entropy_source: &ES, secp_ctx: &Secp256k1, - compact_paths: bool, + never_compact_path: bool, ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; @@ -592,6 +598,31 @@ where let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); + let (size_constrained, padded_path_len) = match &context { + MessageContext::Offers(OffersContext::InvoiceRequest { .. }) + | MessageContext::Offers(OffersContext::OutboundPaymentInRefund { .. }) => { + // When embeding blinded paths within BOLT 12 objects which are generally embedded + // in QR codes, we sadly need to be conservative about size, especially if the QR + // code ultimately also includes an on-chain address. + (true, QR_CODED_PADDED_PATH_LENGTH) + }, + MessageContext::Offers(OffersContext::StaticInvoiceRequested { .. }) => { + // Async Payments aggressively embeds the entire `InvoiceRequest` in the payment + // onion. In a future version it should likely move to embedding only the + // `InvoiceRequest`-specific fields instead, but until then we have to be + // incredibly strict in the size of the blinded path we include in a static payment + // `Offer`. + (true, 0) + }, + _ => { + // If there's no need to be small, pad the path more and never use SCID-based + // next-hops as they carry additional expiry risk. + (false, PADDED_PATH_LENGTH) + }, + }; + + let compact_paths = !never_compact_path && size_constrained; + let has_one_peer = peers.len() == 1; let mut peer_info = peers .map(|peer| MessageForwardNode { @@ -619,12 +650,8 @@ where }); let build_path = |intermediate_hops: &[MessageForwardNode]| { - let dummy_hops_count = if compact_paths { - 0 - } else { - // Add one for the final recipient TLV - PADDED_PATH_LENGTH.saturating_sub(intermediate_hops.len() + 1) - }; + // Calculate the dummy hops given the total hop count target (including the recipient). + let dummy_hops_count = padded_path_len.saturating_sub(intermediate_hops.len() + 1); BlindedMessagePath::new_with_dummy_hops( intermediate_hops, @@ -632,6 +659,7 @@ where dummy_hops_count, local_node_receive_key, context.clone(), + size_constrained, &**entropy_source, secp_ctx, ) @@ -651,12 +679,6 @@ where } } - // Sanity check: Ones the paths are created for the non-compact case, ensure - // each of them are of the length `PADDED_PATH_LENGTH`. - if !compact_paths { - debug_assert!(paths.iter().all(|path| path.blinded_hops().len() == PADDED_PATH_LENGTH)); - } - if compact_paths { for path in &mut paths { path.use_compact_introduction_node(&network_graph); @@ -740,13 +762,15 @@ where peers.into_iter(), &self.entropy_source, secp_ctx, - true, + false, ) } } /// This message router is similar to [`DefaultMessageRouter`], but it always creates -/// full-length blinded paths, using the peer's [`NodeId`]. +/// non-compact blinded paths, using the peer's [`NodeId`]. It uses the same heuristics as +/// [`DefaultMessageRouter`] for deciding when to pad the generated blinded paths with additional +/// hops. /// /// This message router can only route to a directly connected [`Destination`]. /// @@ -755,7 +779,8 @@ where /// Creating [`BlindedMessagePath`]s may affect privacy since, if a suitable path cannot be found, /// it will create a one-hop path using the recipient as the introduction node if it is an announced /// node. Otherwise, there is no way to find a path to the introduction node in order to send a -/// message, and thus an `Err` is returned. +/// message, and thus an `Err` is returned. The impact of this may be somewhat muted when +/// additional padding is added to the blinded path, but this protection is not complete. pub struct NodeIdMessageRouter>, L: Deref, ES: Deref> where L::Target: Logger, @@ -790,8 +815,11 @@ where fn create_blinded_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, + context: MessageContext, mut peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { + for peer in peers.iter_mut() { + peer.short_channel_id = None; + } DefaultMessageRouter::create_blinded_paths_from_iter( &self.network_graph, recipient, @@ -800,7 +828,7 @@ where peers.into_iter(), &self.entropy_source, secp_ctx, - false, + true, ) } }