diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index e1212a527..bd5c57de4 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -976,8 +976,14 @@ class AddressSynchronizer(Logger, EventListener): return coins def is_used(self, address: str) -> bool: + """Whether any tx ever touched `address`.""" return self.get_address_history_len(address) != 0 + def is_used_as_from_address(self, address: str) -> bool: + """Whether any tx ever spent from `address`.""" + received, sent = self.get_addr_io(address) + return len(sent) > 0 + def is_empty(self, address: str) -> bool: coins = self.get_addr_utxo(address) return not bool(coins) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index e14629699..6e85e5ff9 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -426,6 +426,7 @@ class TxEditor(WindowModalDialog): add_cv_action(self.config.cv.WALLET_MERGE_DUPLICATE_OUTPUTS, self.toggle_merge_duplicate_outputs) add_cv_action(self.config.cv.WALLET_SPEND_CONFIRMED_ONLY, self.toggle_confirmed_only) add_cv_action(self.config.cv.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING, self.toggle_output_rounding) + add_cv_action(self.config.cv.WALLET_FREEZE_REUSED_ADDRESS_UTXOS, self.toggle_freeze_reused_address_utxos) self.pref_button = QToolButton() self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) @@ -448,6 +449,13 @@ class TxEditor(WindowModalDialog): self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = b self.trigger_update() + def toggle_freeze_reused_address_utxos(self): + b = not self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS + self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS = b + self.trigger_update() + self.main_window.utxo_list.refresh_all() # for coin frozen status + self.main_window.update_status() # frozen balance + def toggle_use_change(self): self.wallet.use_change = not self.wallet.use_change self.wallet.db.put('use_change', self.wallet.use_change) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 66d7d55c8..c9eaea2a0 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -605,6 +605,13 @@ class SimpleConfig(Logger): short_desc=lambda: _('Send change to Lightning'), long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'), ) + WALLET_FREEZE_REUSED_ADDRESS_UTXOS = ConfigVar( + 'wallet_freeze_reused_address_utxos', default=False, type_=bool, + short_desc=lambda: _('Avoid spending from used addresses'), + long_desc=lambda: _("""Automatically freeze coins received to already used addresses. +This can eliminate a serious privacy issue where a malicious user can track your spends by sending small payments +to a previously-paid address of yours that would then be included with unrelated inputs in your future payments."""), + ) FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool) FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) diff --git a/electrum/wallet.py b/electrum/wallet.py index 7cd2b77b1..6e941074b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1974,6 +1974,10 @@ class Abstract_Wallet(ABC, Logger, EventListener): # State not set. We implicitly mark certain coins as frozen: if self._is_coin_small_and_unconfirmed(utxo): return True + addr = utxo.address + assert addr is not None + if self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS and self.adb.is_used_as_from_address(addr): + return True return False def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool: