json_db: move json to python conversion logic from StoredDict to json_db.
What changed, and why it matters
This commit is a code cleanup that moves data-conversion logic out of a low-level dictionary class and into a dedicated database module. It also changes one place where metadata is stored so it is first converted to plain JSON. The changes appear to be architectural refactoring rather than a fix for an active security bug, but the patch is partial and the commit message does not explain the full motivation.
Treat as a normal refactoring commit. If reviewing for security, verify that the new `_convert_dict` recursion preserves all previously applied key/value conversions, especially for registered wallet keys and parent-key lookups, and that `db_metadata.to_json()` does not lose fields compared to the previous direct assignment.
Security signals we found
Refactoring of serialization/deserialization boundary between JSON storage and Python objects
Change to metadata persistence path (`db_metadata = v.to_json()`)
Removal of depth-first/breadth-first conversion comments suggests behavior change in object lifecycle
Evidence from the diff
The patch refactors StoredDict.__setitem__ in electrum/json_db.py so it no longer calls convert_key/convert_value directly. Instead, JsonDB now performs JSON-to-Python conversion once up front via new _convert_dict, _convert_dict_key, and _convert_dict_value helpers. A related change in electrum/wallet_db.py stores db_metadata by calling .to_json() first. The diff shows a restructuring of how nested dictionaries are recursively converted to StoredDict and how registered constructors are applied. There is no explicit security context in the commit or supplied references.
Changed components
electrum/json_db.pyelectrum/wallet_db.pyInspect captured patch +33 / −22
diff --git a/electrum/json_db.py b/electrum/json_db.py
index a79f691..93a9217 100644
--- a/electrum/json_db.py
+++ b/electrum/json_db.py
@@ -163,16 +163,9 @@ class StoredDict(dict):
for k, vv in v.items():
v.__setitem__(k, vv, patch=False)
# recursively convert dict to StoredDict.
- # _convert_dict is called breadth-first
elif isinstance(v, dict):
- if self.db:
- v = self.db._convert_dict(self.path, key, v)
if not self.db or self.db._should_convert_to_stored_dict(key):
v = StoredDict(v, self.db, self.path + [key])
- # convert_value is called depth-first
- if isinstance(v, dict) or isinstance(v, str) or isinstance(v, int):
- if self.db:
- v = self.db._convert_value(self.path, key, v)
# set parent of StoredObject
if isinstance(v, StoredObject):
v.set_db(self.db, self.path + [key])
@@ -263,7 +256,9 @@ class JsonDB(Logger):
if upgrader:
data, was_upgraded = upgrader(data)
self._modified |= was_upgraded
- # convert to StoredDict
+ # convert json to python objects
+ data = self._convert_dict([], data)
+ # convert dict to StoredDict
self.data = StoredDict(data, self, [])
# write file in case there was a db upgrade
if self.storage and self.storage.file_exists():
@@ -391,7 +386,22 @@ class JsonDB(Logger):
def _should_convert_to_stored_dict(self, key) -> bool:
return True
- def _convert_dict(self, path, key, v):
+ def _convert_dict_key(self, path):
+ key = path[-1]
+ parent_key = path[-2] if len(path) > 1 else None
+ gp_key = path[-3] if len(path) > 2 else None
+ if parent_key and parent_key in registered_dict_keys:
+ convert_key = registered_dict_keys[parent_key]
+ elif gp_key and gp_key in registered_parent_keys:
+ convert_key = registered_parent_keys.get(gp_key)
+ else:
+ convert_key = None
+ if convert_key:
+ key = convert_key(key)
+ return key
+
+ def _convert_dict_value(self, path, v):
+ key = path[-1]
if key in registered_dicts:
constructor, _type = registered_dicts[key]
if _type == dict:
@@ -400,25 +410,26 @@ class JsonDB(Logger):
v = dict((k, constructor(*x)) for k, x in v.items())
else:
v = dict((k, constructor(x)) for k, x in v.items())
- if key in registered_dict_keys:
- convert_key = registered_dict_keys[key]
- elif path and path[-1] in registered_parent_keys:
- convert_key = registered_parent_keys.get(path[-1])
- else:
- convert_key = None
- if convert_key:
- v = dict((convert_key(k), x) for k, x in v.items())
- return v
-
- def _convert_value(self, path, key, v):
- if key in registered_names:
+ elif key in registered_names:
constructor, _type = registered_names[key]
if _type == dict:
v = constructor(**v)
else:
v = constructor(v)
+ if isinstance(v, dict):
+ v = self._convert_dict(path, v)
return v
+ def _convert_dict(self, path, data: dict):
+ # recursively convert dict to StoredDict
+ d = {}
+ for k, v in list(data.items()):
+ child_path = path + [k]
+ k = self._convert_dict_key(child_path)
+ v = self._convert_dict_value(child_path, v)
+ d[k] = v
+ return d
+
@locked
def write(self):
if self.storage.should_do_full_write_next():
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
index 95d038e..586afb4 100644
--- a/electrum/wallet_db.py
+++ b/electrum/wallet_db.py
@@ -1279,7 +1279,7 @@ def upgrade_wallet_db(data: dict, do_upgrade: bool) -> Tuple[dict, bool]:
first_electrum_version_used=ELECTRUM_VERSION,
)
assert data.get("db_metadata", None) is None
- data["db_metadata"] = v
+ data["db_metadata"] = v.to_json()
was_upgraded = True
dbu = WalletDBUpgrader(data)
Why this scored 25/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.