1
0

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).
This commit is contained in:
SomberNight
2025-07-15 13:14:38 +00:00
parent 482d573f55
commit 85fc95c71b
7 changed files with 36 additions and 13 deletions

View File

@@ -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():

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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)