@@ -46,11 +46,12 @@ use bitcoin::network::Network;
46
46
use bitcoin:: secp256k1:: { PublicKey , Secp256k1 } ;
47
47
use core:: time:: Duration ;
48
48
use crate :: blinded_path:: IntroductionNode ;
49
- use crate :: blinded_path:: message:: BlindedMessagePath ;
49
+ use crate :: blinded_path:: message:: { BlindedMessagePath , MAX_DUMMY_HOPS_COUNT } ;
50
50
use crate :: blinded_path:: payment:: { Bolt12OfferContext , Bolt12RefundContext , PaymentContext } ;
51
51
use crate :: blinded_path:: message:: OffersContext ;
52
52
use crate :: events:: { ClosureReason , Event , HTLCHandlingFailureType , PaidBolt12Invoice , PaymentFailureReason , PaymentPurpose } ;
53
53
use crate :: ln:: channelmanager:: { Bolt12PaymentError , PaymentId , RecentPaymentDetails , RecipientOnionFields , Retry , self } ;
54
+ use crate :: offers:: test_utils:: FixedEntropy ;
54
55
use crate :: types:: features:: Bolt12InvoiceFeatures ;
55
56
use crate :: ln:: functional_test_utils:: * ;
56
57
use crate :: ln:: msgs:: { BaseMessageHandler , ChannelMessageHandler , Init , NodeAnnouncement , OnionMessage , OnionMessageHandler , RoutingMessageHandler , SocketAddress , UnsignedGossipMessage , UnsignedNodeAnnouncement } ;
@@ -60,7 +61,7 @@ use crate::offers::invoice_error::InvoiceError;
60
61
use crate :: offers:: invoice_request:: { InvoiceRequest , InvoiceRequestFields } ;
61
62
use crate :: offers:: nonce:: Nonce ;
62
63
use crate :: offers:: parse:: Bolt12SemanticError ;
63
- use crate :: onion_message:: messenger:: { Destination , MessageSendInstructions , NodeIdMessageRouter , NullMessageRouter , PeeledOnion } ;
64
+ use crate :: onion_message:: messenger:: { DefaultMessageRouter , Destination , MessageSendInstructions , NodeIdMessageRouter , NullMessageRouter , PeeledOnion } ;
64
65
use crate :: onion_message:: offers:: OffersMessage ;
65
66
use crate :: routing:: gossip:: { NodeAlias , NodeId } ;
66
67
use crate :: routing:: router:: { PaymentParameters , RouteParameters , RouteParametersConfig } ;
@@ -438,6 +439,89 @@ fn prefers_more_connected_nodes_in_blinded_paths() {
438
439
}
439
440
}
440
441
442
+ /// Tests the dummy hop behavior of Offers based on the message router used:
443
+ /// - Compact paths (`DefaultMessageRouter`) should not include dummy hops.
444
+ /// - Node ID paths (`NodeIdMessageRouter`) may include 0 to [`MAX_DUMMY_HOPS_COUNT`] dummy hops.
445
+ ///
446
+ /// Also verifies that the resulting paths are functional: the counterparty can respond with a valid `invoice_request`.
447
+ #[ test]
448
+ fn check_dummy_hop_pattern_in_offer ( ) {
449
+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
450
+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
451
+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
452
+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
453
+
454
+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , 10_000_000 , 1_000_000_000 ) ;
455
+
456
+ let alice = & nodes[ 0 ] ;
457
+ let alice_id = alice. node . get_our_node_id ( ) ;
458
+ let bob = & nodes[ 1 ] ;
459
+ let bob_id = bob. node . get_our_node_id ( ) ;
460
+
461
+ // Case 1: DefaultMessageRouter → uses compact blinded paths (via SCIDs)
462
+ // Expected: No dummy hops; each path contains only the recipient.
463
+ let default_router = DefaultMessageRouter :: new ( alice. network_graph , & FixedEntropy ) ;
464
+
465
+ let compact_offer = alice. node
466
+ . create_offer_builder_using_router ( & default_router) . unwrap ( )
467
+ . amount_msats ( 10_000_000 )
468
+ . build ( ) . unwrap ( ) ;
469
+
470
+ assert ! ( !compact_offer. paths( ) . is_empty( ) ) ;
471
+
472
+ for path in compact_offer. paths ( ) {
473
+ assert_eq ! (
474
+ path. blinded_hops( ) . len( ) , 1 ,
475
+ "Compact paths must include only the recipient"
476
+ ) ;
477
+ }
478
+
479
+ let payment_id = PaymentId ( [ 1 ; 32 ] ) ;
480
+ bob. node . pay_for_offer ( & compact_offer, None , None , None , payment_id, Retry :: Attempts ( 0 ) , RouteParametersConfig :: default ( ) ) . unwrap ( ) ;
481
+
482
+ let onion_message = bob. onion_messenger . next_onion_message_for_peer ( alice_id) . unwrap ( ) ;
483
+ let ( invoice_request, reply_path) = extract_invoice_request ( alice, & onion_message) ;
484
+
485
+ assert_eq ! ( invoice_request. amount_msats( ) , Some ( 10_000_000 ) ) ;
486
+ assert_ne ! ( invoice_request. payer_signing_pubkey( ) , bob_id) ;
487
+ assert ! ( check_compact_path_introduction_node( & reply_path, alice, bob_id) ) ;
488
+
489
+ // Case 2: NodeIdMessageRouter → uses node ID-based blinded paths
490
+ // Expected: 0 to MAX_DUMMY_HOPS_COUNT dummy hops, followed by recipient.
491
+ let node_id_router = NodeIdMessageRouter :: new ( alice. network_graph , & FixedEntropy ) ;
492
+
493
+ let padded_offer = alice. node
494
+ . create_offer_builder_using_router ( & node_id_router) . unwrap ( )
495
+ . amount_msats ( 10_000_000 )
496
+ . build ( ) . unwrap ( ) ;
497
+
498
+ assert ! ( !padded_offer. paths( ) . is_empty( ) ) ;
499
+
500
+ for path in padded_offer. paths ( ) {
501
+ let hops = path. blinded_hops ( ) ;
502
+ assert ! (
503
+ hops. len( ) > 1 ,
504
+ "Non-compact paths must include at least one dummy hop plus recipient"
505
+ ) ;
506
+
507
+ let dummy_count = hops. len ( ) - 1 ;
508
+ assert ! (
509
+ dummy_count <= MAX_DUMMY_HOPS_COUNT ,
510
+ "Dummy hops must not exceed MAX_DUMMY_HOPS_COUNT"
511
+ ) ;
512
+ }
513
+
514
+ let payment_id = PaymentId ( [ 2 ; 32 ] ) ;
515
+ bob. node . pay_for_offer ( & padded_offer, None , None , None , payment_id, Retry :: Attempts ( 0 ) , RouteParametersConfig :: default ( ) ) . unwrap ( ) ;
516
+
517
+ let onion_message = bob. onion_messenger . next_onion_message_for_peer ( alice_id) . unwrap ( ) ;
518
+ let ( invoice_request, reply_path) = extract_invoice_request ( alice, & onion_message) ;
519
+
520
+ assert_eq ! ( invoice_request. amount_msats( ) , Some ( 10_000_000 ) ) ;
521
+ assert_ne ! ( invoice_request. payer_signing_pubkey( ) , bob_id) ;
522
+ assert ! ( check_compact_path_introduction_node( & reply_path, alice, bob_id) ) ;
523
+ }
524
+
441
525
/// Checks that blinded paths are compact for short-lived offers.
442
526
#[ test]
443
527
fn creates_short_lived_offer ( ) {
0 commit comments