tmake our channels private, and put routing hints in invoices we create - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit 029ec5a5ab4546f8ecd03119cde567208952edd0
 (DIR) parent 09c3e52e62d1bcc1ed46c7b48ecda2105c7ad50c
 (HTM) Author: SomberNight <somber.night@protonmail.com>
       Date:   Mon,  8 Oct 2018 20:31:15 +0200
       
       make our channels private, and put routing hints in invoices we create
       
       Diffstat:
         M electrum/lnbase.py                  |      46 +++++++++++++++++++++++++------
         M electrum/lnhtlc.py                  |       1 +
         M electrum/lnrouter.py                |      11 +++++++----
         M electrum/lnwatcher.py               |       1 +
         M electrum/lnworker.py                |      52 +++++++++++++++++++++++--------
       
       5 files changed, 85 insertions(+), 26 deletions(-)
       ---
 (DIR) diff --git a/electrum/lnbase.py b/electrum/lnbase.py
       t@@ -12,6 +12,7 @@ import time
        import hashlib
        import hmac
        from functools import partial
       +from typing import List
        
        import cryptography.hazmat.primitives.ciphers.aead as AEAD
        import aiorpcx
       t@@ -31,6 +32,7 @@ from .lnutil import (Outpoint, ChannelConfig, LocalState,
                             funding_output_script, get_ecdh, get_per_commitment_secret_from_seed,
                             secret_to_pubkey, LNPeerAddr, PaymentFailure,
                             LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily)
       +from .lnrouter import NotFoundChanAnnouncementForUpdate, RouteEdge
        
        
        def channel_id_from_funding_tx(funding_txid, funding_index):
       t@@ -443,7 +445,16 @@ class Peer(PrintError):
                pass
        
            def on_channel_update(self, payload):
       -        self.channel_db.on_channel_update(payload)
       +        try:
       +            self.channel_db.on_channel_update(payload)
       +        except NotFoundChanAnnouncementForUpdate:
       +            # If it's for a direct channel with this peer, save it in chan.
       +            # Note that this is prone to a race.. we might not have a short_channel_id
       +            # associated with the channel in some cases
       +            short_channel_id = payload['short_channel_id']
       +            for chan in self.channels.values():
       +                if chan.short_channel_id_predicted == short_channel_id:
       +                    chan.pending_channel_update_message = payload
        
            def on_channel_announcement(self, payload):
                self.channel_db.on_channel_announcement(payload)
       t@@ -550,7 +561,7 @@ class Peer(PrintError):
                    first_per_commitment_point=per_commitment_point_first,
                    to_self_delay=local_config.to_self_delay,
                    max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat,
       -            channel_flags=0x01, # publicly announcing channel
       +            channel_flags=0x00,  # not willing to announce channel
                    channel_reserve_satoshis=546
                )
                self.send_message(msg)
       t@@ -833,6 +844,9 @@ class Peer(PrintError):
                Runs on the Network thread.
                """
                if not chan.local_state.was_announced and funding_tx_depth >= 6:
       +            # don't announce our channels
       +            # FIXME should this be a field in chan.local_state maybe?
       +            return
                    chan.local_state=chan.local_state._replace(was_announced=True)
                    coro = self.handle_announcements(chan)
                    self.lnworker.save_channel(chan)
       t@@ -887,25 +901,39 @@ class Peer(PrintError):
                chan.set_state("OPEN")
                self.network.trigger_callback('channel', chan)
                # add channel to database
       -        node_ids = [self.pubkey, self.lnworker.node_keypair.pubkey]
       +        pubkey_ours = self.lnworker.node_keypair.pubkey
       +        pubkey_theirs = self.pubkey
       +        node_ids = [pubkey_theirs, pubkey_ours]
                bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey]
                sorted_node_ids = list(sorted(node_ids))
                if sorted_node_ids != node_ids:
                    node_ids = sorted_node_ids
                    bitcoin_keys.reverse()
       -        now = int(time.time()).to_bytes(4, byteorder="big")
       +        # note: we inject a channel announcement, and a channel update (for outgoing direction)
       +        # This is atm needed for
       +        # - finding routes
       +        # - the ChanAnn is needed so that we can anchor to it a future ChanUpd
       +        #   that the remote sends, even if the channel was not announced
       +        #   (from BOLT-07: "MAY create a channel_update to communicate the channel
       +        #    parameters to the final node, even though the channel has not yet been announced")
                self.channel_db.on_channel_announcement({"short_channel_id": chan.short_channel_id, "node_id_1": node_ids[0], "node_id_2": node_ids[1],
                                                         'chain_hash': constants.net.rev_genesis_bytes(), 'len': b'\x00\x00', 'features': b'',
                                                         'bitcoin_key_1': bitcoin_keys[0], 'bitcoin_key_2': bitcoin_keys[1]},
                                                        trusted=True)
       -        self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'flags': b'\x01', 'cltv_expiry_delta': b'\x90',
       -                                           'htlc_minimum_msat': b'\x03\xe8', 'fee_base_msat': b'\x03\xe8', 'fee_proportional_millionths': b'\x01',
       -                                           'chain_hash': constants.net.rev_genesis_bytes(), 'timestamp': now},
       -                                          trusted=True)
       -        self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'flags': b'\x00', 'cltv_expiry_delta': b'\x90',
       +        # only inject outgoing direction:
       +        flags = b'\x00' if node_ids[0] == pubkey_ours else b'\x01'
       +        now = int(time.time()).to_bytes(4, byteorder="big")
       +        self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'flags': flags, 'cltv_expiry_delta': b'\x90',
                                                   'htlc_minimum_msat': b'\x03\xe8', 'fee_base_msat': b'\x03\xe8', 'fee_proportional_millionths': b'\x01',
                                                   'chain_hash': constants.net.rev_genesis_bytes(), 'timestamp': now},
                                                  trusted=True)
       +        # peer may have sent us a channel update for the incoming direction previously
       +        # note: if we were offline when the 3rd conf happened, lnd will never send us this channel_update
       +        # see https://github.com/lightningnetwork/lnd/issues/1347
       +        #self.send_message(gen_msg("query_short_channel_ids", chain_hash=constants.net.rev_genesis_bytes(),
       +        #                          len=9, encoded_short_ids=b'\x00'+chan.short_channel_id))
       +        if hasattr(chan, 'pending_channel_update_message'):
       +            self.on_channel_update(chan.pending_channel_update_message)
        
                self.print_error("CHANNEL OPENING COMPLETED")
        
 (DIR) diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py
       t@@ -143,6 +143,7 @@ class HTLCStateMachine(PrintError):
                self.funding_outpoint = Outpoint(**decodeAll(state["funding_outpoint"])) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"]
                self.node_id = maybeDecode("node_id", state["node_id"]) if type(state["node_id"]) is not bytes else state["node_id"]
                self.short_channel_id = maybeDecode("short_channel_id", state["short_channel_id"]) if type(state["short_channel_id"]) is not bytes else state["short_channel_id"]
       +        self.short_channel_id_predicted = self.short_channel_id
                self.onion_keys = {int(k): bfh(v) for k,v in state['onion_keys'].items()} if 'onion_keys' in state else {}
        
                # FIXME this is a tx serialised in the custom electrum partial tx format.
 (DIR) diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
       t@@ -45,6 +45,9 @@ from .lnutil import LN_GLOBAL_FEATURE_BITS, LNPeerAddr
        class UnknownEvenFeatureBits(Exception): pass
        
        
       +class NotFoundChanAnnouncementForUpdate(Exception): pass
       +
       +
        class ChannelInfo(PrintError):
        
            def __init__(self, channel_announcement_payload):
       t@@ -126,7 +129,7 @@ class ChannelInfo(PrintError):
                else:
                    self.policy_node2 = new_policy
        
       -    def get_policy_for_node(self, node_id):
       +    def get_policy_for_node(self, node_id: bytes) -> 'ChannelInfoDirectedPolicy':
                if node_id == self.node_id_1:
                    return self.policy_node1
                elif node_id == self.node_id_2:
       t@@ -271,7 +274,7 @@ class ChannelDB(JsonDB):
                JsonDB.__init__(self, path)
        
                self.lock = threading.RLock()
       -        self._id_to_channel_info = {}
       +        self._id_to_channel_info = {}  # type: Dict[bytes, ChannelInfo]
                self._channels_for_node = defaultdict(set)  # node -> set(short_channel_id)
                self.nodes = {}  # node_id -> NodeInfo
                self._recent_peers = []
       t@@ -340,7 +343,7 @@ class ChannelDB(JsonDB):
                # number of channels
                return len(self._id_to_channel_info)
        
       -    def get_channel_info(self, channel_id) -> Optional[ChannelInfo]:
       +    def get_channel_info(self, channel_id: bytes) -> Optional[ChannelInfo]:
                return self._id_to_channel_info.get(channel_id, None)
        
            def get_channels_for_node(self, node_id):
       t@@ -401,7 +404,7 @@ class ChannelDB(JsonDB):
                    channel_info = self._id_to_channel_info.get(short_channel_id, None)
                if channel_info is None:
                    self.print_error("could not find", short_channel_id)
       -            return
       +            raise NotFoundChanAnnouncementForUpdate()
                channel_info.on_channel_update(msg_payload, trusted=trusted)
        
            def on_node_announcement(self, msg_payload):
 (DIR) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
       t@@ -24,6 +24,7 @@ class LNWatcher(PrintError):
                path = os.path.join(network.config.path, "watcher_db")
                storage = WalletStorage(path)
                self.addr_sync = AddressSynchronizer(storage)
       +        self.addr_sync.diagnostic_name = lambda: 'LnWatcherAS'
                self.addr_sync.start_network(network)
                self.lock = threading.RLock()
                self.watched_addresses = set()
 (DIR) diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -48,8 +48,8 @@ class LNWorker(PrintError):
                self.ln_keystore = self._read_ln_keystore()
                self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
                self.config = network.config
       -        self.peers = {}  # pubkey -> Peer
       -        self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))}
       +        self.peers = {}  # type: Dict[bytes, Peer]  # pubkey -> Peer
       +        self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))}  # type: Dict[bytes, HTLCStateMachine]
                for c in self.channels.values():
                    c.lnwatcher = network.lnwatcher
                    c.sweep_address = self.sweep_address
       t@@ -126,21 +126,19 @@ class LNWorker(PrintError):
        
            def save_short_chan_id(self, chan):
                """
       -        Checks if the Funding TX has been mined. If it has save the short channel ID to disk and return the new OpenChannel.
       -
       -        If the Funding TX has not been mined, return None
       +        Checks if Funding TX has been mined. If it has, save the short channel ID in chan;
       +        if it's also deep enough, also save to disk.
       +        Returns tuple (mined_deep_enough, num_confirmations).
                """
                assert chan.get_state() in ["OPEN", "OPENING"]
       -        peer = self.peers[chan.node_id]
                addr_sync = self.network.lnwatcher.addr_sync
                conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf
       -        if conf >= chan.constraints.funding_txn_minimum_depth:
       +        if conf > 0:
                    block_height, tx_pos = addr_sync.get_txpos(chan.funding_outpoint.txid)
       -            if tx_pos == -1:
       -                self.print_error('funding tx is not yet SPV verified.. but there are '
       -                                 'already enough confirmations (currently {})'.format(conf))
       -                return False, conf
       -            chan.short_channel_id = calc_short_channel_id(block_height, tx_pos, chan.funding_outpoint.output_index)
       +            assert tx_pos >= 0
       +            chan.short_channel_id_predicted = calc_short_channel_id(block_height, tx_pos, chan.funding_outpoint.output_index)
       +        if conf >= chan.constraints.funding_txn_minimum_depth > 0:
       +            chan.short_channel_id = chan.short_channel_id_predicted
                    self.save_channel(chan)
                    return True, conf
                return False, conf
       t@@ -244,6 +242,7 @@ class LNWorker(PrintError):
                if amount_sat is None:
                    raise InvoiceError(_("Missing amount"))
                amount_msat = int(amount_sat * 1000)
       +        # TODO use 'r' field from invoice
                path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat)
                if path is None:
                    raise PaymentFailure(_("No path found"))
       t@@ -263,12 +262,39 @@ class LNWorker(PrintError):
                payment_preimage = os.urandom(32)
                RHASH = sha256(payment_preimage)
                amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
       -        pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]), self.node_keypair.privkey)
       +        routing_hints = self._calc_routing_hints_for_invoice(amount_sat)
       +        pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]+routing_hints),
       +                           self.node_keypair.privkey)
                self.invoices[bh2u(payment_preimage)] = pay_req
                self.wallet.storage.put('lightning_invoices', self.invoices)
                self.wallet.storage.write()
                return pay_req
        
       +    def _calc_routing_hints_for_invoice(self, amount_sat):
       +        """calculate routing hints (BOLT-11 'r' field)"""
       +        routing_hints = []
       +        with self.lock:
       +            channels = list(self.channels.values())
       +        # note: currently we add *all* our channels; but this might be a privacy leak?
       +        for chan in channels:
       +            # check channel is open
       +            if chan.get_state() != "OPEN": continue
       +            # check channel has sufficient balance
       +            # FIXME because of on-chain fees of ctx, this check is insufficient
       +            if amount_sat and chan.balance(REMOTE) // 1000 < amount_sat: continue
       +            chan_id = chan.short_channel_id
       +            assert type(chan_id) is bytes, chan_id
       +            channel_info = self.channel_db.get_channel_info(chan_id)
       +            if not channel_info: continue
       +            policy = channel_info.get_policy_for_node(chan.node_id)
       +            if not policy: continue
       +            routing_hints.append(('r', [(chan.node_id,
       +                                         chan_id,
       +                                         policy.fee_base_msat,
       +                                         policy.fee_proportional_millionths,
       +                                         policy.cltv_expiry_delta)]))
       +        return routing_hints
       +
            def delete_invoice(self, payreq_key):
                try:
                    del self.invoices[payreq_key]