commit 7f2083f667caea442eae3184dadba4e29be7c447
parent d5790ea10994f71d4cd0c01d5a08fe87a1d25b82
Author: ThomasV <thomasv@electrum.org>
Date: Tue, 19 Feb 2019 11:56:46 +0100
separate storage and database (JsonDB)
Diffstat:
6 files changed, 559 insertions(+), 465 deletions(-)
diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
@@ -297,7 +297,7 @@ class ElectrumGui(PrintError):
# Show network dialog if config does not exist
if self.daemon.network:
if self.config.get('auto_connect') is None:
- wizard = InstallWizard(self.config, self.app, self.plugins, None)
+ wizard = InstallWizard(self.config, self.app, self.plugins)
wizard.init_network(self.daemon.network)
wizard.terminate()
diff --git a/electrum/json_db.py b/electrum/json_db.py
@@ -0,0 +1,500 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2015 Thomas Voegtlin
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+import os
+import ast
+import threading
+import json
+import copy
+import re
+import stat
+import hashlib
+import base64
+import zlib
+from collections import defaultdict
+
+from . import util, bitcoin, ecc
+from .util import PrintError, profiler, InvalidPassword, WalletFileException, bfh, standardize_path, multisig_type
+from .plugin import run_hook, plugin_loaders
+from .keystore import bip44_derivation
+
+
+# seed_version is now used for the version of the wallet file
+
+OLD_SEED_VERSION = 4 # electrum versions < 2.0
+NEW_SEED_VERSION = 11 # electrum versions >= 2.0
+FINAL_SEED_VERSION = 18 # electrum >= 2.7 will set this to prevent
+ # old versions from overwriting new format
+
+
+
+
+
+
+class JsonDB(PrintError):
+
+ def __init__(self, raw, manual_upgrades):
+ self.data = {}
+ self.manual_upgrades = manual_upgrades
+ if raw:
+ self.load_data(raw)
+ else:
+ self.put('seed_version', FINAL_SEED_VERSION)
+
+ def get(self, key, default=None):
+ v = self.data.get(key)
+ if v is None:
+ v = default
+ else:
+ v = copy.deepcopy(v)
+ return v
+
+ def put(self, key, value):
+ try:
+ json.dumps(key, cls=util.MyEncoder)
+ json.dumps(value, cls=util.MyEncoder)
+ except:
+ self.print_error(f"json error: cannot save {repr(key)} ({repr(value)})")
+ return False
+ if value is not None:
+ if self.data.get(key) != value:
+ self.data[key] = copy.deepcopy(value)
+ return True
+ elif key in self.data:
+ self.data.pop(key)
+ return True
+ return False
+
+ def get_all_data(self) -> dict:
+ return copy.deepcopy(self.data)
+
+ def overwrite_all_data(self, data: dict) -> None:
+ try:
+ json.dumps(data, cls=util.MyEncoder)
+ except:
+ self.print_error(f"json error: cannot save {repr(data)}")
+ return
+ with self.db_lock:
+ self.modified = True
+ self.data = copy.deepcopy(data)
+
+ def commit(self):
+ pass
+
+ def dump(self):
+ return json.dumps(self.data, indent=4, sort_keys=True, cls=util.MyEncoder)
+
+ def load_data(self, s):
+ try:
+ self.data = json.loads(s)
+ except:
+ try:
+ d = ast.literal_eval(s)
+ labels = d.get('labels', {})
+ except Exception as e:
+ raise IOError("Cannot read wallet file '%s'" % self.path)
+ self.data = {}
+ for key, value in d.items():
+ try:
+ json.dumps(key)
+ json.dumps(value)
+ except:
+ self.print_error('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)")
+
+ # check here if I need to load a plugin
+ t = self.get('wallet_type')
+ l = plugin_loaders.get(t)
+ if l: l()
+
+ if not self.manual_upgrades:
+ if self.requires_split():
+ raise WalletFileException("This wallet has multiple accounts and must be split")
+ if self.requires_upgrade():
+ self.upgrade()
+
+ def requires_split(self):
+ d = self.get('accounts', {})
+ return len(d) > 1
+
+ def split_accounts(self):
+ result = []
+ # backward compatibility with old wallets
+ d = self.get('accounts', {})
+ if len(d) < 2:
+ return
+ wallet_type = self.get('wallet_type')
+ if wallet_type == 'old':
+ assert len(d) == 2
+ data1 = copy.deepcopy(self.data)
+ data1['accounts'] = {'0': d['0']}
+ data1['suffix'] = 'deterministic'
+ data2 = copy.deepcopy(self.data)
+ data2['accounts'] = {'/x': d['/x']}
+ data2['seed'] = None
+ data2['seed_version'] = None
+ data2['master_public_key'] = None
+ data2['wallet_type'] = 'imported'
+ data2['suffix'] = 'imported'
+ result = [data1, data2]
+
+ elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip', 'digitalbitbox', 'safe_t']:
+ mpk = self.get('master_public_keys')
+ for k in d.keys():
+ i = int(k)
+ x = d[k]
+ if x.get("pending"):
+ continue
+ xpub = mpk["x/%d'"%i]
+ new_data = copy.deepcopy(self.data)
+ # save account, derivation and xpub at index 0
+ new_data['accounts'] = {'0': x}
+ new_data['master_public_keys'] = {"x/0'": xpub}
+ new_data['derivation'] = bip44_derivation(k)
+ new_data['suffix'] = k
+ result.append(new_data)
+ else:
+ raise WalletFileException("This wallet has multiple accounts and must be split")
+ return result
+
+ def requires_upgrade(self):
+ return self.get_seed_version() < FINAL_SEED_VERSION
+
+ @profiler
+ def upgrade(self):
+ self.print_error('upgrading wallet format')
+ self.convert_imported()
+ self.convert_wallet_type()
+ self.convert_account()
+ self.convert_version_13_b()
+ self.convert_version_14()
+ self.convert_version_15()
+ self.convert_version_16()
+ self.convert_version_17()
+ self.convert_version_18()
+ self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
+
+ def convert_wallet_type(self):
+ if not self._is_upgrade_method_needed(0, 13):
+ return
+
+ wallet_type = self.get('wallet_type')
+ if wallet_type == 'btchip': wallet_type = 'ledger'
+ if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
+ return False
+ assert not self.requires_split()
+ seed_version = self.get_seed_version()
+ seed = self.get('seed')
+ xpubs = self.get('master_public_keys')
+ xprvs = self.get('master_private_keys', {})
+ mpk = self.get('master_public_key')
+ keypairs = self.get('keypairs')
+ key_type = self.get('key_type')
+ if seed_version == OLD_SEED_VERSION or wallet_type == 'old':
+ d = {
+ 'type': 'old',
+ 'seed': seed,
+ 'mpk': mpk,
+ }
+ self.put('wallet_type', 'standard')
+ self.put('keystore', d)
+
+ elif key_type == 'imported':
+ d = {
+ 'type': 'imported',
+ 'keypairs': keypairs,
+ }
+ self.put('wallet_type', 'standard')
+ self.put('keystore', d)
+
+ elif wallet_type in ['xpub', 'standard']:
+ xpub = xpubs["x/"]
+ xprv = xprvs.get("x/")
+ d = {
+ 'type': 'bip32',
+ 'xpub': xpub,
+ 'xprv': xprv,
+ 'seed': seed,
+ }
+ self.put('wallet_type', 'standard')
+ self.put('keystore', d)
+
+ elif wallet_type in ['bip44']:
+ xpub = xpubs["x/0'"]
+ xprv = xprvs.get("x/0'")
+ d = {
+ 'type': 'bip32',
+ 'xpub': xpub,
+ 'xprv': xprv,
+ }
+ self.put('wallet_type', 'standard')
+ self.put('keystore', d)
+
+ elif wallet_type in ['trezor', 'keepkey', 'ledger', 'digitalbitbox', 'safe_t']:
+ xpub = xpubs["x/0'"]
+ derivation = self.get('derivation', bip44_derivation(0))
+ d = {
+ 'type': 'hardware',
+ 'hw_type': wallet_type,
+ 'xpub': xpub,
+ 'derivation': derivation,
+ }
+ self.put('wallet_type', 'standard')
+ self.put('keystore', d)
+
+ elif (wallet_type == '2fa') or multisig_type(wallet_type):
+ for key in xpubs.keys():
+ d = {
+ 'type': 'bip32',
+ 'xpub': xpubs[key],
+ 'xprv': xprvs.get(key),
+ }
+ if key == 'x1/' and seed:
+ d['seed'] = seed
+ self.put(key, d)
+ else:
+ raise WalletFileException('Unable to tell wallet type. Is this even a wallet file?')
+ # remove junk
+ self.put('master_public_key', None)
+ self.put('master_public_keys', None)
+ self.put('master_private_keys', None)
+ self.put('derivation', None)
+ self.put('seed', None)
+ self.put('keypairs', None)
+ self.put('key_type', None)
+
+ def convert_version_13_b(self):
+ # version 13 is ambiguous, and has an earlier and a later structure
+ if not self._is_upgrade_method_needed(0, 13):
+ return
+
+ if self.get('wallet_type') == 'standard':
+ if self.get('keystore').get('type') == 'imported':
+ pubkeys = self.get('keystore').get('keypairs').keys()
+ d = {'change': []}
+ receiving_addresses = []
+ for pubkey in pubkeys:
+ addr = bitcoin.pubkey_to_address('p2pkh', pubkey)
+ receiving_addresses.append(addr)
+ d['receiving'] = receiving_addresses
+ self.put('addresses', d)
+ self.put('pubkeys', None)
+
+ self.put('seed_version', 13)
+
+ def convert_version_14(self):
+ # convert imported wallets for 3.0
+ if not self._is_upgrade_method_needed(13, 13):
+ return
+
+ if self.get('wallet_type') =='imported':
+ addresses = self.get('addresses')
+ if type(addresses) is list:
+ addresses = dict([(x, None) for x in addresses])
+ self.put('addresses', addresses)
+ elif self.get('wallet_type') == 'standard':
+ if self.get('keystore').get('type')=='imported':
+ addresses = set(self.get('addresses').get('receiving'))
+ pubkeys = self.get('keystore').get('keypairs').keys()
+ assert len(addresses) == len(pubkeys)
+ d = {}
+ for pubkey in pubkeys:
+ addr = bitcoin.pubkey_to_address('p2pkh', pubkey)
+ assert addr in addresses
+ d[addr] = {
+ 'pubkey': pubkey,
+ 'redeem_script': None,
+ 'type': 'p2pkh'
+ }
+ self.put('addresses', d)
+ self.put('pubkeys', None)
+ self.put('wallet_type', 'imported')
+ self.put('seed_version', 14)
+
+ def convert_version_15(self):
+ if not self._is_upgrade_method_needed(14, 14):
+ return
+ if self.get('seed_type') == 'segwit':
+ # should not get here; get_seed_version should have caught this
+ raise Exception('unsupported derivation (development segwit, v14)')
+ self.put('seed_version', 15)
+
+ def convert_version_16(self):
+ # fixes issue #3193 for Imported_Wallets with addresses
+ # also, previous versions allowed importing any garbage as an address
+ # which we now try to remove, see pr #3191
+ if not self._is_upgrade_method_needed(15, 15):
+ return
+
+ def remove_address(addr):
+ def remove_from_dict(dict_name):
+ d = self.get(dict_name, None)
+ if d is not None:
+ d.pop(addr, None)
+ self.put(dict_name, d)
+
+ def remove_from_list(list_name):
+ lst = self.get(list_name, None)
+ if lst is not None:
+ s = set(lst)
+ s -= {addr}
+ self.put(list_name, list(s))
+
+ # note: we don't remove 'addr' from self.get('addresses')
+ remove_from_dict('addr_history')
+ remove_from_dict('labels')
+ remove_from_dict('payment_requests')
+ remove_from_list('frozen_addresses')
+
+ if self.get('wallet_type') == 'imported':
+ addresses = self.get('addresses')
+ assert isinstance(addresses, dict)
+ addresses_new = dict()
+ for address, details in addresses.items():
+ if not bitcoin.is_address(address):
+ remove_address(address)
+ continue
+ if details is None:
+ addresses_new[address] = {}
+ else:
+ addresses_new[address] = details
+ self.put('addresses', addresses_new)
+
+ self.put('seed_version', 16)
+
+ def convert_version_17(self):
+ # delete pruned_txo; construct spent_outpoints
+ if not self._is_upgrade_method_needed(16, 16):
+ return
+
+ self.put('pruned_txo', None)
+
+ from .transaction import Transaction
+ transactions = self.get('transactions', {}) # txid -> raw_tx
+ spent_outpoints = defaultdict(dict)
+ for txid, raw_tx in transactions.items():
+ tx = Transaction(raw_tx)
+ for txin in tx.inputs():
+ if txin['type'] == 'coinbase':
+ continue
+ prevout_hash = txin['prevout_hash']
+ prevout_n = txin['prevout_n']
+ spent_outpoints[prevout_hash][prevout_n] = txid
+ self.put('spent_outpoints', spent_outpoints)
+
+ self.put('seed_version', 17)
+
+ def convert_version_18(self):
+ # delete verified_tx3 as its structure changed
+ if not self._is_upgrade_method_needed(17, 17):
+ return
+ self.put('verified_tx3', None)
+ self.put('seed_version', 18)
+
+ # def convert_version_19(self):
+ # TODO for "next" upgrade:
+ # - move "pw_hash_version" from keystore to storage
+ # pass
+
+ def convert_imported(self):
+ if not self._is_upgrade_method_needed(0, 13):
+ return
+
+ # '/x' is the internal ID for imported accounts
+ d = self.get('accounts', {}).get('/x', {}).get('imported',{})
+ if not d:
+ return False
+ addresses = []
+ keypairs = {}
+ for addr, v in d.items():
+ pubkey, privkey = v
+ if privkey:
+ keypairs[pubkey] = privkey
+ else:
+ addresses.append(addr)
+ if addresses and keypairs:
+ raise WalletFileException('mixed addresses and privkeys')
+ elif addresses:
+ self.put('addresses', addresses)
+ self.put('accounts', None)
+ elif keypairs:
+ self.put('wallet_type', 'standard')
+ self.put('key_type', 'imported')
+ self.put('keypairs', keypairs)
+ self.put('accounts', None)
+ else:
+ raise WalletFileException('no addresses or privkeys')
+
+ def convert_account(self):
+ if not self._is_upgrade_method_needed(0, 13):
+ return
+
+ self.put('accounts', None)
+
+ def _is_upgrade_method_needed(self, min_version, max_version):
+ cur_version = self.get_seed_version()
+ if cur_version > max_version:
+ return False
+ elif cur_version < min_version:
+ raise WalletFileException(
+ 'storage upgrade: unexpected version {} (should be {}-{})'
+ .format(cur_version, min_version, max_version))
+ else:
+ return True
+
+ def get_seed_version(self):
+ seed_version = self.get('seed_version')
+ if not seed_version:
+ seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
+ if seed_version > FINAL_SEED_VERSION:
+ raise WalletFileException('This version of Electrum is too old to open this wallet.\n'
+ '(highest supported storage version: {}, version of this file: {})'
+ .format(FINAL_SEED_VERSION, seed_version))
+ if seed_version==14 and self.get('seed_type') == 'segwit':
+ self.raise_unsupported_version(seed_version)
+ if seed_version >=12:
+ return seed_version
+ if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
+ self.raise_unsupported_version(seed_version)
+ return seed_version
+
+ def raise_unsupported_version(self, seed_version):
+ msg = "Your wallet has an unsupported seed version."
+ msg += '\n\nWallet file: %s' % os.path.abspath(self.path)
+ if seed_version in [5, 7, 8, 9, 10, 14]:
+ msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version
+ if seed_version == 6:
+ # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog
+ msg += '\n\nThis file was created because of a bug in version 1.9.8.'
+ if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None:
+ # pbkdf2 (at that time an additional dependency) was not included with the binaries, and wallet creation aborted.
+ msg += "\nIt does not contain any keys, and can safely be removed."
+ else:
+ # creation was complete if electrum was run from source
+ msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
+ raise WalletFileException(msg)
+
diff --git a/electrum/storage.py b/electrum/storage.py
@@ -23,9 +23,7 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
-import ast
import threading
-import json
import copy
import re
import stat
@@ -39,25 +37,8 @@ from .util import PrintError, profiler, InvalidPassword, WalletFileException, bf
from .plugin import run_hook, plugin_loaders
from .keystore import bip44_derivation
+from .json_db import JsonDB
-# seed_version is now used for the version of the wallet file
-
-OLD_SEED_VERSION = 4 # electrum versions < 2.0
-NEW_SEED_VERSION = 11 # electrum versions >= 2.0
-FINAL_SEED_VERSION = 18 # electrum >= 2.7 will set this to prevent
- # old versions from overwriting new format
-
-
-
-def multisig_type(wallet_type):
- '''If wallet_type is mofn multi-sig, return [m, n],
- otherwise return None.'''
- if not wallet_type:
- return None
- match = re.match(r'(\d+)of(\d+)', wallet_type)
- if match:
- match = [int(x) for x in match.group(1, 2)]
- return match
def get_derivation_used_for_hw_device_encryption():
return ("m"
@@ -68,39 +49,37 @@ def get_derivation_used_for_hw_device_encryption():
STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3)
-class JsonDB(PrintError):
- def __init__(self, path):
+class WalletStorage(PrintError):
+
+ def __init__(self, path, manual_upgrades=False):
self.db_lock = threading.RLock()
- self.data = {}
self.path = standardize_path(path)
self._file_exists = self.path and os.path.exists(self.path)
self.modified = False
- def get(self, key, default=None):
+ DB_Class = JsonDB
+ self.path = path
+ self.print_error("wallet path", self.path)
+ self.pubkey = None
+ if self.file_exists():
+ with open(self.path, "r", encoding='utf-8') as f:
+ self.raw = f.read()
+ self._encryption_version = self._init_encryption_version()
+ if not self.is_encrypted():
+ self.db = DB_Class(self.raw, manual_upgrades)
+ else:
+ self._encryption_version = STO_EV_PLAINTEXT
+ # avoid new wallets getting 'upgraded'
+ self.db = DB_Class('', False)
+
+ def put(self, key,value):
with self.db_lock:
- v = self.data.get(key)
- if v is None:
- v = default
- else:
- v = copy.deepcopy(v)
- return v
+ self.modified |= self.db.put(key, value)
- def put(self, key, value):
- try:
- json.dumps(key, cls=util.MyEncoder)
- json.dumps(value, cls=util.MyEncoder)
- except:
- self.print_error(f"json error: cannot save {repr(key)} ({repr(value)})")
- return
+ def get(self, key, default=None):
with self.db_lock:
- if value is not None:
- if self.data.get(key) != value:
- self.modified = True
- self.data[key] = copy.deepcopy(value)
- elif key in self.data:
- self.modified = True
- self.data.pop(key)
+ return self.db.get(key, default)
@profiler
def write(self):
@@ -113,9 +92,8 @@ class JsonDB(PrintError):
return
if not self.modified:
return
- s = json.dumps(self.data, indent=4, sort_keys=True, cls=util.MyEncoder)
- s = self.encrypt_before_writing(s)
-
+ self.db.commit()
+ s = self.encrypt_before_writing(self.db.dump())
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
with open(temp_path, "w", encoding='utf-8') as f:
f.write(s)
@@ -137,57 +115,6 @@ class JsonDB(PrintError):
def file_exists(self):
return self._file_exists
-
-class WalletStorage(JsonDB):
-
- def __init__(self, path, manual_upgrades=False):
- JsonDB.__init__(self, path)
- self.print_error("wallet path", self.path)
- self.manual_upgrades = manual_upgrades
- self.pubkey = None
- if self.file_exists():
- with open(self.path, "r", encoding='utf-8') as f:
- self.raw = f.read()
- self._encryption_version = self._init_encryption_version()
- if not self.is_encrypted():
- self.load_data(self.raw)
- else:
- self._encryption_version = STO_EV_PLAINTEXT
- # avoid new wallets getting 'upgraded'
- self.put('seed_version', FINAL_SEED_VERSION)
-
- def load_data(self, s):
- try:
- self.data = json.loads(s)
- except:
- try:
- d = ast.literal_eval(s)
- labels = d.get('labels', {})
- except Exception as e:
- raise IOError("Cannot read wallet file '%s'" % self.path)
- self.data = {}
- for key, value in d.items():
- try:
- json.dumps(key)
- json.dumps(value)
- except:
- self.print_error('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)")
-
- # check here if I need to load a plugin
- t = self.get('wallet_type')
- l = plugin_loaders.get(t)
- if l: l()
-
- if not self.manual_upgrades:
- if self.requires_split():
- raise WalletFileException("This wallet has multiple accounts and must be split")
- if self.requires_upgrade():
- self.upgrade()
-
def is_past_initial_decryption(self):
"""Return if storage is in a usable state for normal operations.
@@ -254,7 +181,7 @@ class WalletStorage(JsonDB):
s = None
self.pubkey = ec_key.get_public_key_hex()
s = s.decode('utf8')
- self.load_data(s)
+ self.db = JsonDB(s, True)
def encrypt_before_writing(self, plaintext: str) -> str:
s = plaintext
@@ -292,343 +219,28 @@ class WalletStorage(JsonDB):
with self.db_lock:
self.modified = True
- def requires_split(self):
- d = self.get('accounts', {})
- return len(d) > 1
-
- def split_accounts(storage):
- result = []
- # backward compatibility with old wallets
- d = storage.get('accounts', {})
- if len(d) < 2:
- return
- wallet_type = storage.get('wallet_type')
- if wallet_type == 'old':
- assert len(d) == 2
- storage1 = WalletStorage(storage.path + '.deterministic')
- storage1.data = copy.deepcopy(storage.data)
- storage1.put('accounts', {'0': d['0']})
- storage1.upgrade()
- storage1.write()
- storage2 = WalletStorage(storage.path + '.imported')
- storage2.data = copy.deepcopy(storage.data)
- storage2.put('accounts', {'/x': d['/x']})
- storage2.put('seed', None)
- storage2.put('seed_version', None)
- storage2.put('master_public_key', None)
- storage2.put('wallet_type', 'imported')
- storage2.upgrade()
- storage2.write()
- result = [storage1.path, storage2.path]
- elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip', 'digitalbitbox', 'safe_t']:
- mpk = storage.get('master_public_keys')
- for k in d.keys():
- i = int(k)
- x = d[k]
- if x.get("pending"):
- continue
- xpub = mpk["x/%d'"%i]
- new_path = storage.path + '.' + k
- storage2 = WalletStorage(new_path)
- storage2.data = copy.deepcopy(storage.data)
- # save account, derivation and xpub at index 0
- storage2.put('accounts', {'0': x})
- storage2.put('master_public_keys', {"x/0'": xpub})
- storage2.put('derivation', bip44_derivation(k))
- storage2.upgrade()
- storage2.write()
- result.append(new_path)
- else:
- raise WalletFileException("This wallet has multiple accounts and must be split")
- return result
-
def requires_upgrade(self):
- return self.file_exists() and self.get_seed_version() < FINAL_SEED_VERSION
+ return self.db.requires_upgrade()
- @profiler
def upgrade(self):
- self.print_error('upgrading wallet format')
-
- self.convert_imported()
- self.convert_wallet_type()
- self.convert_account()
- self.convert_version_13_b()
- self.convert_version_14()
- self.convert_version_15()
- self.convert_version_16()
- self.convert_version_17()
- self.convert_version_18()
-
- self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
+ self.db.upgrade()
self.write()
- def convert_wallet_type(self):
- if not self._is_upgrade_method_needed(0, 13):
- return
-
- wallet_type = self.get('wallet_type')
- if wallet_type == 'btchip': wallet_type = 'ledger'
- if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
- return False
- assert not self.requires_split()
- seed_version = self.get_seed_version()
- seed = self.get('seed')
- xpubs = self.get('master_public_keys')
- xprvs = self.get('master_private_keys', {})
- mpk = self.get('master_public_key')
- keypairs = self.get('keypairs')
- key_type = self.get('key_type')
- if seed_version == OLD_SEED_VERSION or wallet_type == 'old':
- d = {
- 'type': 'old',
- 'seed': seed,
- 'mpk': mpk,
- }
- self.put('wallet_type', 'standard')
- self.put('keystore', d)
-
- elif key_type == 'imported':
- d = {
- 'type': 'imported',
- 'keypairs': keypairs,
- }
- self.put('wallet_type', 'standard')
- self.put('keystore', d)
-
- elif wallet_type in ['xpub', 'standard']:
- xpub = xpubs["x/"]
- xprv = xprvs.get("x/")
- d = {
- 'type': 'bip32',
- 'xpub': xpub,
- 'xprv': xprv,
- 'seed': seed,
- }
- self.put('wallet_type', 'standard')
- self.put('keystore', d)
-
- elif wallet_type in ['bip44']:
- xpub = xpubs["x/0'"]
- xprv = xprvs.get("x/0'")
- d = {
- 'type': 'bip32',
- 'xpub': xpub,
- 'xprv': xprv,
- }
- self.put('wallet_type', 'standard')
- self.put('keystore', d)
-
- elif wallet_type in ['trezor', 'keepkey', 'ledger', 'digitalbitbox', 'safe_t']:
- xpub = xpubs["x/0'"]
- derivation = self.get('derivation', bip44_derivation(0))
- d = {
- 'type': 'hardware',
- 'hw_type': wallet_type,
- 'xpub': xpub,
- 'derivation': derivation,
- }
- self.put('wallet_type', 'standard')
- self.put('keystore', d)
-
- elif (wallet_type == '2fa') or multisig_type(wallet_type):
- for key in xpubs.keys():
- d = {
- 'type': 'bip32',
- 'xpub': xpubs[key],
- 'xprv': xprvs.get(key),
- }
- if key == 'x1/' and seed:
- d['seed'] = seed
- self.put(key, d)
- else:
- raise WalletFileException('Unable to tell wallet type. Is this even a wallet file?')
- # remove junk
- self.put('master_public_key', None)
- self.put('master_public_keys', None)
- self.put('master_private_keys', None)
- self.put('derivation', None)
- self.put('seed', None)
- self.put('keypairs', None)
- self.put('key_type', None)
-
- def convert_version_13_b(self):
- # version 13 is ambiguous, and has an earlier and a later structure
- if not self._is_upgrade_method_needed(0, 13):
- return
-
- if self.get('wallet_type') == 'standard':
- if self.get('keystore').get('type') == 'imported':
- pubkeys = self.get('keystore').get('keypairs').keys()
- d = {'change': []}
- receiving_addresses = []
- for pubkey in pubkeys:
- addr = bitcoin.pubkey_to_address('p2pkh', pubkey)
- receiving_addresses.append(addr)
- d['receiving'] = receiving_addresses
- self.put('addresses', d)
- self.put('pubkeys', None)
-
- self.put('seed_version', 13)
-
- def convert_version_14(self):
- # convert imported wallets for 3.0
- if not self._is_upgrade_method_needed(13, 13):
- return
-
- if self.get('wallet_type') =='imported':
- addresses = self.get('addresses')
- if type(addresses) is list:
- addresses = dict([(x, None) for x in addresses])
- self.put('addresses', addresses)
- elif self.get('wallet_type') == 'standard':
- if self.get('keystore').get('type')=='imported':
- addresses = set(self.get('addresses').get('receiving'))
- pubkeys = self.get('keystore').get('keypairs').keys()
- assert len(addresses) == len(pubkeys)
- d = {}
- for pubkey in pubkeys:
- addr = bitcoin.pubkey_to_address('p2pkh', pubkey)
- assert addr in addresses
- d[addr] = {
- 'pubkey': pubkey,
- 'redeem_script': None,
- 'type': 'p2pkh'
- }
- self.put('addresses', d)
- self.put('pubkeys', None)
- self.put('wallet_type', 'imported')
- self.put('seed_version', 14)
-
- def convert_version_15(self):
- if not self._is_upgrade_method_needed(14, 14):
- return
- if self.get('seed_type') == 'segwit':
- # should not get here; get_seed_version should have caught this
- raise Exception('unsupported derivation (development segwit, v14)')
- self.put('seed_version', 15)
-
- def convert_version_16(self):
- # fixes issue #3193 for Imported_Wallets with addresses
- # also, previous versions allowed importing any garbage as an address
- # which we now try to remove, see pr #3191
- if not self._is_upgrade_method_needed(15, 15):
- return
-
- def remove_address(addr):
- def remove_from_dict(dict_name):
- d = self.get(dict_name, None)
- if d is not None:
- d.pop(addr, None)
- self.put(dict_name, d)
-
- def remove_from_list(list_name):
- lst = self.get(list_name, None)
- if lst is not None:
- s = set(lst)
- s -= {addr}
- self.put(list_name, list(s))
-
- # note: we don't remove 'addr' from self.get('addresses')
- remove_from_dict('addr_history')
- remove_from_dict('labels')
- remove_from_dict('payment_requests')
- remove_from_list('frozen_addresses')
-
- if self.get('wallet_type') == 'imported':
- addresses = self.get('addresses')
- assert isinstance(addresses, dict)
- addresses_new = dict()
- for address, details in addresses.items():
- if not bitcoin.is_address(address):
- remove_address(address)
- continue
- if details is None:
- addresses_new[address] = {}
- else:
- addresses_new[address] = details
- self.put('addresses', addresses_new)
-
- self.put('seed_version', 16)
-
- def convert_version_17(self):
- # delete pruned_txo; construct spent_outpoints
- if not self._is_upgrade_method_needed(16, 16):
- return
-
- self.put('pruned_txo', None)
-
- from .transaction import Transaction
- transactions = self.get('transactions', {}) # txid -> raw_tx
- spent_outpoints = defaultdict(dict)
- for txid, raw_tx in transactions.items():
- tx = Transaction(raw_tx)
- for txin in tx.inputs():
- if txin['type'] == 'coinbase':
- continue
- prevout_hash = txin['prevout_hash']
- prevout_n = txin['prevout_n']
- spent_outpoints[prevout_hash][prevout_n] = txid
- self.put('spent_outpoints', spent_outpoints)
-
- self.put('seed_version', 17)
-
- def convert_version_18(self):
- # delete verified_tx3 as its structure changed
- if not self._is_upgrade_method_needed(17, 17):
- return
- self.put('verified_tx3', None)
- self.put('seed_version', 18)
-
- # def convert_version_19(self):
- # TODO for "next" upgrade:
- # - move "pw_hash_version" from keystore to storage
- # pass
-
- def convert_imported(self):
- if not self._is_upgrade_method_needed(0, 13):
- return
-
- # '/x' is the internal ID for imported accounts
- d = self.get('accounts', {}).get('/x', {}).get('imported',{})
- if not d:
- return False
- addresses = []
- keypairs = {}
- for addr, v in d.items():
- pubkey, privkey = v
- if privkey:
- keypairs[pubkey] = privkey
- else:
- addresses.append(addr)
- if addresses and keypairs:
- raise WalletFileException('mixed addresses and privkeys')
- elif addresses:
- self.put('addresses', addresses)
- self.put('accounts', None)
- elif keypairs:
- self.put('wallet_type', 'standard')
- self.put('key_type', 'imported')
- self.put('keypairs', keypairs)
- self.put('accounts', None)
- else:
- raise WalletFileException('no addresses or privkeys')
-
- def convert_account(self):
- if not self._is_upgrade_method_needed(0, 13):
- return
-
- self.put('accounts', None)
-
- def _is_upgrade_method_needed(self, min_version, max_version):
- cur_version = self.get_seed_version()
- if cur_version > max_version:
- return False
- elif cur_version < min_version:
- raise WalletFileException(
- 'storage upgrade: unexpected version {} (should be {}-{})'
- .format(cur_version, min_version, max_version))
- else:
- return True
+ def requires_split(self):
+ return self.db.requires_split()
+
+ def split_accounts(self):
+ out = []
+ result = self.db.split_accounts()
+ for data in result:
+ path = self.path + '.' + data['suffix']
+ storage = WalletStorage(path)
+ storage.db.data = data
+ storage.db.upgrade()
+ storage.modified = True
+ storage.write()
+ out.append(path)
+ return out
def get_action(self):
action = run_hook('get_action', self)
@@ -641,34 +253,3 @@ class WalletStorage(JsonDB):
if not self.file_exists():
return 'new'
- def get_seed_version(self):
- seed_version = self.get('seed_version')
- if not seed_version:
- seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
- if seed_version > FINAL_SEED_VERSION:
- raise WalletFileException('This version of Electrum is too old to open this wallet.\n'
- '(highest supported storage version: {}, version of this file: {})'
- .format(FINAL_SEED_VERSION, seed_version))
- if seed_version==14 and self.get('seed_type') == 'segwit':
- self.raise_unsupported_version(seed_version)
- if seed_version >=12:
- return seed_version
- if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
- self.raise_unsupported_version(seed_version)
- return seed_version
-
- def raise_unsupported_version(self, seed_version):
- msg = "Your wallet has an unsupported seed version."
- msg += '\n\nWallet file: %s' % os.path.abspath(self.path)
- if seed_version in [5, 7, 8, 9, 10, 14]:
- msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version
- if seed_version == 6:
- # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog
- msg += '\n\nThis file was created because of a bug in version 1.9.8.'
- if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None:
- # pbkdf2 (at that time an additional dependency) was not included with the binaries, and wallet creation aborted.
- msg += "\nIt does not contain any keys, and can safely be removed."
- else:
- # creation was complete if electrum was run from source
- msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
- raise WalletFileException(msg)
diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py
@@ -8,7 +8,8 @@ from unittest import TestCase
import time
from io import StringIO
-from electrum.storage import WalletStorage, FINAL_SEED_VERSION
+from electrum.storage import WalletStorage
+from electrum.json_db import FINAL_SEED_VERSION
from electrum.wallet import Abstract_Wallet
from electrum.exchange_rate import ExchangeBase, FxThread
from electrum.util import TxMinedInfo
diff --git a/electrum/util.py b/electrum/util.py
@@ -1114,3 +1114,14 @@ class OrderedDictWithIndex(OrderedDict):
self._key_to_pos[key] = pos
self._pos_to_key[pos] = key
return ret
+
+
+def multisig_type(wallet_type):
+ '''If wallet_type is mofn multi-sig, return [m, n],
+ otherwise return None.'''
+ if not wallet_type:
+ return None
+ match = re.match(r'(\d+)of(\d+)', wallet_type)
+ if match:
+ match = [int(x) for x in match.group(1, 2)]
+ return match
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -50,7 +50,8 @@ from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
is_minikey, relayfee, dust_threshold)
from .crypto import sha256d
from .keystore import load_keystore, Hardware_KeyStore
-from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW, WalletStorage
+from .util import multisig_type
+from .storage import STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW, WalletStorage
from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32
from .transaction import Transaction, TxOutput, TxOutputHwInfo
from .plugin import run_hook