Skip to content

Filter route hints when creating invoices #1325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

ViktorTigerstrom
Copy link
Contributor

@ViktorTigerstrom ViktorTigerstrom commented Feb 21, 2022

Filter route hints when creating invoices as described in issue #1279 .

Just a few things worth mentioning:

For create_phantom_invoice I chose not to implement filtering if any of the channels are private, as from my understanding of the phantom node/invoice concept, that would make no sense. If my understanding is incorrect, please let me know.

The test code can be cleaned up a bit, by removing some recurring code. I wanted to push out the filtering code before taking care of that though, to find out if there's anything I need to improve with the filtering. Is there some macro to match channels with the invoice route hints, or would it make sense for me to make one?

Something worth mentioning is that as noted in the issue comment I made, this filtering logic when we filter out all channels that don't exceed or match the invoice amount, makes it easier for an external entity to use the invoice creation to potentially determine the inbound capacity of the channel. This can be seen in the test_hints_has_no_channels_with_lower_inbound_capacity_than_invoice_amt test. If this is not acceptable, I'd suggest that we'd remove this specific filtering and always return the highest capacity channel regardless of it's capacity.

Other than that, I'd more than happy to address any feedback!

@ViktorTigerstrom
Copy link
Contributor Author

Oh sorry, some checks fails. I'll update asap

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch from 4e4ca11 to aa3478b Compare February 21, 2022 22:05
@codecov-commenter
Copy link

codecov-commenter commented Feb 21, 2022

Codecov Report

Merging #1325 (0e1ccd8) into main (ca163c3) will increase coverage by 0.01%.
The diff coverage is 98.87%.

❗ Current head 0e1ccd8 differs from pull request most recent head e13f318. Consider uploading reports for the commit e13f318 to get more accurate results

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1325      +/-   ##
==========================================
+ Coverage   90.65%   90.67%   +0.01%     
==========================================
  Files          73       72       -1     
  Lines       40462    40464       +2     
==========================================
+ Hits        36682    36691       +9     
+ Misses       3780     3773       -7     
Impacted Files Coverage Δ
lightning/src/ln/functional_test_utils.rs 95.10% <88.46%> (-0.45%) ⬇️
lightning-invoice/src/utils.rs 97.14% <99.52%> (+6.61%) ⬆️
lightning/src/ln/functional_tests.rs 97.02% <100.00%> (-0.12%) ⬇️
lightning-block-sync/src/init.rs 93.56% <0.00%> (-2.25%) ⬇️
lightning/src/routing/router.rs 92.10% <0.00%> (-0.30%) ⬇️
lightning/src/ln/channelmanager.rs 84.60% <0.00%> (-0.18%) ⬇️
lightning/src/routing/scoring.rs 94.16% <0.00%> (-0.14%) ⬇️
lightning-background-processor/src/lib.rs 93.04% <0.00%> (-0.06%) ⬇️
... and 13 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ca163c3...e13f318. Read the comment docs.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Hmm, it'd be really nice to DRY this up somewhat, either via some method that accepts a &[ChannelDetails] and returns an iterator over ChannelDetails's to use, or a macro if you can't figure out the typing on that.

@@ -62,9 +62,17 @@ pub fn create_phantom_invoice<Signer: Sign, K: Deref>(
if let Some(amt) = amt_msat {
invoice = invoice.amount_milli_satoshis(amt);
}
let mut route_hints: HashMap<PublicKey, (u64, RouteHint)> = HashMap::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

For phantoms, this should be per-"real"-node, not across the full outer loop (this will make it more similar to the non-phantom case as well).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks!

@ViktorTigerstrom
Copy link
Contributor Author

Hmm, it'd be really nice to DRY this up somewhat, either via some method that accepts a &[ChannelDetails] and returns an iterator over ChannelDetails's to use, or a macro if you can't figure out the typing on that.

Sure, I'll DRY that and the tests up!

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch from aa3478b to b929554 Compare February 23, 2022 23:01
@ViktorTigerstrom
Copy link
Contributor Author

Just pushed some DRY changes! Hope the trade-offs I had to make in regards to code readability are ok :)

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch 2 times, most recently from 945816a to 7585bcf Compare February 23, 2022 23:08
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Cleanups look quite nice. A few comments that may enable further cleanups.

