URI: 
       twallet: implement reserving addresses, and use it for LN SRK to_remote - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 6040e953a32803a65a10f62d4f7ac815d1a9e590
   DIR parent 6457bb141dc2e4848c932c7478ab21b4424ba502
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Fri, 22 May 2020 15:34:30 +0200
       
       wallet: implement reserving addresses, and use it for LN SRK to_remote
       
       - Use change addresses (instead of receive) for the static_remotekey to_remote outputs,
         and reserve these to greatly reduce the chance of address-reuse
       - Use change addresses (instead of receive) for LN channel sweep addresses.
         Note that these atm are not getting reserved.
       
       Diffstat:
         M electrum/lnchannel.py               |      10 +++++++++-
         M electrum/lnpeer.py                  |       2 +-
         M electrum/lnworker.py                |       9 +++++++--
         M electrum/wallet.py                  |      34 ++++++++++++++++++++++++++++++-
       
       4 files changed, 50 insertions(+), 5 deletions(-)
       ---
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -51,7 +51,7 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
                             ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script,
                             ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr,
                             LN_MAX_HTLC_VALUE_MSAT, fee_for_htlc_output, offered_htlc_trim_threshold_sat,
       -                     received_htlc_trim_threshold_sat)
       +                     received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address)
        from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx
        from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo
        from .lnhtlc import HTLCManager
       t@@ -601,6 +601,14 @@ class Channel(AbstractChannel):
            def is_static_remotekey_enabled(self) -> bool:
                return bool(self.storage.get('static_remotekey_enabled'))
        
       +    def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
       +        ret = []
       +        if self.is_static_remotekey_enabled():
       +            our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey
       +            to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
       +            ret.append(to_remote_address)
       +        return ret
       +
            def get_feerate(self, subject: HTLCOwner, *, ctn: int) -> int:
                # returns feerate in sat/kw
                return self.hm.get_feerate(subject, ctn)
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -496,7 +496,7 @@ class Peer(Logger):
                    # we will want to derive that key
                    wallet = self.lnworker.wallet
                    assert wallet.txin_type == 'p2wpkh'
       -            addr = wallet.get_unused_address()
       +            addr = wallet.get_new_sweep_address_for_channel()
                    static_remotekey = bfh(wallet.get_public_key(addr))
                else:
                    static_remotekey = None
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -488,7 +488,7 @@ class LNWallet(LNWorker):
                self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
                self.payments = self.db.get_dict('lightning_payments')     # RHASH -> amount, direction, is_paid
                self.preimages = self.db.get_dict('lightning_preimages')   # RHASH -> preimage
       -        self.sweep_address = wallet.get_receiving_address()
       +        self.sweep_address = wallet.get_new_sweep_address_for_channel()  # TODO possible address-reuse
                self.logs = defaultdict(list)  # type: Dict[str, List[PaymentAttemptLog]]  # key is RHASH  # (not persisted)
                self.is_routing = set()        # (not persisted) keys of invoices that are in PR_ROUTING state
                # used in tests
       t@@ -770,6 +770,8 @@ class LNWallet(LNWorker):
                self.add_channel(chan)
                channels_db = self.db.get_dict('channels')
                channels_db[chan.channel_id.hex()] = chan.storage
       +        for addr in chan.get_wallet_addresses_channel_might_want_reserved():
       +            self.wallet.set_reserved_state_of_address(addr, reserved=True)
                self.wallet.save_backup()
        
            def mktx_for_open_channel(self, *, coins: Sequence[PartialTxInput], funding_sat: int,
       t@@ -1309,6 +1311,8 @@ class LNWallet(LNWorker):
                with self.lock:
                    self._channels.pop(chan_id)
                    self.db.get('channels').pop(chan_id.hex())
       +        for addr in chan.get_wallet_addresses_channel_might_want_reserved():
       +            self.wallet.set_reserved_state_of_address(addr, reserved=False)
        
                util.trigger_callback('channels_updated', self.wallet)
                util.trigger_callback('wallet_updated', self.wallet)
       t@@ -1404,7 +1408,8 @@ class LNBackups(Logger):
        
            @property
            def sweep_address(self) -> str:
       -        return self.wallet.get_receiving_address()
       +        # TODO possible address-reuse
       +        return self.wallet.get_new_sweep_address_for_channel()
        
            def channel_state_changed(self, chan):
                util.trigger_callback('channel', chan)
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -247,6 +247,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                self.fiat_value            = db.get_dict('fiat_value')
                self.receive_requests      = db.get_dict('payment_requests')
                self.invoices              = db.get_dict('invoices')
       +        self._reserved_addresses   = set(db.get('reserved_addresses', []))
        
                self._prepare_onchain_invoice_paid_detection()
                self.calc_unused_change_addresses()
       t@@ -386,7 +387,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                        addrs = self._unused_change_addresses
                    else:
                        addrs = self.get_change_addresses()
       -            self._unused_change_addresses = [addr for addr in addrs if not self.is_used(addr)]
       +            self._unused_change_addresses = [addr for addr in addrs
       +                                             if not self.is_used(addr) and not self.is_address_reserved(addr)]
                    return list(self._unused_change_addresses)
        
            def is_deterministic(self) -> bool:
       t@@ -1046,6 +1048,22 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                max_change = self.max_change_outputs if self.multiple_change else 1
                return change_addrs[:max_change]
        
       +    @check_returned_address_for_corruption
       +    def get_new_sweep_address_for_channel(self) -> str:
       +        # Recalc and get unused change addresses
       +        addrs = self.calc_unused_change_addresses()
       +        if addrs:
       +            selected_addr = addrs[0]
       +        else:
       +            # if there are none, take one randomly from the last few
       +            addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
       +            if addrs:
       +                selected_addr = random.choice(addrs)
       +            else:  # fallback for e.g. imported wallets
       +                selected_addr = self.get_receiving_address()
       +        assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
       +        return selected_addr
       +
            def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
                                          outputs: List[PartialTxOutput], fee=None,
                                          change_addr: str = None, is_sweep=False) -> PartialTransaction:
       t@@ -1182,6 +1200,20 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                    self.frozen_coins -= set(utxos)
                self.db.put('frozen_coins', list(self.frozen_coins))
        
       +    def is_address_reserved(self, addr: str) -> bool:
       +        # note: atm 'reserved' status is only taken into consideration for 'change addresses'
       +        return addr in self._reserved_addresses
       +
       +    def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:
       +        if not self.is_mine(addr):
       +            return
       +        with self.lock:
       +            if reserved:
       +                self._reserved_addresses.add(addr)
       +            else:
       +                self._reserved_addresses.discard(addr)
       +        self.db.put('reserved_addresses', list(self._reserved_addresses))
       +
            def can_export(self):
                return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')