1
0

Merge pull request #10031 from SomberNight/202507_walletdb_add_configvar_partial_writes_4

wallet_db: add configvar for partial_writes, disable by default
This commit is contained in:
ThomasV
2025-07-15 16:28:33 +02:00
committed by GitHub
7 changed files with 36 additions and 13 deletions

View File

@@ -507,7 +507,7 @@ class Daemon(Logger):
config: SimpleConfig, config: SimpleConfig,
) -> Optional[Abstract_Wallet]: ) -> Optional[Abstract_Wallet]:
path = standardize_path(path) path = standardize_path(path)
storage = WalletStorage(path) storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES)
if not storage.file_exists(): if not storage.file_exists():
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path) raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
if storage.is_encrypted(): if storage.is_encrypted():

View File

@@ -421,11 +421,7 @@ class JsonDB(Logger):
@locked @locked
def write(self): def write(self):
if ( if self.storage.should_do_full_write_next():
not self.storage.file_exists()
or self.storage.is_encrypted()
or self.storage.needs_consolidation()
):
self.write_and_force_consolidation() self.write_and_force_consolidation()
else: else:
self._append_pending_changes() 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 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."""), 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_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool)
FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) 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. # 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(). # 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) Logger.__init__(self)
self.path = standardize_path(path) self.path = standardize_path(path)
self._file_exists = bool(self.path and os.path.exists(self.path)) self._file_exists = bool(self.path and os.path.exists(self.path))
self.logger.info(f"wallet path {self.path}") self.logger.info(f"wallet path {self.path}")
self._allow_partial_writes = allow_partial_writes
self.pubkey = None self.pubkey = None
self.decrypted = '' self.decrypted = ''
try: try:
@@ -112,6 +118,7 @@ class WalletStorage(Logger):
def append(self, data: str) -> None: def append(self, data: str) -> None:
""" append data to file. for the moment, only non-encrypted file""" """ append data to file. for the moment, only non-encrypted file"""
assert self._allow_partial_writes
assert not self.is_encrypted() assert not self.is_encrypted()
with open(self.path, "rb+") as f: with open(self.path, "rb+") as f:
pos = f.seek(0, os.SEEK_END) pos = f.seek(0, os.SEEK_END)
@@ -122,9 +129,18 @@ class WalletStorage(Logger):
f.flush() f.flush()
os.fsync(f.fileno()) os.fsync(f.fileno())
def needs_consolidation(self): def _needs_consolidation(self):
return self.pos > 2 * self.init_pos 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: def file_exists(self) -> bool:
return self._file_exists return self._file_exists

View File

@@ -4189,7 +4189,7 @@ def create_new_wallet(
gap_limit: Optional[int] = None gap_limit: Optional[int] = None
) -> dict: ) -> dict:
"""Create a new wallet""" """Create a new wallet"""
storage = WalletStorage(path) storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES)
if storage.file_exists(): if storage.file_exists():
raise UserFacingException("Remove the existing wallet first!") raise UserFacingException("Remove the existing wallet first!")
db = WalletDB('', storage=storage, upgrade=True) db = WalletDB('', storage=storage, upgrade=True)
@@ -4226,7 +4226,7 @@ def restore_wallet_from_text(
if path is None: # create wallet in-memory if path is None: # create wallet in-memory
storage = None storage = None
else: else:
storage = WalletStorage(path) storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES)
if storage.file_exists(): if storage.file_exists():
raise UserFacingException("Remove the existing wallet first!") raise UserFacingException("Remove the existing wallet first!")
if encrypt_file is None: if encrypt_file is None:

View File

@@ -1289,7 +1289,13 @@ class WalletDB(JsonDB):
storage: Optional['WalletStorage'] = None, storage: Optional['WalletStorage'] = None,
upgrade: bool = False, 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 # create pointers
self.load_transactions() self.load_transactions()
# load plugins that are conditional on wallet type # 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) sys_exit(1)
# instantiate wallet for command-line # 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(): if cmd.requires_wallet and not storage.file_exists():
print_msg("Error: Wallet file not found.") 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: if 'wallet_path' in cmd.options and config_options.get('wallet_path') is None:
config_options['wallet_path'] = wallet_path config_options['wallet_path'] = wallet_path
if cmd.requires_wallet: 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():
if storage.is_encrypted_with_hw_device(): if storage.is_encrypted_with_hw_device():
password = get_password_for_hw_device_encrypted_storage(plugins) password = get_password_for_hw_device_encrypted_storage(plugins)