@@ -13,10 +13,18 @@ use core::time::Duration;
1313
1414use crate :: {
1515 crypto:: chacha20:: ChaCha20 ,
16+ ln:: channel:: TOTAL_BITCOIN_SUPPLY_SATOSHIS ,
1617 prelude:: { hash_map:: Entry , new_hash_map, HashMap } ,
1718 sign:: EntropySource ,
1819} ;
1920
21+ #[ derive( Clone , PartialEq , Eq , Debug ) ]
22+ enum BucketAssigned {
23+ General ,
24+ Congestion ,
25+ Protected ,
26+ }
27+
2028struct GeneralBucket {
2129 /// Our SCID
2230 scid : u64 ,
@@ -232,6 +240,176 @@ impl BucketResources {
232240 }
233241}
234242
243+ #[ derive( Debug , Clone ) ]
244+ struct PendingHTLC {
245+ incoming_amount_msat : u64 ,
246+ fee : u64 ,
247+ outgoing_channel : u64 ,
248+ outgoing_accountable : bool ,
249+ added_at_unix_seconds : u64 ,
250+ in_flight_risk : u64 ,
251+ bucket : BucketAssigned ,
252+ }
253+
254+ #[ derive( Debug , PartialEq , Eq , Hash ) ]
255+ struct HtlcRef {
256+ incoming_channel_id : u64 ,
257+ htlc_id : u64 ,
258+ }
259+
260+ struct Channel {
261+ /// The reputation this channel has accrued as an outgoing link.
262+ outgoing_reputation : DecayingAverage ,
263+
264+ /// The revenue this channel has earned us as an incoming link.
265+ incoming_revenue : AggregatedWindowAverage ,
266+
267+ /// HTLC Ref incoming channel -> pending HTLC outgoing.
268+ /// It tracks all the pending HTLCs where this channel is the outgoing link.
269+ pending_htlcs : HashMap < HtlcRef , PendingHTLC > ,
270+
271+ general_bucket : GeneralBucket ,
272+ congestion_bucket : BucketResources ,
273+ /// SCID -> unix seconds timestamp
274+ /// Tracks which channels have misused the congestion bucket and the unix timestamp.
275+ last_congestion_misuse : HashMap < u64 , u64 > ,
276+ protected_bucket : BucketResources ,
277+ }
278+
279+ impl Channel {
280+ fn new (
281+ scid : u64 , max_htlc_value_in_flight_msat : u64 , max_accepted_htlcs : u16 ,
282+ general_bucket_pct : u8 , congestion_bucket_pct : u8 , reputation_window : Duration ,
283+ revenue_window_weeks : u8 , revenue_week_avg : u8 , timestamp_unix_secs : u64 ,
284+ ) -> Result < Self , ( ) > {
285+ if max_accepted_htlcs > 483
286+ || ( max_htlc_value_in_flight_msat / 1000 ) >= TOTAL_BITCOIN_SUPPLY_SATOSHIS
287+ {
288+ return Err ( ( ) ) ;
289+ }
290+
291+ if general_bucket_pct + congestion_bucket_pct >= 100 {
292+ return Err ( ( ) ) ;
293+ }
294+
295+ let general_bucket_slots_allocated = max_accepted_htlcs * general_bucket_pct as u16 / 100 ;
296+ let general_bucket_liquidity_allocated =
297+ max_htlc_value_in_flight_msat * general_bucket_pct as u64 / 100 ;
298+
299+ let congestion_bucket_slots_allocated =
300+ max_accepted_htlcs * congestion_bucket_pct as u16 / 100 ;
301+ let congestion_bucket_liquidity_allocated =
302+ max_htlc_value_in_flight_msat * congestion_bucket_pct as u64 / 100 ;
303+
304+ let protected_bucket_slots_allocated =
305+ max_accepted_htlcs - general_bucket_slots_allocated - congestion_bucket_slots_allocated;
306+ let protected_bucket_liquidity_allocated = max_htlc_value_in_flight_msat
307+ - general_bucket_liquidity_allocated
308+ - congestion_bucket_liquidity_allocated;
309+
310+ Ok ( Channel {
311+ outgoing_reputation : DecayingAverage :: new ( timestamp_unix_secs, reputation_window) ,
312+ incoming_revenue : AggregatedWindowAverage :: new (
313+ revenue_week_avg,
314+ revenue_window_weeks,
315+ timestamp_unix_secs,
316+ ) ,
317+ pending_htlcs : new_hash_map ( ) ,
318+ general_bucket : GeneralBucket :: new (
319+ scid,
320+ general_bucket_slots_allocated,
321+ general_bucket_liquidity_allocated,
322+ ) ,
323+ congestion_bucket : BucketResources :: new (
324+ congestion_bucket_slots_allocated,
325+ congestion_bucket_liquidity_allocated,
326+ ) ,
327+ last_congestion_misuse : new_hash_map ( ) ,
328+ protected_bucket : BucketResources :: new (
329+ protected_bucket_slots_allocated,
330+ protected_bucket_liquidity_allocated,
331+ ) ,
332+ } )
333+ }
334+
335+ fn general_available < ES : EntropySource > (
336+ & mut self , incoming_amount_msat : u64 , outgoing_channel_id : u64 , entropy_source : & ES ,
337+ ) -> Result < bool , ( ) > {
338+ Ok ( self . general_bucket . can_add_htlc (
339+ outgoing_channel_id,
340+ incoming_amount_msat,
341+ entropy_source,
342+ ) ?)
343+ }
344+
345+ fn congestion_eligible (
346+ & mut self , pending_htlcs_in_congestion : bool , incoming_amount_msat : u64 ,
347+ outgoing_channel_id : u64 , at_timestamp : u64 ,
348+ ) -> Result < bool , ( ) > {
349+ Ok ( !pending_htlcs_in_congestion
350+ && self . can_add_htlc_congestion (
351+ outgoing_channel_id,
352+ incoming_amount_msat,
353+ at_timestamp,
354+ ) ?)
355+ }
356+
357+ fn misused_congestion ( & mut self , channel_id : u64 , misuse_timestamp : u64 ) {
358+ self . last_congestion_misuse . insert ( channel_id, misuse_timestamp) ;
359+ }
360+
361+ // Returns whether the outgoing channel has misused the congestion bucket in the last two
362+ // weeks.
363+ fn has_misused_congestion (
364+ & mut self , outgoing_scid : u64 , at_timestamp : u64 ,
365+ ) -> Result < bool , ( ) > {
366+ match self . last_congestion_misuse . entry ( outgoing_scid) {
367+ Entry :: Vacant ( _) => Ok ( false ) ,
368+ Entry :: Occupied ( last_misuse) => {
369+ // If the last misuse of the congestion bucket was over more than the
370+ // revenue window, remote the entry.
371+ if at_timestamp < * last_misuse. get ( ) {
372+ return Err ( ( ) ) ;
373+ }
374+ const TWO_WEEKS : u64 = 2016 * 10 * 60 ;
375+ let since_last_misuse = at_timestamp - last_misuse. get ( ) ;
376+ if since_last_misuse < TWO_WEEKS {
377+ return Ok ( true ) ;
378+ } else {
379+ last_misuse. remove ( ) ;
380+ return Ok ( false ) ;
381+ }
382+ } ,
383+ }
384+ }
385+
386+ fn can_add_htlc_congestion (
387+ & mut self , channel_id : u64 , htlc_amount_msat : u64 , at_timestamp : u64 ,
388+ ) -> Result < bool , ( ) > {
389+ let congestion_resources_available =
390+ self . congestion_bucket . resources_available ( htlc_amount_msat) ;
391+ let misused_congestion = self . has_misused_congestion ( channel_id, at_timestamp) ?;
392+
393+ let below_liquidity_limit = htlc_amount_msat
394+ <= self . congestion_bucket . liquidity_allocated
395+ / self . congestion_bucket . slots_allocated as u64 ;
396+
397+ Ok ( congestion_resources_available && !misused_congestion && below_liquidity_limit)
398+ }
399+
400+ fn sufficient_reputation (
401+ & mut self , in_flight_htlc_risk : u64 , outgoing_reputation : i64 ,
402+ outgoing_in_flight_risk : u64 , at_timestamp : u64 ,
403+ ) -> Result < bool , ( ) > {
404+ let incoming_revenue_threshold = self . incoming_revenue . value_at_timestamp ( at_timestamp) ?;
405+
406+ Ok ( outgoing_reputation
407+ . saturating_sub ( i64:: try_from ( outgoing_in_flight_risk) . unwrap_or ( i64:: MAX ) )
408+ . saturating_sub ( i64:: try_from ( in_flight_htlc_risk) . unwrap_or ( i64:: MAX ) )
409+ >= incoming_revenue_threshold)
410+ }
411+ }
412+
235413/// A weighted average that decays over a specified window.
236414///
237415/// It enables tracking of historical behavior without storing individual data points.
@@ -326,8 +504,11 @@ mod tests {
326504
327505 use crate :: {
328506 crypto:: chacha20:: ChaCha20 ,
329- ln:: resource_manager:: {
330- AggregatedWindowAverage , BucketResources , DecayingAverage , GeneralBucket ,
507+ ln:: {
508+ channel:: TOTAL_BITCOIN_SUPPLY_SATOSHIS ,
509+ resource_manager:: {
510+ AggregatedWindowAverage , BucketResources , Channel , DecayingAverage , GeneralBucket ,
511+ } ,
331512 } ,
332513 util:: test_utils:: TestKeysInterface ,
333514 } ;
@@ -543,6 +724,34 @@ mod tests {
543724 assert_eq ! ( bucket_resources. liquidity_used, 0 ) ;
544725 }
545726
727+ #[ test]
728+ fn test_invalid_channel_configs ( ) {
729+ // (max_inflight, max_accepted_htlcs, general_pct, congestion_pct, protected_pct)
730+ let cases: Vec < ( u64 , u16 , u8 , u8 ) > = vec ! [
731+ // Invalid max_accepted_htlcs (> 483)
732+ ( 100_000 , 500 , 40 , 20 ) ,
733+ // Invalid max_htlc_value_in_flight_msat (>= total bitcoin supply)
734+ ( TOTAL_BITCOIN_SUPPLY_SATOSHIS * 1000 + 1 , 483 , 40 , 20 ) ,
735+ // Invalid bucket percentages
736+ ( 100_000 , 483 , 70 , 50 ) ,
737+ ] ;
738+
739+ for ( max_inflight, max_htlcs, general_pct, congestion_pct) in cases {
740+ assert ! ( Channel :: new(
741+ 0 ,
742+ max_inflight,
743+ max_htlcs,
744+ general_pct,
745+ congestion_pct,
746+ WINDOW ,
747+ 12 ,
748+ 2 ,
749+ 0 ,
750+ )
751+ . is_err( ) ) ;
752+ }
753+ }
754+
546755 #[ test]
547756 fn test_decaying_average_error ( ) {
548757 let timestamp = 1000 ;
0 commit comments