tmove connection string decoding to lnworker, fix test_lnutil - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit efc8d50570670ccb5aaf0bc022b579ea4a2b93d6 DIR parent 24cf4e7eb0e444f635835350aca3f2e9eb345a05 HTML Author: Janus <ysangkok@gmail.com> Date: Thu, 27 Sep 2018 16:43:33 +0200 move connection string decoding to lnworker, fix test_lnutil Diffstat: M electrum/commands.py | 6 +++--- M electrum/gui/qt/channels_list.py | 54 +++---------------------------- M electrum/lnutil.py | 47 +++++++++++++++++++++++++++++++ M electrum/lnworker.py | 49 +++++++++++++++++++++++++------ M electrum/tests/test_lnutil.py | 46 +++++++++++++++++++++++++------ 5 files changed, 132 insertions(+), 70 deletions(-) --- DIR diff --git a/electrum/commands.py b/electrum/commands.py t@@ -765,9 +765,9 @@ class Commands: # lightning network commands @command('wpn') - def open_channel(self, node_id, amount, channel_push=0, password=None): - f = self.wallet.lnworker.open_channel(bytes.fromhex(node_id), satoshis(amount), satoshis(channel_push), password) - return f.result() + def open_channel(self, connection_string, amount, channel_push=0, password=None): + f = self.wallet.lnworker.open_channel(connection_string, satoshis(amount), satoshis(channel_push), password) + return f.result(5) @command('wn') def reestablish_channel(self): DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py t@@ -5,8 +5,7 @@ from PyQt5.QtWidgets import * from electrum.util import inv_dict, bh2u, bfh from electrum.i18n import _ from electrum.lnhtlc import HTLCStateMachine -from electrum.lnaddr import lndecode -from electrum.lnutil import LOCAL, REMOTE +from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton from .amountedit import BTCAmountEdit t@@ -108,55 +107,12 @@ class ChannelsList(MyTreeWidget): return local_amt = local_amt_inp.get_amount() push_amt = push_amt_inp.get_amount() - connect_contents = str(remote_nodeid.text()) - nodeid_hex, rest = self.parse_connect_contents(connect_contents) - try: - node_id = bfh(nodeid_hex) - assert len(node_id) == 33 - except: - self.parent.show_error(_('Invalid node ID, must be 33 bytes and hexadecimal')) - return - - peer = lnworker.peers.get(node_id) - if not peer: - all_nodes = self.parent.network.channel_db.nodes - node_info = all_nodes.get(node_id, None) - if rest is not None: - try: - host, port = rest.split(":") - except ValueError: - self.parent.show_error(_('Connection strings must be in <node_pubkey>@<host>:<port> format')) - return - elif node_info: - host, port = node_info.addresses[0] - else: - self.parent.show_error(_('Unknown node:') + ' ' + nodeid_hex) - return - try: - int(port) - except: - self.parent.show_error(_('Port number must be decimal')) - return - lnworker.add_peer(host, port, node_id) - - self.main_window.protect(self.open_channel, (node_id, local_amt, push_amt)) + connect_contents = str(remote_nodeid.text()).strip() - @classmethod - def parse_connect_contents(cls, connect_contents: str): - rest = None try: - # connection string? - nodeid_hex, rest = connect_contents.split("@") - except ValueError: - try: - # invoice? - invoice = lndecode(connect_contents) - nodeid_bytes = invoice.pubkey.serialize() - nodeid_hex = bh2u(nodeid_bytes) - except: - # node id as hex? - nodeid_hex = connect_contents - return nodeid_hex, rest + self.main_window.protect(self.open_channel, (connect_contents, local_amt, push_amt)) + except ConnStringFormatError as e: + self.parent.show_error(str(e)) def open_channel(self, *args, **kwargs): self.parent.wallet.lnworker.open_channel(*args, **kwargs) DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py t@@ -2,6 +2,7 @@ from enum import IntFlag import json from collections import namedtuple from typing import NamedTuple, List, Tuple +import re from .util import bfh, bh2u, inv_dict from .crypto import sha256 t@@ -11,6 +12,7 @@ from . import ecc, bitcoin, crypto, transaction from .transaction import opcodes, TxOutput from .bitcoin import push_script from . import segwit_addr +from .i18n import _ HTLC_TIMEOUT_WEIGHT = 663 HTLC_SUCCESS_WEIGHT = 703 t@@ -478,3 +480,48 @@ def make_closing_tx(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, c_input['sequence'] = 0xFFFF_FFFF tx = Transaction.from_io([c_input], outputs, locktime=0, version=2) return tx + +class ConnStringFormatError(Exception): + pass + +def split_host_port(host_port: str) -> Tuple[str, str]: # port returned as string + ipv6 = re.compile(r'\[(?P<host>[:0-9]+)\](?P<port>:\d+)?$') + other = re.compile(r'(?P<host>[^:]+)(?P<port>:\d+)?$') + m = ipv6.match(host_port) + if not m: + m = other.match(host_port) + if not m: + raise ConnStringFormatError(_('Connection strings must be in <node_pubkey>@<host>:<port> format')) + host = m.group('host') + if m.group('port'): + port = m.group('port')[1:] + else: + port = '9735' + try: + int(port) + except ValueError: + raise ConnStringFormatError(_('Port number must be decimal')) + return host, port + +def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]: + rest = None + try: + # connection string? + nodeid_hex, rest = connect_contents.split("@", 1) + except ValueError: + try: + # invoice? + invoice = lndecode(connect_contents) + nodeid_bytes = invoice.pubkey.serialize() + nodeid_hex = bh2u(nodeid_bytes) + except: + # node id as hex? + nodeid_hex = connect_contents + if rest == '': + raise ConnStringFormatError(_('At least a hostname must be supplied after the at symbol.')) + try: + node_id = bfh(nodeid_hex) + assert len(node_id) == 33 + except: + raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal')) + return node_id, rest DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -3,9 +3,10 @@ import os from decimal import Decimal import random import time -from typing import Optional, Sequence +from typing import Optional, Sequence, Tuple, List import threading from functools import partial +import socket import dns.resolver import dns.exception t@@ -17,8 +18,10 @@ from .lnbase import Peer, privkey_to_pubkey, aiosafe from .lnaddr import lnencode, LnAddr, lndecode from .ecc import der_sig_from_sig_string from .lnhtlc import HTLCStateMachine -from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, get_compressed_pubkey_from_bech32, - PaymentFailure) +from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, + get_compressed_pubkey_from_bech32, extract_nodeid, + PaymentFailure, split_host_port, ConnStringFormatError) +from electrum.lnaddr import lndecode from .i18n import _ t@@ -30,7 +33,6 @@ FALLBACK_NODE_LIST = ( LNPeerAddr('ecdsa.net', 9735, bfh('038370f0e7a03eded3e1d41dc081084a87f0afa1c5b22090b4f3abb391eb15d8ff')), ) - class LNWorker(PrintError): def __init__(self, wallet, network): t@@ -89,6 +91,7 @@ class LNWorker(PrintError): asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(peer.main_loop()), self.network.asyncio_loop) self.peers[node_id] = peer self.network.trigger_callback('ln_status') + return peer def save_channel(self, openchannel): assert type(openchannel) is HTLCStateMachine t@@ -154,8 +157,10 @@ class LNWorker(PrintError): conf = self.wallet.get_tx_height(chan.funding_outpoint.txid).conf peer.on_network_update(chan, conf) - async def _open_channel_coroutine(self, node_id, local_amount_sat, push_sat, password): - peer = self.peers[node_id] + async def _open_channel_coroutine(self, peer, local_amount_sat, push_sat, password): + # peer might just have been connected to + await asyncio.wait_for(peer.initialized, 5) + openingchannel = await peer.channel_establishment_flow(self.wallet, self.config, password, funding_sat=local_amount_sat + push_sat, push_msat=push_sat * 1000, t@@ -171,8 +176,34 @@ class LNWorker(PrintError): def on_channels_updated(self): self.network.trigger_callback('channels') - def open_channel(self, node_id, local_amt_sat, push_amt_sat, pw): - coro = self._open_channel_coroutine(node_id, local_amt_sat, push_amt_sat, None if pw == "" else pw) + @staticmethod + def choose_preferred_address(addr_list: List[Tuple[str, int]]) -> Tuple[str, int]: + for host, port in addr_list: + if is_ip_address(host): + return host, port + # TODO maybe filter out onion if not on tor? + self.print_error('Chose random address from ' + str(node_info.addresses)) + return random.choice(node_info.addresses) + + def open_channel(self, connect_contents, local_amt_sat, push_amt_sat, pw): + node_id, rest = extract_nodeid(connect_contents) + + peer = self.peers.get(node_id) + if not peer: + all_nodes = self.network.channel_db.nodes + node_info = all_nodes.get(node_id, None) + if rest is not None: + host, port = split_host_port(rest) + elif node_info and len(node_info.addresses) > 0: + host, port = self.choose_preferred_address(node_info.addresses) + else: + raise ConnStringFormatError(_('Unknown node:') + ' ' + bh2u(node_id)) + try: + socket.getaddrinfo(host, int(port)) + except socket.gaierror: + raise ConnStringFormatError(_('Hostname does not resolve (getaddrinfo failed)')) + peer = self.add_peer(host, port, node_id) + coro = self._open_channel_coroutine(peer, local_amt_sat, push_amt_sat, None if pw == "" else pw) return asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) def pay(self, invoice, amount_sat=None): t@@ -262,7 +293,7 @@ class LNWorker(PrintError): if node is None: continue addresses = node.addresses if not addresses: continue - host, port = random.choice(addresses) + host, port = self.choose_preferred_address(addresses) peer = LNPeerAddr(host, port, node_id) if peer.pubkey in self.peers: continue if peer in self._last_tried_peer: continue DIR diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py t@@ -5,7 +5,9 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see make_received_htlc, make_commitment, make_htlc_tx_witness, make_htlc_tx_output, make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey, derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret, - get_compressed_pubkey_from_bech32) + get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, + ScriptHtlc, extract_nodeid) +from electrum import lnhtlc from electrum.util import bh2u, bfh from electrum.transaction import Transaction t@@ -488,13 +490,14 @@ class TestLNUtil(unittest.TestCase): remote_signature = "304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606" parazyd.org:70 /git/electrum/commit/efc8d50570670ccb5aaf0bc022b579ea4a2b93d6.gph:290: line too long