Skip to content

Commit b91dacb

Browse files
committed
Make DefaultMessageRouter use the context to pad/compact paths
After much discussion in #3246 we mostly decided to allow downstream developers to override whatever decisions the `DefaultMessageRouter` makes regarding blinded path selection by providing easy overrides for the selected `OnionMessageRouter`. We did not, however, actually select good defaults for `DefaultMessageRouter`. Here we add those defaults, taking advantage of the `MessageContext` we're given to detect why we're building a blinded path and selecting blinding and compaction parameters based on it. Specifically, if the blinded path is not being built for an offers context, we always use a non-compact blinded path and always pad it to four hops (including the recipient). However, if the blinded path is being built for an `Offers` context which implies it might need to fit in a QR code (or, worse, a payment onion), we reduce our padding and try to build a compact blinded path if possible. We retain the `NodeIdMessageRouter` to disable compact blinded path creation but use the same path-padding heuristic as for `DefaultMessageRouter`.
1 parent b60ba66 commit b91dacb

File tree

2 files changed

+92
-51
lines changed

2 files changed

+92
-51
lines changed

lightning/src/ln/offers_tests.rs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ use crate::offers::invoice_error::InvoiceError;
6060
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer};
6161
use crate::offers::nonce::Nonce;
6262
use crate::offers::parse::Bolt12SemanticError;
63-
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH};
63+
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH, QR_CODED_PADDED_PATH_LENGTH};
6464
use crate::onion_message::offers::OffersMessage;
6565
use crate::routing::gossip::{NodeAlias, NodeId};
6666
use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig};
@@ -163,6 +163,17 @@ fn check_compact_path_introduction_node<'a, 'b, 'c>(
163163
&& matches!(path.introduction_node(), IntroductionNode::DirectedShortChannelId(..))
164164
}
165165

166+
fn check_padded_path_length<'a, 'b, 'c>(
167+
path: &BlindedMessagePath,
168+
lookup_node: &Node<'a, 'b, 'c>,
169+
expected_introduction_node: PublicKey,
170+
expected_path_length: usize,
171+
) -> bool {
172+
let introduction_node_id = resolve_introduction_node(lookup_node, path);
173+
introduction_node_id == expected_introduction_node
174+
&& path.blinded_hops().len() == expected_path_length
175+
}
176+
166177
fn route_bolt12_payment<'a, 'b, 'c>(
167178
node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], invoice: &Bolt12Invoice
168179
) {
@@ -455,7 +466,7 @@ fn check_dummy_hop_pattern_in_offer() {
455466
let bob_id = bob.node.get_our_node_id();
456467

457468
// Case 1: DefaultMessageRouter → uses compact blinded paths (via SCIDs)
458-
// Expected: No dummy hops; each path contains only the recipient.
469+
// Expected: Padded to QR_CODED_PADDED_PATH_LENGTH for QR code size optimization
459470
let default_router = DefaultMessageRouter::new(alice.network_graph, alice.keys_manager);
460471

461472
let compact_offer = alice.node
@@ -467,8 +478,8 @@ fn check_dummy_hop_pattern_in_offer() {
467478

468479
for path in compact_offer.paths() {
469480
assert_eq!(
470-
path.blinded_hops().len(), 1,
471-
"Compact paths must include only the recipient"
481+
path.blinded_hops().len(), QR_CODED_PADDED_PATH_LENGTH,
482+
"Compact offer paths are padded to QR_CODED_PADDED_PATH_LENGTH"
472483
);
473484
}
474485

@@ -480,10 +491,10 @@ fn check_dummy_hop_pattern_in_offer() {
480491

481492
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
482493
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
483-
assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id));
494+
assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH));
484495

485496
// Case 2: NodeIdMessageRouter → uses node ID-based blinded paths
486-
// Expected: 0 to MAX_DUMMY_HOPS_COUNT dummy hops, followed by recipient.
497+
// Expected: Also padded to QR_CODED_PADDED_PATH_LENGTH for QR code size optimization
487498
let node_id_router = NodeIdMessageRouter::new(alice.network_graph, alice.keys_manager);
488499

489500
let padded_offer = alice.node
@@ -492,7 +503,7 @@ fn check_dummy_hop_pattern_in_offer() {
492503
.build().unwrap();
493504

494505
assert!(!padded_offer.paths().is_empty());
495-
assert!(padded_offer.paths().iter().all(|path| path.blinded_hops().len() == PADDED_PATH_LENGTH));
506+
assert!(padded_offer.paths().iter().all(|path| path.blinded_hops().len() == QR_CODED_PADDED_PATH_LENGTH));
496507

497508
let payment_id = PaymentId([2; 32]);
498509
bob.node.pay_for_offer(&padded_offer, None, payment_id, Default::default()).unwrap();
@@ -502,7 +513,7 @@ fn check_dummy_hop_pattern_in_offer() {
502513

503514
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
504515
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
505-
assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id));
516+
assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH));
506517
}
507518