fn filter_channels<F1, F2>(
channels: Vec<ChannelDetails>,
min_inbound_capacity_required: Option<u64>,
empty_result_if_private_channels_exists: bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, I'm not sure if this captures what we want exactly, if our channels are public and we're creating a phantom route hint, instead of including all the channels, we can instead just create route hints that are one hop instead of two. id instead of creating the route hint in a callback, we can instead have the create_phantom_invoice callsite push a second hop onto all the route hints we get (or, if we get none, create a single route hint that is just one hop long). Then we could remove this option, I believe.

Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Feb 24, 2022

Choose a reason for hiding this comment

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

Thanks! If the filtering logic explained below is correct, we can indeed remove it.

channels: Vec<ChannelDetails>,
min_inbound_capacity_required: Option<u64>,
empty_result_if_private_channels_exists: bool,
channel_pubkey_selector: F1,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given this always returns a constant (ie cause we call filter_channels inside the per-node loop in create_phantom_invoice), do we need this at all?

Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Feb 24, 2022

Choose a reason for hiding this comment

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

In the create_invoice_from_channelmanager it actually does vary, given that the channel counterparty node is different. However if the filtering logic explained below is correct, it will always be the channel counterparty node id we are interested in, and this parameter wouldn't be needed at all!
That would make the code a lot cleaner :)

@ViktorTigerstrom
Copy link
Contributor Author

ViktorTigerstrom commented Feb 24, 2022

@TheBlueMatt Thanks a lot for the feedback!
Hmmmm, I've realized that I probably had an incorrect understanding of how the phantom invoice hints are supposed to be filtered. Sorry about that! Therefore I just to ensure that my current understanding is correct so I don't end up repeating my mistakes. Just to clarify, imagine the following scenario:

We have two nodes that are our phantom payment-receiving nodes that share same cross_node_seed:

  • node1
  • node2

Our nodes have channels connected to two counterparty nodes:

  • cp_node3
  • cp_node4

node1 have the following channels with inbound capacity amount:

  • channel_1 for 100k sat:
    node1 -> cp_node3

  • channel_2 for 200k sat:
    node1 -> cp_node3

node2 have the following channels with inbound capacity amount:

  • channel_3 for 100k sat:
    node2 -> cp_node3

  • channel_4 for 50k sat:
    node2 -> cp_node4

After the filtering is done we want the invoice to include the following channels in the hints (plus the RouteHintHop with src_node_id: hint.real_node_pubkey and short_channel_id: hint.phantom_scid):

  • channel_2
  • channel_3
  • channel_4

In other words, the highest inbound capacity channel for every counterparty node for each of our participating phantom payment-receiving nodes.

I previously thought we wanted to only include one channel per phantom payment-receiving node, which would have resulted in channel_2 and channel_3 (which is what the currently implemented code does).

  1. Is this new filtering logic explained above correct?

  2. Given the latest feedback, do I understand you correctly that if a phantom payment-receiving node has any private channel, we would like to just push a single RouteHintHop to the invoice for that node with the src_node_id: hint.real_node_pubkey and the short_channel_id: hint.phantom_scid, and let the sender find the path to the hint.real_node_pubkey phantom payment-receiving node by looking at the public channels? Thinking about it, that would make a lot of sense.

Ps. if I'm completely incorrect about how the filtering for phantom invoices are supposed to work, please let me know :).

@TheBlueMatt
Copy link
Collaborator

In other words, the highest inbound capacity channel for every counterparty node for each of our participating phantom payment-receiving nodes.

Correct.

Given the latest feedback, do I understand you correctly that if a phantom payment-receiving node has any private channel, we would like to just push a single RouteHintHop to the invoice for that node with the src_node_id: hint.real_node_pubkey and the short_channel_id: hint.phantom_scid, and let the sender find the path to the hint.real_node_pubkey phantom payment-receiving node by looking at the public channels?

Correct.

@ViktorTigerstrom
Copy link
Contributor Author

