1
0

Separate WalletDB from storage upgrades.

Make sure that WalletDB.data is always a StoredDict.
Perform db upgrades in a separate class, since they
operate on a dict object.
This commit is contained in:
ThomasV
2023-08-18 15:13:33 +02:00
parent af2b0f758d
commit b5bc5ff9ed
10 changed files with 159 additions and 146 deletions

View File

@@ -36,7 +36,7 @@ import time
import attr
from . import util, bitcoin
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh, MyEncoder
from .invoices import Invoice, Request
from .keystore import bip44_derivation
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
@@ -49,6 +49,11 @@ from .plugin import run_hook, plugin_loaders
from .version import ELECTRUM_VERSION
class WalletRequiresUpgrade(WalletFileException):
pass
class WalletRequiresSplit(WalletFileException):
def __init__(self, split_data):
self._split_data = split_data
# seed_version is now used for the version of the wallet file
@@ -100,46 +105,21 @@ for key in ['locked_in', 'fails', 'settles']:
json_db.register_parent_key(key, lambda x: HTLCOwner(int(x)))
class WalletDB(JsonDB):
def __init__(self, data, *, storage=None, manual_upgrades: bool):
JsonDB.__init__(self, data, storage)
if not data:
# create new DB
self.put('seed_version', FINAL_SEED_VERSION)
self._add_db_creation_metadata()
self._after_upgrade_tasks()
self._manual_upgrades = manual_upgrades
self._called_after_upgrade_tasks = False
if not self._manual_upgrades and self.requires_split():
raise WalletFileException("This wallet has multiple accounts and must be split")
if not self.requires_upgrade():
self._after_upgrade_tasks()
elif not self._manual_upgrades:
self.upgrade()
# load plugins that are conditional on wallet type
self.load_plugins()
class WalletDBUpgrader(Logger):
def load_data(self, s):
try:
JsonDB.load_data(self, s)
except Exception:
try:
d = ast.literal_eval(s)
labels = d.get('labels', {})
except Exception as e:
raise WalletFileException("Cannot read wallet file. (parsing failed)")
self.data = {}
for key, value in d.items():
try:
json.dumps(key)
json.dumps(value)
except Exception:
self.logger.info(f'Failed to convert label to json format: {key}')
continue
self.data[key] = value
if not isinstance(self.data, dict):
raise WalletFileException("Malformed wallet file (not dict)")
def __init__(self, data):
Logger.__init__(self)
self.data = data
def get(self, key, default=None):
return self.data.get(key, default)
def put(self, key, value):
if value is not None:
self.data[key] = value
else:
self.data.pop(key, None)
def requires_split(self):
d = self.get('accounts', {})
@@ -192,9 +172,6 @@ class WalletDB(JsonDB):
@profiler
def upgrade(self):
self.logger.info('upgrading wallet format')
if self._called_after_upgrade_tasks:
# we need strict ordering between upgrade() and after_upgrade_tasks()
raise Exception("'after_upgrade_tasks' must NOT be called before 'upgrade'")
self._convert_imported()
self._convert_wallet_type()
self._convert_account()
@@ -242,12 +219,6 @@ class WalletDB(JsonDB):
self._convert_version_54()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks()
def _after_upgrade_tasks(self):
self._called_after_upgrade_tasks = True
self._load_transactions()
def _convert_wallet_type(self):
if not self._is_upgrade_method_needed(0, 13):
return
@@ -1140,7 +1111,6 @@ class WalletDB(JsonDB):
else:
return True
@locked
def get_seed_version(self):
seed_version = self.get('seed_version')
if not seed_version:
@@ -1191,14 +1161,62 @@ class WalletDB(JsonDB):
# generic exception
raise WalletFileException(msg)
def _add_db_creation_metadata(self):
# store this for debugging purposes
v = DBMetadata(
creation_timestamp=int(time.time()),
first_electrum_version_used=ELECTRUM_VERSION,
)
assert self.get("db_metadata", None) is None
self.put("db_metadata", v)
class WalletDB(JsonDB):
def __init__(self, s, *, storage=None, manual_upgrades=True):
self._upgrade = not manual_upgrades
JsonDB.__init__(self, s, storage, encoder=MyEncoder)
# create pointers
self.load_transactions()
# load plugins that are conditional on wallet type
self.load_plugins()
def load_data(self, s):
try:
data = JsonDB.load_data(self, s)
except Exception:
try:
d = ast.literal_eval(s)
labels = d.get('labels', {})
except Exception as e:
raise WalletFileException("Cannot read wallet file. (parsing failed)")
data = {}
for key, value in d.items():
try:
json.dumps(key)
json.dumps(value)
except Exception:
self.logger.info(f'Failed to convert label to json format: {key}')
continue
data[key] = value
if not isinstance(data, dict):
raise WalletFileException("Malformed wallet file (not dict)")
if len(data) == 0:
# create new DB
data['seed_version'] = FINAL_SEED_VERSION
# store this for debugging purposes
v = DBMetadata(
creation_timestamp=int(time.time()),
first_electrum_version_used=ELECTRUM_VERSION,
)
assert data.get("db_metadata", None) is None
data["db_metadata"] = v
dbu = WalletDBUpgrader(data)
if dbu.requires_split():
raise WalletRequiresSplit(dbu.get_split_accounts())
if self._upgrade:
dbu.upgrade()
if dbu.requires_upgrade():
raise WalletRequiresUpgrade()
return dbu.data
@locked
def get_seed_version(self):
return self.get('seed_version')
def get_db_metadata(self) -> Optional[DBMetadata]:
# field only present for wallet files created with ver 4.4.0 or later
@@ -1560,8 +1578,7 @@ class WalletDB(JsonDB):
self._addr_to_addr_index[addr] = (1, i)
@profiler
def _load_transactions(self):
self.data = StoredDict(self.data, self, [])
def load_transactions(self):
# references in self.data
# TODO make all these private
# txid -> address -> prev_outpoint -> value
@@ -1608,21 +1625,19 @@ class WalletDB(JsonDB):
return True
def is_ready_to_be_used_by_wallet(self):
return not self.requires_upgrade() and self._called_after_upgrade_tasks
return not self._requires_upgrade
def split_accounts(self, root_path):
@classmethod
def split_accounts(klass, root_path, split_data):
from .storage import WalletStorage
out = []
result = self.get_split_accounts()
for data in result:
file_list = []
for data in split_data:
path = root_path + '.' + data['suffix']
storage = WalletStorage(path)
db = WalletDB(json.dumps(data), storage=storage, manual_upgrades=False)
db._called_after_upgrade_tasks = False
db.upgrade()
item_storage = WalletStorage(path)
db = WalletDB(json.dumps(data), storage=item_storage, manual_upgrades=False)
db.write()
out.append(path)
return out
file_list.append(path)
return file_list
def get_action(self):
action = run_hook('get_action', self)