508519
/// 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() {
687698
});
688699
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
689700
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
690-
assert!(check_compact_path_introduction_node(&reply_path, bob, charlie_id));
701+
assert!(check_padded_path_length(&reply_path, bob, charlie_id, PADDED_PATH_LENGTH));
691702

692703
let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap();
693704
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() {
706717
// to Alice when she's handling the message. Therefore, either Bob or Charlie could
707718
// serve as the introduction node for the reply path back to Alice.
708719
assert!(
709-
check_compact_path_introduction_node(&reply_path, david, bob_id) ||
710-
check_compact_path_introduction_node(&reply_path, david, charlie_id)
720+
check_padded_path_length(&reply_path, david, bob_id, PADDED_PATH_LENGTH) ||
721+
check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH)
711722
);
712723

713724
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
@@ -790,7 +801,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
790801
for path in invoice.payment_paths() {
791802
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id));
792803
}
793-
assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id));
804+
assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH));
794805

795806
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
796807
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);
@@ -845,7 +856,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
845856
});
846857
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
847858
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
848-
assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id));
859+
assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH));
849860

850861
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
851862
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() {
857868
for path in invoice.payment_paths() {
858869
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
859870
}
860-
assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id));
871+
assert!(check_padded_path_length(&reply_path, bob, alice_id, PADDED_PATH_LENGTH));
861872

862873
route_bolt12_payment(bob, &[alice], &invoice);
863874
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
@@ -913,7 +924,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
913924
for path in invoice.payment_paths() {
914925
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
915926
}
916-
assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id));
927+
assert!(check_padded_path_length(&reply_path, bob, alice_id, PADDED_PATH_LENGTH));
917928

918929
route_bolt12_payment(bob, &[alice], &invoice);
919930
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
@@ -1059,6 +1070,7 @@ fn send_invoice_requests_with_distinct_reply_path() {
10591070
let bob_id = bob.node.get_our_node_id();
10601071
let charlie_id = charlie.node.get_our_node_id();
10611072
let david_id = david.node.get_our_node_id();
1073+
let frank_id = nodes[6].node.get_our_node_id();
10621074

10631075
disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5], &nodes[6]]);
10641076
disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]);
@@ -1089,7 +1101,7 @@ fn send_invoice_requests_with_distinct_reply_path() {
10891101
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
10901102

10911103
let (_, reply_path) = extract_invoice_request(alice, &onion_message);
1092-
assert!(check_compact_path_introduction_node(&reply_path, alice, charlie_id));
1104+
assert!(check_padded_path_length(&reply_path, alice, charlie_id, PADDED_PATH_LENGTH));
10931105

10941106
// Send, extract and verify the second Invoice Request message
10951107
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() {
10991111
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
11001112

11011113
let (_, reply_path) = extract_invoice_request(alice, &onion_message);
1102-
assert!(check_compact_path_introduction_node(&reply_path, alice, nodes[6].node.get_our_node_id()));
1114+
assert!(check_padded_path_length(&reply_path, alice, frank_id, PADDED_PATH_LENGTH));
11031115
}
11041116

11051117
/// 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() {
11701182
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
11711183

11721184
let (_, reply_path) = extract_invoice(alice, &onion_message);
1173-
assert!(check_compact_path_introduction_node(&reply_path, alice, charlie_id));
1185+
assert!(check_padded_path_length(&reply_path, alice, charlie_id, PADDED_PATH_LENGTH));
11741186

11751187
// Send, extract and verify the second Invoice Request message
11761188
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() {
11791191
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
11801192

11811193
let (_, reply_path) = extract_invoice(alice, &onion_message);
1182-
assert!(check_compact_path_introduction_node(&reply_path, alice, nodes[6].node.get_our_node_id()));
1194+
assert!(check_padded_path_length(&reply_path, alice, nodes[6].node.get_our_node_id(), PADDED_PATH_LENGTH));
11831195
}
11841196

11851197
/// 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() {
12331245
});
12341246
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
12351247
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
1236-
assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id));
1248+
assert!(check_padded_path_length(&reply_path, alice, bob_id, PADDED_PATH_LENGTH));
12371249
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
12381250
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
12391251

@@ -1534,7 +1546,7 @@ fn fails_authentication_when_handling_invoice_request() {
15341546
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
15351547
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
15361548
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
1537-
assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id));
1549+
assert!(check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH));
15381550

15391551
assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None);
15401552

@@ -1563,7 +1575,7 @@ fn fails_authentication_when_handling_invoice_request() {
15631575
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
15641576
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
15651577
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
1566-
assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id));
1578+
assert!(check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH));
15671579

15681580
assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None);
15691581
}
@@ -1663,7 +1675,7 @@ fn fails_authentication_when_handling_invoice_for_offer() {
16631675
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
16641676
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
16651677
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
1666-
assert!(check_compact_path_introduction_node(&reply_path, david, charlie_id));
1678+
assert!(check_padded_path_length(&reply_path, david, charlie_id, PADDED_PATH_LENGTH));
16671679

16681680
let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap();
16691681
charlie.onion_messenger.handle_onion_message(alice_id, &onion_message);

lightning/src/onion_message/messenger.rs

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,11 @@ pub trait MessageRouter {
524524

525525
/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
526526
///
527-
/// [`DefaultMessageRouter`] constructs compact [`BlindedMessagePath`]s on a best-effort basis.
528-
/// That is, if appropriate SCID information is available for the intermediate peers, it will
529-
/// default to creating compact paths.
527+
/// [`DefaultMessageRouter`] tries to construct compact and padded [`BlindedMessagePath`]s based on
528+
/// the [`MessageContext`] given to [`MessageRouter::create_blinded_paths`]. That is, if the
529+
/// provided context implies the path may be used in a BOLT 12 object which might appear in a QR
530+
/// code, it reduces the amount of padding and prefers building compact paths when short channel
531+
/// IDs (SCIDs) are available for intermediate peers.
530532
///
531533
/// # Compact Blinded Paths
532534
///
@@ -545,7 +547,8 @@ pub trait MessageRouter {
545547
/// Creating [`BlindedMessagePath`]s may affect privacy since, if a suitable path cannot be found,
546548
/// it will create a one-hop path using the recipient as the introduction node if it is an announced
547549
/// node. Otherwise, there is no way to find a path to the introduction node in order to send a
548-
/// message, and thus an `Err` is returned.
550+
/// message, and thus an `Err` is returned. The impact of this may be somewhat muted when
551+
/// additional padding is added to the blinded path, but this protection is not complete.
549552
pub struct DefaultMessageRouter<G: Deref<Target = NetworkGraph<L>>, L: Deref, ES: Deref>
550553
where
551554
L::Target: Logger,
@@ -555,14 +558,17 @@ where
555558
entropy_source: ES,
556559
}
557560

558-
// Target total length (in hops) for non-compact blinded paths.
559-
// We pad with dummy hops until the path reaches this length,
560-
// obscuring the recipient's true position.
561+
// Target total length (in hops) for blinded paths used outside of QR codes.
561562
//
562-
// Compact paths are optimized for minimal size, so we avoid
563-
// adding dummy hops to them.
563+
// We pad with dummy hops until the path reaches this length (including the recipient).
564564
pub(crate) const PADDED_PATH_LENGTH: usize = 4;
565565

566+
// Target total length (in hops) for blinded paths which are included in objects which may appear
567+
// in a QR code.
568+
//
569+
// We pad with dummy hops until the path reaches this length (including the recipient).
570+
pub(crate) const QR_CODED_PADDED_PATH_LENGTH: usize = 2;
571+
566572
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, ES: Deref> DefaultMessageRouter<G, L, ES>
567573
where
568574
L::Target: Logger,
@@ -574,12 +580,12 @@ where
574580
}
575581

576582
pub(crate) fn create_blinded_paths_from_iter<
577-
I: ExactSizeIterator<Item = MessageForwardNode>,
583+
I: ExactSizeIterator<Item = MessageForwardNode> + Clone,
578584
T: secp256k1::Signing + secp256k1::Verification,
579585
>(
580586
network_graph: &G, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey,
581587
context: MessageContext, peers: I, entropy_source: &ES, secp_ctx: &Secp256k1<T>,
582-
compact_paths: bool,
588+
never_compact_path: bool,
583589
) -> Result<Vec<BlindedMessagePath>, ()> {
584590
// Limit the number of blinded paths that are computed.
585591
const MAX_PATHS: usize = 3;
@@ -592,6 +598,33 @@ where
592598
let is_recipient_announced =
593599
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));
594600