Ok great, thanks! I'll cleanup the code some more then, add support for the feedback and add a few more test cases to cover those scenarios.

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch from 7585bcf to 0712091 Compare February 28, 2022 21:18
@ViktorTigerstrom
Copy link
Contributor Author

Just push some changes! If it's not suitable that commit a094d7d and commit e29c401 are part of this PR, let me know and I'll drop them and create a new PR with them and base this upon that PR.

@jkczyz jkczyz self-requested a review March 2, 2022 07:49
@@ -7586,33 +7586,7 @@ fn test_priv_forwarding_rejection() {

let chan_id_1 = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 500_000_000, InitFeatures::known(), InitFeatures::known()).2;

// Note that the create_*_chan functions in utils requires announcement_signatures, which we do
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like you somehow ended up with some upstream changes here - can you rebase on upstream/main?

Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Mar 2, 2022

Choose a reason for hiding this comment

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

That specific line/section is actually intentionally changed by me, to use the new create_private_chan_between_nodes_with_value utility function I created in lightning/src/ln/functional_test_utils.rs in the functional_tests as well. Hence my comment above if it's suitable to include that commit e29c401 (now cc5b7cb) in this PR or if I should drop it :)

But I'll make sure to fetch the latest upstream and rebase this on that!

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch 2 times, most recently from fd43e2d to 8bac21f Compare March 2, 2022 19:15
Comment on lines 755 to 758
if nodes[a].node.get_current_default_configuration().channel_options.announced_channel {
panic!("`Nodes[a]` must have channel_options.announced_channel set to false")
}
nodes[a].node.create_channel(nodes[b].node.get_our_node_id(), channel_value, push_msat, 42, None).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

The last parameter to create_channel can be used to override the configuration values, I believe. So I think this function can simply override channel_options.announced_channel and re-use the other helpers instead of writing everything below this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Ok, I'll look into which other helpers I can use that doesn't require announcement signatures.

Comment on lines 225 to 235
RouteHint(vec![RouteHintHop {
src_node_id: channel.counterparty.node_id,
short_channel_id,
fees: RoutingFees {
base_msat: forwarding_info.fee_base_msat,
proportional_millionths: forwarding_info.fee_proportional_millionths,
},
cltv_expiry_delta: forwarding_info.cltv_expiry_delta,
htlc_minimum_msat: None,
htlc_maximum_msat: None,}])
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update to use tabs rather than spaces.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh my bad!

@@ -218,6 +198,85 @@ where
}
}

/// Utility to filter channels for an invoice, and return the corresponding route hints to include
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: No need to say "Utility to". Simply say "Filters channels ... and returns ... ".

/// Utility to filter channels for an invoice, and return the corresponding route hints to include
/// in the invoice.
///
/// The filtering ensures that the RouteHints returned only include the highest inbound capacity
Copy link
Contributor

Choose a reason for hiding this comment

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

Use ticks on identifiers: RouteHints. Likewise throughout.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do!

/// in the invoice.
///
/// The filtering ensures that the RouteHints returned only include the highest inbound capacity
/// channel per counterparty node for which channels are present. Channels with a lower inbound
Copy link
Contributor

Choose a reason for hiding this comment

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

Drop "for which channels are present" since the previous part of the sentence already establishes a channel exists.

let has_private_channels = true;
return (vec![], has_private_channels);
}
let short_channel_id = match channel.clone().short_channel_id {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't need a clone for a primitive type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I'll make sure to do the referencing and cloning in a more suitable way.

Some(id) => id,
None => continue,
};
let forwarding_info = match channel.clone().counterparty.forwarding_info {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use references rather than cloning:

let forwarding_info = match &channel.counterparty.forwarding_info {

And then have the closure take references.

}
entry.insert((
channel.inbound_capacity_msat,
route_hint_from_channel(channel, short_channel_id, forwarding_info),
Copy link
Contributor

Choose a reason for hiding this comment

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

Pass channel by reference.

Comment on lines 273 to 275
let route_hints = filtered_channels.iter()
.map(|(_channel, (_max_capacity, route_hint))| route_hint.clone())
.collect::<Vec<RouteHint>>();
Copy link
Contributor

Choose a reason for hiding this comment

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

You can avoid the clone by using into_iter instead of iter.

// If any of the channels are private, return no hints and let the sender look at the
// public channels instead.
let has_private_channels = true;
return (vec![], has_private_channels);
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than returning a bool, can the caller simply check if the returned Vec is empty?

Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Mar 2, 2022

Choose a reason for hiding this comment

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

That depends on if we would like the invoice to include a single hop hint for phantom payment-receiving nodes which only have public channels with inbound capacities below the invoice amt and therefore are filtered out.

Currently the code will not include hints to such nodes. But If we're ok with including them even though there are no channels that can be used to route to such nodes (at least at the point of the invoice creation), that would certainly work!

That would definitely make the code cleaner :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, that's an excellent point. I'd imagine we could have this method return Result<Vec<RouteHint, ()>> (or possible using a custom error type instead of ()) and let the caller determine what to do if no channels have sufficient inbound capacity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True! Ok I'll make the function return a Result type instead.

@ViktorTigerstrom
Copy link
Contributor Author

ViktorTigerstrom commented Mar 2, 2022

Thank a lot for the feedback @jkczyz! I'll make sure to update the PR asap.

If possible, it would be great if I could get a clarification on:

  • If we should include route hints for a node with only private channels
  • If we want the invoice to include a single hop route hint for a phantom payment-receiving node which only has channels with inbound capacities lower than the invoice amt.

@TheBlueMatt
Copy link
Collaborator

If we should include route hints for a node with only private channels

Yes, that's the primary purpose of route hints :)

If we want the invoice to include a single hop route hint for a phantom payment-receiving node which only has channels with inbound capacities lower than the invoice amt.

Yea, good question, at the risk of complexifying the PR, I think if all channels have less than the payment amount, we should just include all channels the same as we would if all channels had sufficient balance (still ignoring private channels if we have public ones).

@ViktorTigerstrom
Copy link
Contributor Author

Great, thanks for the clarifications!

Yea, good question, at the risk of complexifying the PR, I think if all channels have less than the payment amount, we should just include all channels the same as we would if all channels had sufficient balance (still ignoring private channels if we have public ones).

Ok, I'll add that to the filtering function and also change the output to only return an empty Vec if we have a mix between public and private channels.

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch from 8bac21f to 0e1ccd8 Compare March 6, 2022 23:24
@ViktorTigerstrom
Copy link
Contributor Author

ViktorTigerstrom commented Mar 6, 2022

Updated the filtering logic as per the above feedback.

Note that I pushed the changes in separate commits to easier visualize to see the changes. Therefore the intermediate commits won't pass the tests/checks currently. But I'll squash them with the corresponding commit if they look good to fix that!

Please note that a phantom invoice will now include route hints for phantom payment-receiving node(s) that have NO channels with higher inbound capacities than the invoice amt, even if another phantom payment-receiving node(s) exists that has such channels.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Looks basically good, I think.

let mut filtered_channels: HashMap<PublicKey, (u64, RouteHint)> = HashMap::new();
let min_inbound_capacity = min_inbound_capacity_msat.unwrap_or(0);

let mut is_public_values: HashSet<bool> = HashSet::new();
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 never any reason to have a HashSet<bool>, the table is on the heap and the pointers and seeds are substantially larger than just storing two bools on the stack here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok thanks, I'll replace it with bools instead. It was an attempt to remove some lines of code, but given your feedback I agree that thats just silly :)

}
}
}
if is_public_values.contains(&false) && is_public_values.contains(&true) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we actually care about if there are private channels or not here - if there are any public channels, we don't need to return any channels at all.

Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Mar 7, 2022

Choose a reason for hiding this comment

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

Oh, I must have totally misunderstood in which scenario we want to filter the hints for the invoice then, I'm sorry about that!

Just to clarify based on this feedback:
For a node with only public channels (not a mix of public and private channels), we don't want to include any route hints in the invoice then? Even if that node has multiple channels to the same counterparty node, which otherwise would make the hints filtering signal which of those channels is the highest inbound capacity one/has enough capacity for the invoice amt.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we have public channels, then everyone can see the channel values because we've announced which UTXOs they're associated with, so there's no need to hint anything in the invoices.

Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Mar 7, 2022

