1
0

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:
f321x
2025-01-16 16:46:07 +01:00
parent 0ef7235147
commit 1f626f3ad8
3 changed files with 100 additions and 6 deletions

View File

@@ -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

View File

@@ -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,

View File

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