601+
let (mut compact_paths, padded_path_len) = match &context {
602+
MessageContext::Offers(OffersContext::InvoiceRequest { .. })
603+
|MessageContext::Offers(OffersContext::OutboundPaymentInRefund { .. }) => {
604+
// When embeding blinded paths within BOLT 12 objects which are generally embedded
605+
// in QR codes, we sadly need to be conservative about size, especially if the QR
606+
// code ultimately also includes an on-chain address.
607+
(true, QR_CODED_PADDED_PATH_LENGTH)
608+
},
609+
MessageContext::Offers(OffersContext::StaticInvoiceRequested { .. }) => {
610+
// Async Payments aggressively embeds the entire `InvoiceRequest` in the payment
611+
// onion. In a future version it should likely move to embedding only the
612+
// `InvoiceRequest`-specific fields instead, but until then we have to be
613+
// incredibly strict in the size of the blinded path we include in a static payment
614+
// `Offer`.
615+
(true, 0)
616+
},
617+
_ => {
618+
// If there's no need to be small, pad the path more and never use SCID-based
619+
// next-hops as they carry additional expiry risk.
620+
(false, PADDED_PATH_LENGTH)
621+
},
622+
};
623+
624+
if never_compact_path {
625+
compact_paths = false;
626+
}
627+
595628
let has_one_peer = peers.len() == 1;
596629
let mut peer_info = peers
597630
.map(|peer| MessageForwardNode {
@@ -619,12 +652,8 @@ where
619652
});
620653

621654
let build_path = |intermediate_hops: &[MessageForwardNode]| {
622-
let dummy_hops_count = if compact_paths {
623-
0
624-
} else {
625-
// Add one for the final recipient TLV
626-
PADDED_PATH_LENGTH.saturating_sub(intermediate_hops.len() + 1)
627-
};
655+
// Calculate the dummy hops given the total hop count target (including the recipient).
656+
let dummy_hops_count = padded_path_len.saturating_sub(intermediate_hops.len() + 1);
628657

629658
BlindedMessagePath::new_with_dummy_hops(
630659
intermediate_hops,
@@ -651,12 +680,6 @@ where
651680
}
652681
}
653682

654-
// Sanity check: Ones the paths are created for the non-compact case, ensure
655-
// each of them are of the length `PADDED_PATH_LENGTH`.
656-
if !compact_paths {
657-
debug_assert!(paths.iter().all(|path| path.blinded_hops().len() == PADDED_PATH_LENGTH));
658-
}
659-
660683
if compact_paths {
661684
for path in &mut paths {
662685
path.use_compact_introduction_node(&network_graph);
@@ -740,13 +763,15 @@ where
740763
peers.into_iter(),
741764
&self.entropy_source,
742765
secp_ctx,
743-
true,
766+
false,
744767
)
745768
}
746769
}
747770

748771
/// This message router is similar to [`DefaultMessageRouter`], but it always creates
749-
/// full-length blinded paths, using the peer's [`NodeId`].
772+
/// non-compact blinded paths, using the peer's [`NodeId`]. It uses the same heuristics as
773+
/// [`DefaultMessageRouter`] for deciding when to pad the generated blinded paths with additional
774+
/// hops.
750775
///
751776
/// This message router can only route to a directly connected [`Destination`].
752777
///
@@ -755,7 +780,8 @@ where
755780
/// Creating [`BlindedMessagePath`]s may affect privacy since, if a suitable path cannot be found,
756781
/// it will create a one-hop path using the recipient as the introduction node if it is an announced
757782
/// node. Otherwise, there is no way to find a path to the introduction node in order to send a
758-
/// message, and thus an `Err` is returned.
783+
/// message, and thus an `Err` is returned. The impact of this may be somewhat muted when
784+
/// additional padding is added to the blinded path, but this protection is not complete.
759785
pub struct NodeIdMessageRouter<G: Deref<Target = NetworkGraph<L>>, L: Deref, ES: Deref>
760786
where
761787
L::Target: Logger,
@@ -790,8 +816,11 @@ where
790816

791817
fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
792818
&self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey,
793-
context: MessageContext, peers: Vec<MessageForwardNode>, secp_ctx: &Secp256k1<T>,
819+
context: MessageContext, mut peers: Vec<MessageForwardNode>, secp_ctx: &Secp256k1<T>,
794820
) -> Result<Vec<BlindedMessagePath>, ()> {
821+
for peer in peers.iter_mut() {
822+
peer.short_channel_id = None;
823+
}
795824
DefaultMessageRouter::create_blinded_paths_from_iter(
796825
&self.network_graph,
797826
recipient,
@@ -800,7 +829,7 @@ where
800829
peers.into_iter(),
801830
&self.entropy_source,
802831
secp_ctx,
803-
false,
832+
true,
804833
)
805834
}
806835
}

0 commit comments

Comments
 (0)