@@ -184,11 +184,38 @@ 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
+ . map ( |( _, ( _, tlvs) ) | tlvs) ;
196
+ for node in next_nodes {
197
+ // The min htlc for a hop is that hop's htlc_minimum_msat minus the fees paid from that hop to
198
+ // the end of the path, because the sender will automatically include that following fee
199
+ // amount in the amount that this hop receives.
200
+ let prop_fee = node. payment_relay . fee_proportional_millionths as u128 ;
201
+ let base_fee = node. payment_relay . fee_base_msat as u128 ;
202
+ let hop_fee = htlc_min_candidate
203
+ . checked_mul ( prop_fee)
204
+ . and_then ( |prop_fee| ( prop_fee / 1_000_000 ) . checked_add ( base_fee) )
205
+ . ok_or ( ( ) ) ?;
206
+ htlc_min_candidate = htlc_min_candidate. saturating_sub ( hop_fee) ;
207
+ if htlc_min_candidate == 0 { break }
208
+ }
209
+ htlc_minimum_msat = core:: cmp:: max ( htlc_min_candidate, htlc_minimum_msat) ;
210
+ }
211
+ htlc_minimum_msat =
212
+ core:: cmp:: max ( payee_tlvs. payment_constraints . htlc_minimum_msat as u128 , htlc_minimum_msat) ;
213
+
187
214
Ok ( BlindedPayInfo {
188
215
fee_base_msat : u32:: try_from ( curr_base_fee) . map_err ( |_| ( ) ) ?,
189
216
fee_proportional_millionths : u32:: try_from ( curr_prop_mil) . map_err ( |_| ( ) ) ?,
190
217
cltv_expiry_delta,
191
- htlc_minimum_msat : 1 , // TODO
218
+ htlc_minimum_msat : u64 :: try_from ( htlc_minimum_msat ) . map_err ( |_| ( ) ) ? ,
192
219
htlc_maximum_msat : 21_000_000 * 100_000_000 * 1_000 , // TODO
193
220
features : BlindedHopFeatures :: empty ( ) ,
194
221
} )
@@ -253,6 +280,7 @@ mod tests {
253
280
assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
254
281
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
255
282
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 288 ) ;
283
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 900 ) ;
256
284
}
257
285
258
286
#[ test]
@@ -268,5 +296,89 @@ mod tests {
268
296
assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
269
297
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
270
298
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 0 ) ;
299
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 1 ) ;
300
+ }
301
+
302
+ #[ test]
303
+ fn simple_aggregated_htlc_min ( ) {
304
+ // If no hops charge fees, the htlc_minimum_msat should just be the maximum htlc_minimum_msat
305
+ // along the path.
306
+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
307
+ let intermediate_nodes = vec ! [ ( dummy_pk, ForwardTlvs {
308
+ short_channel_id: 0 ,
309
+ payment_relay: PaymentRelay {
310
+ cltv_expiry_delta: 0 ,
311
+ fee_proportional_millionths: 0 ,
312
+ fee_base_msat: 0 ,
313
+ } ,
314
+ payment_constraints: PaymentConstraints {
315
+ max_cltv_expiry: 0 ,
316
+ htlc_minimum_msat: 1 ,
317
+ } ,
318
+ features: BlindedHopFeatures :: empty( ) ,
319
+ } ) , ( dummy_pk, ForwardTlvs {
320
+ short_channel_id: 0 ,
321
+ payment_relay: PaymentRelay {
322
+ cltv_expiry_delta: 0 ,
323
+ fee_proportional_millionths: 0 ,
324
+ fee_base_msat: 0 ,
325
+ } ,
326
+ payment_constraints: PaymentConstraints {
327
+ max_cltv_expiry: 0 ,
328
+ htlc_minimum_msat: 2_000 ,
329
+ } ,
330
+ features: BlindedHopFeatures :: empty( ) ,
331
+ } ) ] ;
332
+ let recv_tlvs = ReceiveTlvs {
333
+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
334
+ payment_constraints : PaymentConstraints {
335
+ max_cltv_expiry : 0 ,
336
+ htlc_minimum_msat : 3 ,
337
+ } ,
338
+ } ;
339
+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
340
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 2_000 ) ;
341
+ }
342
+
343
+ #[ test]
344
+ fn aggregated_htlc_min ( ) {
345
+ // Create a path with varying fees and htlc_mins, and make sure htlc_minimum_msat ends up as the
346
+ // max (htlc_min - following_fees) along the path.
347
+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
348
+ let intermediate_nodes = vec ! [ ( dummy_pk, ForwardTlvs {
349
+ short_channel_id: 0 ,
350
+ payment_relay: PaymentRelay {
351
+ cltv_expiry_delta: 0 ,
352
+ fee_proportional_millionths: 500 ,
353
+ fee_base_msat: 1_000 ,
354
+ } ,
355
+ payment_constraints: PaymentConstraints {
356
+ max_cltv_expiry: 0 ,
357
+ htlc_minimum_msat: 5_000 ,
358
+ } ,
359
+ features: BlindedHopFeatures :: empty( ) ,
360
+ } ) , ( dummy_pk, ForwardTlvs {
361
+ short_channel_id: 0 ,
362
+ payment_relay: PaymentRelay {
363
+ cltv_expiry_delta: 0 ,
364
+ fee_proportional_millionths: 500 ,
365
+ fee_base_msat: 200 ,
366
+ } ,
367
+ payment_constraints: PaymentConstraints {
368
+ max_cltv_expiry: 0 ,
369
+ htlc_minimum_msat: 2_000 ,
370
+ } ,
371
+ features: BlindedHopFeatures :: empty( ) ,
372
+ } ) ] ;
373
+ let recv_tlvs = ReceiveTlvs {
374
+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
375
+ payment_constraints : PaymentConstraints {
376
+ max_cltv_expiry : 0 ,
377
+ htlc_minimum_msat : 1 ,
378
+ } ,
379
+ } ;
380
+
381
+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
382
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 3797 ) ;
271
383
}
272
384
}
0 commit comments