From b9a81cd03e5cef379558dfffba8dc3d787a22a13 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 20 Oct 2024 10:28:57 +0000 Subject: [PATCH] lnworker: reserve wallet addresses also for chan backups We were already reserving wallet addresses for full channels. Now we also do the same for imported channel backups. (but not for onchain, as we don't have enough info for that) Without this, if the same seed is used on multiple devices (with each device having its own set of LN channels), the wallet instances will reuse keys (specifically the payment_basepoint, which for static_remotekey chans is used as the to_remote output). Now with this change, at least if the wallet instances have imported channel backups of each other, this reuse is avoided. --- electrum/lnchannel.py | 21 +++++++++++++++++++++ electrum/lnworker.py | 15 +++++++++------ electrum/wallet.py | 10 +++++++++- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index e417a4534..7363b7d48 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -487,6 +487,14 @@ class AbstractChannel(Logger, ABC): def can_be_deleted(self) -> bool: pass + @abstractmethod + def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: + """Returns a list of addrs that the wallet should not use, to avoid address-reuse. + Typically, these addresses are wallet.is_mine, but that is not guaranteed, + in which case the wallet can just ignore those. + """ + pass + class ChannelBackup(AbstractChannel): """ @@ -638,6 +646,19 @@ class ChannelBackup(AbstractChannel): ret.append(ChanCloseOption.REQUEST_REMOTE_FCLOSE) return ret + def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: + if self.is_imported: + # For v1 imported cbs, we have the local_payment_pubkey, which is + # directly used as p2wpkh() of static_remotekey channels. + # (for v0 imported cbs, the correct local_payment_pubkey is missing, and so + # we might calculate a different address here, which might not be wallet.is_mine, + # but that should be harmless) + our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey + to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey) + return [to_remote_address] + else: # on-chain backup + return [] + class Channel(AbstractChannel): # note: try to avoid naming ctns/ctxs/etc as "current" and "pending". diff --git a/electrum/lnworker.py b/electrum/lnworker.py index c0be79bf0..b54f870f2 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -840,14 +840,16 @@ class LNWallet(LNWorker): self._channels = {} # type: Dict[bytes, Channel] channels = self.db.get_dict("channels") for channel_id, c in random_shuffled_copy(channels.items()): - self._channels[bfh(channel_id)] = Channel(c, lnworker=self) + self._channels[bfh(channel_id)] = chan = Channel(c, lnworker=self) + self.wallet.set_reserved_addresses_for_chan(chan, reserved=True) self._channel_backups = {} # type: Dict[bytes, ChannelBackup] # order is important: imported should overwrite onchain for name in ["onchain_channel_backups", "imported_channel_backups"]: channel_backups = self.db.get_dict(name) for channel_id, storage in channel_backups.items(): - self._channel_backups[bfh(channel_id)] = ChannelBackup(storage, lnworker=self) + self._channel_backups[bfh(channel_id)] = cb = ChannelBackup(storage, lnworker=self) + self.wallet.set_reserved_addresses_for_chan(cb, reserved=True) self._paysessions = dict() # type: Dict[bytes, PaySession] self.sent_htlcs_info = dict() # type: Dict[SentHtlcKey, SentHtlcInfo] @@ -1330,8 +1332,7 @@ 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.set_reserved_addresses_for_chan(chan, reserved=True) try: self.save_channel(chan) except Exception: @@ -2850,8 +2851,7 @@ 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) + self.wallet.set_reserved_addresses_for_chan(chan, reserved=False) util.trigger_callback('channels_updated', self.wallet) util.trigger_callback('wallet_updated', self.wallet) @@ -2984,6 +2984,7 @@ class LNWallet(LNWorker): with self.lock: cb = ChannelBackup(cb_storage, lnworker=self) self._channel_backups[channel_id] = cb + self.wallet.set_reserved_addresses_for_chan(cb, reserved=True) self.wallet.save_db() util.trigger_callback('channels_updated', self.wallet) self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) @@ -3011,6 +3012,7 @@ class LNWallet(LNWorker): raise Exception('Channel not found') with self.lock: self._channel_backups.pop(channel_id) + self.wallet.set_reserved_addresses_for_chan(chan, reserved=False) self.wallet.save_db() util.trigger_callback('channels_updated', self.wallet) @@ -3097,6 +3099,7 @@ class LNWallet(LNWorker): d = self.db.get_dict("onchain_channel_backups") d[channel_id] = cb_storage cb = ChannelBackup(cb_storage, lnworker=self) + self.wallet.set_reserved_addresses_for_chan(cb, reserved=True) self.wallet.save_db() with self.lock: self._channel_backups[bfh(channel_id)] = cb diff --git a/electrum/wallet.py b/electrum/wallet.py index ed146b7b2..7efdefe7e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -93,6 +93,7 @@ if TYPE_CHECKING: from .network import Network from .exchange_rate import FxThread from .submarine_swaps import SwapData + from .lnchannel import AbstractChannel _logger = get_logger(__name__) @@ -2033,13 +2034,20 @@ class Abstract_Wallet(ABC, Logger, EventListener): def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None: if not self.is_mine(addr): + # silently ignore non-ismine addresses return with self.lock: + has_changed = (addr in self._reserved_addresses) != reserved if reserved: self._reserved_addresses.add(addr) else: self._reserved_addresses.discard(addr) - self.db.put('reserved_addresses', list(self._reserved_addresses)) + if has_changed: + self.db.put('reserved_addresses', list(self._reserved_addresses)) + + def set_reserved_addresses_for_chan(self, chan: 'AbstractChannel', *, reserved: bool) -> None: + for addr in chan.get_wallet_addresses_channel_might_want_reserved(): + self.set_reserved_state_of_address(addr, reserved=reserved) def can_export(self): return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')