From 85fc95c71b05d7cd63b23f4b86b295e2395e4a94 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 15 Jul 2025 13:14:38 +0000 Subject: [PATCH] wallet_db: add configvar for partial_writes, disable by default Adds a new configvar `WALLET_PARTIAL_WRITES` to enable/disable partial writes for the walletDB. This is a further restriction on top of the existing restrictions, e.g. wallet files still need to have file encryption disabled for partial writes. It defaults to off, so even for unencrypted wallets we disable partial writes for now. This is used as a stopgap measure until we fix the issues found with the partial writes impl (see https://github.com/spesmilo/electrum/issues/10000). --- electrum/daemon.py | 2 +- electrum/json_db.py | 6 +----- electrum/simple_config.py | 5 +++++ electrum/storage.py | 20 ++++++++++++++++++-- electrum/wallet.py | 4 ++-- electrum/wallet_db.py | 8 +++++++- run_electrum | 4 ++-- 7 files changed, 36 insertions(+), 13 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index d5745e403..1d50ceb04 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -507,7 +507,7 @@ class Daemon(Logger): config: SimpleConfig, ) -> Optional[Abstract_Wallet]: path = standardize_path(path) - storage = WalletStorage(path) + storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES) if not storage.file_exists(): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path) if storage.is_encrypted(): diff --git a/electrum/json_db.py b/electrum/json_db.py index 56f635ba3..a79f691ae 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -421,11 +421,7 @@ class JsonDB(Logger): @locked def write(self): - if ( - not self.storage.file_exists() - or self.storage.is_encrypted() - or self.storage.needs_consolidation() - ): + if self.storage.should_do_full_write_next(): self.write_and_force_consolidation() else: self._append_pending_changes() diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 971f17ab4..fc8312a8c 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -687,6 +687,11 @@ class SimpleConfig(Logger): 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."""), ) + WALLET_PARTIAL_WRITES = ConfigVar( + 'wallet_partial_writes', default=False, type_=bool, + long_desc=lambda: _("""Allows partial updates to be written to disk for the wallet DB. +If disabled, the full wallet file is written to disk for every change. Experimental."""), + ) FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool) FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) diff --git a/electrum/storage.py b/electrum/storage.py index a5806cf0e..b7d2a6bdb 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -64,11 +64,17 @@ class WalletStorage(Logger): # TODO maybe split this into separate create() and open() classmethods, to prevent some bugs. # Until then, the onus is on the caller to check file_exists(). - def __init__(self, path): + def __init__( + self, + path, + *, + allow_partial_writes: bool = False, + ): Logger.__init__(self) self.path = standardize_path(path) self._file_exists = bool(self.path and os.path.exists(self.path)) self.logger.info(f"wallet path {self.path}") + self._allow_partial_writes = allow_partial_writes self.pubkey = None self.decrypted = '' try: @@ -112,6 +118,7 @@ class WalletStorage(Logger): def append(self, data: str) -> None: """ append data to file. for the moment, only non-encrypted file""" + assert self._allow_partial_writes assert not self.is_encrypted() with open(self.path, "rb+") as f: pos = f.seek(0, os.SEEK_END) @@ -122,9 +129,18 @@ class WalletStorage(Logger): f.flush() os.fsync(f.fileno()) - def needs_consolidation(self): + def _needs_consolidation(self): return self.pos > 2 * self.init_pos + def should_do_full_write_next(self) -> bool: + """If false, next action can be a partial-write ('append').""" + return ( + not self.file_exists() + or self.is_encrypted() + or self._needs_consolidation() + or not self._allow_partial_writes + ) + def file_exists(self) -> bool: return self._file_exists diff --git a/electrum/wallet.py b/electrum/wallet.py index 0a4608eec..9c8c46d05 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -4189,7 +4189,7 @@ def create_new_wallet( gap_limit: Optional[int] = None ) -> dict: """Create a new wallet""" - storage = WalletStorage(path) + storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES) if storage.file_exists(): raise UserFacingException("Remove the existing wallet first!") db = WalletDB('', storage=storage, upgrade=True) @@ -4226,7 +4226,7 @@ def restore_wallet_from_text( if path is None: # create wallet in-memory storage = None else: - storage = WalletStorage(path) + storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES) if storage.file_exists(): raise UserFacingException("Remove the existing wallet first!") if encrypt_file is None: diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 6aedd4741..8f37430dc 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -1289,7 +1289,13 @@ class WalletDB(JsonDB): storage: Optional['WalletStorage'] = None, upgrade: bool = False, ): - JsonDB.__init__(self, s, storage=storage, encoder=MyEncoder, upgrader=partial(upgrade_wallet_db, do_upgrade=upgrade)) + JsonDB.__init__( + self, + s, + storage=storage, + encoder=MyEncoder, + upgrader=partial(upgrade_wallet_db, do_upgrade=upgrade), + ) # create pointers self.load_transactions() # load plugins that are conditional on wallet type diff --git a/run_electrum b/run_electrum index 7510f590b..fc50a584e 100755 --- a/run_electrum +++ b/run_electrum @@ -151,7 +151,7 @@ def init_cmdline(config_options, wallet_path, *, rpcserver: bool, config: 'Simpl sys_exit(1) # instantiate wallet for command-line - storage = WalletStorage(wallet_path) if wallet_path else None + storage = WalletStorage(wallet_path, allow_partial_writes=config.WALLET_PARTIAL_WRITES) if wallet_path else None if cmd.requires_wallet and not storage.file_exists(): print_msg("Error: Wallet file not found.") @@ -234,7 +234,7 @@ async def run_offline_command(config: 'SimpleConfig', config_options: dict, wall if 'wallet_path' in cmd.options and config_options.get('wallet_path') is None: config_options['wallet_path'] = wallet_path if cmd.requires_wallet: - storage = WalletStorage(wallet_path) + storage = WalletStorage(wallet_path, allow_partial_writes=config.WALLET_PARTIAL_WRITES) if storage.is_encrypted(): if storage.is_encrypted_with_hw_device(): password = get_password_for_hw_device_encrypted_storage(plugins)