Skip to content
This repository has been archived by the owner on Sep 26, 2022. It is now read-only.

Implements BOLT1 and BOLT9 #208

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open

Implements BOLT1 and BOLT9 #208

wants to merge 16 commits into from

Conversation

sr-gi
Copy link
Member

@sr-gi sr-gi commented Sep 3, 2020

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 of TLVRecord. 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

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
@sr-gi sr-gi requested a review from bigspider September 3, 2020 20:02
Copy link
Collaborator

@bigspider bigspider left a 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.

common/net/bigsize.py Show resolved Hide resolved
common/net/bigsize.py Outdated Show resolved Hide resolved
common/net/bigsize.py Outdated Show resolved Hide resolved
common/net/bigsize.py Outdated Show resolved Hide resolved
common/net/bigsize.py Outdated Show resolved Hide resolved
common/net/bolt1.py Outdated Show resolved Hide resolved
common/net/bolt1.py Outdated Show resolved Hide resolved
common/net/bolt1.py Outdated Show resolved Hide resolved
common/net/bolt1.py Outdated Show resolved Hide resolved
common/net/bolt1.py Outdated Show resolved Hide resolved
@sr-gi sr-gi force-pushed the ln-net branch 2 times, most recently from 88401b6 to 06f0001 Compare September 7, 2020 15:07
Copy link
Collaborator

@bigspider bigspider left a 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.

test/common/unit/net/test_bolt1.py Outdated Show resolved Hide resolved
test/common/unit/net/test_bolt1.py Show resolved Hide resolved
test/common/unit/net/test_bolt1.py Outdated Show resolved Hide resolved
test/common/unit/net/test_bolt1.py Outdated Show resolved Hide resolved
test/common/unit/net/test_bolt1.py Outdated Show resolved Hide resolved
test/common/unit/net/test_bolt1.py Outdated Show resolved Hide resolved
test/common/unit/net/test_bolt9.py Outdated Show resolved Hide resolved
test/common/unit/net/test_bolt9.py Outdated Show resolved Hide resolved
test/common/unit/net/test_tlv.py Outdated Show resolved Hide resolved
test/common/unit/net/test_tlv.py Outdated Show resolved Hide resolved
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
Copy link
Collaborator

@bigspider bigspider left a 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.

Copy link

@cdecker cdecker left a 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
Copy link

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""):
Copy link

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.

Comment on lines 65 to 72
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
Copy link

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 :-)

"""

if not isinstance(message, bytes):
raise TypeError(f"message be must a bytearray")
Copy link

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.

Comment on lines 52 to 59
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)
Copy link

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.

# 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())
Copy link

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.

Comment on lines +111 to +114
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])
Copy link

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:
Copy link

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
Copy link

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.

Copy link

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.

Comment on lines 96 to 113
super().__init__(mtype=message_types["init"], payload=payload, extension=networks.serialize())
super().__init__(mtype=message_types["init"], payload=payload, extension=[networks])
Copy link

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 👍

@sr-gi sr-gi changed the base branch from split-cli-client to master November 13, 2020 15:25
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants