@@ -46,11 +46,12 @@ use bitcoin::network::Network;
4646use bitcoin:: secp256k1:: { PublicKey , Secp256k1 } ;
4747use core:: time:: Duration ;
4848use crate :: blinded_path:: IntroductionNode ;
49- use crate :: blinded_path:: message:: BlindedMessagePath ;
49+ use crate :: blinded_path:: message:: { BlindedMessagePath , MAX_DUMMY_HOPS_COUNT } ;
5050use crate :: blinded_path:: payment:: { Bolt12OfferContext , Bolt12RefundContext , PaymentContext } ;
5151use crate :: blinded_path:: message:: OffersContext ;
5252use crate :: events:: { ClosureReason , Event , HTLCHandlingFailureType , PaidBolt12Invoice , PaymentFailureReason , PaymentPurpose } ;
5353use crate :: ln:: channelmanager:: { Bolt12PaymentError , PaymentId , RecentPaymentDetails , RecipientOnionFields , Retry , self } ;
54+ use crate :: offers:: test_utils:: FixedEntropy ;
5455use crate :: types:: features:: Bolt12InvoiceFeatures ;
5556use crate :: ln:: functional_test_utils:: * ;
5657use crate :: ln:: msgs:: { BaseMessageHandler , ChannelMessageHandler , Init , NodeAnnouncement , OnionMessage , OnionMessageHandler , RoutingMessageHandler , SocketAddress , UnsignedGossipMessage , UnsignedNodeAnnouncement } ;
@@ -60,11 +61,12 @@ use crate::offers::invoice_error::InvoiceError;
6061use crate :: offers:: invoice_request:: { InvoiceRequest , InvoiceRequestFields } ;
6162use crate :: offers:: nonce:: Nonce ;
6263use 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 } ;
6465use crate :: onion_message:: offers:: OffersMessage ;
6566use crate :: routing:: gossip:: { NodeAlias , NodeId } ;
6667use crate :: routing:: router:: { PaymentParameters , RouteParameters , RouteParametersConfig } ;
6768use crate :: sign:: { NodeSigner , Recipient } ;
69+ use crate :: sync:: Arc ;
6870use crate :: util:: ser:: Writeable ;
6971
7072/// This used to determine whether we built a compact path or not, but now its just a random
@@ -438,6 +440,63 @@ fn prefers_more_connected_nodes_in_blinded_paths() {
438440 }
439441}
440442
443+ /// Tests the dummy hop behavior of Offers based on the message router used:
444+ /// - Compact paths (`DefaultMessageRouter`) should not include dummy hops.
445+ /// - Node ID paths (`NodeIdMessageRouter`) may include 0 to [`MAX_DUMMY_HOPS_COUNT`] dummy hops.
446+ #[ test]
447+ fn check_dummy_hop_pattern_in_offer ( ) {
448+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
449+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
450+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
451+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
452+
453+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , 10_000_000 , 1_000_000_000 ) ;
454+
455+ let alice = & nodes[ 0 ] ;
456+ let deterministic_entropy = Arc :: new ( FixedEntropy ) ;
457+
458+ // Case 1: DefaultMessageRouter → uses compact blinded paths (via SCIDs)
459+ // Expected: No dummy hops; each path contains only the recipient.
460+ let default_router = DefaultMessageRouter :: new ( alice. network_graph , deterministic_entropy. clone ( ) ) ;
461+
462+ let compact_offer = alice. node
463+ . create_offer_builder_using_router ( & default_router) . unwrap ( )
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+ // Case 2: NodeIdMessageRouter → uses node ID-based blinded paths
476+ // Expected: 0 to MAX_DUMMY_HOPS_COUNT dummy hops, followed by recipient.
477+ let node_id_router = NodeIdMessageRouter :: new ( alice. network_graph , deterministic_entropy) ;
478+
479+ let padded_offer = alice. node
480+ . create_offer_builder_using_router ( & node_id_router) . unwrap ( )
481+ . build ( ) . unwrap ( ) ;
482+
483+ assert ! ( !padded_offer. paths( ) . is_empty( ) ) ;
484+
485+ for path in padded_offer. paths ( ) {
486+ let hops = path. blinded_hops ( ) ;
487+ assert ! (
488+ hops. len( ) > 1 ,
489+ "Non-compact paths must include at least one dummy hop plus recipient"
490+ ) ;
491+
492+ let dummy_count = hops. len ( ) - 1 ;
493+ assert ! (
494+ dummy_count <= MAX_DUMMY_HOPS_COUNT ,
495+ "Dummy hops must not exceed MAX_DUMMY_HOPS_COUNT"
496+ ) ;
497+ }
498+ }
499+
441500/// Checks that blinded paths are compact for short-lived offers.
442501#[ test]
443502fn creates_short_lived_offer ( ) {
0 commit comments