Skip to content

Commit 483b950

Browse files
committed
Add Channel to resource manager
The Channel struct introduced here has the core information that will be used by the resource manager to make forwarding decisions on HTLCs: - Reputation that this channel has accrued as an outgoing link in HTLC forwards. - Revenue (forwarding fees) that the channel has earned us as an incoming link. - Pending HTLCs this channel is currently holding as an outgoing link. - Bucket resources that are currently in use in general, congestion and protected.
1 parent e871165 commit 483b950

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

lightning/src/ln/resource_manager.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@ use crate::{
2020
/// The minimum number of slots required for the general bucket to function.
2121
const 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+
2343
struct 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

Comments
 (0)