What changed, and why it matters
This commit fixes a bug in Electrum's wallet storage recovery. If a wallet file was partially corrupted (a 'patch' was cut off mid-write), the app could recover the old data but then fail to mark the wallet as changed. As a result, later updates might be appended to the still-corrupted file instead of replacing it, and those updates could be lost the next time the wallet was opened. The fix marks the wallet as modified after recovery so it gets rewritten cleanly. This is a data-loss bug, not a remote attack.
Apply the patch. It is a targeted bug fix with a regression test and low risk. Users who experienced wallet file corruption or partial writes should ensure they are on a version containing this fix.
Security signals we found
Data-loss / integrity failure in wallet storage recovery path
Missing dirty flag after fallback data recovery
Append-only storage file not rewritten after corruption cleanup
Regression test added for truncated-patch recovery scenario
Evidence from the diff
JsonDB uses JSON patch append-only storage. When maybe_load_incomplete_data() strips a trailing incomplete patch and returns recovered data, the in-memory database was not marked dirty. Subsequent write() calls could append new patches to the existing on-disk file (which still ended with the corrupted fragment), and the next load could again discard everything after the recovery point. The patch adds self.set_modified(True) after incomplete-data recovery and updates the log message. A regression test simulates a truncated patch, recovery, a new write, and verifies the new data survives reload.
Changed components
electrum/json_db.pytests/test_wallet.pyInspect captured patch +22 / −1
diff --git a/electrum/json_db.py b/electrum/json_db.py
index dbf3c3d..db72e89 100644
--- a/electrum/json_db.py
+++ b/electrum/json_db.py
@@ -126,6 +126,7 @@ class JsonDB(Logger):
data, patches = r, []
elif r := self.maybe_load_incomplete_data(s):
data, patches = r, []
+ self.set_modified(True)
else:
raise WalletFileException("Cannot read wallet file. (parsing failed)")
if not isinstance(data, dict):
@@ -170,7 +171,7 @@ class JsonDB(Logger):
if n == 0:
s = s[0:i]
assert s[-2:] == ',\n'
- self.logger.info('found incomplete data {s[i:]}')
+ self.logger.info('found incomplete data')
return self.load_data(s[0:-2])
def set_modified(self, b):
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index de1a278..372add2 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -91,6 +91,26 @@ class TestWalletStorage(WalletTestCase):
for key, value in some_dict.items():
self.assertEqual(d[key], value)
+ async def test_appends_after_incomplete_data_recovery_survive_reload(self):
+ storage = WalletStorage(self.wallet_path, allow_partial_writes=True)
+ db = JsonDB('', storage=storage)
+ db.put("a", "b")
+ db.write()
+ # simulate a crash that truncated an appended patch
+ with open(self.wallet_path, "a") as f:
+ f.write(',\n{"op": "add", "path": "/x", "value": {"inco')
+ # reopen: recovery drops the incomplete patch
+ storage = WalletStorage(self.wallet_path, allow_partial_writes=True)
+ db = JsonDB(storage.read(), storage=storage)
+ self.assertEqual(db.get("a"), "b")
+ # append a new patch to the recovered wallet
+ db.put("c", "d")
+ db.write()
+ # reopen: the appended patch must not be lost
+ storage = WalletStorage(self.wallet_path, allow_partial_writes=True)
+ db = JsonDB(storage.read(), storage=storage)
+ self.assertEqual(db.get("c"), "d")
+
async def test_storage_imported_add_privkeys_persistence_test(self):
text = ' '.join([
'p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',
Why this scored 47/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.