@@ -20,6 +20,26 @@ use crate::{
2020/// The minimum number of slots required for the general bucket to function.
2121const MIN_GENERAL_BUCKET_SLOTS : u16 = 5 ;
2222
23+ /// The minimum number of accepted HTLCs required for a channel to be added to the resource
24+ /// manager. With the default bucket allocations of 40%/20%/40% (general/congestion/protected),
25+ /// the general bucket (40%) needs at least 5 usable HTLC slots to function effectively. To
26+ /// ensure the general bucket gets 5 slots at its 40% share, we need at least 12 total HTLC
27+ /// slots.
28+ const MIN_ACCEPTED_HTLCS : u16 = 12 ;
29+
30+ /// The minimum `max_htlc_value_in_flight_msat` required for a channel to be added to the resource
31+ /// manager. This corresponds to the default `min_funding_satoshis` of 1000 in
32+ /// [`crate::util::config::ChannelHandshakeLimits`], which is the smallest channel size LDK will
33+ /// accept.
34+ const MIN_MAX_IN_FLIGHT_MSAT : u64 = 1_000_000 ;
35+
36+ #[ derive( Clone , PartialEq , Eq , Debug ) ]
37+ enum BucketAssigned {
38+ General ,
39+ Congestion ,
40+ Protected ,
41+ }
42+
2343struct GeneralBucket {
2444 /// Our SCID
2545 scid : u64 ,
@@ -260,6 +280,187 @@ impl BucketResources {
260280 }
261281}
262282
283+ struct BucketAllocations {
284+ general_slots : u16 ,
285+ general_liquidity : u64 ,
286+ congestion_slots : u16 ,
287+ congestion_liquidity : u64 ,
288+ protected_slots : u16 ,
289+ protected_liquidity : u64 ,
290+ }
291+
292+ fn bucket_allocations (
293+ max_accepted_htlcs : u16 , max_htlc_value_in_flight_msat : u64 , general_pct : u8 ,
294+ congestion_pct : u8 ,
295+ ) -> BucketAllocations {
296+ let general_slots = ( max_accepted_htlcs as f64 * general_pct as f64 / 100.0 ) . round ( ) as u16 ;
297+ let general_liquidity =
298+ ( max_htlc_value_in_flight_msat as f64 * general_pct as f64 / 100.0 ) . round ( ) as u64 ;
299+
300+ let congestion_slots =
301+ ( max_accepted_htlcs as f64 * congestion_pct as f64 / 100.0 ) . round ( ) as u16 ;
302+ let congestion_liquidity =
303+ ( max_htlc_value_in_flight_msat as f64 * congestion_pct as f64 / 100.0 ) . round ( ) as u64 ;
304+
305+ let protected_slots = max_accepted_htlcs - general_slots - congestion_slots;
306+ let protected_liquidity =
307+ max_htlc_value_in_flight_msat - general_liquidity - congestion_liquidity;
308+
309+ BucketAllocations {
310+ general_slots,
311+ general_liquidity,
312+ congestion_slots,
313+ congestion_liquidity,
314+ protected_slots,
315+ protected_liquidity,
316+ }
317+ }
318+
319+ #[ derive( Debug , Clone ) ]
320+ struct PendingHTLC {
321+ incoming_amount_msat : u64 ,
322+ fee : u64 ,
323+ outgoing_accountable : bool ,
324+ added_at_unix_seconds : u64 ,
325+ in_flight_risk : u64 ,
326+ bucket : BucketAssigned ,
327+ }
328+
329+ #[ derive( Debug , PartialEq , Eq , Hash ) ]
330+ struct HtlcRef {
331+ incoming_channel_id : u64 ,
332+ htlc_id : u64 ,
333+ }
334+
335+ struct Channel {
336+ /// The reputation this channel has accrued as an outgoing link.
337+ outgoing_reputation : DecayingAverage ,
338+
339+ /// The revenue this channel has earned us as an incoming link.
340+ incoming_revenue : AggregatedWindowAverage ,
341+
342+ /// HTLC Ref incoming channel -> pending HTLC outgoing.
343+ /// It tracks all the pending HTLCs where this channel is the outgoing link.
344+ pending_htlcs : HashMap < HtlcRef , PendingHTLC > ,
345+
346+ general_bucket : GeneralBucket ,
347+ congestion_bucket : BucketResources ,
348+ /// SCID -> unix seconds timestamp
349+ /// Tracks which channels have misused the congestion bucket and the unix timestamp.
350+ last_congestion_misuse : HashMap < u64 , u64 > ,
351+ protected_bucket : BucketResources ,
352+ }
353+
354+ impl Channel {
355+ fn new (
356+ scid : u64 , bucket_allocations : BucketAllocations , reputation_window : Duration ,
357+ revenue_week_avg_duration : Duration , timestamp_unix_secs : u64 ,
358+ ) -> Result < Self , ( ) > {
359+ const REVENUE_WEEK_MULTIPLIER : u8 = 6 ;
360+
361+ Ok ( Channel {
362+ outgoing_reputation : DecayingAverage :: new ( timestamp_unix_secs, reputation_window) ,
363+ incoming_revenue : AggregatedWindowAverage :: new (
364+ revenue_week_avg_duration,
365+ REVENUE_WEEK_MULTIPLIER ,
366+ timestamp_unix_secs,
367+ ) ,
368+ pending_htlcs : new_hash_map ( ) ,
369+ general_bucket : GeneralBucket :: new (
370+ scid,
371+ bucket_allocations. general_slots ,
372+ bucket_allocations. general_liquidity ,
373+ ) ?,
374+ congestion_bucket : BucketResources :: new (
375+ bucket_allocations. congestion_slots ,
376+ bucket_allocations. congestion_liquidity ,
377+ ) ,
378+ last_congestion_misuse : new_hash_map ( ) ,
379+ protected_bucket : BucketResources :: new (
380+ bucket_allocations. protected_slots ,
381+ bucket_allocations. protected_liquidity ,
382+ ) ,
383+ } )
384+ }
385+
386+ fn general_available < ES : EntropySource > (
387+ & mut self , incoming_amount_msat : u64 , outgoing_channel_id : u64 , entropy_source : & ES ,
388+ ) -> Result < bool , ( ) > {
389+ Ok ( self . general_bucket . can_add_htlc (
390+ outgoing_channel_id,
391+ incoming_amount_msat,
392+ entropy_source,
393+ ) ?)
394+ }
395+
396+ fn congestion_eligible (
397+ & mut self , pending_htlcs_in_congestion : bool , incoming_amount_msat : u64 ,
398+ outgoing_channel_id : u64 , at_timestamp : u64 ,
399+ ) -> Result < bool , ( ) > {
400+ Ok ( !pending_htlcs_in_congestion
401+ && self . can_add_htlc_congestion (
402+ outgoing_channel_id,
403+ incoming_amount_msat,
404+ at_timestamp,
405+ ) ?)
406+ }
407+
408+ fn misused_congestion ( & mut self , channel_id : u64 , misuse_timestamp : u64 ) {
409+ self . last_congestion_misuse . insert ( channel_id, misuse_timestamp) ;
410+ }
411+
412+ // Returns whether the outgoing channel has misused the congestion bucket in the last two
413+ // weeks.
414+ fn has_misused_congestion (
415+ & mut self , outgoing_scid : u64 , at_timestamp : u64 ,
416+ ) -> Result < bool , ( ) > {
417+ match self . last_congestion_misuse . entry ( outgoing_scid) {
418+ Entry :: Vacant ( _) => Ok ( false ) ,
419+ Entry :: Occupied ( last_misuse) => {
420+ if at_timestamp < * last_misuse. get ( ) {
421+ return Err ( ( ) ) ;
422+ }
423+ // If the last misuse of the congestion bucket was over more than two
424+ // weeks ago, remove the entry.
425+ const TWO_WEEKS : u64 = 2016 * 10 * 60 ;
426+ let since_last_misuse = at_timestamp - last_misuse. get ( ) ;
427+ if since_last_misuse < TWO_WEEKS {
428+ return Ok ( true ) ;
429+ } else {
430+ last_misuse. remove ( ) ;
431+ return Ok ( false ) ;
432+ }
433+ } ,
434+ }
435+ }
436+
437+ fn can_add_htlc_congestion (
438+ & mut self , channel_id : u64 , htlc_amount_msat : u64 , at_timestamp : u64 ,
439+ ) -> Result < bool , ( ) > {
440+ let congestion_resources_available =
441+ self . congestion_bucket . resources_available ( htlc_amount_msat) ;
442+ let misused_congestion = self . has_misused_congestion ( channel_id, at_timestamp) ?;
443+
444+ let below_liquidity_limit = htlc_amount_msat
445+ <= self . congestion_bucket . liquidity_allocated
446+ / self . congestion_bucket . slots_allocated as u64 ;
447+
448+ Ok ( congestion_resources_available && !misused_congestion && below_liquidity_limit)
449+ }
450+
451+ fn sufficient_reputation (
452+ & mut self , in_flight_htlc_risk : u64 , outgoing_reputation : i64 ,
453+ outgoing_in_flight_risk : u64 , at_timestamp : u64 ,
454+ ) -> bool {
455+ let incoming_revenue_threshold = self . incoming_revenue . value_at_timestamp ( at_timestamp) ;
456+
457+ outgoing_reputation
458+ . saturating_sub ( i64:: try_from ( outgoing_in_flight_risk) . unwrap_or ( i64:: MAX ) )
459+ . saturating_sub ( i64:: try_from ( in_flight_htlc_risk) . unwrap_or ( i64:: MAX ) )
460+ >= incoming_revenue_threshold
461+ }
462+ }
463+
263464/// A weighted average that decays over a specified window.
264465///
265466/// It enables tracking of historical behavior without storing individual data points.
0 commit comments