-
Notifications
You must be signed in to change notification settings - Fork 15
Implements BOLT1 and BOLT9 #208
base: master
Are you sure you want to change the base?
Conversation
bigsize includes tools for encoding, decoding, and parsing BisSize data utils includes functions for message sanity checking lnnet - Adds bigsize docs and some additional sanity checks lnnet - Adds utils file for sanity checks
Includes the general Message class along with classes for init, error, ping and pong messages
Adds features and feature vector
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.
Initial review. Many comments are just nits, and some are style suggestions, feel free to disregard them if you disagree.
Did not check the tests yet.
88401b6
to
06f0001
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.
Only some additional minor comments on the tests.
Several optional values were defaulted to None, this mean that checking some properties of this values required checking if they were None first (e.g. checking the length of the for a ). This sets the default value to the same type as expected for non-defaults, so the type can be assumed in every case
Adds to_dict to the class so the content of a message can be easily checked in a human readable way
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.
Looks good to me.
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.
Very nice implementation, I added some suggestions inline, but most of them were already addressed in later commits.
The proposal to switch to an io
-based interface is probably too late, but maybe it comes in handy in the future :-)
|
||
if prefix < 253: | ||
# prefix is actually the value to be parsed | ||
return decode(value[0:1]), 1 |
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.
One problem with this take-what-you-need-and-return-its-length is that it is not well-suited for unbuffered streams, since we can't take 9 bytes, parse them, and then reset the stream back by the remainder of the bytes. I usually use io.RawBaseIO
as argument, wrapping str
and bytes
if necessary, that removes the need for this type of bookkeeping.
v (:obj:`bytes`): the message value. | ||
""" | ||
|
||
def __init__(self, t=b"", l=b"", v=b""): |
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.
Using mypy
annotations would remove the need for the isinstance
checks below.
common/net/tlv.py
Outdated
if t.to_bytes(t_offset, "big") == tlv_types["networks"]: | ||
return NetworksTLV.from_bytes(message) | ||
else: | ||
l, l_offset = bigsize.parse(message[t_offset:]) | ||
v = message[t_offset + l_offset :] | ||
if l > len(v): | ||
# Value is not long enough | ||
raise ValueError() # This message get overwritten so it does not matter |
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.
Might be helpful to define a dict for t
-> TLV class, since you're likely to add a few going forward :-)
common/net/bolt1.py
Outdated
""" | ||
|
||
if not isinstance(message, bytes): | ||
raise TypeError(f"message be must a bytearray") |
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.
bytearray
is not bytes
it's a separate class.
common/net/bolt1.py
Outdated
if message[:2] == message_types["init"]: | ||
return InitMessage.from_bytes(message) | ||
elif message[:2] == message_types["error"]: | ||
return ErrorMessage.from_bytes(message) | ||
elif message[:2] == message_types["ping"]: | ||
return PingMessage.from_bytes(message) | ||
elif message[:2] == message_types["pong"]: | ||
return PongMessage.from_bytes(message) |
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.
Dict messages[:2]
-> cls
will avoid this boilerplate.
common/net/bolt1.py
Outdated
# Add extensions if needed (this follows TLV format) | ||
# FIXME: Only networks for now | ||
if networks: | ||
super().__init__(mtype=message_types["init"], payload=payload, extension=networks.serialize()) |
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 you don't distinguish between a single TLV and a TLV stream, that is ok for now, but once we get to streams with multiple TLVs that might put more work on the caller.
gflen = int.from_bytes(message[2:4], "big") | ||
global_features = FeatureVector.from_bytes(message[4 : gflen + 4]) | ||
flen = int.from_bytes(message[gflen + 4 : gflen + 6], "big") | ||
local_features = FeatureVector.from_bytes(message[gflen + 6 : gflen + flen + 6]) |
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 bookkeeping would also be easier if you used an io
instance 😉
is unknown. | ||
""" | ||
|
||
if name in known_features: |
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.
You can turn the three known_features
lookups into a single one with this:
f = known_features.get(name, None)
And then check if f is None
to see if it was in the dict.
elif not check_feature_name_bit_pair(key, value.bit): | ||
raise ValueError("Feature name and bit do not match") | ||
|
||
vars(self)[key] = value |
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 is equivalent to self.__dict__[key] = value
I guess? Just wondering why vars()
is used here.
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.
An alternative would be to have a custom __getattr__
that first looks up into self._features
and then calls the superclass __getattr__
. That way the self._features
dict remains authoritative and there's no chance for the two to be out of sync.
common/net/bolt1.py
Outdated
super().__init__(mtype=message_types["init"], payload=payload, extension=networks.serialize()) | ||
super().__init__(mtype=message_types["init"], payload=payload, extension=[networks]) |
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.
Very nice, this addresses my earlier TLV vs. TLVStream comment 👍
This PR takes a first stab to use LN as transport layer for the tower.
It implements BOLTs 1 and 9 for encoding and decoding of network messages and features.
For message decoding, known messages types create a child instance of
Message
(e.g:InitMessage
) whereas unknown messages types are rejected (given they cannot be parsed).For TLVs, known types create a children of
TLVRecord
, whereas unknown create an instance ofTLVRecord
. Unknown even type TLVs do not raise any error since that's a rather node behaviour instead of an encoder / decoder behaviour. Same goes for features.For BOLT1, only
NetworksTLV
is currently implemented.Finally, there is currently no TLVStream parser, that will be part of a followup.
Review materials:
BOLT#1
BOLT#9