Skip to content
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

Implement Flat Features #428

Merged
merged 6 commits into from
Jan 13, 2020

Conversation

TheBlueMatt
Copy link
Collaborator

This merges local and global features into one struct, which is
parameterized by where it appers. The parameterization restricts
which queries can be made and which features can be set, in line
with the latest BOLT 9.

Closes #427.

@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch 2 times, most recently from 99f5126 to 862b82a Compare December 24, 2019 17:21
@TheBlueMatt
Copy link
Collaborator Author

Needs clarification on whether we can send to/through a node which has an even feature bit set on its node_announcement. See lightning/bolts#723.

@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch 8 times, most recently from 241cfa9 to cde784d Compare December 29, 2019 21:38
lightning/src/ln/msgs.rs Outdated Show resolved Hide resolved
mark: PhantomData<T>,
}

impl<T: FeatureContext> Clone for Features<T> {
Copy link

Choose a reason for hiding this comment

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

nit: you can use a derive macro on the generic struct Features + concrete struct ones to avoid implementing clone(),eq(), debug()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For some reason the compiler barfs at it - I assume derive macros somehow don't work for templated types.

(self.features.flags[i] & ((1 << (14 - 8)) - 1)).write(w)?;
}
}
self.features.write(w)
Copy link

Choose a reason for hiding this comment

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

Not sure if this respect the spec, encoding the whole logical features instead of two bitmaps ? Legacy global features bits are going to be encoded twice, which seems contrary to "SHOULD use the minimum length required to represent the features field."

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm. The way I read that is just that you SHOULDn't pad with 0 bytes, not that you should change the meaning of the data you send. I think the spec is pretty mum on the issue here - it doesn't say which bits you can set in globablfeatures, just that you cant send bits higher than 13. In practice, I think this should be fine - if there's some unknown bit the peer will do the right thing and ignore it if its odd or disconnect if its even.

@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch from cde784d to 925c73a Compare January 5, 2020 18:18
Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

Seems good to me, I still have a doubt on the right processing of required unknown bits channels by get_route but I don't think it's worse

if first_hops.is_none() || chan.two_to_one.src_node_id != network.our_node_id {
if chan.two_to_one.enabled {
add_entry!(chan_id, chan.one_to_two.src_node_id, chan.two_to_one, $fee_to_target_msat);
if !chan.features.requires_unknown_bits() {
Copy link

Choose a reason for hiding this comment

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

Are you filtering out well first_hop and last_hop channel ? Not sure..

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

last_hops no cause we call add_entry!() directly. first hops also no but because Features::relevant_init_flags_to_channel currently doesnt convert anything that is unknown (which should maybe be changed, but at least in this case it doesn't matter too much because we certainly wont stay connected if their Init message requires unknown bits).

lightning/src/ln/msgs.rs Outdated Show resolved Hide resolved
@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch from 925c73a to 72c384b Compare January 6, 2020 04:45
lightning/src/ln/msgs.rs Outdated Show resolved Hide resolved
pub struct FeatureContextInvoice {}

/// An internal trait capturing the various future context types
pub trait FeatureContext {}
Copy link
Contributor

Choose a reason for hiding this comment

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

This trait may not be needed given it has no methods and any methods on Features bounded by this trait could be implemented without the bound.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Gonna leave this one in as otherwise you can construct the Features objects with random types.

// Used to test encoding of diverse msgs
#[cfg(test)]
pub flags: Vec<u8>,
mark: PhantomData<T>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider naming this context and removing Features trait bound on T as per an earlier comment.

Comment on lines 137 to 150
pub(crate) fn new() -> Features<T> {
Features {
flags: vec![2 | 1 << 5],
mark: PhantomData,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The call sites of each new could be less verbose if these were module-level functions (e.g., new_init_features()). The return type could then be the exact specialization desired for the method.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The callsites are much nicer with the type aliases, so I think this is resolved.

lightning/src/ln/msgs.rs Outdated Show resolved Hide resolved
lightning/src/ln/msgs.rs Outdated Show resolved Hide resolved
@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch 3 times, most recently from fb28274 to bc7d64c Compare January 7, 2020 00:37
@TheBlueMatt
Copy link
Collaborator Author

TheBlueMatt commented Jan 7, 2020

I think this is correct as of the proposed changes in RFC PR 723, which, after the meeting today, I don't think should have much of an issue, so I'm happy to merge with ACK(s).

lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
impl FeatureContext for FeatureContextChannel {}
//impl FeatureContext for FeatureContextInvoice {}

pub trait DataLossProtect: FeatureContext {}
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the drawback of using the trait bound on Feature<T> is requiring the supertrait. I'm not overly concerned if you want to keep it as is though.

Comment on lines 98 to 95
let len = cmp::min(2, self.flags.len());
w.size_hint(len + 2);
(len as u16).write(w)?;
for i in (0..len).rev() {
if i == 0 {
self.flags[i].write(w)?;
} else {
(self.flags[i] & ((1 << (14 - 8)) - 1)).write(w)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you make some constants for the magic numbers in this method? As an uninformed reader, I don't know what all of these mean. :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I just left a comment instead cause its just 13 + 1 and a byte modifier, nothing that really makes sense to create a constant for.

lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch from bc7d64c to 6a0deb6 Compare January 7, 2020 18:02
@TheBlueMatt
Copy link
Collaborator Author

I think I fixed or responded to all your comments @jkczyz, though I was a bit lazy about stuffing the fixes into the last two commits instead of putting them in earlier.

@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch from 6a0deb6 to 90421e5 Compare January 7, 2020 18:06
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch from 90421e5 to b7d1ea1 Compare January 7, 2020 21:17
lightning/src/ln/features.rs Outdated Show resolved Hide resolved

/// Writes all features present up to, and including, 13.
pub(crate) fn write_up_to_13<W: Writer>(&self, w: &mut W) -> Result<(), ::std::io::Error> {
let len = cmp::min(2, self.flags.len());
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider replacing flags.len() with byte_count().

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm. At least personally I generally prefer direct access when in the struct anyway. It avoids "wait, does that local method really do nothing or does it mangle the value a bit" errors.

impl InitFeatures {
/// Create a Features with the features we support
#[cfg(not(feature = "fuzztarget"))]
pub(crate) fn our_features() -> InitFeatures {
Copy link
Contributor

Choose a reason for hiding this comment

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

I have preference to name these default since "our" implies that the result should be used within a certain context, even though no such restriction exists. It would also make the call sites more readable if the word "features" is not repeated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm. default is strange IMO because the "our" refers to "rust-lightning's set of supported". Agree "our" is maybe less clear, do you have another idea for a better way to say "the set of features supported by rust-lightning, which should be advertised to our peers".

Copy link
Contributor

@jkczyz jkczyz Jan 7, 2020

Choose a reason for hiding this comment

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

Oh, I thought you were using "our" for "our node" since elsewhere you are using "their" for peer features. :) Or maybe there is some overloading of the word.

I suggested "default" because features are similar to a config/options structs, and it's pretty typical to see code such as:

config.default()
      .set_option_foo()
      .set_option_bar();

as a way to set default options and then override some. Otherwise, maybe just call it new.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, given its just pub(crate), I dont think we need to worry too much about overriding some. new() is a bit too generic, too, cause its not clear that its intended specifically for the "this is what i want to send a peer" use-case. Maybe features_we_implement?

Copy link
Contributor

Choose a reason for hiding this comment

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

The suggestion of default was mainly to demonstrate this as a convention for specifying typical options. I see how new can be too generic especially given we have the more specific empty.

Using pronouns like we/our/my in identifiers is bit ambiguous for conveying information in my opinion. Don't get me going on identifiers like MyFoo. ;)

I would suggest either known or supported. That is, these features are either known or supported by the implementation. Using known also contrasts with uses of unknown_bits in the code. Optionally, adding either _bits or _features to the end may offer more clarity, though it would be inconsistent with empty unless also updated.

Copy link
Collaborator Author

@TheBlueMatt TheBlueMatt Jan 9, 2020

Choose a reason for hiding this comment

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

Gonna go with supported(), seems to capture what we want. Note that it is not guaranteed that all non-unknown fetures are supported().

lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
lightning/src/ln/features.rs Outdated Show resolved Hide resolved
Comment on lines +1466 to +1476
assert_eq!(route.hops[0].pubkey, node8);
assert_eq!(route.hops[0].short_channel_id, 42);
assert_eq!(route.hops[0].fee_msat, 200);
assert_eq!(route.hops[0].cltv_expiry_delta, (13 << 8) | 1);

assert_eq!(route.hops[1].pubkey, node3);
assert_eq!(route.hops[1].short_channel_id, 13);
assert_eq!(route.hops[1].fee_msat, 100);
assert_eq!(route.hops[1].cltv_expiry_delta, 42);
Copy link
Contributor

Choose a reason for hiding this comment

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

Are all these assertions necessary for checking the route?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For the most part, yes. Not only does it give us full test coverage for each field in the RouteHop entry (which I think is useful), but in some cases, eg where we're overriding the route map with local channel or last-hop data, it is used to test that the data came from the correct source (the override) and ignored the original.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that the override should be tested. However, tests should focus on testing individual behaviors. That is, testing the override behavior should not fail if, for example, the fee calculation behavior changes. This seems to be testing multiple behaviors.

I'm willing to concede, however, that the test as written may not be conducive for testing individual behaviors yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, sadly its super nontrivial to test exact behavior here since override somewhat by definition relies on fee calculation behavior. The fact that this could fail if we add fee randomization sucks, but removing chunks of it means much worse test coverage.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this test is a good candidate for refactoring into smaller unit tests but can be done in a different PR. I may be interested in tackling this once I read more on routing.

Comment on lines +1521 to +1528
{ // Re-enable nodes 1, 2, and 8
let mut network = router.network_map.write().unwrap();
network.nodes.get_mut(&node1).unwrap().features.clear_require_unknown_bits();
network.nodes.get_mut(&node2).unwrap().features.clear_require_unknown_bits();
network.nodes.get_mut(&node8).unwrap().features.clear_require_unknown_bits();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

route_test is tough to comprehend. It looks like in actuality it is many small tests with a common setup and possible dependencies on the preceding tests. Are there any plans to refactor this into smaller standalone tests? That would be more understandable and less fragile.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, right, maybe a better introductory comment would help. It is one big "create the route map" block followed by a bunch of "calculate routes through it and check them" blocks. We could split it up into a common fn create_routemap() -> Router {} function and a bunch of #[test]s that actually do the work, which is a pattern we use a bunch elsewhere, but lets do that in another PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

SGTM

@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch 2 times, most recently from 7df6b15 to 8eaa47e Compare January 8, 2020 00:22
@jkczyz jkczyz self-assigned this Jan 8, 2020
@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch from 8eaa47e to a682fb4 Compare January 9, 2020 21:56
Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

ACK a682fb4

if first_hops.is_none() || chan.one_to_two.src_node_id != network.our_node_id {
if chan.one_to_two.enabled {
add_entry!(chan_id, chan.two_to_one.src_node_id, chan.one_to_two, $fee_to_target_msat);
if !$node.features.requires_unknown_bits() {
Copy link

Choose a reason for hiding this comment

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

Sadly we're not going to route through a node which requires a local policy feature like data_loss_protect while this doesn't concern payment path at all. That's a spec wrongdoing

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.

81ac36c should be split into two commits since it contains both a bug fix and an unrelated refactor. Also, the message needs to be updated with supported instead of our_features.

Comment on lines +1466 to +1476
assert_eq!(route.hops[0].pubkey, node8);
assert_eq!(route.hops[0].short_channel_id, 42);
assert_eq!(route.hops[0].fee_msat, 200);
assert_eq!(route.hops[0].cltv_expiry_delta, (13 << 8) | 1);

assert_eq!(route.hops[1].pubkey, node3);
assert_eq!(route.hops[1].short_channel_id, 13);
assert_eq!(route.hops[1].fee_msat, 100);
assert_eq!(route.hops[1].cltv_expiry_delta, 42);
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that the override should be tested. However, tests should focus on testing individual behaviors. That is, testing the override behavior should not fail if, for example, the fee calculation behavior changes. This seems to be testing multiple behaviors.

I'm willing to concede, however, that the test as written may not be conducive for testing individual behaviors yet.

@@ -1441,6 +1439,98 @@ mod tests {
assert_eq!(route.hops[1].cltv_expiry_delta, 42);
}

{ // Disable 4 and 12 by requiring unknown feature bits
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be clearer if it said "channels 4 and 12" (likewise on line 1479). Otherwise, it wasn't readily apparent how the following tests differed from the ones that disabled nodes 1, 2, and 8.

let mut network = router.network_map.write().unwrap();
network.nodes.get_mut(&node1).unwrap().features.set_require_unknown_bits();
network.nodes.get_mut(&node2).unwrap().features.set_require_unknown_bits();
network.nodes.get_mut(&node8).unwrap().features.set_require_unknown_bits();
Copy link
Contributor

Choose a reason for hiding this comment

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

More of a question on correct behavior: If you have a channel to node 8 as an override (as you do below) but node 8 is disabled in the network map (as done here), shouldn't get_route fail?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In this case the override is via "first_hops" aka "the list of channels with peers we currently have a tcp connection with, according to our ChannelManager". ISTM that such channels should definitely be taken as the first hop, if possible, especially given if they have unknown features, we probably shouldn't be able to make a connection to them in the first place....that said, the spec is currently a bit awkward around init context. See lightning/bolts#726 but tl;dr: unknown required bits currently both mean "cannot connect to" and "cannot route through", which is...confusing.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. Thanks for the explanation and link to discussion!

This merges local and global features into one struct, which is
parameterized by where it appers. The parameterization restricts
which queries can be made and which features can be set, in line
with the latest BOLT 9.

Closes lightningdevkit#427.
This change was made in the flat features BOLT PR, as if a channel
requires some unknown feature bits we should still rumor it, we just
shouldn't route through it.
The spec is a bit mum on feature endianness, so I suppose it falls
under the "everything is big endian unless otherwise specified"
clause, but we were treating it as little.
The Features::new() method is nonsense and doesn't describe what
features were being set - we introduce an empty() and supported()
constructors instead.
@TheBlueMatt TheBlueMatt force-pushed the 2019-12-flat-features branch from a682fb4 to 49f88ec Compare January 13, 2020 18:53
@TheBlueMatt
Copy link
Collaborator Author

Oops, forgot to push with the split commit. I think all your points were addressed.

@jkczyz
Copy link
Contributor

jkczyz commented Jan 13, 2020

ACK 49f88ec

@TheBlueMatt TheBlueMatt merged commit a252f81 into lightningdevkit:master Jan 13, 2020
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.

Implement Flat Features
3 participants