1212use bitcoin:: secp256k1:: ecdh:: SharedSecret ;
1313use bitcoin:: secp256k1:: { self , PublicKey , Secp256k1 , SecretKey } ;
1414
15+ use crate :: blinded_path:: message:: MAX_DUMMY_HOPS_COUNT ;
1516use crate :: blinded_path:: utils:: { self , BlindedPathWithPadding } ;
1617use crate :: blinded_path:: { BlindedHop , BlindedPath , IntroductionNode , NodeIdLookUp } ;
1718use crate :: crypto:: streams:: ChaChaDualPolyReadAdapter ;
@@ -124,6 +125,74 @@ impl BlindedPaymentPath {
124125 where
125126 ES :: Target : EntropySource ,
126127 {
128+ BlindedPaymentPath :: new_inner (
129+ intermediate_nodes,
130+ payee_node_id,
131+ local_node_receive_key,
132+ 0 ,
133+ None ,
134+ payee_tlvs,
135+ htlc_maximum_msat,
136+ min_final_cltv_expiry_delta,
137+ entropy_source,
138+ secp_ctx,
139+ )
140+ }
141+
142+ /// Same as [`BlindedPaymentPath::new`], but allows specifying a number of dummy hops.
143+ ///
144+ /// Dummy TLVs allow callers to override the payment relay values used for dummy hops.
145+ /// Any additional fees introduced by these dummy hops are ultimately paid to the final
146+ /// recipient as part of the total amount.
147+ ///
148+ /// This improves privacy by making path-length analysis based on fee and CLTV delta
149+ /// values less reliable.
150+ ///
151+ /// Note:
152+ /// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path.
153+ pub fn new_with_dummy_hops < ES : Deref , T : secp256k1:: Signing + secp256k1:: Verification > (
154+ intermediate_nodes : & [ PaymentForwardNode ] , payee_node_id : PublicKey ,
155+ dummy_hop_count : usize , local_node_receive_key : ReceiveAuthKey , payee_tlvs : ReceiveTlvs ,
156+ htlc_maximum_msat : u64 , min_final_cltv_expiry_delta : u16 , entropy_source : ES ,
157+ secp_ctx : & Secp256k1 < T > ,
158+ ) -> Result < Self , ( ) >
159+ where
160+ ES :: Target : EntropySource ,
161+ {
162+ // TODO: Expose DummyTlvs in the public API instead of always using defaults.
163+ let dummy_tlvs = DummyTlvs :: default ( ) ;
164+
165+ BlindedPaymentPath :: new_inner (
166+ intermediate_nodes,
167+ payee_node_id,
168+ local_node_receive_key,
169+ dummy_hop_count,
170+ Some ( dummy_tlvs) ,
171+ payee_tlvs,
172+ htlc_maximum_msat,
173+ min_final_cltv_expiry_delta,
174+ entropy_source,
175+ secp_ctx,
176+ )
177+ }
178+
179+ fn new_inner < ES : Deref , T : secp256k1:: Signing + secp256k1:: Verification > (
180+ intermediate_nodes : & [ PaymentForwardNode ] , payee_node_id : PublicKey ,
181+ local_node_receive_key : ReceiveAuthKey , dummy_hop_count : usize ,
182+ dummy_tlvs_opt : Option < DummyTlvs > , payee_tlvs : ReceiveTlvs , htlc_maximum_msat : u64 ,
183+ min_final_cltv_expiry_delta : u16 , entropy_source : ES , secp_ctx : & Secp256k1 < T > ,
184+ ) -> Result < Self , ( ) >
185+ where
186+ ES :: Target : EntropySource ,
187+ {
188+ debug_assert ! (
189+ ( dummy_hop_count > 0 ) == dummy_tlvs_opt. is_some( ) ,
190+ "dummy_hop_count and dummy_tlvs must either both be present or both be absent"
191+ ) ;
192+
193+ let dummy_count = core:: cmp:: min ( dummy_hop_count, MAX_DUMMY_HOPS_COUNT ) ;
194+ let dummy_tlvs = dummy_tlvs_opt. unwrap_or_default ( ) ;
195+
127196 let introduction_node = IntroductionNode :: NodeId (
128197 intermediate_nodes. first ( ) . map_or ( payee_node_id, |n| n. node_id ) ,
129198 ) ;
@@ -133,10 +202,13 @@ impl BlindedPaymentPath {
133202
134203 let blinded_payinfo = compute_payinfo (
135204 intermediate_nodes,
205+ dummy_count,
206+ & dummy_tlvs,
136207 & payee_tlvs,
137208 htlc_maximum_msat,
138209 min_final_cltv_expiry_delta,
139210 ) ?;
211+
140212 Ok ( Self {
141213 inner_path : BlindedPath {
142214 introduction_node,
@@ -145,6 +217,8 @@ impl BlindedPaymentPath {
145217 secp_ctx,
146218 intermediate_nodes,
147219 payee_node_id,
220+ dummy_count,
221+ dummy_tlvs,
148222 payee_tlvs,
149223 & blinding_secret,
150224 local_node_receive_key,
@@ -393,6 +467,7 @@ pub(crate) enum BlindedTrampolineTlvs {
393467}
394468
395469// Used to include forward and receive TLVs in the same iterator for encoding.
470+ #[ derive( Clone ) ]
396471enum BlindedPaymentTlvsRef < ' a > {
397472 Forward ( & ' a ForwardTlvs ) ,
398473 Dummy ( & ' a DummyTlvs ) ,
@@ -678,21 +753,46 @@ pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30;
678753/// Construct blinded payment hops for the given `intermediate_nodes` and payee info.
679754pub ( super ) fn blinded_hops < T : secp256k1:: Signing + secp256k1:: Verification > (
680755 secp_ctx : & Secp256k1 < T > , intermediate_nodes : & [ PaymentForwardNode ] , payee_node_id : PublicKey ,
681- payee_tlvs : ReceiveTlvs , session_priv : & SecretKey , local_node_receive_key : ReceiveAuthKey ,
756+ dummy_count : usize , dummy_tlvs : DummyTlvs , payee_tlvs : ReceiveTlvs , session_priv : & SecretKey ,
757+ local_node_receive_key : ReceiveAuthKey ,
682758) -> Vec < BlindedHop > {
683759 let pks = intermediate_nodes
684760 . iter ( )
685761 . map ( |node| ( node. node_id , None ) )
762+ . chain ( core:: iter:: repeat ( ( payee_node_id, Some ( local_node_receive_key) ) ) . take ( dummy_count) )
686763 . chain ( core:: iter:: once ( ( payee_node_id, Some ( local_node_receive_key) ) ) ) ;
687764 let tlvs = intermediate_nodes
688765 . iter ( )
689766 . map ( |node| BlindedPaymentTlvsRef :: Forward ( & node. tlvs ) )
767+ . chain ( core:: iter:: repeat ( BlindedPaymentTlvsRef :: Dummy ( & dummy_tlvs) ) . take ( dummy_count) )
690768 . chain ( core:: iter:: once ( BlindedPaymentTlvsRef :: Receive ( & payee_tlvs) ) ) ;
691769
692770 let path = pks. zip (
693771 tlvs. map ( |tlv| BlindedPathWithPadding { tlvs : tlv, round_off : PAYMENT_PADDING_ROUND_OFF } ) ,
694772 ) ;
695773
774+ // Debug invariant: all non-final hops must have identical serialized size.
775+ #[ cfg( debug_assertions) ]
776+ {
777+ let mut iter = path. clone ( ) ;
778+ if let Some ( ( _, first) ) = iter. next ( ) {
779+ let remaining = iter. clone ( ) . count ( ) ; // includes intermediate + final
780+
781+ // At least one intermediate hop
782+ if remaining > 1 {
783+ let expected = first. serialized_length ( ) ;
784+
785+ // skip final hop: take(remaining - 1)
786+ for ( _, hop) in iter. take ( remaining - 1 ) {
787+ debug_assert ! (
788+ hop. serialized_length( ) == expected,
789+ "All intermediate blinded hops must have identical serialized size"
790+ ) ;
791+ }
792+ }
793+ }
794+ }
795+
696796 utils:: construct_blinded_hops ( secp_ctx, path, session_priv)
697797}
698798
@@ -752,14 +852,24 @@ where
752852}
753853
754854pub ( super ) fn compute_payinfo (
755- intermediate_nodes : & [ PaymentForwardNode ] , payee_tlvs : & ReceiveTlvs ,
756- payee_htlc_maximum_msat : u64 , min_final_cltv_expiry_delta : u16 ,
855+ intermediate_nodes : & [ PaymentForwardNode ] , dummy_count : usize , dummy_tlvs : & DummyTlvs ,
856+ payee_tlvs : & ReceiveTlvs , payee_htlc_maximum_msat : u64 , min_final_cltv_expiry_delta : u16 ,
757857) -> Result < BlindedPayInfo , ( ) > {
758- let ( aggregated_base_fee, aggregated_prop_fee) =
759- compute_aggregated_base_prop_fee ( intermediate_nodes. iter ( ) . map ( |node| RoutingFees {
858+ let dummy_tlvs_iter = core:: iter:: repeat ( dummy_tlvs) . take ( dummy_count) ;
859+
860+ let routing_fees = intermediate_nodes
861+ . iter ( )
862+ . map ( |node| RoutingFees {
760863 base_msat : node. tlvs . payment_relay . fee_base_msat ,
761864 proportional_millionths : node. tlvs . payment_relay . fee_proportional_millionths ,
762- } ) ) ?;
865+ } )
866+ . chain ( dummy_tlvs_iter. clone ( ) . map ( |tlvs| RoutingFees {
867+ base_msat : tlvs. payment_relay . fee_base_msat ,
868+ proportional_millionths : tlvs. payment_relay . fee_proportional_millionths ,
869+ } ) ) ;
870+
871+ let ( aggregated_base_fee, aggregated_prop_fee) =
872+ compute_aggregated_base_prop_fee ( routing_fees) ?;
763873
764874 let mut htlc_minimum_msat: u64 = 1 ;
765875 let mut htlc_maximum_msat: u64 = 21_000_000 * 100_000_000 * 1_000 ; // Total bitcoin supply
@@ -788,6 +898,16 @@ pub(super) fn compute_payinfo(
788898 )
789899 . ok_or ( ( ) ) ?; // If underflow occurs, we cannot send to this hop without exceeding their max
790900 }
901+ for dummy_tlvs in dummy_tlvs_iter {
902+ cltv_expiry_delta =
903+ cltv_expiry_delta. checked_add ( dummy_tlvs. payment_relay . cltv_expiry_delta ) . ok_or ( ( ) ) ?;
904+
905+ htlc_minimum_msat = amt_to_forward_msat (
906+ core:: cmp:: max ( dummy_tlvs. payment_constraints . htlc_minimum_msat , htlc_minimum_msat) ,
907+ & dummy_tlvs. payment_relay ,
908+ )
909+ . unwrap_or ( 1 ) ; // If underflow occurs, we definitely reached this node's min
910+ }
791911 htlc_minimum_msat =
792912 core:: cmp:: max ( payee_tlvs. payment_constraints . htlc_minimum_msat , htlc_minimum_msat) ;
793913 htlc_maximum_msat = core:: cmp:: min ( payee_htlc_maximum_msat, htlc_maximum_msat) ;
@@ -874,8 +994,8 @@ impl_writeable_tlv_based!(Bolt12RefundContext, {});
874994#[ cfg( test) ]
875995mod tests {
876996 use crate :: blinded_path:: payment:: {
877- Bolt12RefundContext , ForwardTlvs , PaymentConstraints , PaymentContext , PaymentForwardNode ,
878- PaymentRelay , ReceiveTlvs ,
997+ Bolt12RefundContext , DummyTlvs , ForwardTlvs , PaymentConstraints , PaymentContext ,
998+ PaymentForwardNode , PaymentRelay , ReceiveTlvs ,
879999 } ;
8801000 use crate :: ln:: functional_test_utils:: TEST_FINAL_CLTV ;
8811001 use crate :: types:: features:: BlindedHopFeatures ;
@@ -931,9 +1051,15 @@ mod tests {
9311051 payment_context : PaymentContext :: Bolt12Refund ( Bolt12RefundContext { } ) ,
9321052 } ;
9331053 let htlc_maximum_msat = 100_000 ;
934- let blinded_payinfo =
935- super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs, htlc_maximum_msat, 12 )
936- . unwrap ( ) ;
1054+ let blinded_payinfo = super :: compute_payinfo (
1055+ & intermediate_nodes[ ..] ,
1056+ 0 ,
1057+ & DummyTlvs :: default ( ) ,
1058+ & recv_tlvs,
1059+ htlc_maximum_msat,
1060+ 12 ,
1061+ )
1062+ . unwrap ( ) ;
9371063 assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
9381064 assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
9391065 assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 300 ) ;
@@ -948,8 +1074,15 @@ mod tests {
9481074 payment_constraints : PaymentConstraints { max_cltv_expiry : 0 , htlc_minimum_msat : 1 } ,
9491075 payment_context : PaymentContext :: Bolt12Refund ( Bolt12RefundContext { } ) ,
9501076 } ;
951- let blinded_payinfo =
952- super :: compute_payinfo ( & [ ] , & recv_tlvs, 4242 , TEST_FINAL_CLTV as u16 ) . unwrap ( ) ;
1077+ let blinded_payinfo = super :: compute_payinfo (
1078+ & [ ] ,
1079+ 0 ,
1080+ & DummyTlvs :: default ( ) ,
1081+ & recv_tlvs,
1082+ 4242 ,
1083+ TEST_FINAL_CLTV as u16 ,
1084+ )
1085+ . unwrap ( ) ;
9531086 assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
9541087 assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
9551088 assert_eq ! ( blinded_payinfo. cltv_expiry_delta, TEST_FINAL_CLTV as u16 ) ;
@@ -1008,6 +1141,8 @@ mod tests {
10081141 let htlc_maximum_msat = 100_000 ;
10091142 let blinded_payinfo = super :: compute_payinfo (
10101143 & intermediate_nodes[ ..] ,
1144+ 0 ,
1145+ & DummyTlvs :: default ( ) ,
10111146 & recv_tlvs,
10121147 htlc_maximum_msat,
10131148 TEST_FINAL_CLTV as u16 ,
@@ -1067,6 +1202,8 @@ mod tests {
10671202 let htlc_minimum_msat = 3798 ;
10681203 assert ! ( super :: compute_payinfo(
10691204 & intermediate_nodes[ ..] ,
1205+ 0 ,
1206+ & DummyTlvs :: default ( ) ,
10701207 & recv_tlvs,
10711208 htlc_minimum_msat - 1 ,
10721209 TEST_FINAL_CLTV as u16
@@ -1076,6 +1213,8 @@ mod tests {
10761213 let htlc_maximum_msat = htlc_minimum_msat + 1 ;
10771214 let blinded_payinfo = super :: compute_payinfo (
10781215 & intermediate_nodes[ ..] ,
1216+ 0 ,
1217+ & DummyTlvs :: default ( ) ,
10791218 & recv_tlvs,
10801219 htlc_maximum_msat,
10811220 TEST_FINAL_CLTV as u16 ,
@@ -1136,6 +1275,8 @@ mod tests {
11361275
11371276 let blinded_payinfo = super :: compute_payinfo (
11381277 & intermediate_nodes[ ..] ,
1278+ 0 ,
1279+ & DummyTlvs :: default ( ) ,
11391280 & recv_tlvs,
11401281 10_000 ,
11411282 TEST_FINAL_CLTV as u16 ,
0 commit comments