URI: 
       tUse separate lightning nodes for gossip and channel operations. - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 730be17aba20e0c799b1aaf6c577761a6105ea5b
   DIR parent 12743bda93179e34f9da31f26c631780c324a091
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Fri, 26 Apr 2019 12:48:02 +0200
       
       Use separate lightning nodes for gossip and channel operations.
       
       Diffstat:
         M electrum/lnpeer.py                  |       5 +----
         M electrum/lnworker.py                |     361 +++++++++++++++++--------------
         M electrum/network.py                 |       4 ++++
         M electrum/tests/test_lnpeer.py       |      27 ++++++++++++++-------------
         M electrum/wallet.py                  |       4 ++--
       
       5 files changed, 215 insertions(+), 186 deletions(-)
       ---
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -63,6 +63,7 @@ class Peer(PrintError):
                self.pubkey = pubkey
                self.lnworker = lnworker
                self.privkey = lnworker.node_keypair.privkey
       +        self.localfeatures = self.lnworker.localfeatures
                self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
                self.network = lnworker.network
                self.lnwatcher = lnworker.network.lnwatcher
       t@@ -76,10 +77,6 @@ class Peer(PrintError):
                self.announcement_signatures = defaultdict(asyncio.Queue)
                self.closing_signed = defaultdict(asyncio.Queue)
                self.payment_preimages = defaultdict(asyncio.Queue)
       -        self.localfeatures = LnLocalFeatures(0)
       -        self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_REQ
       -        #self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       -        #self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
                self.attempted_route = {}
                self.orphan_channel_updates = OrderedDict()
                self.sent_commitment_for_ctn_last = defaultdict(lambda: None)  # type: Dict[Channel, Optional[int]]
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -39,7 +39,7 @@ from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
                             generate_keypair, LnKeyFamily, LOCAL, REMOTE,
                             UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
                             NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
       -                     UpdateAddHtlc, Direction)
       +                     UpdateAddHtlc, Direction, LnLocalFeatures)
        from .i18n import _
        from .lnrouter import RouteEdge, is_route_sane_to_use
        from .address_synchronizer import TX_HEIGHT_LOCAL
       t@@ -74,16 +74,178 @@ encoder = ChannelJsonEncoder()
        
        class LNWorker(PrintError):
        
       +    def __init__(self, xprv):
       +        self.node_keypair = generate_keypair(keystore.from_xprv(xprv), LnKeyFamily.NODE_KEY, 0)
       +        self.peers = {}  # type: Dict[bytes, Peer]  # pubkey -> Peer
       +        self.localfeatures = LnLocalFeatures(0)
       +
       +    async def maybe_listen(self):
       +        listen_addr = self.config.get('lightning_listen')
       +        if listen_addr:
       +            addr, port = listen_addr.rsplit(':', 2)
       +            if addr[0] == '[':
       +                # ipv6
       +                addr = addr[1:-1]
       +            async def cb(reader, writer):
       +                transport = LNResponderTransport(self.node_keypair.privkey, reader, writer)
       +                try:
       +                    node_id = await transport.handshake()
       +                except:
       +                    self.print_error('handshake failure from incoming connection')
       +                    return
       +                peer = Peer(self, node_id, transport)
       +                self.peers[node_id] = peer
       +                await self.network.main_taskgroup.spawn(peer.main_loop())
       +                self.network.trigger_callback('ln_status')
       +            await asyncio.start_server(cb, addr, int(port))
       +
       +    async def main_loop(self):
       +        while True:
       +            await asyncio.sleep(1)
       +            now = time.time()
       +            if len(self.peers) >= NUM_PEERS_TARGET:
       +                continue
       +            peers = self._get_next_peers_to_try()
       +            for peer in peers:
       +                last_tried = self._last_tried_peer.get(peer, 0)
       +                if last_tried + PEER_RETRY_INTERVAL < now:
       +                    await self.add_peer(peer.host, peer.port, peer.pubkey)
       +
       +    async def add_peer(self, host, port, node_id):
       +        if node_id in self.peers:
       +            return self.peers[node_id]
       +        port = int(port)
       +        peer_addr = LNPeerAddr(host, port, node_id)
       +        transport = LNTransport(self.node_keypair.privkey, peer_addr)
       +        self._last_tried_peer[peer_addr] = time.time()
       +        self.print_error("adding peer", peer_addr)
       +        peer = Peer(self, node_id, transport)
       +        await self.network.main_taskgroup.spawn(peer.main_loop())
       +        self.peers[node_id] = peer
       +        self.network.trigger_callback('ln_status')
       +        return peer
       +
       +    def start_network(self, network: 'Network'):
       +        self.network = network
       +        self.config = network.config
       +        self.channel_db = self.network.channel_db
       +        self._last_tried_peer = {}  # LNPeerAddr -> unix timestamp
       +        self._add_peers_from_config()
       +        # wait until we see confirmations
       +        asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(self.main_loop()), self.network.asyncio_loop)
       +        self.first_timestamp_requested = None
       +
       +    def _add_peers_from_config(self):
       +        peer_list = self.config.get('lightning_peers', [])
       +        for host, port, pubkey in peer_list:
       +            asyncio.run_coroutine_threadsafe(
       +                self.add_peer(host, int(port), bfh(pubkey)),
       +                self.network.asyncio_loop)
       +
       +    def _get_next_peers_to_try(self) -> Sequence[LNPeerAddr]:
       +        now = time.time()
       +        recent_peers = self.channel_db.get_recent_peers()
       +        # maintenance for last tried times
       +        # due to this, below we can just test membership in _last_tried_peer
       +        for peer in list(self._last_tried_peer):
       +            if now >= self._last_tried_peer[peer] + PEER_RETRY_INTERVAL:
       +                del self._last_tried_peer[peer]
       +        # first try from recent peers
       +        for peer in recent_peers:
       +            if peer.pubkey in self.peers: continue
       +            if peer in self._last_tried_peer: continue
       +            return [peer]
       +        # try random peer from graph
       +        unconnected_nodes = self.channel_db.get_200_randomly_sorted_nodes_not_in(self.peers.keys())
       +        if unconnected_nodes:
       +            for node in unconnected_nodes:
       +                addrs = self.channel_db.get_node_addresses(node)
       +                if not addrs:
       +                    continue
       +                host, port = self.choose_preferred_address(addrs)
       +                peer = LNPeerAddr(host, port, bytes.fromhex(node.node_id))
       +                if peer in self._last_tried_peer: continue
       +                self.print_error('taking random ln peer from our channel db')
       +                return [peer]
       +
       +        # TODO remove this. For some reason the dns seeds seem to ignore the realm byte
       +        # and only return mainnet nodes. so for the time being dns seeding is disabled:
       +        if constants.net in (constants.BitcoinTestnet, ):
       +            return [random.choice(FALLBACK_NODE_LIST_TESTNET)]
       +        elif constants.net in (constants.BitcoinMainnet, ):
       +            return [random.choice(FALLBACK_NODE_LIST_MAINNET)]
       +        else:
       +            return []
       +
       +        # try peers from dns seed.
       +        # return several peers to reduce the number of dns queries.
       +        if not constants.net.LN_DNS_SEEDS:
       +            return []
       +        dns_seed = random.choice(constants.net.LN_DNS_SEEDS)
       +        self.print_error('asking dns seed "{}" for ln peers'.format(dns_seed))
       +        try:
       +            # note: this might block for several seconds
       +            # this will include bech32-encoded-pubkeys and ports
       +            srv_answers = resolve_dns_srv('r{}.{}'.format(
       +                constants.net.LN_REALM_BYTE, dns_seed))
       +        except dns.exception.DNSException as e:
       +            return []
       +        random.shuffle(srv_answers)
       +        num_peers = 2 * NUM_PEERS_TARGET
       +        srv_answers = srv_answers[:num_peers]
       +        # we now have pubkeys and ports but host is still needed
       +        peers = []
       +        for srv_ans in srv_answers:
       +            try:
       +                # note: this might block for several seconds
       +                answers = dns.resolver.query(srv_ans['host'])
       +            except dns.exception.DNSException:
       +                continue
       +            try:
       +                ln_host = str(answers[0])
       +                port = int(srv_ans['port'])
       +                bech32_pubkey = srv_ans['host'].split('.')[0]
       +                pubkey = get_compressed_pubkey_from_bech32(bech32_pubkey)
       +                peers.append(LNPeerAddr(ln_host, port, pubkey))
       +            except Exception as e:
       +                self.print_error('error with parsing peer from dns seed: {}'.format(e))
       +                continue
       +        self.print_error('got {} ln peers from dns seed'.format(len(peers)))
       +        return peers
       +
       +
       +
       +class LNGossip(LNWorker):
       +
       +    def __init__(self, network):
       +        seed = os.urandom(32)
       +        node = BIP32Node.from_rootseed(seed, xtype='standard')
       +        xprv = node.to_xprv()
       +        super().__init__(xprv)
       +        self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_REQ
       +
       +
       +class LNWallet(LNWorker):
       +
            def __init__(self, wallet: 'Abstract_Wallet'):
                self.wallet = wallet
                self.storage = wallet.storage
       +        xprv = self.storage.get('lightning_privkey2')
       +        if xprv is None:
       +            # TODO derive this deterministically from wallet.keystore at keystore generation time
       +            # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ )
       +            seed = os.urandom(32)
       +            node = BIP32Node.from_rootseed(seed, xtype='standard')
       +            xprv = node.to_xprv()
       +            self.storage.put('lightning_privkey2', xprv)
       +        super().__init__(xprv)
       +        self.ln_keystore = keystore.from_xprv(xprv)
       +        #self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       +        #self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
                self.invoices = self.storage.get('lightning_invoices', {})        # RHASH -> (invoice, direction, is_paid)
                self.preimages = self.storage.get('lightning_preimages', {})      # RHASH -> preimage
                self.sweep_address = wallet.get_receiving_address()
                self.lock = threading.RLock()
       -        self.ln_keystore = self._read_ln_keystore()
       -        self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
       -        self.peers = {}  # type: Dict[bytes, Peer]  # pubkey -> Peer
                self.channels = {}  # type: Dict[bytes, Channel]
                for x in wallet.storage.get("channels", []):
                    c = Channel(x, sweep_address=self.sweep_address, lnworker=self)
       t@@ -95,19 +257,20 @@ class LNWorker(PrintError):
        
            def start_network(self, network: 'Network'):
                self.network = network
       -        self.config = network.config
       -        self.channel_db = self.network.channel_db
       -        for chan_id, chan in self.channels.items():
       -            self.network.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address())
       -            chan.lnwatcher = network.lnwatcher
       -        self._last_tried_peer = {}  # LNPeerAddr -> unix timestamp
       -        self._add_peers_from_config()
       -        # wait until we see confirmations
                self.network.register_callback(self.on_network_update, ['wallet_updated', 'network_updated', 'verified', 'fee'])  # thread safe
                self.network.register_callback(self.on_channel_open, ['channel_open'])
                self.network.register_callback(self.on_channel_closed, ['channel_closed'])
       -        asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(self.main_loop()), self.network.asyncio_loop)
       -        self.first_timestamp_requested = None
       +        for chan_id, chan in self.channels.items():
       +            self.network.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address())
       +            chan.lnwatcher = network.lnwatcher
       +        super().start_network(network)
       +        for coro in [
       +                self.maybe_listen(),
       +                self.on_network_update('network_updated'),  # shortcut (don't block) if funding tx locked and verified
       +                self.network.lnwatcher.on_network_update('network_updated'),  # ping watcher to check our channels
       +                self.reestablish_peers_and_channels()
       +        ]:
       +            asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(coro), self.network.asyncio_loop)
        
            def payment_completed(self, chan: Channel, direction: Direction,
                                  htlc: UpdateAddHtlc):
       t@@ -193,17 +356,6 @@ class LNWorker(PrintError):
                    item['balance_msat'] = balance_msat
                return out
        
       -    def _read_ln_keystore(self) -> BIP32_KeyStore:
       -        xprv = self.storage.get('lightning_privkey2')
       -        if xprv is None:
       -            # TODO derive this deterministically from wallet.keystore at keystore generation time
       -            # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ )
       -            seed = os.urandom(32)
       -            node = BIP32Node.from_rootseed(seed, xtype='standard')
       -            xprv = node.to_xprv()
       -            self.storage.put('lightning_privkey2', xprv)
       -        return keystore.from_xprv(xprv)
       -
            def get_and_inc_counter_for_channel_keys(self):
                with self.lock:
                    ctr = self.storage.get('lightning_channel_key_der_ctr', -1)
       t@@ -212,14 +364,6 @@ class LNWorker(PrintError):
                    self.storage.write()
                    return ctr
        
       -    def _add_peers_from_config(self):
       -        peer_list = self.config.get('lightning_peers', [])
       -        for host, port, pubkey in peer_list:
       -            asyncio.run_coroutine_threadsafe(
       -                self.add_peer(host, int(port), bfh(pubkey)),
       -                self.network.asyncio_loop)
       -
       -
            def suggest_peer(self):
                for node_id, peer in self.peers.items():
                    if not peer.initialized.is_set():
       t@@ -233,20 +377,6 @@ class LNWorker(PrintError):
                with self.lock:
                    return {x: y for (x, y) in self.channels.items() if y.node_id == node_id}
        
       -    async def add_peer(self, host, port, node_id):
       -        if node_id in self.peers:
       -            return self.peers[node_id]
       -        port = int(port)
       -        peer_addr = LNPeerAddr(host, port, node_id)
       -        transport = LNTransport(self.node_keypair.privkey, peer_addr)
       -        self._last_tried_peer[peer_addr] = time.time()
       -        self.print_error("adding peer", peer_addr)
       -        peer = Peer(self, node_id, transport)
       -        await self.network.main_taskgroup.spawn(peer.main_loop())
       -        self.peers[node_id] = peer
       -        self.network.trigger_callback('ln_status')
       -        return peer
       -
            def save_channel(self, openchannel):
                assert type(openchannel) is Channel
                if openchannel.config[REMOTE].next_per_commitment_point == openchannel.config[REMOTE].current_per_commitment_point:
       t@@ -692,77 +822,6 @@ class LNWorker(PrintError):
                await self.network.broadcast_transaction(tx)
                return tx.txid()
        
       -    def _get_next_peers_to_try(self) -> Sequence[LNPeerAddr]:
       -        now = time.time()
       -        recent_peers = self.channel_db.get_recent_peers()
       -        # maintenance for last tried times
       -        # due to this, below we can just test membership in _last_tried_peer
       -        for peer in list(self._last_tried_peer):
       -            if now >= self._last_tried_peer[peer] + PEER_RETRY_INTERVAL:
       -                del self._last_tried_peer[peer]
       -        # first try from recent peers
       -        for peer in recent_peers:
       -            if peer.pubkey in self.peers: continue
       -            if peer in self._last_tried_peer: continue
       -            return [peer]
       -        # try random peer from graph
       -        unconnected_nodes = self.channel_db.get_200_randomly_sorted_nodes_not_in(self.peers.keys())
       -        if unconnected_nodes:
       -            for node in unconnected_nodes:
       -                addrs = self.channel_db.get_node_addresses(node)
       -                if not addrs:
       -                    continue
       -                host, port = self.choose_preferred_address(addrs)
       -                peer = LNPeerAddr(host, port, bytes.fromhex(node.node_id))
       -                if peer in self._last_tried_peer: continue
       -                self.print_error('taking random ln peer from our channel db')
       -                return [peer]
       -
       -        # TODO remove this. For some reason the dns seeds seem to ignore the realm byte
       -        # and only return mainnet nodes. so for the time being dns seeding is disabled:
       -        if constants.net in (constants.BitcoinTestnet, ):
       -            return [random.choice(FALLBACK_NODE_LIST_TESTNET)]
       -        elif constants.net in (constants.BitcoinMainnet, ):
       -            return [random.choice(FALLBACK_NODE_LIST_MAINNET)]
       -        else:
       -            return []
       -
       -        # try peers from dns seed.
       -        # return several peers to reduce the number of dns queries.
       -        if not constants.net.LN_DNS_SEEDS:
       -            return []
       -        dns_seed = random.choice(constants.net.LN_DNS_SEEDS)
       -        self.print_error('asking dns seed "{}" for ln peers'.format(dns_seed))
       -        try:
       -            # note: this might block for several seconds
       -            # this will include bech32-encoded-pubkeys and ports
       -            srv_answers = resolve_dns_srv('r{}.{}'.format(
       -                constants.net.LN_REALM_BYTE, dns_seed))
       -        except dns.exception.DNSException as e:
       -            return []
       -        random.shuffle(srv_answers)
       -        num_peers = 2 * NUM_PEERS_TARGET
       -        srv_answers = srv_answers[:num_peers]
       -        # we now have pubkeys and ports but host is still needed
       -        peers = []
       -        for srv_ans in srv_answers:
       -            try:
       -                # note: this might block for several seconds
       -                answers = dns.resolver.query(srv_ans['host'])
       -            except dns.exception.DNSException:
       -                continue
       -            try:
       -                ln_host = str(answers[0])
       -                port = int(srv_ans['port'])
       -                bech32_pubkey = srv_ans['host'].split('.')[0]
       -                pubkey = get_compressed_pubkey_from_bech32(bech32_pubkey)
       -                peers.append(LNPeerAddr(ln_host, port, pubkey))
       -            except Exception as e:
       -                self.print_error('error with parsing peer from dns seed: {}'.format(e))
       -                continue
       -        self.print_error('got {} ln peers from dns seed'.format(len(peers)))
       -        return peers
       -
            async def reestablish_peers_and_channels(self):
                async def reestablish_peer_for_given_channel():
                    # try last good address first
       t@@ -784,21 +843,23 @@ class LNWorker(PrintError):
                    if last_tried + PEER_RETRY_INTERVAL_FOR_CHANNELS < now:
                        await self.add_peer(host, port, chan.node_id)
        
       -        with self.lock:
       -            channels = list(self.channels.values())
       -        now = time.time()
       -        for chan in channels:
       -            if chan.is_closed():
       -                continue
       -            if constants.net is not constants.BitcoinRegtest:
       -                ratio = chan.constraints.feerate / self.current_feerate_per_kw()
       -                if ratio < 0.5:
       -                    self.print_error(f"WARNING: fee level for channel {bh2u(chan.channel_id)} is {chan.constraints.feerate} sat/kiloweight, current recommended feerate is {self.current_feerate_per_kw()} sat/kiloweight, consider force closing!")
       -            if not chan.should_try_to_reestablish_peer():
       -                continue
       -            peer = self.peers.get(chan.node_id, None)
       -            coro = peer.reestablish_channel(chan) if peer else reestablish_peer_for_given_channel()
       -            await self.network.main_taskgroup.spawn(coro)
       +        while True:
       +            await asyncio.sleep(1)
       +            with self.lock:
       +                channels = list(self.channels.values())
       +            now = time.time()
       +            for chan in channels:
       +                if chan.is_closed():
       +                    continue
       +                if constants.net is not constants.BitcoinRegtest:
       +                    ratio = chan.constraints.feerate / self.current_feerate_per_kw()
       +                    if ratio < 0.5:
       +                        self.print_error(f"WARNING: fee level for channel {bh2u(chan.channel_id)} is {chan.constraints.feerate} sat/kiloweight, current recommended feerate is {self.current_feerate_per_kw()} sat/kiloweight, consider force closing!")
       +                if not chan.should_try_to_reestablish_peer():
       +                    continue
       +                peer = self.peers.get(chan.node_id, None)
       +                coro = peer.reestablish_channel(chan) if peer else reestablish_peer_for_given_channel()
       +                await self.network.main_taskgroup.spawn(coro)
        
            def current_feerate_per_kw(self):
                from .simple_config import FEE_LN_ETA_TARGET, FEERATE_FALLBACK_STATIC_FEE, FEERATE_REGTEST_HARDCODED
       t@@ -808,37 +869,3 @@ class LNWorker(PrintError):
                if feerate_per_kvbyte is None:
                    feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE
                return max(253, feerate_per_kvbyte // 4)
       -
       -    async def main_loop(self):
       -        await self.on_network_update('network_updated')  # shortcut (don't block) if funding tx locked and verified
       -        await self.network.lnwatcher.on_network_update('network_updated')  # ping watcher to check our channels
       -        listen_addr = self.config.get('lightning_listen')
       -        if listen_addr:
       -            addr, port = listen_addr.rsplit(':', 2)
       -            if addr[0] == '[':
       -                # ipv6
       -                addr = addr[1:-1]
       -            async def cb(reader, writer):
       -                transport = LNResponderTransport(self.node_keypair.privkey, reader, writer)
       -                try:
       -                    node_id = await transport.handshake()
       -                except:
       -                    self.print_error('handshake failure from incoming connection')
       -                    return
       -                peer = Peer(self, node_id, transport)
       -                self.peers[node_id] = peer
       -                await self.network.main_taskgroup.spawn(peer.main_loop())
       -                self.network.trigger_callback('ln_status')
       -
       -            await asyncio.start_server(cb, addr, int(port))
       -        while True:
       -            await asyncio.sleep(1)
       -            now = time.time()
       -            await self.reestablish_peers_and_channels()
       -            if len(self.peers) >= NUM_PEERS_TARGET:
       -                continue
       -            peers = self._get_next_peers_to_try()
       -            for peer in peers:
       -                last_tried = self._last_tried_peer.get(peer, 0)
       -                if last_tried + PEER_RETRY_INTERVAL < now:
       -                    await self.add_peer(peer.host, peer.port, peer.pubkey)
   DIR diff --git a/electrum/network.py b/electrum/network.py
       t@@ -298,10 +298,12 @@ class Network(Logger):
        
                # lightning network
                from . import lnwatcher
       +        from . import lnworker
                from . import lnrouter
                self.channel_db = lnrouter.ChannelDB(self)
                self.path_finder = lnrouter.LNPathFinder(self.channel_db)
                self.lnwatcher = lnwatcher.LNWatcher(self)
       +        self.lngossip = lnworker.LNGossip(self)
        
            def run_from_another_thread(self, coro):
                assert self._loop_thread != threading.current_thread(), 'must not be called from network thread'
       t@@ -1158,6 +1160,8 @@ class Network(Logger):
                asyncio.run_coroutine_threadsafe(main(), self.asyncio_loop)
        
                self.trigger_callback('network_updated')
       +        #
       +        self.lngossip.start_network(self)
        
            def start(self, jobs: List=None):
                self._jobs = jobs or []
   DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
       t@@ -14,9 +14,9 @@ from electrum.util import bh2u, set_verbosity, create_and_start_event_loop
        from electrum.lnpeer import Peer
        from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
        from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving
       -from electrum.lnutil import PaymentFailure
       +from electrum.lnutil import PaymentFailure, LnLocalFeatures
        from electrum.lnrouter import ChannelDB, LNPathFinder
       -from electrum.lnworker import LNWorker
       +from electrum.lnworker import LNWallet
        from electrum.lnmsg import encode_msg, decode_msg
        
        from .test_lnchannel import create_test_channels
       t@@ -74,7 +74,7 @@ class MockStorage:
        class MockWallet:
            storage = MockStorage()
        
       -class MockLNWorker:
       +class MockLNWallet:
            def __init__(self, remote_keypair, local_keypair, chan, tx_queue):
                self.chan = chan
                self.remote_keypair = remote_keypair
       t@@ -85,6 +85,7 @@ class MockLNWorker:
                self.preimages = {}
                self.inflight = {}
                self.wallet = MockWallet()
       +        self.localfeatures = LnLocalFeatures(0)
        
            @property
            def lock(self):
       t@@ -112,12 +113,12 @@ class MockLNWorker:
            def save_invoice(*args, is_paid=False):
                pass
        
       -    get_invoice = LNWorker.get_invoice
       -    get_preimage = LNWorker.get_preimage
       -    _create_route_from_invoice = LNWorker._create_route_from_invoice
       -    _check_invoice = staticmethod(LNWorker._check_invoice)
       -    _pay_to_route = LNWorker._pay_to_route
       -    force_close_channel = LNWorker.force_close_channel
       +    get_invoice = LNWallet.get_invoice
       +    get_preimage = LNWallet.get_preimage
       +    _create_route_from_invoice = LNWallet._create_route_from_invoice
       +    _check_invoice = staticmethod(LNWallet._check_invoice)
       +    _pay_to_route = LNWallet._pay_to_route
       +    force_close_channel = LNWallet.force_close_channel
            get_first_timestamp = lambda self: 0
        
        class MockTransport:
       t@@ -179,8 +180,8 @@ class TestPeer(SequentialTestCase):
                k1, k2 = keypair(), keypair()
                t1, t2 = transport_pair(self.alice_channel.name, self.bob_channel.name)
                q1, q2 = asyncio.Queue(), asyncio.Queue()
       -        w1 = MockLNWorker(k1, k2, self.alice_channel, tx_queue=q1)
       -        w2 = MockLNWorker(k2, k1, self.bob_channel, tx_queue=q2)
       +        w1 = MockLNWallet(k1, k2, self.alice_channel, tx_queue=q1)
       +        w2 = MockLNWallet(k2, k1, self.bob_channel, tx_queue=q2)
                p1 = Peer(w1, k1.pubkey, t1)
                p2 = Peer(w2, k2.pubkey, t2)
                w1.peer = p1
       t@@ -215,7 +216,7 @@ class TestPeer(SequentialTestCase):
            def prepare_ln_message_future(w2 # receiver
                    ):
                fut = asyncio.Future()
       -        def evt_set(event, _lnworker, msg, _htlc_id):
       +        def evt_set(event, _lnwallet, msg, _htlc_id):
                    fut.set_result(msg)
                w2.network.register_callback(evt_set, ['ln_message'])
                return fut
       t@@ -226,7 +227,7 @@ class TestPeer(SequentialTestCase):
                fut = self.prepare_ln_message_future(w2)
        
                async def pay():
       -            addr, peer, coro = await LNWorker._pay(w1, pay_req, same_thread=True)
       +            addr, peer, coro = await LNWallet._pay(w1, pay_req, same_thread=True)
                    await coro
                    print("HTLC ADDED")
                    self.assertEqual(await fut, 'Payment received')
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -64,7 +64,7 @@ from .interface import NetworkException
        from .ecc_fast import is_using_fast_ecc
        from .mnemonic import Mnemonic
        from .logging import get_logger
       -from .lnworker import LNWorker
       +from .lnworker import LNWallet
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -230,7 +230,7 @@ class Abstract_Wallet(AddressSynchronizer):
                    self.storage.put('wallet_type', self.wallet_type)
        
                # lightning
       -        self.lnworker = LNWorker(self)
       +        self.lnworker = LNWallet(self)
                # invoices and contacts
                self.invoices = InvoiceStore(self.storage)
                self.contacts = Contacts(self.storage)