-
Notifications
You must be signed in to change notification settings - Fork 373
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
Implement Flat Features #428
Conversation
99f5126
to
862b82a
Compare
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. |
241cfa9
to
cde784d
Compare
lightning/src/ln/msgs.rs
Outdated
mark: PhantomData<T>, | ||
} | ||
|
||
impl<T: FeatureContext> Clone for Features<T> { |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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."
There was a problem hiding this comment.
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.
cde784d
to
925c73a
Compare
There was a problem hiding this 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
lightning/src/ln/router.rs
Outdated
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() { |
There was a problem hiding this comment.
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..
There was a problem hiding this comment.
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).
925c73a
to
72c384b
Compare
lightning/src/ln/msgs.rs
Outdated
pub struct FeatureContextInvoice {} | ||
|
||
/// An internal trait capturing the various future context types | ||
pub trait FeatureContext {} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
lightning/src/ln/msgs.rs
Outdated
// Used to test encoding of diverse msgs | ||
#[cfg(test)] | ||
pub flags: Vec<u8>, | ||
mark: PhantomData<T>, |
There was a problem hiding this comment.
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.
lightning/src/ln/msgs.rs
Outdated
pub(crate) fn new() -> Features<T> { | ||
Features { | ||
flags: vec![2 | 1 << 5], | ||
mark: PhantomData, | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
fb28274
to
bc7d64c
Compare
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
impl FeatureContext for FeatureContextChannel {} | ||
//impl FeatureContext for FeatureContextInvoice {} | ||
|
||
pub trait DataLossProtect: FeatureContext {} |
There was a problem hiding this comment.
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.
lightning/src/ln/features.rs
Outdated
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)?; |
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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.
bc7d64c
to
6a0deb6
Compare
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. |
6a0deb6
to
90421e5
Compare
90421e5
to
b7d1ea1
Compare
|
||
/// 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()); |
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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.
lightning/src/ln/features.rs
Outdated
impl InitFeatures { | ||
/// Create a Features with the features we support | ||
#[cfg(not(feature = "fuzztarget"))] | ||
pub(crate) fn our_features() -> InitFeatures { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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().
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
{ // 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(); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM
7df6b15
to
8eaa47e
Compare
8eaa47e
to
a682fb4
Compare
There was a problem hiding this 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() { |
There was a problem hiding this comment.
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
There was a problem hiding this 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
.
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); |
There was a problem hiding this comment.
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.
lightning/src/ln/router.rs
Outdated
@@ -1441,6 +1439,98 @@ mod tests { | |||
assert_eq!(route.hops[1].cltv_expiry_delta, 42); | |||
} | |||
|
|||
{ // Disable 4 and 12 by requiring unknown feature bits |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
a682fb4
to
49f88ec
Compare
Oops, forgot to push with the split commit. I think all your points were addressed. |
ACK 49f88ec |
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.