-
Notifications
You must be signed in to change notification settings - Fork 14
Implements BOLT1 and BOLT9 #208
base: master
Are you sure you want to change the base?
Changes from 10 commits
73a74d4
abda49d
06e680e
613ee82
8b96771
8ff6f5a
4f09f50
74fb734
89717fd
647962b
785502a
b9f06f4
fe914bc
2690660
dc4655b
13b5e13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| def encode(value): | ||
| """ | ||
| Encodes a value to BigSize. | ||
|
|
||
| Args: | ||
| value (:obj:`int`): the integer value to be encoded. | ||
|
|
||
| Returns: | ||
| :obj:`bytes`: the BigSize encoding of the given value. | ||
|
|
||
| Raises: | ||
| :obj:`TypeError`: If the provided value is not an integer. | ||
| :obj:`ValueError`: If the provided value is negative or bigger than ``pow(2, 64)``. | ||
| """ | ||
|
|
||
| if not isinstance(value, int): | ||
| raise TypeError(f"value must be integer, {type(value)} received") | ||
|
|
||
| if value < 0: | ||
| raise ValueError(f"value must be a positive integer, {value} received") | ||
|
|
||
| if value < pow(2, 8) - 3: | ||
| return value.to_bytes(1, "big") | ||
| elif value < pow(2, 16): | ||
| return b"\xfd" + value.to_bytes(2, "big") | ||
| elif value < pow(2, 32): | ||
| return b"\xfe" + value.to_bytes(4, "big") | ||
| elif value <= pow(2, 64): | ||
| return b"\xff" + value.to_bytes(8, "big") | ||
| else: | ||
| raise ValueError("BigSize can only encode up to 8-byte values") | ||
|
|
||
|
|
||
| def decode(value): | ||
| """ | ||
| Decodes a value fro BigSize. | ||
|
sr-gi marked this conversation as resolved.
Outdated
|
||
|
|
||
| Args: | ||
| value (:obj:`bytes`): the value to be decoded. | ||
|
|
||
| Returns: | ||
| :obj:`int`: the integer decoding of the provided value. | ||
|
|
||
| Raises: | ||
| :obj:`TypeError`: If the provided value is not in bytes. | ||
| :obj:`ValueError`: If the provided value is bigger than 9-bytes or the value is not properly encoded. | ||
| """ | ||
|
|
||
| if not isinstance(value, bytes): | ||
| raise TypeError(f"value must be bytes, {type(value)} received") | ||
|
|
||
| if len(value) > 9: | ||
| raise ValueError(f"value must be, at most, 9-bytes long, {len(value)} received") | ||
|
bigspider marked this conversation as resolved.
|
||
|
|
||
| if len(value) > 1: | ||
| prefix = value[0] | ||
| decoded_value = int.from_bytes(value[1:], "big") | ||
| else: | ||
| prefix = None | ||
| decoded_value = int.from_bytes(value, "big") | ||
|
|
||
| if not prefix and len(value) == 1 and decoded_value < pow(2, 8) - 3: | ||
| return decoded_value | ||
| elif prefix == 253 and len(value) == 3 and pow(2, 8) - 3 <= decoded_value < pow(2, 16): | ||
|
bigspider marked this conversation as resolved.
Outdated
|
||
| return decoded_value | ||
| elif prefix == 254 and len(value) == 5 and pow(2, 16) <= decoded_value < pow(2, 32): | ||
| return decoded_value | ||
| elif prefix == 255 and len(value) == 9 and pow(2, 32) <= decoded_value: | ||
| return decoded_value | ||
| else: | ||
| raise ValueError("value is not properly encoded") | ||
|
bigspider marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| def parse(value): | ||
| """ | ||
| Parses a BigSize from a bytearray. | ||
|
|
||
| Args: | ||
| value (:obj:`bytes`): the bytearray from where the BigSize value will be parsed. | ||
|
|
||
| Returns: | ||
| :obj:`tuple`: A 2 items tuple containing the parsed BigSize and its encoded length (offset of the bytearray). | ||
|
sr-gi marked this conversation as resolved.
Outdated
|
||
|
|
||
| Raises: | ||
| :obj:`TypeError`: If the provided value is not in bytes. | ||
| :obj:`ValueError`: If the provided value is not, at least, 1-byte long or if the value cannot be parsed. | ||
| """ | ||
|
|
||
| if not isinstance(value, bytes): | ||
| raise TypeError("value must be bytes") | ||
| if len(value) < 1: | ||
| raise ValueError("value must be at least 1-byte long") | ||
|
|
||
| prefix = value[0] | ||
|
|
||
| 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 commentThe 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 |
||
| else: | ||
| if prefix == 253: | ||
| return decode(value[0:3]), 3 | ||
| elif prefix == 254: | ||
| return decode(value[0:5]), 5 | ||
| else: | ||
| return decode(value[0:9]), 9 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the cases with a prefix, these calls will pass to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd add a comment. This was intentional to avoid double checking the same thing. |
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,283 @@ | ||||||
| from common.tools import is_256b_hex_str | ||||||
|
|
||||||
| from common.net.tlv import TLVRecord, NetworksTLV | ||||||
| from common.net.bolt9 import FeatureVector | ||||||
| from common.net.utils import message_sanity_checks | ||||||
|
|
||||||
|
|
||||||
| message_types = {"init": b"\x00\x10", "error": b"\x00\x11", "ping": b"\x00\x12", "pong": b"\x00\x13"} | ||||||
|
|
||||||
|
|
||||||
| class Message: | ||||||
| """ | ||||||
| Message class. Used as a based class for all other messages. | ||||||
|
sr-gi marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| Args: | ||||||
| mtype (:obj:`bytes`): the message type. | ||||||
| payload (:obj:`bytes`): the message payload. | ||||||
| extension (:obj:`bytes`): the message extension, if any (optional). | ||||||
|
|
||||||
| Attributes: | ||||||
| type (:obj:`bytes`): the message type. | ||||||
| payload (:obj:`bytes`): the message payload. | ||||||
| extension (:obj:`bytes`): the message extension, if any (optional). | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, mtype, payload, extension=None): | ||||||
| if not isinstance(mtype, bytes): | ||||||
| raise TypeError("mtype must be bytes") | ||||||
| if not isinstance(payload, bytes): | ||||||
| raise TypeError("payload must be bytes") | ||||||
| if extension is not None and not isinstance(extension, list): | ||||||
| raise TypeError("extension must be a list if set") | ||||||
| else: | ||||||
| # Normalize the default extension type (for empty lists) | ||||||
| if not extension: | ||||||
| extension = None | ||||||
| elif not all(isinstance(tlv, TLVRecord) for tlv in extension): | ||||||
| raise TypeError("All items in extension must be TLVRecords") | ||||||
|
|
||||||
| self.type = mtype | ||||||
| self.payload = payload | ||||||
| self.extension = extension | ||||||
|
|
||||||
| @classmethod | ||||||
| def from_bytes(cls, message): | ||||||
| """ | ||||||
| Builds a message from its byte representation. | ||||||
|
|
||||||
| Args: | ||||||
| message (:obj:`bytes`): the byte-encoded message. | ||||||
|
|
||||||
| Returns: | ||||||
| The Message children class depending on the received message type. Check ``message_types`` for more info. | ||||||
|
|
||||||
| Raises: | ||||||
| :obj:`TypeError`: If the given message is not in bytes. | ||||||
| :obj:`ValueError`: If the message can not be parsed. | ||||||
| """ | ||||||
|
|
||||||
| if not isinstance(message, bytes): | ||||||
| raise TypeError("message be must a bytearray") | ||||||
| if len(message) < 2: | ||||||
| raise ValueError("message be must at least 2-byte long") | ||||||
|
|
||||||
| mtype = message[:2] | ||||||
|
|
||||||
| if mtype == message_types["init"]: | ||||||
| return InitMessage.from_bytes(message) | ||||||
| elif mtype == message_types["error"]: | ||||||
| return ErrorMessage.from_bytes(message) | ||||||
| elif mtype == message_types["ping"]: | ||||||
| return PingMessage.from_bytes(message) | ||||||
| elif mtype == message_types["pong"]: | ||||||
| return PongMessage.from_bytes(message) | ||||||
| else: | ||||||
| raise ValueError("Cannot decode unknown message type") | ||||||
|
|
||||||
| def serialize(self): | ||||||
| """Serialises the message.""" | ||||||
|
sr-gi marked this conversation as resolved.
Outdated
|
||||||
| if not self.extension: | ||||||
| return self.type + self.payload | ||||||
| else: | ||||||
| tlvs = b"".join([tlv.serialize() for tlv in self.extension]) | ||||||
| return self.type + self.payload + tlvs | ||||||
|
|
||||||
|
|
||||||
| class InitMessage(Message): | ||||||
| """ | ||||||
| First message exchange by the nodes, it reveals the features supported by each end. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| Args: | ||||||
| global_features (:obj:`FeatureVector <teos.net.bolt9.FeatureVector>`): the global features vector. | ||||||
| local_features (:obj:`FeatureVector <teos.net.bolt9.FeatureVector>`): the local features vector. | ||||||
| local_features (:obj:`list`): a list of genesis block hashes (optional). | ||||||
|
sr-gi marked this conversation as resolved.
Outdated
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| """ | ||||||
|
|
||||||
| def __init__(self, global_features, local_features, networks=None): | ||||||
| if not (isinstance(global_features, FeatureVector) and isinstance(local_features, FeatureVector)): | ||||||
| raise TypeError("global_features and local_features must be FeatureVector instances") | ||||||
| if networks: | ||||||
| if not isinstance(networks, NetworksTLV): | ||||||
| raise TypeError("networks must be of type NetworksTLV (if set)") | ||||||
|
|
||||||
| gf = global_features.serialize() | ||||||
| lf = local_features.serialize() | ||||||
| gflen = len(gf).to_bytes(2, "big") | ||||||
| flen = len(lf).to_bytes(2, "big") | ||||||
| payload = gflen + gf + flen + lf | ||||||
|
|
||||||
| # 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]) | ||||||
| else: | ||||||
| super().__init__(mtype=message_types["init"], payload=payload) | ||||||
| self.global_features = global_features | ||||||
| self.local_features = local_features | ||||||
| self.networks = networks | ||||||
|
|
||||||
| @classmethod | ||||||
| def from_bytes(cls, message): | ||||||
| """Builds an InitMessage from its byte representation.""" | ||||||
| # Message should be at least: type (2-byte) + gflen (2-byte) + flen (2 byte) | ||||||
| message_sanity_checks(message, message_types["init"], 6) | ||||||
|
|
||||||
| try: | ||||||
| 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]) | ||||||
|
Comment on lines
+133
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This bookkeeping would also be easier if you used an |
||||||
| if gflen + flen + 6 > len(message): | ||||||
| raise ValueError() # Unexpected EOF | ||||||
|
bigspider marked this conversation as resolved.
|
||||||
|
|
||||||
| # Check if there are TLVs (optional) | ||||||
| if len(message) > gflen + flen + 6: | ||||||
| # FIXME: Only accepting networks TLV for now | ||||||
| networks = NetworksTLV.from_bytes(message[gflen + flen + 6 :]) | ||||||
| return cls(global_features, local_features, networks) | ||||||
|
|
||||||
| return cls(global_features, local_features) | ||||||
|
|
||||||
| except ValueError: | ||||||
| raise ValueError("Wrong message format. Unexpected EOF") | ||||||
|
|
||||||
|
|
||||||
| class ErrorMessage(Message): | ||||||
| """ | ||||||
| Message for error reporting. | ||||||
|
|
||||||
| Args: | ||||||
| channel_id (:obj:`str`): a 32-byte long hex str identifying the channel that originated the error, or 0 if it | ||||||
| refers to all channels. | ||||||
| data (:obj:`str`): the error message. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, channel_id, data=None): | ||||||
| if not is_256b_hex_str(channel_id): | ||||||
| raise ValueError("channel_id must be a 256-bit hex string") | ||||||
|
|
||||||
| payload = bytes.fromhex(channel_id) | ||||||
|
|
||||||
| if data: | ||||||
| if not isinstance(data, str): | ||||||
| raise ValueError("data must be string if set") | ||||||
|
|
||||||
| encoded_message = data.encode("utf-8") | ||||||
| if len(encoded_message) > pow(2, 16): | ||||||
| raise ValueError( | ||||||
| f"Encoded data length cannot be bigger than {pow(2, 16)}, {len(encoded_message)} received" | ||||||
| ) | ||||||
|
|
||||||
| payload += len(encoded_message).to_bytes(2, "big") + encoded_message | ||||||
|
|
||||||
| super().__init__(message_types["error"], payload) | ||||||
| self.channel_id = channel_id | ||||||
| self.data = data | ||||||
|
|
||||||
| @classmethod | ||||||
| def from_bytes(cls, message): | ||||||
| """Builds an ErrorMessage from its byte representation.""" | ||||||
| # Message should be at least: type (2-byte) + channel_id (32-byte) + data_len (2-bytes) | ||||||
| message_sanity_checks(message, message_types["error"], 36) | ||||||
| channel_id = message[2:34].hex() | ||||||
| data_len = int.from_bytes(message[34:36], "big") | ||||||
|
|
||||||
| # There's associated data | ||||||
| if data_len: | ||||||
| data = message[36 : 36 + data_len] | ||||||
|
|
||||||
| if len(data) < data_len: | ||||||
| raise ValueError("Wrong message format. Unexpected EOF") | ||||||
| if len(message) > 36 + data_len: | ||||||
| raise ValueError("Wrong data format. message has additional tailing data") | ||||||
|
sr-gi marked this conversation as resolved.
Outdated
|
||||||
| return cls(channel_id, data.decode("utf-8")) | ||||||
|
|
||||||
| return cls(channel_id) | ||||||
|
|
||||||
|
|
||||||
| class PingMessage(Message): | ||||||
| """ | ||||||
| Message to test the reachability of the other side of the channel. Useful to allow long lived communications. | ||||||
|
|
||||||
| Args: | ||||||
| num_pong_bytes (:obj:`int`): the number of bytes to be responded by the peer. | ||||||
| ignored_bytes (:obj:`bytes`): filling bytes added to the message by the sender. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, num_pong_bytes, ignored_bytes=None): | ||||||
| if not 0 <= num_pong_bytes < pow(2, 16): | ||||||
| raise ValueError(f"num_pong_bytes must be between 0 and {pow(2, 16)}") | ||||||
|
bigspider marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| payload = num_pong_bytes.to_bytes(2, "big") | ||||||
|
|
||||||
| if ignored_bytes: | ||||||
| if not isinstance(ignored_bytes, bytes): | ||||||
| raise TypeError("ignored_bytes must be bytes if set") | ||||||
| if len(ignored_bytes) > pow(2, 16) - 4: | ||||||
| raise ValueError(f"ignored_bytes cannot be higher than {pow(2, 16) - 4}") | ||||||
| payload += len(ignored_bytes).to_bytes(2, "big") + ignored_bytes | ||||||
|
|
||||||
| super().__init__(message_types["ping"], payload) | ||||||
| self.num_pong_bytes = num_pong_bytes | ||||||
| self.ignored_bytes = ignored_bytes | ||||||
|
|
||||||
| @classmethod | ||||||
| def from_bytes(cls, message): | ||||||
| """Builds a PingMessage from its byte representation.""" | ||||||
| # Message should be at least: type (2-byte) + num_pong_bytes (2-bytes) + byteslen (2-bytes) | ||||||
| message_sanity_checks(message, message_types["ping"], 6) | ||||||
| num_pong_bytes = int.from_bytes(message[2:4], "big") | ||||||
| byteslen = int.from_bytes(message[4:6], "big") | ||||||
|
|
||||||
| if byteslen: | ||||||
| ignored = message[6 : 6 + byteslen] | ||||||
| if len(ignored) < byteslen: | ||||||
| raise ValueError("Wrong message format. Unexpected EOF") | ||||||
| if len(message) > 6 + byteslen: | ||||||
| raise ValueError("Wrong data format. message has additional tailing data") | ||||||
|
bigspider marked this conversation as resolved.
Outdated
|
||||||
| return cls(num_pong_bytes, ignored) | ||||||
|
|
||||||
| return cls(num_pong_bytes) | ||||||
|
|
||||||
|
|
||||||
| class PongMessage(Message): | ||||||
| """ | ||||||
| Message to be sent in response to a ``PingMessage``. | ||||||
|
|
||||||
| Args: | ||||||
| ignored_bytes (:obj:`bytes`): filling bytes added to the message by the sender. Should match the ones requested | ||||||
| by the sender of the ``PingMessage``. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, ignored_bytes=None): | ||||||
| if ignored_bytes: | ||||||
| if not isinstance(ignored_bytes, bytes): | ||||||
| raise TypeError("ignored_bytes must be bytes if set") | ||||||
| if len(ignored_bytes) > pow(2, 16) - 4: | ||||||
| raise ValueError(f"ignored_bytes cannot be higher than {pow(2, 16) -4}") | ||||||
|
sr-gi marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| payload = len(ignored_bytes).to_bytes(2, "big") + ignored_bytes | ||||||
|
|
||||||
| else: | ||||||
| payload = int.to_bytes(0, 2, "big") | ||||||
|
|
||||||
| super().__init__(message_types["pong"], payload) | ||||||
| self.ignored_bytes = ignored_bytes | ||||||
|
|
||||||
| @classmethod | ||||||
| def from_bytes(cls, message): | ||||||
| """Builds a PongMessage from its byte representation.""" | ||||||
| # Message should be at least: type (2-byte) + byteslen (2-bytes) | ||||||
| message_sanity_checks(message, message_types["pong"], 4) | ||||||
| byteslen = int.from_bytes(message[2:4], "big") | ||||||
|
|
||||||
| if byteslen: | ||||||
| ignored_bytes = message[4 : 4 + byteslen] | ||||||
| if len(ignored_bytes) < byteslen: | ||||||
| raise ValueError("Wrong message format. Unexpected EOF") | ||||||
| if len(message) != 4 + byteslen: | ||||||
| raise ValueError("Wrong data format. message has additional tailing data") | ||||||
|
sr-gi marked this conversation as resolved.
Outdated
|
||||||
| return cls(ignored_bytes) | ||||||
|
|
||||||
| return cls() | ||||||
Uh oh!
There was an error while loading. Please reload this page.