Choose a reason for hiding this comment

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

Thanks for clarifying! I'm still learning about lightning, so sorry if I'm a bit confused about when hints should be included.

I'll update the filtering so that no hints are returned if any channel is public, and I'll also need to update a bunch of the tests to account for that.

Copy link
Contributor

Choose a reason for hiding this comment

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

@TheBlueMatt Isn't ChannelDetails:: inbound_capacity_msat actually the channel liquidity balance, which is not publicly known?

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, and I'm not sure how it got linked here, but because of #1278 - our counterparty will generally not actually care which SCID is present, they'll use whatever has enough balance. Its kinda nice to give them the "best" one, but its really not all that important. Only LDK hasn't done #1278, as far as I understand.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering, and I think this is what @jkczyz's question is referring to as well: Why doesn't it make sense to include route hints when our node only has public channels, if we're using non public info (the inbound capacity) to generate the hints?

Yeah, that was my thinking.

No, and I'm not sure how it got linked here, but because of #1278 - our counterparty will generally not actually care which SCID is present, they'll use whatever has enough balance. Its kinda nice to give them the "best" one, but its really not all that important. Only LDK hasn't done #1278, as far as I understand.

Hmm... can't these channels be with different counterparties?

Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Mar 9, 2022

Choose a reason for hiding this comment

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

Hmm... can't these channels be with different counterparties?

Yes, that's correct!

If a node has 2 public channels with 2 different counterparty nodes, and one of the channels inbound capacity is below the invoice amt, the current code will only include the channel to the other counterparty which has higher inbound capacity than the invoice amount in the hints.
The test test_channels_with_lower_inbound_capacity_than_invoice_amt_hints_filtering, shows that behavior in action :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why doesn't it make sense to include route hints when our node only has public channels, if we're using non public info (the inbound capacity) to generate the hints?

Ah, I see your point. Yea, I mean that's not entirely unreasonable, but its also not crazy to just have them retry if they try to use a channel that doesn't have enough capacity. My other concern here is that if a node has some public channels there's a good chance they're a routing node and they have lots of public channels. One goal is to cut down the number of channels listed in the invoice, which can prevent QR code usage, and its easier to just include nothing if they're a routing node than it is to try to have some channel count limit behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, that makes sense! Ok I'll update the PR asap

@ViktorTigerstrom
Copy link
Contributor Author

Updated the code based on latest feedback, which makes the invoice filtering include no hints for a node if it has any public channels.

If it looks good, I'll squash the commits and also rebase on upstream as I see that there are now conflicts with upstream.

htlc_maximum_msat: None,}])
};
for channel in channels {
if channel.is_public {
Copy link
Contributor Author

@ViktorTigerstrom ViktorTigerstrom Mar 10, 2022

Choose a reason for hiding this comment

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

Please check that it's correct to place the public channel filtering here, before checking if any channel.counterparty.forwarding_info exists.

The reason why I ask this is that channel.counterparty.forwarding_info will only be assigned after the first node.handle_channel_update with the ChannelUpdate for the channel has been executed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, yea, I mean wont matter much, but might as well move this check to after the forwarding_info check/continue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok great thanks! Fixed this in the latest pushed version

@TheBlueMatt
Copy link
Collaborator

This needs rebase, sadly.

@TheBlueMatt
Copy link
Collaborator

The bulk of the code LGTM, though, haven't reviewed the tests in detail.

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch from a5ad4aa to e13f318 Compare March 13, 2022 23:18
@ViktorTigerstrom
Copy link
Contributor Author

Rebased on upstream, and therefore also added scid alias support.

In the latest upstream, I also found that a util to create unannounced channels had been added. I therefore dropped my version, and used the upstream one for this PR.

Let me know if there's anything more that needs to be fixed :)

TheBlueMatt
TheBlueMatt previously approved these changes Mar 15, 2022
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

These tests are great, thanks! Two trivial comments, LGTM!

let accept_channel = get_event_msg!(nodes[0], MessageSendEvent::SendAcceptChannel, nodes[2].node.get_our_node_id());
nodes[2].node.handle_accept_channel(&nodes[0].node.get_our_node_id(), InitFeatures::known(), &accept_channel);

let (temporary_channel_id, tx, _) = create_funding_transaction(&nodes[2], 1_000_000, 42);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: this hunk and the next hunk could be replaced with sign_funding_transaction.


#[test]
fn test_from_channelmanager() {
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let _chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known());
let mut private_chan_cfg = UserConfig::default();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like this is now unused.

@ViktorTigerstrom
Copy link
Contributor Author

Thanks! Fixed those comments :)

TheBlueMatt
TheBlueMatt previously approved these changes Mar 15, 2022
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Yes, looks much simpler and shaping up!

@@ -10,7 +10,7 @@ use lightning::chain;
use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
use lightning::chain::keysinterface::{Recipient, KeysInterface, Sign};
use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, MIN_FINAL_CLTV_EXPIRY};
use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, CounterpartyForwardingInfo, MIN_FINAL_CLTV_EXPIRY};
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: could you move CounterpartyForwardingInfo immediately after ChannelManager?

