From 6ab2f0c1653858b26c04ca396134de22d7d61e27 Mon Sep 17 00:00:00 2001 From: Peter Neuroth Date: Thu, 6 Mar 2025 13:02:54 +0100 Subject: [PATCH 1/2] gl-plugin: Add channel filter for trampoline pay We need to filter out channels that are drained and can not end out an htlc with an actual amount greater than 0. Signed-off-by: Peter Neuroth --- libs/gl-plugin/src/tramp.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/libs/gl-plugin/src/tramp.rs b/libs/gl-plugin/src/tramp.rs index 5a53279b3..5003bda2e 100644 --- a/libs/gl-plugin/src/tramp.rs +++ b/libs/gl-plugin/src/tramp.rs @@ -32,6 +32,8 @@ const PAY_UNPARSEABLE_ONION_MSG: &str = "Malformed error reply"; const PAY_UNPARSEABLE_ONION_CODE: i32 = 202; // How long do we wait for channels to re-establish? const AWAIT_CHANNELS_TIMEOUT_SEC: u64 = 20; +// Minimum amount we can send through a channel. +const MIN_HTLC_AMOUNT: u64 = 1; fn feature_guard(features: impl Into>, feature_bit: usize) -> Result<()> { let mut features = features.into(); @@ -215,9 +217,20 @@ pub async fn trampolinepay( return None; } }; + let min_htlc_out_msat = match ch.minimum_htlc_out_msat { + Some(m) => m.msat(), + None => { + warn!( + "Missing missing minimum_htlc_out_msat on channel with scid={}", + short_channel_id.to_string() + ); + return None; + } + }; return Some(ChannelData { short_channel_id, spendable_msat, + min_htlc_out_msat, }); }) .collect(); @@ -245,6 +258,16 @@ pub async fn trampolinepay( break; } + // Filter out channels that lack minimum funds and can not send an htlc. + if std::cmp::max(MIN_HTLC_AMOUNT, channel.min_htlc_out_msat) > channel.spendable_msat { + debug!("Skip channel {}: has spendable_msat={} and minimum_htlc_out_msat={} and can not send htlc.", + channel.short_channel_id, + channel.spendable_msat, + channel.min_htlc_out_msat, + ); + continue; + } + if (channel.spendable_msat + acc) <= amount_msat { choosen.push((channel.short_channel_id, channel.spendable_msat)); acc += channel.spendable_msat; @@ -463,6 +486,7 @@ async fn reestablished_channels( struct ChannelData { short_channel_id: cln_rpc::primitives::ShortChannelId, spendable_msat: u64, + min_htlc_out_msat: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] From 9477d42ad6b6ef4c86e419bbc6f4cd270a5a65e3 Mon Sep 17 00:00:00 2001 From: Peter Neuroth Date: Thu, 6 Mar 2025 13:33:58 +0100 Subject: [PATCH 2/2] gl-plugin: Add test for trampoline channel filter Signed-off-by: Peter Neuroth --- libs/gl-plugin/src/tramp.rs | 120 ++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 33 deletions(-) diff --git a/libs/gl-plugin/src/tramp.rs b/libs/gl-plugin/src/tramp.rs index 5003bda2e..60d49bb0c 100644 --- a/libs/gl-plugin/src/tramp.rs +++ b/libs/gl-plugin/src/tramp.rs @@ -244,45 +244,14 @@ pub async fn trampolinepay( // Await and filter out re-established channels. let deadline = Instant::now() + Duration::from_secs(AWAIT_CHANNELS_TIMEOUT_SEC); - let mut channels = + let channels = reestablished_channels(channels, node_id, rpc_path.as_ref().to_path_buf(), deadline) .await?; // Note: We can also do this inside the reestablished_channels function // but as we want to be greedy picking our channels we don't want to // introduce a race of the choosen channels for now. - let mut acc = 0; - let mut choosen = vec![]; - while let Some(channel) = channels.pop() { - if acc == amount_msat { - break; - } - - // Filter out channels that lack minimum funds and can not send an htlc. - if std::cmp::max(MIN_HTLC_AMOUNT, channel.min_htlc_out_msat) > channel.spendable_msat { - debug!("Skip channel {}: has spendable_msat={} and minimum_htlc_out_msat={} and can not send htlc.", - channel.short_channel_id, - channel.spendable_msat, - channel.min_htlc_out_msat, - ); - continue; - } - - if (channel.spendable_msat + acc) <= amount_msat { - choosen.push((channel.short_channel_id, channel.spendable_msat)); - acc += channel.spendable_msat; - } else { - let rest = amount_msat - acc; - choosen.push((channel.short_channel_id, rest)); - acc += rest; - break; - } - } - - // Check that we found enough spendables - if acc < amount_msat { - return Err(anyhow!("missing balance {}msat<{}msat", acc, amount_msat)); - } + let choosen = pick_channels(amount_msat, channels)?; // FIXME should not be neccessary as we already check on the amount. let parts = choosen.len(); @@ -370,6 +339,45 @@ pub async fn trampolinepay( } } +fn pick_channels( + amount_msat: u64, + mut channels: Vec, +) -> Result> { + let mut acc = 0; + let mut choosen = vec![]; + while let Some(channel) = channels.pop() { + if acc == amount_msat { + break; + } + + // Filter out channels that lack minimum funds and can not send an htlc. + if std::cmp::max(MIN_HTLC_AMOUNT, channel.min_htlc_out_msat) > channel.spendable_msat { + debug!("Skip channel {}: has spendable_msat={} and minimum_htlc_out_msat={} and can not send htlc.", + channel.short_channel_id, + channel.spendable_msat, + channel.min_htlc_out_msat, + ); + continue; + } + + if (channel.spendable_msat + acc) <= amount_msat { + choosen.push((channel.short_channel_id, channel.spendable_msat)); + acc += channel.spendable_msat; + } else { + let rest = amount_msat - acc; + choosen.push((channel.short_channel_id, rest)); + acc += rest; + break; + } + } + + if acc < amount_msat { + return Err(anyhow!("missing balance {}msat<{}msat", acc, amount_msat)); + } + + Ok(choosen) +} + async fn do_pay( rpc: &mut ClnRpc, node_id: PublicKey, @@ -512,3 +520,49 @@ pub struct SendpayRequest { pub payment_hash: cln_rpc::primitives::Sha256, pub route: Vec, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_picking_channels() { + let scid1 = ShortChannelId::from_str("100000x100x0").unwrap(); + let scid2 = ShortChannelId::from_str("100000x101x0").unwrap(); + let scid3 = ShortChannelId::from_str("100000x102x0").unwrap(); + let scid4 = ShortChannelId::from_str("100000x103x0").unwrap(); + let channels = vec![ + ChannelData { + short_channel_id: scid1, + spendable_msat: 100000, + min_htlc_out_msat: 0, + }, + // Below MIN_HTLC_AMOUNT. + ChannelData { + short_channel_id: scid2, + spendable_msat: 0, + min_htlc_out_msat: 0, + }, + // min_htlc_out_msat is larger than spendable_msat. + ChannelData { + short_channel_id: scid3, + spendable_msat: 1, + min_htlc_out_msat: 2, + }, + ChannelData { + short_channel_id: scid4, + spendable_msat: 55000, + min_htlc_out_msat: 55000, + }, + ]; + + let amount_msat = 150000; + let choosen = pick_channels(amount_msat, channels).unwrap(); + + assert_eq!(choosen.len(), 2); + assert_eq!(choosen[0].0, scid4); + assert_eq!(choosen[0].1, 55000); + assert_eq!(choosen[1].0, scid1); + assert_eq!(choosen[1].1, 95000); + } +}