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