@@ -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 , ( ) > {
@@ -183,11 +205,26 @@ pub(super) fn compute_payinfo(
183
205
184
206
cltv_expiry_delta = cltv_expiry_delta. checked_add ( tlvs. payment_relay . cltv_expiry_delta ) . ok_or ( ( ) ) ?;
185
207
}
208
+
209
+ let mut htlc_minimum_msat: u64 = 1 ;
210
+ for ( _, tlvs) in intermediate_nodes. iter ( ) {
211
+ // The min htlc for an intermediate node is that node's min minus the fees charged by all of the
212
+ // following hops for forwarding that min, since that fee amount will automatically be included
213
+ // in the amount that this node receives and contribute towards reaching its min.
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 ) ; // If underflow occurs, we definitely reached this node's min
218
+ }
219
+ htlc_minimum_msat = core:: cmp:: max (
220
+ payee_tlvs. payment_constraints . htlc_minimum_msat , htlc_minimum_msat
221
+ ) ;
222
+
186
223
Ok ( BlindedPayInfo {
187
224
fee_base_msat : u32:: try_from ( curr_base_fee) . map_err ( |_| ( ) ) ?,
188
225
fee_proportional_millionths : u32:: try_from ( curr_prop_mil) . map_err ( |_| ( ) ) ?,
189
226
cltv_expiry_delta,
190
- htlc_minimum_msat : 1 , // TODO
227
+ htlc_minimum_msat,
191
228
htlc_maximum_msat : 21_000_000 * 100_000_000 * 1_000 , // TODO
192
229
features : BlindedHopFeatures :: empty ( ) ,
193
230
} )
@@ -252,6 +289,7 @@ mod tests {
252
289
assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
253
290
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
254
291
assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 288 ) ;
292
+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 900 ) ;
255
293
}
256
294
257
295
#[ test]
@@ -267,5 +305,89 @@ mod tests {
267
305
assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
268
306
assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
269
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) ;
270
392
}
271
393
}
0 commit comments