@@ -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,11 +61,12 @@ 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 } ;
67
68
use crate :: sign:: { NodeSigner , Recipient } ;
69
+ use crate :: sync:: Arc ;
68
70
use crate :: util:: ser:: Writeable ;
69
71
70
72
/// 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() {
438
440
}
439
441
}
440
442
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
+
441
500
/// Checks that blinded paths are compact for short-lived offers.
442
501
#[ test]
443
502
fn creates_short_lived_offer ( ) {
0 commit comments