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)