@@ -184,11 +184,39 @@ pub(super) fn compute_payinfo(
184
184
185
185
cltv_expiry_delta = cltv_expiry_delta. checked_add ( tlvs. payment_relay . cltv_expiry_delta ) . ok_or ( ( ) ) ?;
186
186
}
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
+
187
215
Ok ( BlindedPayInfo {
188
216
fee_base_msat : u32:: try_from ( curr_base_fee) . map_err ( |_| ( ) ) ?,
189
217
fee_proportional_millionths : u32:: try_from ( curr_prop_mil) . map_err ( |_| ( ) ) ?,
190
218
cltv_expiry_delta,
191
- htlc_minimum_msat : 1 , // TODO
219
+ htlc_minimum_msat : u64 :: try_from ( htlc_minimum_msat ) . map_err ( |_| ( ) ) ? ,
192
220
htlc_maximum_msat : 21_000_000 * 100_000_000 * 1_000 , // TODO
193
221
features : BlindedHopFeatures :: empty ( ) ,
194
222
} )
@@ -253,6 +281,7 @@ mod tests {
253
281
assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
254
282
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
255
283
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 288 ) ;
284
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 1000 ) ;
256
285
}
257
286
258
287
#[ test]
@@ -268,5 +297,88 @@ mod tests {
268
297
assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
269
298
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
270
299
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 ) ;
271
383
}
272
384
}
0 commit comments