electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 6040e953a32803a65a10f62d4f7ac815d1a9e590
parent 6457bb141dc2e4848c932c7478ab21b4424ba502
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:
Melectrum/lnchannel.py | 10+++++++++-
Melectrum/lnpeer.py | 2+-
Melectrum/lnworker.py | 9+++++++--
Melectrum/wallet.py | 34+++++++++++++++++++++++++++++++++-
4 files changed, 50 insertions(+), 5 deletions(-)

diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -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 @@ -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) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -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 diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -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 @@ -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, @@ -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) @@ -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) diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -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() @@ -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: @@ -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: @@ -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')