@@ -60,7 +60,7 @@ use crate::offers::invoice_error::InvoiceError;
6060use crate :: offers:: invoice_request:: { InvoiceRequest , InvoiceRequestFields } ;
6161use crate :: offers:: nonce:: Nonce ;
6262use crate :: offers:: parse:: Bolt12SemanticError ;
63- use crate :: onion_message:: messenger:: { Destination , MessageSendInstructions , NodeIdMessageRouter , NullMessageRouter , PeeledOnion } ;
63+ use crate :: onion_message:: messenger:: { DefaultMessageRouter , Destination , MessageSendInstructions , NodeIdMessageRouter , NullMessageRouter , PeeledOnion , PADDED_PATH_LENGTH } ;
6464use crate :: onion_message:: offers:: OffersMessage ;
6565use crate :: routing:: gossip:: { NodeAlias , NodeId } ;
6666use crate :: routing:: router:: { PaymentParameters , RouteParameters , RouteParametersConfig } ;
@@ -435,6 +435,76 @@ fn prefers_more_connected_nodes_in_blinded_paths() {
435435 }
436436}
437437
438+ /// Tests the dummy hop behavior of Offers based on the message router used:
439+ /// - Compact paths (`DefaultMessageRouter`) should not include dummy hops.
440+ /// - Node ID paths (`NodeIdMessageRouter`) may include 0 to [`MAX_DUMMY_HOPS_COUNT`] dummy hops.
441+ ///
442+ /// Also verifies that the resulting paths are functional: the counterparty can respond with a valid `invoice_request`.
443+ #[ test]
444+ fn check_dummy_hop_pattern_in_offer ( ) {
445+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
446+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
447+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
448+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
449+
450+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , 10_000_000 , 1_000_000_000 ) ;
451+
452+ let alice = & nodes[ 0 ] ;
453+ let alice_id = alice. node . get_our_node_id ( ) ;
454+ let bob = & nodes[ 1 ] ;
455+ let bob_id = bob. node . get_our_node_id ( ) ;
456+
457+ // Case 1: DefaultMessageRouter → uses compact blinded paths (via SCIDs)
458+ // Expected: No dummy hops; each path contains only the recipient.
459+ let default_router = DefaultMessageRouter :: new ( alice. network_graph , alice. keys_manager ) ;
460+
461+ let compact_offer = alice. node
462+ . create_offer_builder_using_router ( & default_router) . unwrap ( )
463+ . amount_msats ( 10_000_000 )
464+ . build ( ) . unwrap ( ) ;
465+
466+ assert ! ( !compact_offer. paths( ) . is_empty( ) ) ;
467+
468+ for path in compact_offer. paths ( ) {
469+ assert_eq ! (
470+ path. blinded_hops( ) . len( ) , 1 ,
471+ "Compact paths must include only the recipient"
472+ ) ;
473+ }
474+
475+ let payment_id = PaymentId ( [ 1 ; 32 ] ) ;
476+ bob. node . pay_for_offer ( & compact_offer, None , None , None , payment_id, Retry :: Attempts ( 0 ) , RouteParametersConfig :: default ( ) ) . unwrap ( ) ;
477+
478+ let onion_message = bob. onion_messenger . next_onion_message_for_peer ( alice_id) . unwrap ( ) ;
479+ let ( invoice_request, reply_path) = extract_invoice_request ( alice, & onion_message) ;
480+
481+ assert_eq ! ( invoice_request. amount_msats( ) , Some ( 10_000_000 ) ) ;
482+ assert_ne ! ( invoice_request. payer_signing_pubkey( ) , bob_id) ;
483+ assert ! ( check_compact_path_introduction_node( & reply_path, alice, bob_id) ) ;
484+
485+ // Case 2: NodeIdMessageRouter → uses node ID-based blinded paths
486+ // Expected: 0 to MAX_DUMMY_HOPS_COUNT dummy hops, followed by recipient.
487+ let node_id_router = NodeIdMessageRouter :: new ( alice. network_graph , alice. keys_manager ) ;
488+
489+ let padded_offer = alice. node
490+ . create_offer_builder_using_router ( & node_id_router) . unwrap ( )
491+ . amount_msats ( 10_000_000 )
492+ . build ( ) . unwrap ( ) ;
493+
494+ assert ! ( !padded_offer. paths( ) . is_empty( ) ) ;
495+ assert ! ( padded_offer. paths( ) . iter( ) . all( |path| path. blinded_hops( ) . len( ) == PADDED_PATH_LENGTH ) ) ;
496+
497+ let payment_id = PaymentId ( [ 2 ; 32 ] ) ;
498+ bob. node . pay_for_offer ( & padded_offer, None , None , None , payment_id, Retry :: Attempts ( 0 ) , RouteParametersConfig :: default ( ) ) . unwrap ( ) ;
499+
500+ let onion_message = bob. onion_messenger . next_onion_message_for_peer ( alice_id) . unwrap ( ) ;
501+ let ( invoice_request, reply_path) = extract_invoice_request ( alice, & onion_message) ;
502+
503+ assert_eq ! ( invoice_request. amount_msats( ) , Some ( 10_000_000 ) ) ;
504+ assert_ne ! ( invoice_request. payer_signing_pubkey( ) , bob_id) ;
505+ assert ! ( check_compact_path_introduction_node( & reply_path, alice, bob_id) ) ;
506+ }
507+
438508/// Checks that blinded paths are compact for short-lived offers.
439509#[ test]
440510fn creates_short_lived_offer ( ) {
0 commit comments