add gossip address field serialization, parsing and tests
add space add gossip address field serialization, parsing and tests fix linter consolidate tests, fix intendation refactor test in loops add gossip address field serialization, parsing and tests
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import ipaddress
|
||||
import time
|
||||
import random
|
||||
import os
|
||||
@@ -41,7 +42,7 @@ from electrum_ecc import ECPubkey
|
||||
|
||||
from .sql_db import SqlDB, sql
|
||||
from . import constants, util
|
||||
from .util import profiler, get_headers_dir, is_ip_address, json_normalize, UserFacingException
|
||||
from .util import profiler, get_headers_dir, is_ip_address, json_normalize, UserFacingException, is_private_netaddress
|
||||
from .logging import Logger
|
||||
from .lntransport import LNPeerAddr
|
||||
from .lnutil import (format_short_channel_id, ShortChannelID,
|
||||
@@ -192,6 +193,48 @@ class NodeInfo(NamedTuple):
|
||||
payload_dict = decode_msg(raw)[1]
|
||||
return NodeInfo.from_msg(payload_dict)
|
||||
|
||||
@staticmethod
|
||||
def to_addresses_field(hostname: str, port: int) -> bytes:
|
||||
"""Encodes a hostname/port pair into a BOLT-7 'addresses' field."""
|
||||
if (NodeInfo.invalid_announcement_hostname(hostname)
|
||||
or port is None or port <= 0 or port > 65535):
|
||||
return b''
|
||||
port_bytes = port.to_bytes(2, 'big')
|
||||
if is_ip_address(hostname): # ipv4 or ipv6
|
||||
ip_addr = ipaddress.ip_address(hostname)
|
||||
if ip_addr.version == 4:
|
||||
return b'\x01' + ip_addr.packed + port_bytes
|
||||
elif ip_addr.version == 6:
|
||||
return b'\x02' + ip_addr.packed + port_bytes
|
||||
elif hostname.endswith('.onion'): # Tor onion v3
|
||||
onion_addr: bytes = base64.b32decode(hostname[:-6], casefold=True)
|
||||
return b'\x04' + onion_addr + port_bytes
|
||||
else:
|
||||
try:
|
||||
hostname_ascii: bytes = hostname.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
# encoding single characters to punycode (according to spec) doesn't make sense
|
||||
# as you can't differentiate them from regular ascii? encoding the whole string to punycode
|
||||
# doesn't work either as the receiver would interpret it as regular ascii.
|
||||
# hostname_ascii: bytes = hostname.encode('punycode')
|
||||
return b''
|
||||
if len(hostname_ascii) + 3 > 258: # + 1 byte for length and 2 for port
|
||||
return b'' # too long
|
||||
return b'\x05' + len(hostname_ascii).to_bytes(1, "big") + hostname_ascii + port_bytes
|
||||
|
||||
@staticmethod
|
||||
def invalid_announcement_hostname(hostname: Optional[str]) -> bool:
|
||||
"""Returns True if hostname unsuited for publishing in a NodeAnnouncement."""
|
||||
if (hostname is None or hostname == ""
|
||||
or is_private_netaddress(hostname)
|
||||
or hostname.startswith("http://") # not catching 'http' due to onion addresses
|
||||
or hostname.startswith("https://")):
|
||||
return True
|
||||
if hostname.endswith('.onion'):
|
||||
if len(hostname) != 62: # not an onion v3 link (probably onion v2)
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def parse_addresses_field(addresses_field):
|
||||
buf = addresses_field
|
||||
@@ -216,15 +259,18 @@ class NodeInfo(NamedTuple):
|
||||
if is_ip_address(ipv6_addr) and port != 0:
|
||||
addresses.append((ipv6_addr, port))
|
||||
elif atype == 3: # onion v2
|
||||
host = base64.b32encode(read(10)) + b'.onion'
|
||||
host = host.decode('ascii').lower()
|
||||
port = int.from_bytes(read(2), 'big')
|
||||
addresses.append((host, port))
|
||||
read(12) # we skip onion v2 as it is deprecated
|
||||
elif atype == 4: # onion v3
|
||||
host = base64.b32encode(read(35)) + b'.onion'
|
||||
host = host.decode('ascii').lower()
|
||||
port = int.from_bytes(read(2), 'big')
|
||||
addresses.append((host, port))
|
||||
elif atype == 5: # dns hostname
|
||||
len_hostname = int.from_bytes(read(1), 'big')
|
||||
host = read(len_hostname).decode('ascii')
|
||||
port = int.from_bytes(read(2), 'big')
|
||||
if not NodeInfo.invalid_announcement_hostname(host) and port > 0:
|
||||
addresses.append((host, port))
|
||||
else:
|
||||
# unknown address type
|
||||
# we don't know how long it is -> have to escape
|
||||
|
||||
@@ -1455,6 +1455,7 @@ class Peer(Logger, EventListener):
|
||||
self.maybe_mark_open(chan)
|
||||
|
||||
def send_node_announcement(self, alias:str):
|
||||
from .channel_db import NodeInfo
|
||||
timestamp = int(time.time())
|
||||
node_id = privkey_to_pubkey(self.privkey)
|
||||
features = self.features.for_node_announcement()
|
||||
@@ -1463,7 +1464,11 @@ class Peer(Logger, EventListener):
|
||||
rgb_color = bytes.fromhex('000000')
|
||||
alias = bytes(alias, 'utf8')
|
||||
alias += bytes(32 - len(alias))
|
||||
addresses = b''
|
||||
addr = self.lnworker.config.LIGHTNING_LISTEN
|
||||
hostname, port = addr.split(':')
|
||||
if port is None: # use default port if not specified
|
||||
port = 9735
|
||||
addresses = NodeInfo.to_addresses_field(hostname, int(port))
|
||||
raw_msg = encode_msg(
|
||||
"node_announcement",
|
||||
flen=flen,
|
||||
|
||||
@@ -8,6 +8,7 @@ from electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNo
|
||||
from electrum.lnonion import OnionRoutingFailure
|
||||
from electrum.util import bfh
|
||||
from electrum.lnutil import ShortChannelID, LnFeatures
|
||||
from electrum.channel_db import NodeInfo
|
||||
from electrum import constants
|
||||
|
||||
from . import ElectrumTestCase
|
||||
@@ -370,3 +371,45 @@ class TestLNMsg(ElectrumTestCase):
|
||||
OnionWireSerializer.decode_msg(orf2.to_bytes())
|
||||
self.assertEqual(None, orf2.decode_data())
|
||||
|
||||
def test_address_parsing_and_serialization(self):
|
||||
"""Tests the NodeInfo bolt7 node_announcement addresses field serialization and parsing"""
|
||||
taf = NodeInfo.to_addresses_field
|
||||
paf = NodeInfo.parse_addresses_field
|
||||
|
||||
# -- INVALID INPUTS --
|
||||
invalid_inputs_parsing = (
|
||||
b'', # empty input
|
||||
b'\x06\x00', # address type 6 (\x06) is not specified
|
||||
)
|
||||
invalid_inputs_serialization = (
|
||||
("::1", 9735), # local ipv6
|
||||
("::", 9735), # local ipv6
|
||||
("::1", 0), # local host, invalid port
|
||||
("::1", 65536), # local host, invalid port
|
||||
("127.0.0.1", 9735), # local ipv4
|
||||
("localhost", 9735), # local host
|
||||
("domain.com", 0), # domain, invalid port
|
||||
("domain.com", 65536), # domain, invalid port
|
||||
("domain.com", -1), # domain, invalid port
|
||||
("expyuzz4wqqyqhjn.onion", 9735), # onion v2, not supported
|
||||
("", 9735), # empty address
|
||||
)
|
||||
for invalid_input in invalid_inputs_parsing:
|
||||
self.assertEqual(paf(invalid_input), [])
|
||||
for host, port in invalid_inputs_serialization:
|
||||
self.assertEqual(taf(host, port), b'')
|
||||
|
||||
# -- VALID INPUTS --
|
||||
valid_inputs = (
|
||||
("34.138.100.228", 9735), # ipv4
|
||||
("2001:41d0:0001:b40d:0000:0000:0000:0001", 9735), # ipv6
|
||||
("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", 9735), # onion v3
|
||||
("ecb.europa.eu", 8624), # domain
|
||||
)
|
||||
valid_inputs_with_defined_output = [
|
||||
[["2001:41d0:1:b40d::1", 9735], [("2001:41d0:0001:b40d:0000:0000:0000:0001", 9735)]] # ipv6
|
||||
]
|
||||
for host, port in valid_inputs:
|
||||
self.assertEqual(paf(taf(host, port)), [(host, port)])
|
||||
for input_, output in valid_inputs_with_defined_output:
|
||||
self.assertEqual(paf(taf(*input_)), output)
|
||||
|
||||
Reference in New Issue
Block a user