Comment on lines 164 to 167
let route_hints = filter_channels(
channelmanager.list_usable_channels(),
amt_msat,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Though not entirely consistent, we try to wrap at 100 characters, so this should fit on one line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok thanks for clarifying, will keep that in mind from now on

Comment on lines 208 to 211
/// channel per counterparty node. If any channel with a higher inbound capacity than the given
/// `min_inbound_capacity_msat` exists, channels to other counterparty nodes with a lower inbound
/// capacity than `min_inbound_capacity_msat` will be filtered out, even if they are the highest
/// inbound capacity channel for that specific counterparty node.
Copy link
Contributor

Choose a reason for hiding this comment

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

The second sentence here is a bit verbose. IIUC, I think the same thing could be conveyed by extending the previous sentence to say "... with at least min_inbound_capacity_msat balance or the highest for each counterparty if min_inbound_capacity_msat is not met". I don't think that last part is mentioned until later in the docs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, for this comment and the one that follows, using the list from the commit message would suffice. I thought that was pretty well written, FWIW.

}
entry.insert((
channel.inbound_capacity_msat,
route_hint_from_channel(&channel, short_channel_id, forwarding_info),
Copy link
Contributor

@jkczyz jkczyz Mar 15, 2022

Choose a reason for hiding this comment

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

I think we can get by having the map contain &ChannelDetails rather than (u64, RouteHint) and only creating the RouteHint in the map call below. Then you won't need the earlier lambda or create RouteHints if they are never used (e.g., if another channel has a larger capacity or if all channels are public).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do!

Comment on lines 215 to 221
/// Input:
/// `channels`: The channels to filter.
/// `min_inbound_capacity_msat`: Defines the lowest inbound capacity channels must have to not
/// be filtered out, if any other channel above that amount exists.
///
/// Result:
/// `Vec<RouteHint>`: The filtered `RouteHints`, which will be empty if any public channel exists.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd recommend removing this especially if it's repeating what's in the previous paragraphs in a slightly different way. Prefer to say it just once in a succinct way without loss of meaning.

}])
);
let mut route_hints = filter_channels(
hint.channels.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the function takes hints by value, you can avoid this clone by using the de-structuring syntax in the for loop:

for PhantomRouteHints { channels, phantom_scid, real_node_pubkey } in phantom_route_hints {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah that's neat! Thanks for letting me know

for hint in hints {
let hint_short_chan_id = (hint.0).0[0].short_channel_id;
assert!(chan_ids_to_match.contains(&hint_short_chan_id));
chan_ids_to_match.remove(&hint_short_chan_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you want to check that chan_ids_to_match is empty after iterating through the hints? This could be more useful than the earlier length check because you could include in the assertion failure which channels had been missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True!

invoice_amt: Option<u64>,
invoice_node: &Node<'a, 'b, 'c>,
mut chan_ids_to_match: HashSet<u64>
){
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: space between: ) {

Comment on lines 638 to 639
assert!(chan_ids_to_match.contains(&hint_short_chan_id));
chan_ids_to_match.remove(&hint_short_chan_id);
Copy link
Contributor

@jkczyz jkczyz Mar 15, 2022

Choose a reason for hiding this comment

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

Could combine these by checking the result of remove instead:

assert!(chan_ids_to_match.remove(&hint_short_chan_id))

Comment on lines 1070 to 1071
assert!(chan_ids_to_match.contains(&hint_short_chan_id));
chan_ids_to_match.remove(&hint_short_chan_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly here.

@ViktorTigerstrom
Copy link
Contributor Author

Thanks a lot for the feedback! Will try to address it shortly :)

@ViktorTigerstrom
Copy link
Contributor Author

Pushed changes that addresses the feedback. I pushed the cleanup of the filtering function in a separate commit that I'll squash if it looks good.

/// * If any public channel exists, the returned `RouteHint`s will be empty, and the sender will
/// need to find the path by looking at the public channels instead
fn filter_channels(channels: Vec<ChannelDetails>, min_inbound_capacity_msat: Option<u64>) -> Vec<RouteHint>{
let mut filtered_channels: HashMap<PublicKey, ChannelDetails> = HashMap::new();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't able to make this take a borrowed &ChannelDetails, as the borrow doesn't live outside of the channel loop context. Is that ok?

Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of borrowing channel directly within the loop, you can change the loop to iterate over references using:

for channel in channels.iter() {

Otherwise, into_iter is implicitly used, which consumes channels and yields values instead of references.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh nice, thanks for clarifying!

let mut min_capacity_channel_exists = false;

for channel in channels {
if channel.get_inbound_payment_scid().is_none() || channel.counterparty.forwarding_info.is_none() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made this over 100 characters intentionally as I thought it looked cleaner than the alternative.

Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Looking good. Thanks for bearing with me. Just some minor improvements left.

/// * If any public channel exists, the returned `RouteHint`s will be empty, and the sender will
/// need to find the path by looking at the public channels instead
fn filter_channels(channels: Vec<ChannelDetails>, min_inbound_capacity_msat: Option<u64>) -> Vec<RouteHint>{
let mut filtered_channels: HashMap<PublicKey, ChannelDetails> = HashMap::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of borrowing channel directly within the loop, you can change the loop to iterate over references using:

for channel in channels.iter() {

Otherwise, into_iter is implicitly used, which consumes channels and yields values instead of references.

let hint_short_chan_id = (hint.0).0[0].short_channel_id;
assert!(chan_ids_to_match.remove(&hint_short_chan_id));
}
assert!(chan_ids_to_match.is_empty());
Copy link
Contributor

Choose a reason for hiding this comment

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

Could add a message so that unmatched ids are printed, something like this:

 assert!(chan_ids_to_match.is_empty(), "Unmatched short channel ids: {:?}", chan_ids_to_match);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah snap, missed that one.. thanks!

_ => panic!("Incorrect hint length generated")
}
}
assert!(chan_ids_to_match.is_empty());
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise here.

Filter the route hints in `create_invoice_from_channelmanager` based on
the following criteria:

* Only one channel per counterparty node
* Always select the channel with the highest inbound capacity
* Filter out channels with a lower inbound capacity than the invoice
amount, if any channel exists with enough capacity to cover the invoice
amount
* If any public channel exists, the invoice route_hints should be empty,
and the sender will need to find the path to the payment-receiving node
by looking at the public channels instead
Filter the route hints in `create_phantom_invoice` based on the
following criteria:

* Only one channel for every counterparty node per phantom
payment-receiving node in the invoice
* Always select the channel with the highest inbound capacity
* For each payment-receiving node, filter out channels with a lower
inbound capacity than the invoice amount, if any channel exists with
enough capacity to cover the invoice amount
* If any public channels exists for a payment-receiving node, push a
single RouteHintHop with the phantom route and let the sender find the
path to the payment-receiving node through the public channels.
@ViktorTigerstrom ViktorTigerstrom force-pushed the 2022-02-filter-route-hints branch from 4632ca7 to ff792e0 Compare March 16, 2022 10:26
@ViktorTigerstrom
Copy link
Contributor Author

Fixed the latest feedback, and squashed the temporary commit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants