Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 101 additions & 23 deletions libs/gl-plugin/src/tramp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>>, feature_bit: usize) -> Result<()> {
let mut features = features.into();
Expand Down Expand Up @@ -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();
Expand All @@ -231,35 +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;
}

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();
Expand Down Expand Up @@ -347,6 +339,45 @@ pub async fn trampolinepay(
}
}

fn pick_channels(
amount_msat: u64,
mut channels: Vec<ChannelData>,
) -> Result<Vec<(cln_rpc::primitives::ShortChannelId, u64)>> {
let mut acc = 0;
let mut choosen = vec![];
while let Some(channel) = channels.pop() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simpler, but let's keep this as a cleanup, merge this one so we address the issue asap:

for c in channels.iter().filter(|c| c.spendable_msat.unwrap_or_default() > MIN_HTLC_AMOUNT) {
...
}

And we can also add a second .filter() that filters out the ones that have spendable_msat below min_htlc_out_msat, no need to mix those conditions either.

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;
Comment on lines +367 to +369
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still a case where rest does not add up to the minimum htlc amount. Do you know a good way to handle that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we may want to check and skip those channels as well. 🤔
I'll add a follow up.

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,
Expand Down Expand Up @@ -463,6 +494,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)]
Expand All @@ -488,3 +520,49 @@ pub struct SendpayRequest {
pub payment_hash: cln_rpc::primitives::Sha256,
pub route: Vec<cln_rpc::model::requests::SendpayRoute>,
}

#[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);
}
}
Loading