Skip to content

Commit a99d724

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 0a6b6a8 commit a99d724

1 file changed

Lines changed: 211 additions & 2 deletions

File tree

lightning/src/ln/resource_manager.rs

Lines changed: 211 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ use core::time::Duration;
1313

1414
use 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+
2028
struct 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

Comments
 (0)