From d52762a2e84c5d07d75030ebad162f41593a1982 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 00:24:40 +0000 Subject: [PATCH] wallet: add new config option "FREEZE_REUSED_ADDRESS_UTXOS" Adds a new config option: `WALLET_FREEZE_REUSED_ADDRESS_UTXOS`. This is based on Bitcoin Core's "avoid_reuse" wallet flag. [0] This opt-in feature, if enabled: > 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. Note that currently we only have a single coinchooser policy, `CoinChooserPrivacy`, which interacts well with this option, as it spends all coins from any selected address. However, if we later add a different coinchooser policy, which allowed "partial spends", care should be taken re e.g. disallowing using that when this option is set. Also note that this PR adds this as a config option, but arguably it could be wallet-specific instead, such as `use_change`. [0]: https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-0.19.0.1.md#wallet closes https://github.com/spesmilo/electrum/issues/7497 --- electrum/address_synchronizer.py | 6 ++++++ electrum/gui/qt/confirm_tx_dialog.py | 8 ++++++++ electrum/simple_config.py | 7 +++++++ electrum/wallet.py | 4 ++++ 4 files changed, 25 insertions(+) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 1d559a762..37b2533bd 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -966,8 +966,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 cdaec1f2d..fa73b048c 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1968,6 +1968,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: