@@ -84,7 +84,8 @@ pub struct PaymentConstraints {
84
84
///
85
85
///[`BlindedHop`]: crate::blinded_path::BlindedHop
86
86
pub max_cltv_expiry : u32 ,
87
- /// The minimum value, in msat, that may be relayed over this [`BlindedHop`].
87
+ /// The minimum value, in msat, that may be accepted by the node corresponding to this
88
+ /// [`BlindedHop`].
88
89
pub htlc_minimum_msat : u64 ,
89
90
}
90
91
@@ -153,6 +154,27 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
153
154
utils:: construct_blinded_hops ( secp_ctx, pks, tlvs, session_priv)
154
155
}
155
156
157
+ /// `None` if underflow occurs.
158
+ fn amt_to_forward_msat ( inbound_amt_msat : u64 , payment_relay : & PaymentRelay ) -> Option < u64 > {
159
+ let inbound_amt = inbound_amt_msat as u128 ;
160
+ let base = payment_relay. fee_base_msat as u128 ;
161
+ let prop = payment_relay. fee_proportional_millionths as u128 ;
162
+
163
+ let post_base_fee_inbound_amt =
164
+ if let Some ( amt) = inbound_amt. checked_sub ( base) { amt } else { return None } ;
165
+ let mut amt_to_forward =
166
+ ( post_base_fee_inbound_amt * 1_000_000 + 1_000_000 + prop - 1 ) / ( prop + 1_000_000 ) ;
167
+
168
+ let fee = ( ( amt_to_forward * prop) / 1_000_000 ) + base;
169
+ if inbound_amt - fee < amt_to_forward {
170
+ // Rounding up the forwarded amount resulted in underpaying this node, so take an extra 1 msat
171
+ // in fee to compensate.
172
+ amt_to_forward -= 1 ;
173
+ }
174
+ debug_assert_eq ! ( amt_to_forward + fee, inbound_amt) ;
175
+ u64:: try_from ( amt_to_forward) . ok ( )
176
+ }
177
+
156
178
pub ( super ) fn compute_payinfo (
157
179
intermediate_nodes : & [ ( PublicKey , ForwardTlvs ) ] , payee_tlvs : & ReceiveTlvs
158
180
) -> Result < BlindedPayInfo , ( ) > {
@@ -184,11 +206,26 @@ pub(super) fn compute_payinfo(
184
206
185
207
cltv_expiry_delta = cltv_expiry_delta. checked_add ( tlvs. payment_relay . cltv_expiry_delta ) . ok_or ( ( ) ) ?;
186
208
}
209
+
210
+ let mut htlc_minimum_msat: u64 = 1 ;
211
+ for ( _, tlvs) in intermediate_nodes. iter ( ) {
212
+ // The min htlc for an intermediate node is that node's min minus the fees charged by all of the
213
+ // following hops for forwarding that min, since that fee amount will automatically be included
214
+ // in the amount that this node receives and contribute towards reaching its min.
215
+ htlc_minimum_msat = amt_to_forward_msat (
216
+ core:: cmp:: max ( tlvs. payment_constraints . htlc_minimum_msat , htlc_minimum_msat) ,
217
+ & tlvs. payment_relay
218
+ ) . unwrap_or ( 1 ) ; // If underflow occurs, we definitely reached this node's min
219
+ }
220
+ htlc_minimum_msat = core:: cmp:: max (
221
+ payee_tlvs. payment_constraints . htlc_minimum_msat , htlc_minimum_msat
222
+ ) ;
223
+
187
224
Ok ( BlindedPayInfo {
188
225
fee_base_msat : u32:: try_from ( curr_base_fee) . map_err ( |_| ( ) ) ?,
189
226
fee_proportional_millionths : u32:: try_from ( curr_prop_mil) . map_err ( |_| ( ) ) ?,
190
227
cltv_expiry_delta,
191
- htlc_minimum_msat : 1 , // TODO
228
+ htlc_minimum_msat,
192
229
htlc_maximum_msat : 21_000_000 * 100_000_000 * 1_000 , // TODO
193
230
features : BlindedHopFeatures :: empty ( ) ,
194
231
} )
@@ -253,6 +290,7 @@ mod tests {
253
290
assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
254
291
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
255
292
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 288 ) ;
293
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 900 ) ;
256
294
}
257
295
258
296
#[ test]
@@ -268,5 +306,89 @@ mod tests {
268
306
assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
269
307
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
270
308
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 0 ) ;
309
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 1 ) ;
310
+ }
311
+
312
+ #[ test]
313
+ fn simple_aggregated_htlc_min ( ) {
314
+ // If no hops charge fees, the htlc_minimum_msat should just be the maximum htlc_minimum_msat
315
+ // along the path.
316
+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
317
+ let intermediate_nodes = vec ! [ ( 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: 1 ,
327
+ } ,
328
+ features: BlindedHopFeatures :: empty( ) ,
329
+ } ) , ( dummy_pk, ForwardTlvs {
330
+ short_channel_id: 0 ,
331
+ payment_relay: PaymentRelay {
332
+ cltv_expiry_delta: 0 ,
333
+ fee_proportional_millionths: 0 ,
334
+ fee_base_msat: 0 ,
335
+ } ,
336
+ payment_constraints: PaymentConstraints {
337
+ max_cltv_expiry: 0 ,
338
+ htlc_minimum_msat: 2_000 ,
339
+ } ,
340
+ features: BlindedHopFeatures :: empty( ) ,
341
+ } ) ] ;
342
+ let recv_tlvs = ReceiveTlvs {
343
+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
344
+ payment_constraints : PaymentConstraints {
345
+ max_cltv_expiry : 0 ,
346
+ htlc_minimum_msat : 3 ,
347
+ } ,
348
+ } ;
349
+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
350
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 2_000 ) ;
351
+ }
352
+
353
+ #[ test]
354
+ fn aggregated_htlc_min ( ) {
355
+ // Create a path with varying fees and htlc_mins, and make sure htlc_minimum_msat ends up as the
356
+ // max (htlc_min - following_fees) along the path.
357
+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
358
+ let intermediate_nodes = vec ! [ ( 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: 1_000 ,
364
+ } ,
365
+ payment_constraints: PaymentConstraints {
366
+ max_cltv_expiry: 0 ,
367
+ htlc_minimum_msat: 5_000 ,
368
+ } ,
369
+ features: BlindedHopFeatures :: empty( ) ,
370
+ } ) , ( dummy_pk, ForwardTlvs {
371
+ short_channel_id: 0 ,
372
+ payment_relay: PaymentRelay {
373
+ cltv_expiry_delta: 0 ,
374
+ fee_proportional_millionths: 500 ,
375
+ fee_base_msat: 200 ,
376
+ } ,
377
+ payment_constraints: PaymentConstraints {
378
+ max_cltv_expiry: 0 ,
379
+ htlc_minimum_msat: 2_000 ,
380
+ } ,
381
+ features: BlindedHopFeatures :: empty( ) ,
382
+ } ) ] ;
383
+ let recv_tlvs = ReceiveTlvs {
384
+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
385
+ payment_constraints : PaymentConstraints {
386
+ max_cltv_expiry : 0 ,
387
+ htlc_minimum_msat : 1 ,
388
+ } ,
389
+ } ;
390
+ let htlc_minimum_msat = 3798 ;
391
+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
392
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, htlc_minimum_msat) ;
271
393
}
272
394
}
0 commit comments