@@ -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,29 @@ 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 or overflow occurs.
158
+ fn amt_to_forward_msat ( inbound_amt_msat : u64 , payment_relay : & PaymentRelay ) -> Option < u64 > {
159
+ let amt_to_fwd_numerator = inbound_amt_msat. checked_sub ( payment_relay. fee_base_msat as u64 )
160
+ . and_then ( |a| a. checked_mul ( 1_000_000 ) )
161
+ . and_then ( |a| a. checked_add ( 1_000_000 - 1 ) )
162
+ . and_then ( |a| a. checked_add ( payment_relay. fee_proportional_millionths as u64 ) ) ;
163
+ let amt_to_fwd_denominator = payment_relay. fee_proportional_millionths . checked_add ( 1_000_000 ) ;
164
+ let mut amt_to_forward = match ( amt_to_fwd_numerator, amt_to_fwd_denominator) {
165
+ ( Some ( num) , Some ( denom) ) => num / denom as u64 ,
166
+ _ => return None ,
167
+ } ;
168
+ let fee_opt = amt_to_forward. checked_mul ( payment_relay. fee_proportional_millionths as u64 )
169
+ . and_then ( |prop| ( prop / 1000000 ) . checked_add ( payment_relay. fee_base_msat as u64 ) ) ;
170
+ let fee = if let Some ( f) = fee_opt { f } else { return None } ;
171
+ if inbound_amt_msat - fee < amt_to_forward {
172
+ // Rounding up the forwarded amount resulted in underpaying this node, so take an extra 1 msat
173
+ // in fee to compensate.
174
+ amt_to_forward -= 1 ;
175
+ }
176
+ debug_assert_eq ! ( amt_to_forward + fee, inbound_amt_msat) ;
177
+ Some ( amt_to_forward)
178
+ }
179
+
156
180
pub ( super ) fn compute_payinfo (
157
181
intermediate_nodes : & [ ( PublicKey , ForwardTlvs ) ] , payee_tlvs : & ReceiveTlvs
158
182
) -> Result < BlindedPayInfo , ( ) > {
@@ -184,11 +208,23 @@ pub(super) fn compute_payinfo(
184
208
185
209
cltv_expiry_delta = cltv_expiry_delta. checked_add ( tlvs. payment_relay . cltv_expiry_delta ) . ok_or ( ( ) ) ?;
186
210
}
211
+
212
+ let mut htlc_minimum_msat: u64 = 1 ;
213
+ for ( _, tlvs) in intermediate_nodes. iter ( ) {
214
+ htlc_minimum_msat = amt_to_forward_msat (
215
+ core:: cmp:: max ( tlvs. payment_constraints . htlc_minimum_msat , htlc_minimum_msat) ,
216
+ & tlvs. payment_relay
217
+ ) . unwrap_or ( 1 ) ;
218
+ }
219
+ htlc_minimum_msat = core:: cmp:: max (
220
+ payee_tlvs. payment_constraints . htlc_minimum_msat , htlc_minimum_msat
221
+ ) ;
222
+
187
223
Ok ( BlindedPayInfo {
188
224
fee_base_msat : u32:: try_from ( curr_base_fee) . map_err ( |_| ( ) ) ?,
189
225
fee_proportional_millionths : u32:: try_from ( curr_prop_mil) . map_err ( |_| ( ) ) ?,
190
226
cltv_expiry_delta,
191
- htlc_minimum_msat : 1 , // TODO
227
+ htlc_minimum_msat,
192
228
htlc_maximum_msat : 21_000_000 * 100_000_000 * 1_000 , // TODO
193
229
features : BlindedHopFeatures :: empty ( ) ,
194
230
} )
@@ -253,6 +289,7 @@ mod tests {
253
289
assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
254
290
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
255
291
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 288 ) ;
292
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 900 ) ;
256
293
}
257
294
258
295
#[ test]
@@ -268,5 +305,89 @@ mod tests {
268
305
assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
269
306
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
270
307
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 0 ) ;
308
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 1 ) ;
309
+ }
310
+
311
+ #[ test]
312
+ fn simple_aggregated_htlc_min ( ) {
313
+ // If no hops charge fees, the htlc_minimum_msat should just be the maximum htlc_minimum_msat
314
+ // along the path.
315
+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
316
+ let intermediate_nodes = vec ! [ ( dummy_pk, ForwardTlvs {
317
+ short_channel_id: 0 ,
318
+ payment_relay: PaymentRelay {
319
+ cltv_expiry_delta: 0 ,
320
+ fee_proportional_millionths: 0 ,
321
+ fee_base_msat: 0 ,
322
+ } ,
323
+ payment_constraints: PaymentConstraints {
324
+ max_cltv_expiry: 0 ,
325
+ htlc_minimum_msat: 1 ,
326
+ } ,
327
+ features: BlindedHopFeatures :: empty( ) ,
328
+ } ) , ( dummy_pk, ForwardTlvs {
329
+ short_channel_id: 0 ,
330
+ payment_relay: PaymentRelay {
331
+ cltv_expiry_delta: 0 ,
332
+ fee_proportional_millionths: 0 ,
333
+ fee_base_msat: 0 ,
334
+ } ,
335
+ payment_constraints: PaymentConstraints {
336
+ max_cltv_expiry: 0 ,
337
+ htlc_minimum_msat: 2_000 ,
338
+ } ,
339
+ features: BlindedHopFeatures :: empty( ) ,
340
+ } ) ] ;
341
+ let recv_tlvs = ReceiveTlvs {
342
+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
343
+ payment_constraints : PaymentConstraints {
344
+ max_cltv_expiry : 0 ,
345
+ htlc_minimum_msat : 3 ,
346
+ } ,
347
+ } ;
348
+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
349
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 2_000 ) ;
350
+ }
351
+
352
+ #[ test]
353
+ fn aggregated_htlc_min ( ) {
354
+ // Create a path with varying fees and htlc_mins, and make sure htlc_minimum_msat ends up as the
355
+ // max (htlc_min - following_fees) along the path.
356
+ let dummy_pk = PublicKey :: from_slice ( & [ 2 ; 33 ] ) . unwrap ( ) ;
357
+ let intermediate_nodes = vec ! [ ( dummy_pk, ForwardTlvs {
358
+ short_channel_id: 0 ,
359
+ payment_relay: PaymentRelay {
360
+ cltv_expiry_delta: 0 ,
361
+ fee_proportional_millionths: 500 ,
362
+ fee_base_msat: 1_000 ,
363
+ } ,
364
+ payment_constraints: PaymentConstraints {
365
+ max_cltv_expiry: 0 ,
366
+ htlc_minimum_msat: 5_000 ,
367
+ } ,
368
+ features: BlindedHopFeatures :: empty( ) ,
369
+ } ) , ( dummy_pk, ForwardTlvs {
370
+ short_channel_id: 0 ,
371
+ payment_relay: PaymentRelay {
372
+ cltv_expiry_delta: 0 ,
373
+ fee_proportional_millionths: 500 ,
374
+ fee_base_msat: 200 ,
375
+ } ,
376
+ payment_constraints: PaymentConstraints {
377
+ max_cltv_expiry: 0 ,
378
+ htlc_minimum_msat: 2_000 ,
379
+ } ,
380
+ features: BlindedHopFeatures :: empty( ) ,
381
+ } ) ] ;
382
+ let recv_tlvs = ReceiveTlvs {
383
+ payment_secret : PaymentSecret ( [ 0 ; 32 ] ) ,
384
+ payment_constraints : PaymentConstraints {
385
+ max_cltv_expiry : 0 ,
386
+ htlc_minimum_msat : 1 ,
387
+ } ,
388
+ } ;
389
+ let htlc_minimum_msat = 3798 ;
390
+ let blinded_payinfo = super :: compute_payinfo ( & intermediate_nodes[ ..] , & recv_tlvs) . unwrap ( ) ;
391
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, htlc_minimum_msat) ;
271
392
}
272
393
}
0 commit comments