@@ -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 ,
@@ -256,6 +276,175 @@ impl BucketResources {
256276 }
257277}
258278
279+ struct BucketAllocations {
280+ general_slots : u16 ,
281+ general_liquidity : u64 ,
282+ congestion_slots : u16 ,
283+ congestion_liquidity : u64 ,
284+ protected_slots : u16 ,
285+ protected_liquidity : u64 ,
286+ }
287+
288+ fn bucket_allocations (
289+ max_accepted_htlcs : u16 , max_htlc_value_in_flight_msat : u64 , general_pct : u8 ,
290+ congestion_pct : u8 ,
291+ ) -> BucketAllocations {
292+ let general_slots = ( max_accepted_htlcs as f64 * general_pct as f64 / 100.0 ) . round ( ) as u16 ;
293+ let general_liquidity =
294+ ( max_htlc_value_in_flight_msat as f64 * general_pct as f64 / 100.0 ) . round ( ) as u64 ;
295+
296+ let congestion_slots =
297+ ( max_accepted_htlcs as f64 * congestion_pct as f64 / 100.0 ) . round ( ) as u16 ;
298+ let congestion_liquidity =
299+ ( max_htlc_value_in_flight_msat as f64 * congestion_pct as f64 / 100.0 ) . round ( ) as u64 ;
300+
301+ let protected_slots = max_accepted_htlcs - general_slots - congestion_slots;
302+ let protected_liquidity =
303+ max_htlc_value_in_flight_msat - general_liquidity - congestion_liquidity;
304+
305+ BucketAllocations {
306+ general_slots,
307+ general_liquidity,
308+ congestion_slots,
309+ congestion_liquidity,
310+ protected_slots,
311+ protected_liquidity,
312+ }
313+ }
314+
315+ #[ derive( Debug , Clone ) ]
316+ struct PendingHTLC {
317+ incoming_amount_msat : u64 ,
318+ fee : u64 ,
319+ outgoing_accountable : bool ,
320+ added_at_unix_seconds : u64 ,
321+ in_flight_risk : u64 ,
322+ bucket : BucketAssigned ,
323+ }
324+
325+ #[ derive( Debug , PartialEq , Eq , Hash ) ]
326+ struct HtlcRef {
327+ incoming_channel_id : u64 ,
328+ htlc_id : u64 ,
329+ }
330+
331+ struct Channel {
332+ /// The reputation this channel has accrued as an outgoing link.
333+ outgoing_reputation : DecayingAverage ,
334+
335+ /// The revenue this channel has earned us as an incoming link.
336+ incoming_revenue : AggregatedWindowAverage ,
337+
338+ /// HTLC Ref incoming channel -> pending HTLC outgoing.
339+ /// It tracks all the pending HTLCs where this channel is the outgoing link.
340+ pending_htlcs : HashMap < HtlcRef , PendingHTLC > ,
341+
342+ general_bucket : GeneralBucket ,
343+ congestion_bucket : BucketResources ,
344+ /// SCID -> unix seconds timestamp
345+ /// Tracks which channels have misused the congestion bucket and the unix timestamp.
346+ last_congestion_misuse : HashMap < u64 , u64 > ,
347+ protected_bucket : BucketResources ,
348+ }
349+
350+ impl Channel {
351+ fn new (
352+ scid : u64 , bucket_allocations : BucketAllocations , reputation_window : Duration ,
353+ revenue_week_avg_duration : Duration , timestamp_unix_secs : u64 ,
354+ ) -> Result < Self , ( ) > {
355+ const REVENUE_WEEK_MULTIPLIER : u8 = 6 ;
356+
357+ Ok ( Channel {
358+ outgoing_reputation : DecayingAverage :: new ( timestamp_unix_secs, reputation_window) ,
359+ incoming_revenue : AggregatedWindowAverage :: new (
360+ revenue_week_avg_duration,
361+ REVENUE_WEEK_MULTIPLIER ,
362+ timestamp_unix_secs,
363+ ) ,
364+ pending_htlcs : new_hash_map ( ) ,
365+ general_bucket : GeneralBucket :: new (
366+ scid,
367+ bucket_allocations. general_slots ,
368+ bucket_allocations. general_liquidity ,
369+ ) ?,
370+ congestion_bucket : BucketResources :: new (
371+ bucket_allocations. congestion_slots ,
372+ bucket_allocations. congestion_liquidity ,
373+ ) ,
374+ last_congestion_misuse : new_hash_map ( ) ,
375+ protected_bucket : BucketResources :: new (
376+ bucket_allocations. protected_slots ,
377+ bucket_allocations. protected_liquidity ,
378+ ) ,
379+ } )
380+ }
381+
382+ fn general_available < ES : EntropySource > (
383+ & mut self , incoming_amount_msat : u64 , outgoing_channel_id : u64 , entropy_source : & ES ,
384+ ) -> Result < bool , ( ) > {
385+ self . general_bucket . can_add_htlc ( outgoing_channel_id, incoming_amount_msat, entropy_source)
386+ }
387+
388+ fn congestion_eligible (
389+ & mut self , pending_htlcs_in_congestion : bool , incoming_amount_msat : u64 ,
390+ outgoing_channel_id : u64 , at_timestamp : u64 ,
391+ ) -> bool {
392+ !pending_htlcs_in_congestion
393+ && self . can_add_htlc_congestion ( outgoing_channel_id, incoming_amount_msat, at_timestamp)
394+ }
395+
396+ fn misused_congestion ( & mut self , channel_id : u64 , misuse_timestamp : u64 ) {
397+ self . last_congestion_misuse . insert ( channel_id, misuse_timestamp) ;
398+ }
399+
400+ // Returns whether the outgoing channel has misused the congestion bucket in the last two
401+ // weeks.
402+ fn has_misused_congestion ( & mut self , outgoing_scid : u64 , at_timestamp : u64 ) -> bool {
403+ match self . last_congestion_misuse . entry ( outgoing_scid) {
404+ Entry :: Vacant ( _) => false ,
405+ Entry :: Occupied ( last_misuse) => {
406+ let timestamp = u64:: max ( at_timestamp, * last_misuse. get ( ) ) ;
407+ // If the last misuse of the congestion bucket was over more than two
408+ // weeks ago, remove the entry.
409+ const TWO_WEEKS : u64 = 2016 * 10 * 60 ;
410+ let since_last_misuse = timestamp - last_misuse. get ( ) ;
411+ if since_last_misuse < TWO_WEEKS {
412+ true
413+ } else {
414+ last_misuse. remove ( ) ;
415+ false
416+ }
417+ } ,
418+ }
419+ }
420+
421+ fn can_add_htlc_congestion (
422+ & mut self , channel_id : u64 , htlc_amount_msat : u64 , at_timestamp : u64 ,
423+ ) -> bool {
424+ let congestion_resources_available =
425+ self . congestion_bucket . resources_available ( htlc_amount_msat) ;
426+ let misused_congestion = self . has_misused_congestion ( channel_id, at_timestamp) ;
427+
428+ let below_liquidity_limit = htlc_amount_msat
429+ <= self . congestion_bucket . liquidity_allocated
430+ / self . congestion_bucket . slots_allocated as u64 ;
431+
432+ congestion_resources_available && !misused_congestion && below_liquidity_limit
433+ }
434+
435+ fn sufficient_reputation (
436+ & mut self , in_flight_htlc_risk : u64 , outgoing_reputation : i64 ,
437+ outgoing_in_flight_risk : u64 , at_timestamp : u64 ,
438+ ) -> bool {
439+ let incoming_revenue_threshold = self . incoming_revenue . value_at_timestamp ( at_timestamp) ;
440+
441+ outgoing_reputation
442+ . saturating_sub ( i64:: try_from ( outgoing_in_flight_risk) . unwrap_or ( i64:: MAX ) )
443+ . saturating_sub ( i64:: try_from ( in_flight_htlc_risk) . unwrap_or ( i64:: MAX ) )
444+ >= incoming_revenue_threshold
445+ }
446+ }
447+
259448/// A weighted average that decays over a specified window.
260449///
261450/// It enables tracking of historical behavior without storing individual data points.
0 commit comments