@@ -184,11 +184,39 @@ pub(super) fn compute_payinfo(
184184
185185 cltv_expiry_delta = cltv_expiry_delta. checked_add ( tlvs. payment_relay . cltv_expiry_delta ) . ok_or ( ( ) ) ?;
186186 }
187+
188+ let mut htlc_minimum_msat = 0 ;
189+ for ( idx, node) in intermediate_nodes. iter ( ) . map ( |( _, tlvs) | tlvs) . enumerate ( ) {
190+ let mut htlc_min_candidate = node. payment_constraints . htlc_minimum_msat as u128 ;
191+ // Get an iterator over `(curr_hop_tlvs..last_intermediate_hop_tlvs]`.
192+ let next_nodes = intermediate_nodes. iter ( )
193+ . enumerate ( )
194+ . skip_while ( |( i, _) | * i != idx)
195+ . skip ( 1 )
196+ . map ( |( _, ( _, tlvs) ) | tlvs) ;
197+ for node in next_nodes {
198+ // The min htlc for a hop is that hop's htlc_minimum_msat minus the following hops' fees
199+ // because the sender will automatically include that following fee in the amount that this
200+ // hop forwards.
201+ let prop_fee = node. payment_relay . fee_proportional_millionths as u128 ;
202+ let base_fee = node. payment_relay . fee_base_msat as u128 ;
203+ let hop_fee = htlc_min_candidate
204+ . checked_mul ( prop_fee)
205+ . and_then ( |prop_fee| ( prop_fee / 1_000_000 ) . checked_add ( base_fee) )
206+ . ok_or ( ( ) ) ?;
207+ htlc_min_candidate = htlc_min_candidate. saturating_sub ( hop_fee) ;
208+ if htlc_min_candidate == 0 { break }
209+ }
210+ htlc_minimum_msat = core:: cmp:: max ( htlc_min_candidate, htlc_minimum_msat) ;
211+ }
212+ htlc_minimum_msat =
213+ core:: cmp:: max ( payee_tlvs. payment_constraints . htlc_minimum_msat as u128 , htlc_minimum_msat) ;
214+
187215 Ok ( BlindedPayInfo {
188216 fee_base_msat : u32:: try_from ( curr_base_fee) . map_err ( |_| ( ) ) ?,
189217 fee_proportional_millionths : u32:: try_from ( curr_prop_mil) . map_err ( |_| ( ) ) ?,
190218 cltv_expiry_delta,
191- htlc_minimum_msat : 1 , // TODO
219+ htlc_minimum_msat : u64 :: try_from ( htlc_minimum_msat ) . map_err ( |_| ( ) ) ? ,
192220 htlc_maximum_msat : 21_000_000 * 100_000_000 * 1_000 , // TODO
193221 features : BlindedHopFeatures :: empty ( ) ,
194222 } )
@@ -253,6 +281,7 @@ mod tests {
253281 assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
254282 assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
255283 assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 288 ) ;
284+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 1000 ) ;
256285 }
257286
258287 #[ test]
@@ -268,5 +297,88 @@ mod tests {
268297 assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
269298 assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
270299 assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 0 ) ;
300+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 1 ) ;
301+ }
302+
303+ #[ test]
304+ fn simple_aggregated_htlc_min ( ) {
305+ // If no hops charge fees, the htlc_minimum_msat should just be the maximum htlc_minimum_msat
306+ // along the path.
307+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
308+ let intermediate_nodes = vec ! [ ( dummy_pk, ForwardTlvs {
309+ short_channel_id: 0 ,
310+ payment_relay: PaymentRelay {
311+ cltv_expiry_delta: 0 ,
312+ fee_proportional_millionths: 0 ,
313+ fee_base_msat: 0 ,
314+ } ,
315+ payment_constraints: PaymentConstraints {
316+ max_cltv_expiry: 0 ,
317+ htlc_minimum_msat: 1 ,
318+ } ,
319+ features: BlindedHopFeatures :: empty( ) ,
320+ } ) , ( dummy_pk, ForwardTlvs {
321+ short_channel_id: 0 ,
322+ payment_relay: PaymentRelay {
323+ cltv_expiry_delta: 0 ,
324+ fee_proportional_millionths: 0 ,
325+ fee_base_msat: 0 ,
326+ } ,
327+ payment_constraints: PaymentConstraints {
328+ max_cltv_expiry: 0 ,
329+ htlc_minimum_msat: 2_000 ,
330+ } ,
331+ features: BlindedHopFeatures :: empty( ) ,
332+ } ) ] ;
333+ let recv_tlvs = ReceiveTlvs {
334+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
335+ payment_constraints : PaymentConstraints {
336+ max_cltv_expiry : 0 ,
337+ htlc_minimum_msat : 3 ,
338+ } ,
339+ } ;
340+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
341+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 2_000 ) ;
342+ }
343+
344+ #[ test]
345+ fn aggregated_htlc_min ( ) {
346+ // Create a path with varying fees and htlc_mins, and make sure htlc_minimum_msat ends up as the
347+ // max (htlc_min - following_fees) along the path.
348+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
349+ let intermediate_nodes = vec ! [ ( dummy_pk, ForwardTlvs {
350+ short_channel_id: 0 ,
351+ payment_relay: PaymentRelay {
352+ cltv_expiry_delta: 0 ,
353+ fee_proportional_millionths: 500 ,
354+ fee_base_msat: 1_000 ,
355+ } ,
356+ payment_constraints: PaymentConstraints {
357+ max_cltv_expiry: 0 ,
358+ htlc_minimum_msat: 5_000 ,
359+ } ,
360+ features: BlindedHopFeatures :: empty( ) ,
361+ } ) , ( dummy_pk, ForwardTlvs {
362+ short_channel_id: 0 ,
363+ payment_relay: PaymentRelay {
364+ cltv_expiry_delta: 0 ,
365+ fee_proportional_millionths: 500 ,
366+ fee_base_msat: 200 ,
367+ } ,
368+ payment_constraints: PaymentConstraints {
369+ max_cltv_expiry: 0 ,
370+ htlc_minimum_msat: 2_000 ,
371+ } ,
372+ features: BlindedHopFeatures :: empty( ) ,
373+ } ) ] ;
374+ let recv_tlvs = ReceiveTlvs {
375+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
376+ payment_constraints : PaymentConstraints {
377+ max_cltv_expiry : 0 ,
378+ htlc_minimum_msat : 1 ,
379+ } ,
380+ } ;
381+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
382+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 4798 ) ;
271383 }
272384}
0 commit comments