@@ -84,7 +84,8 @@ pub struct PaymentConstraints {
8484 ///
8585 ///[`BlindedHop`]: crate::blinded_path::BlindedHop
8686 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`].
8889 pub htlc_minimum_msat : u64 ,
8990}
9091
@@ -153,6 +154,27 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
153154 utils:: construct_blinded_hops ( secp_ctx, pks, tlvs, session_priv)
154155}
155156
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+
156178pub ( super ) fn compute_payinfo (
157179 intermediate_nodes : & [ ( PublicKey , ForwardTlvs ) ] , payee_tlvs : & ReceiveTlvs
158180) -> Result < BlindedPayInfo , ( ) > {
@@ -184,11 +206,26 @@ pub(super) fn compute_payinfo(
184206
185207 cltv_expiry_delta = cltv_expiry_delta. checked_add ( tlvs. payment_relay . cltv_expiry_delta ) . ok_or ( ( ) ) ?;
186208 }
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+
187224 Ok ( BlindedPayInfo {
188225 fee_base_msat : u32:: try_from ( curr_base_fee) . map_err ( |_| ( ) ) ?,
189226 fee_proportional_millionths : u32:: try_from ( curr_prop_mil) . map_err ( |_| ( ) ) ?,
190227 cltv_expiry_delta,
191- htlc_minimum_msat : 1 , // TODO
228+ htlc_minimum_msat,
192229 htlc_maximum_msat : 21_000_000 * 100_000_000 * 1_000 , // TODO
193230 features : BlindedHopFeatures :: empty ( ) ,
194231 } )
@@ -253,6 +290,7 @@ mod tests {
253290 assert_eq ! ( blinded_payinfo. fee_base_msat, 201 ) ;
254291 assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 1001 ) ;
255292 assert_eq ! ( blinded_payinfo. cltv_expiry_delta, 288 ) ;
293+ assert_eq ! ( blinded_payinfo. htlc_minimum_msat, 900 ) ;
256294 }
257295
258296 #[ test]
@@ -268,5 +306,89 @@ mod tests {
268306 assert_eq ! ( blinded_payinfo. fee_base_msat, 0 ) ;
269307 assert_eq ! ( blinded_payinfo. fee_proportional_millionths, 0 ) ;
270308 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) ;
271393 }
272394}
0 commit comments