electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 9d1c31255c235a7506d29514b90628c3927d83be
parent cde1d0f6c07c9c340dcd89c937e8c3c0c2526c8d
Author: thomasv <thomasv@gitorious>
Date:   Mon,  2 Sep 2013 11:16:35 +0200

Merge branch '1.9' of git://github.com/spesmilo/electrum into 1.9

Diffstat:
Melectrum | 47++++++++++++++++++++++-------------------------
Mgui/gui_classic.py | 180+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mgui/gui_text.py | 16+++++++++++++---
Mgui/installwizard.py | 24+++++++++++++++---------
Mgui/network_dialog.py | 2+-
Mgui/password_dialog.py | 4----
Mlib/__init__.py | 5+++--
Mlib/account.py | 74++++++++++++++++++++++++--------------------------------------------------
Alib/blockchain.py | 329+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/interface.py | 2+-
Mlib/simple_config.py | 135++++++++++++++++++++++---------------------------------------------------------
Mlib/verifier.py | 301++++++-------------------------------------------------------------------------
Mlib/version.py | 2+-
Mlib/wallet.py | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msetup.py | 1+
15 files changed, 738 insertions(+), 640 deletions(-)

diff --git a/electrum b/electrum @@ -107,7 +107,7 @@ if __name__ == '__main__': util.check_windows_wallet_migration() config = SimpleConfig(config_options) - + storage = WalletStorage(config) if len(args)==0: url = None @@ -133,10 +133,14 @@ if __name__ == '__main__': interface.start(wait = False) interface.send([('server.peers.subscribe',[])]) - gui = gui.ElectrumGui(config,interface) + blockchain = BlockchainVerifier(interface, config) + blockchain.start() + + gui = gui.ElectrumGui(config, interface, blockchain) gui.main(url) interface.stop() + blockchain.stop() # we use daemon threads, their termination is enforced. # this sleep command gives them time to terminate cleanly. @@ -145,12 +149,12 @@ if __name__ == '__main__': # instanciate wallet for command-line - wallet = Wallet(config) + wallet = Wallet(storage) if cmd not in known_commands: cmd = 'help' - if not config.wallet_file_exists and cmd not in ['help','create','restore']: + if not storage.file_exists and cmd not in ['help','create','restore']: print_msg("Error: Wallet file not found.") print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option") sys.exit(0) @@ -197,13 +201,11 @@ if __name__ == '__main__': if not interface.start(wait=True): print_msg("Not connected, aborting. Try option -o if you want to restore offline.") sys.exit(1) - wallet.interface = interface - verifier = WalletVerifier(interface, config) - verifier.start() - wallet.set_verifier(verifier) + blockchain = BlockchainVerifier(interface, config) + blockchain.start() + wallet.start_threads(interface, blockchain) print_msg("Recovering wallet...") - WalletSynchronizer(wallet, config).start() wallet.update() if wallet.is_found(): print_msg("Recovery successful") @@ -317,26 +319,21 @@ if __name__ == '__main__': message = ' '.join(args[min_args:]) print_msg("Warning: Final argument was reconstructed from several arguments:", repr(message)) args = args[0:min_args] + [ message ] - - - # open session if cmd not in offline_commands and not options.offline: interface = Interface(config) interface.register_callback('connected', lambda: sys.stderr.write("Connected to " + interface.connection_msg + "\n")) + if not interface.start(wait=True): print_msg("Not connected, aborting.") sys.exit(1) - wallet.interface = interface - verifier = WalletVerifier(interface, config) - verifier.start() - wallet.set_verifier(verifier) - synchronizer = WalletSynchronizer(wallet, config) - synchronizer.start() + blockchain = BlockchainVerifier(interface, config) + blockchain.start() + wallet.start_threads(interface, blockchain) wallet.update() - #wallet.save() + # run the command @@ -350,9 +347,9 @@ if __name__ == '__main__': if raw_input("Are you sure you want to continue? (y/n) ") in ['y','Y','yes']: wallet.config.path = ns wallet.seed = '' - wallet.config.set_key('seed', '', True) + wallet.storage.put('seed', '', True) wallet.use_encryption = False - wallet.config.set_key('use_encryption', wallet.use_encryption, True) + wallet.storage.put('use_encryption', wallet.use_encryption, True) for k in wallet.imported_keys.keys(): wallet.imported_keys[k] = '' wallet.config.set_key('imported_keys',wallet.imported_keys, True) print_msg("Done.") @@ -361,12 +358,12 @@ if __name__ == '__main__': elif cmd == 'getconfig': key = args[1] - print_msg(wallet.config.get(key)) + print_msg(config.get(key)) elif cmd == 'setconfig': key, value = args[1:3] if key not in ['seed', 'seed_version', 'master_public_key', 'use_encryption']: - wallet.config.set_key(key, value, True) + config.set_key(key, value, True) print_msg(True) else: print_msg(False) @@ -394,8 +391,8 @@ if __name__ == '__main__': if cmd not in offline_commands and not options.offline: - verifier.stop() - synchronizer.stop() + wallet.stop_threads() interface.stop() + blockchain.stop() time.sleep(0.1) sys.exit(0) diff --git a/gui/gui_classic.py b/gui/gui_classic.py @@ -42,7 +42,9 @@ except: from electrum.wallet import format_satoshis from electrum.bitcoin import Transaction, is_valid from electrum import mnemonic -from electrum import util, bitcoin, commands, Interface, Wallet, WalletVerifier, WalletSynchronizer +from electrum import util, bitcoin, commands, Interface, Wallet +from electrum import SimpleConfig, Wallet, WalletStorage + import bmp, pyqrnative import exchange_rate @@ -225,9 +227,12 @@ class ElectrumWindow(QMainWindow): def __init__(self, config): QMainWindow.__init__(self) + + self.config = config + self.init_plugins() + self._close_electrum = False self.lite = None - self.config = config self.current_account = self.config.get("current_account", None) self.icon = QIcon(os.getcwd() + '/icons/electrum.png') @@ -237,14 +242,13 @@ class ElectrumWindow(QMainWindow): self.build_menu() self.tray.show() - - self.init_plugins() self.create_status_bar() self.need_update = threading.Event() - self.expert_mode = config.get('classic_expert_mode', False) + self.expert_mode = config.get('classic_expert_mode', False) self.decimal_point = config.get('decimal_point', 8) + self.num_zeros = int(config.get('num_zeros',0)) set_language(config.get('language')) @@ -287,11 +291,12 @@ class ElectrumWindow(QMainWindow): tabs.setCurrentIndex (n) tabs.setCurrentIndex (0) - # plugins that need to change the GUI do it here self.run_hook('init') + + def load_wallet(self, wallet): import electrum self.wallet = wallet @@ -301,7 +306,7 @@ class ElectrumWindow(QMainWindow): self.wallet.interface.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status'))) self.wallet.interface.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status'))) self.wallet.interface.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal'))) - title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path + title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path if not self.wallet.seed: title += ' [%s]' % (_('seedless')) self.setWindowTitle( title ) self.update_wallet() @@ -312,59 +317,59 @@ class ElectrumWindow(QMainWindow): # account selector accounts = self.wallet.get_accounts() + self.account_selector.clear() if len(accounts) > 1: self.account_selector.addItems([_("All accounts")] + accounts.values()) self.account_selector.setCurrentIndex(0) + self.account_selector.show() + else: + self.account_selector.hide() + self.update_lock_icon() + self.update_buttons_on_seed() + self.update_console() def select_wallet_file(self): - wallet_folder = self.wallet.config.path + wallet_folder = self.wallet.storage.path re.sub("(\/\w*.dat)$", "", wallet_folder) file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder, "*.dat") ) return file_name def open_wallet(self): - from electrum import SimpleConfig, Wallet, WalletSynchronizer filename = self.select_wallet_file() if not filename: return - config = SimpleConfig({'wallet_path': filename}) - if not config.wallet_file_exists: + storage = WalletStorage({'wallet_path': filename}) + if not storage.file_exists: self.show_message("file not found "+ filename) return interface = self.wallet.interface - verifier = self.wallet.verifier - self.wallet.synchronizer.stop() + blockchain = self.wallet.verifier.blockchain + self.wallet.stop_threads() - self.config = config - - # create wallet - wallet = Wallet(config) - wallet.interface = interface - wallet.verifier = verifier - synchronizer = WalletSynchronizer(wallet, config) - synchronizer.start() + # create new wallet + wallet = Wallet(storage) + wallet.start_threads(interface, blockchain) self.load_wallet(wallet) def new_wallet(self): - from electrum import SimpleConfig, Wallet, WalletSynchronizer import installwizard - wallet_folder = self.wallet.config.path + wallet_folder = self.wallet.storage.path re.sub("(\/\w*.dat)$", "", wallet_folder) filename = self.getSaveFileName("Select your wallet file", wallet_folder, "*.dat") - config = SimpleConfig({'wallet_path': filename}) - assert not config.wallet_file_exists + storage = WalletStorage({'wallet_path': filename}) + assert not storage.file_exists - wizard = installwizard.InstallWizard(config, self.wallet.interface) + wizard = installwizard.InstallWizard(self.config, self.wallet.interface, self.wallet.verifier.blockchain, storage) wallet = wizard.run() if wallet: self.load_wallet(wallet) @@ -374,22 +379,29 @@ class ElectrumWindow(QMainWindow): def init_menubar(self): menubar = QMenuBar() - electrum_menu = menubar.addMenu(_("&File")) - open_wallet_action = electrum_menu.addAction(_("Open wallet")) + file_menu = menubar.addMenu(_("&File")) + open_wallet_action = file_menu.addAction(_("&Open")) open_wallet_action.triggered.connect(self.open_wallet) - new_wallet_action = electrum_menu.addAction(_("New wallet")) + new_wallet_action = file_menu.addAction(_("&Create/Restore")) new_wallet_action.triggered.connect(self.new_wallet) - preferences_name = _("Preferences") - if sys.platform == 'darwin': - preferences_name = _("Electrum preferences") # Settings / Preferences are all reserved keywords in OSX using this as work around + wallet_backup = file_menu.addAction(_("&Copy")) + wallet_backup.triggered.connect(lambda: backup_wallet(self.wallet.storage.path)) + + quit_item = file_menu.addAction(_("&Close")) + quit_item.triggered.connect(self.close) + + wallet_menu = menubar.addMenu(_("&Wallet")) - preferences_menu = electrum_menu.addAction(preferences_name) + # Settings / Preferences are all reserved keywords in OSX using this as work around + preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences") + preferences_menu = wallet_menu.addAction(preferences_name) preferences_menu.triggered.connect(self.settings_dialog) - electrum_menu.addSeparator() - raw_transaction_menu = electrum_menu.addMenu(_("&Load raw transaction")) + wallet_menu.addSeparator() + + raw_transaction_menu = wallet_menu.addMenu(_("&Load raw transaction")) raw_transaction_file = raw_transaction_menu.addAction(_("&From file")) raw_transaction_file.triggered.connect(self.do_process_from_file) @@ -397,13 +409,7 @@ class ElectrumWindow(QMainWindow): raw_transaction_text = raw_transaction_menu.addAction(_("&From text")) raw_transaction_text.triggered.connect(self.do_process_from_text) - electrum_menu.addSeparator() - quit_item = electrum_menu.addAction(_("&Close")) - quit_item.triggered.connect(self.close) - - wallet_menu = menubar.addMenu(_("&Wallet")) - wallet_backup = wallet_menu.addAction(_("&Create backup")) - wallet_backup.triggered.connect(lambda: backup_wallet(self.config.path)) + wallet_menu.addSeparator() show_menu = wallet_menu.addMenu(_("Show")) @@ -514,14 +520,15 @@ class ElectrumWindow(QMainWindow): return - + + def set_label(self, name, text = None): changed = False old_text = self.wallet.labels.get(name) if text: if old_text != text: self.wallet.labels[name] = text - self.wallet.config.set_key('labels', self.wallet.labels) + self.wallet.storage.put('labels', self.wallet.labels) changed = True else: if old_text: @@ -564,7 +571,7 @@ class ElectrumWindow(QMainWindow): self.run_hook('timer_actions') def format_amount(self, x, is_diff=False, whitespaces=False): - return format_satoshis(x, is_diff, self.wallet.num_zeros, self.decimal_point, whitespaces) + return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces) def read_amount(self, x): if x in['.', '']: return None @@ -850,14 +857,10 @@ class ElectrumWindow(QMainWindow): _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\ + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\ + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3) - b = '' - if 1:#self.wallet.seed: - b = EnterButton(_("Send"), self.do_send) - else: - b = EnterButton(_("Create unsigned transaction"), self.do_send) - grid.addWidget(b, 6, 1) + self.send_button = EnterButton(_("Send"), self.do_send) + grid.addWidget(self.send_button, 6, 1) b = EnterButton(_("Clear"),self.do_clear) grid.addWidget(b, 6, 2) @@ -1339,10 +1342,12 @@ class ElectrumWindow(QMainWindow): from qt_console import Console self.console = console = Console() return console - # - self.console.history = self.config.get("console-history",[]) - self.console.history_index = len(self.console.history) + + def update_console(self): + console = self.console + console.history = self.config.get("console-history",[]) + console.history_index = len(console.history) console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self}) console.updateNamespace({'util' : util, 'bitcoin':bitcoin}) @@ -1356,7 +1361,7 @@ class ElectrumWindow(QMainWindow): methods[m] = mkfunc(c._run, m) console.updateNamespace(methods) - return console + def change_account(self,s): if s == _("All accounts"): @@ -1389,13 +1394,14 @@ class ElectrumWindow(QMainWindow): if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7): sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) ) - if 1:#self.wallet.seed: - self.lock_icon = QIcon(":icons/lock.png") #if self.wallet.use_encryption else QIcon(":icons/unlock.png") - self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog ) - sb.addPermanentWidget( self.password_button ) + + self.lock_icon = QIcon() + self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog ) + sb.addPermanentWidget( self.password_button ) + sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) ) - if 1:#self.wallet.seed: - sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) ) + self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) + sb.addPermanentWidget( self.seed_button ) self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog ) sb.addPermanentWidget( self.status_button ) @@ -1404,10 +1410,27 @@ class ElectrumWindow(QMainWindow): self.setStatusBar(sb) + def update_lock_icon(self): + icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png") + self.password_button.setIcon( icon ) + + + def update_buttons_on_seed(self): + if self.wallet.seed: + self.seed_button.show() + self.password_button.show() + self.send_button.setText(_("Send")) + else: + self.password_button.hide() + self.seed_button.hide() + self.send_button.setText(_("Create unsigned transaction")) + + def change_password_dialog(self): from password_dialog import PasswordDialog d = PasswordDialog(self.wallet, self) d.run() + self.update_lock_icon() def go_lite(self): @@ -1420,6 +1443,7 @@ class ElectrumWindow(QMainWindow): self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self) self.lite.main(None) + def new_contact_dialog(self): text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':') address = unicode(text) @@ -1441,7 +1465,7 @@ class ElectrumWindow(QMainWindow): addr = self.wallet.new_account_address() vbox = QVBoxLayout() - vbox.addWidget(QLabel("To add another account, please send bitcoins to the following address:")) + vbox.addWidget(QLabel(_("To create a new account, please send coins to the first address of that account:"))) e = QLineEdit(addr) e.setReadOnly(True) vbox.addWidget(e) @@ -1941,7 +1965,7 @@ class ElectrumWindow(QMainWindow): nz_label = QLabel(_('Display zeros')) grid_ui.addWidget(nz_label, 0, 0) nz_e = AmountEdit(None,True) - nz_e.setText("%d"% self.wallet.num_zeros) + nz_e.setText("%d"% self.num_zeros) grid_ui.addWidget(nz_e, 0, 1) msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"') grid_ui.addWidget(HelpButton(msg), 0, 2) @@ -2097,8 +2121,8 @@ class ElectrumWindow(QMainWindow): QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK')) return - if self.wallet.num_zeros != nz: - self.wallet.num_zeros = nz + if self.num_zeros != nz: + self.num_zeros = nz self.config.set_key('num_zeros', nz, True) self.update_history_tab() self.update_receive_tab() @@ -2155,7 +2179,7 @@ class ElectrumWindow(QMainWindow): g = self.geometry() self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True) self.save_column_widths() - self.config.set_key("console-history",self.console.history[-50:]) + self.config.set_key("console-history", self.console.history[-50:], True) event.accept() class OpenFileEventFilter(QObject): @@ -2175,9 +2199,10 @@ class OpenFileEventFilter(QObject): class ElectrumGui: - def __init__(self, config, interface, app=None): + def __init__(self, config, interface, blockchain, app=None): self.interface = interface self.config = config + self.blockchain = blockchain self.windows = [] self.efilter = OpenFileEventFilter(self.windows) if app is None: @@ -2186,24 +2211,18 @@ class ElectrumGui: def main(self, url): - - found = self.config.wallet_file_exists - if not found: + + storage = WalletStorage(self.config) + if not storage.file_exists: import installwizard - wizard = installwizard.InstallWizard(self.config, self.interface) + wizard = installwizard.InstallWizard(self.config, self.interface, self.blockchain, storage) wallet = wizard.run() if not wallet: exit() else: - wallet = Wallet(self.config) - - wallet.interface = self.interface + wallet = Wallet(storage) - verifier = WalletVerifier(self.interface, self.config) - verifier.start() - wallet.set_verifier(verifier) - synchronizer = WalletSynchronizer(wallet, self.config) - synchronizer.start() + wallet.start_threads(self.interface, self.blockchain) s = Timer() s.start() @@ -2219,7 +2238,6 @@ class ElectrumGui: self.app.exec_() - verifier.stop() - synchronizer.stop() + wallet.stop_threads() diff --git a/gui/gui_text.py b/gui/gui_text.py @@ -5,12 +5,24 @@ _ = lambda x:x from electrum.util import format_satoshis, set_verbosity from electrum.bitcoin import is_valid +from electrum import Wallet, WalletStorage + import tty, sys class ElectrumGui: - def __init__(self, wallet, config, app=None): + def __init__(self, config, interface, blockchain): + + self.config = config + storage = WalletStorage(config) + if not storage.file_exists: + print "Wallet not found. try 'electrum create'" + exit() + + self.wallet = Wallet(storage) + self.wallet.start_threads(interface, blockchain) + self.stdscr = curses.initscr() curses.noecho() curses.cbreak() @@ -24,8 +36,6 @@ class ElectrumGui: self.set_cursor(0) self.w = curses.newwin(10, 50, 5, 5) - self.wallet = wallet - self.config = config set_verbosity(False) self.tab = 0 self.pos = 0 diff --git a/gui/installwizard.py b/gui/installwizard.py @@ -3,7 +3,7 @@ from PyQt4.QtCore import * import PyQt4.QtCore as QtCore from i18n import _ -from electrum import Wallet, mnemonic, WalletVerifier, WalletSynchronizer +from electrum import Wallet, mnemonic from seed_dialog import SeedDialog from network_dialog import NetworkDialog @@ -14,10 +14,12 @@ import sys class InstallWizard(QDialog): - def __init__(self, config, interface): + def __init__(self, config, interface, blockchain, storage): QDialog.__init__(self) self.config = config self.interface = interface + self.blockchain = blockchain + self.storage = storage def restore_or_create(self): @@ -124,6 +126,15 @@ class InstallWizard(QDialog): wallet.set_up_to_date(False) wallet.interface.poke('synchronizer') waiting_dialog(waiting) + + # try to restore old account + if not wallet.is_found(): + print "trying old method" + wallet.create_old_account() + wallet.set_up_to_date(False) + wallet.interface.poke('synchronizer') + waiting_dialog(waiting) + if wallet.is_found(): QMessageBox.information(None, _('Information'), _("Recovery successful"), _('OK')) else: @@ -137,8 +148,7 @@ class InstallWizard(QDialog): a = self.restore_or_create() if not a: exit() - wallet = Wallet(self.config) - wallet.interface = self.interface + wallet = Wallet(self.storage) if a =='create': wallet.init_seed(None) @@ -170,11 +180,7 @@ class InstallWizard(QDialog): #self.interface.start(wait = False) # start wallet threads - verifier = WalletVerifier(self.interface, self.config) - verifier.start() - wallet.set_verifier(verifier) - synchronizer = WalletSynchronizer(wallet, self.config) - synchronizer.start() + wallet.start_threads(self.interface, self.blockchain) # generate the first addresses, in case we are offline diff --git a/gui/network_dialog.py b/gui/network_dialog.py @@ -44,7 +44,7 @@ class NetworkDialog(QDialog): if parent: if interface.is_connected: - status = _("Connected to")+" %s"%(interface.host) + "\n%d "%(parent.wallet.verifier.height)+_("blocks") + status = _("Connected to")+" %s"%(interface.host) + "\n%d "%(parent.wallet.verifier.blockchain.height)+_("blocks") else: status = _("Not connected") server = interface.server diff --git a/gui/password_dialog.py b/gui/password_dialog.py @@ -96,9 +96,5 @@ class PasswordDialog(QDialog): QMessageBox.information(self.parent, _('Success'), _('Password was updated successfully'), _('OK')) - if self.parent: - icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png") - self.parent.password_button.setIcon( icon ) - diff --git a/lib/__init__.py b/lib/__init__.py @@ -1,8 +1,9 @@ from version import ELECTRUM_VERSION from util import format_satoshis, print_msg, print_json, print_error, set_verbosity -from wallet import WalletSynchronizer +from wallet import WalletSynchronizer, WalletStorage from wallet_factory import WalletFactory as Wallet -from verifier import WalletVerifier +from verifier import TxVerifier +from blockchain import BlockchainVerifier from interface import Interface, pick_random_server, DEFAULT_SERVERS from simple_config import SimpleConfig import bitcoin diff --git a/lib/account.py b/lib/account.py @@ -48,10 +48,13 @@ class Account(object): class OldAccount(Account): """ Privatekey(type,n) = Master_private_key + H(n|S|type) """ - def __init__(self, mpk, mpk2 = None, mpk3 = None): - self.mpk = mpk - self.mpk2 = mpk2 - self.mpk3 = mpk3 + def __init__(self, v): + self.addresses = v.get(0, []) + self.change = v.get(1, []) + self.mpk = v['mpk'].decode('hex') + + def dump(self): + return {0:self.addresses, 1:self.change} @classmethod def mpk_from_seed(klass, seed): @@ -68,48 +71,34 @@ class OldAccount(Account): seed = hashlib.sha256(seed + oldseed).digest() return string_to_number( seed ) - def get_sequence(self, sequence, mpk): - for_change, n = sequence - return string_to_number( Hash( "%d:%d:"%(n,for_change) + mpk.decode('hex') ) ) - - def get_address(self, sequence): - if not self.mpk2: - pubkey = self.get_pubkey(sequence) - address = public_key_to_bc_address( pubkey.decode('hex') ) - elif not self.mpk3: - pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) - address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"] - else: - pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence, mpk = self.mpk2) - pubkey3 = self.get_pubkey(sequence, mpk = self.mpk3) - address = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)["address"] + def get_sequence(self, for_change, n): + return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.mpk ) ) + + def get_address(self, for_change, n): + pubkey = self.get_pubkey(for_change, n) + address = public_key_to_bc_address( pubkey.decode('hex') ) return address - def get_pubkey(self, sequence, mpk=None): + def get_pubkey(self, for_change, n): curve = SECP256k1 - if mpk is None: mpk = self.mpk - z = self.get_sequence(sequence, mpk) - master_public_key = ecdsa.VerifyingKey.from_string( mpk.decode('hex'), curve = SECP256k1 ) + mpk = self.mpk + z = self.get_sequence(for_change, n) + master_public_key = ecdsa.VerifyingKey.from_string( mpk, curve = SECP256k1 ) pubkey_point = master_public_key.pubkey.point + z*curve.generator public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 ) return '04' + public_key2.to_string().encode('hex') - def get_private_key_from_stretched_exponent(self, sequence, secexp): + def get_private_key_from_stretched_exponent(self, for_change, n, secexp): order = generator_secp256k1.order() - secexp = ( secexp + self.get_sequence(sequence, self.mpk) ) % order + secexp = ( secexp + self.get_sequence(for_change, n) ) % order pk = number_to_string( secexp, generator_secp256k1.order() ) compressed = False return SecretToASecret( pk, compressed ) - def get_private_key(self, sequence, seed): - secexp = self.stretch_key(seed) - return self.get_private_key_from_stretched_exponent(sequence, secexp) - - def get_private_keys(self, sequence_list, seed): + def get_private_key(self, seed, sequence): + for_change, n = sequence secexp = self.stretch_key(seed) - return [ self.get_private_key_from_stretched_exponent( sequence, secexp) for sequence in sequence_list] + return self.get_private_key_from_stretched_exponent(for_change, n, secexp) def check_seed(self, seed): curve = SECP256k1 @@ -121,23 +110,8 @@ class OldAccount(Account): raise BaseException('Invalid password') return True - def get_input_info(self, sequence): - if not self.mpk2: - pk_addr = self.get_address(sequence) - redeemScript = None - elif not self.mpk3: - pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence,mpk=self.mpk2) - pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key - redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript'] - else: - pubkey1 = self.get_pubkey(sequence) - pubkey2 = self.get_pubkey(sequence, mpk=self.mpk2) - pubkey3 = self.get_pubkey(sequence, mpk=self.mpk3) - pk_addr = public_key_to_bc_address( pubkey1.decode('hex') ) # we need to return that address to get the right private key - redeemScript = Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 2)['redeemScript'] - return pk_addr, redeemScript - + def redeem_script(self, sequence): + return None class BIP32_Account(Account): diff --git a/lib/blockchain.py b/lib/blockchain.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2012 thomasv@ecdsa.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import threading, time, Queue, os, sys, shutil +from util import user_dir, appdata_dir, print_error +from bitcoin import * + + +class BlockchainVerifier(threading.Thread): + """ Simple Payment Verification """ + + def __init__(self, interface, config): + threading.Thread.__init__(self) + self.daemon = True + self.config = config + self.interface = interface + self.interface.register_channel('verifier') + self.lock = threading.Lock() + self.pending_headers = [] # headers that have not been verified + self.height = 0 + self.local_height = 0 + self.running = False + self.headers_url = 'http://headers.electrum.org/blockchain_headers' + + + def stop(self): + with self.lock: self.running = False + self.interface.poke('verifier') + + def is_running(self): + with self.lock: return self.running + + def run(self): + + self.init_headers_file() + self.set_local_height() + + with self.lock: + self.running = True + requested_chunks = [] + requested_headers = [] + all_chunks = False + + # subscribe to block headers + self.interface.send([ ('blockchain.headers.subscribe',[])], 'verifier') + + while self.is_running(): + # request missing chunks + if not all_chunks and self.height and not requested_chunks: + + if self.local_height + 50 < self.height: + min_index = (self.local_height + 1)/2016 + max_index = (self.height + 1)/2016 + for i in range(min_index, max_index + 1): + print_error( "requesting chunk", i ) + self.interface.send([ ('blockchain.block.get_chunk',[i])], 'verifier') + requested_chunks.append(i) + break + else: + all_chunks = True + print_error("downloaded all chunks") + + + # process pending headers + if self.pending_headers and all_chunks: + done = [] + for header in self.pending_headers: + if self.verify_header(header): + done.append(header) + else: + # request previous header + i = header.get('block_height') - 1 + if i not in requested_headers: + print_error("requesting header %d"%i) + self.interface.send([ ('blockchain.block.get_header',[i])], 'verifier') + requested_headers.append(i) + # no point continuing + break + if done: + self.interface.trigger_callback('updated') + for header in done: + self.pending_headers.remove(header) + + try: + r = self.interface.get_response('verifier',timeout=1) + except Queue.Empty: + continue + if not r: continue + + if r.get('error'): + print_error('Verifier received an error:', r) + continue + + # 3. handle response + method = r['method'] + params = r['params'] + result = r['result'] + + if method == 'blockchain.block.get_chunk': + index = params[0] + self.verify_chunk(index, result) + requested_chunks.remove(index) + + elif method in ['blockchain.headers.subscribe', 'blockchain.block.get_header']: + + self.pending_headers.append(result) + if method == 'blockchain.block.get_header': + requested_headers.remove(result.get('block_height')) + else: + self.height = result.get('block_height') + ## fixme # self.interface.poke('synchronizer') + + self.pending_headers.sort(key=lambda x: x.get('block_height')) + # print "pending headers", map(lambda x: x.get('block_height'), self.pending_headers) + + + + + + def verify_chunk(self, index, hexdata): + data = hexdata.decode('hex') + height = index*2016 + num = len(data)/80 + print_error("validating headers %d"%height) + + if index == 0: + previous_hash = ("0"*64) + else: + prev_header = self.read_header(index*2016-1) + if prev_header is None: raise + previous_hash = self.hash_header(prev_header) + + bits, target = self.get_target(index) + + for i in range(num): + height = index*2016 + i + raw_header = data[i*80:(i+1)*80] + header = self.header_from_string(raw_header) + _hash = self.hash_header(header) + assert previous_hash == header.get('prev_block_hash') + assert bits == header.get('bits') + assert eval('0x'+_hash) < target + + previous_header = header + previous_hash = _hash + + self.save_chunk(index, data) + + + def verify_header(self, header): + # add header to the blockchain file + # if there is a reorg, push it in a stack + + height = header.get('block_height') + + prev_header = self.read_header(height -1) + if not prev_header: + # return False to request previous header + return False + + prev_hash = self.hash_header(prev_header) + bits, target = self.get_target(height/2016) + _hash = self.hash_header(header) + try: + assert prev_hash == header.get('prev_block_hash') + assert bits == header.get('bits') + assert eval('0x'+_hash) < target + except: + # this can be caused by a reorg. + print_error("verify header failed"+ repr(header)) + # undo verifications + with self.lock: + items = self.verified_tx.items()[:] + for tx_hash, item in items: + tx_height, timestamp, pos = item + if tx_height >= height: + print_error("redoing", tx_hash) + with self.lock: + self.verified_tx.pop(tx_hash) + if tx_hash in self.merkle_roots: + self.merkle_roots.pop(tx_hash) + # return False to request previous header. + return False + + self.save_header(header) + print_error("verify header:", _hash, height) + return True + + + + + def header_to_string(self, res): + s = int_to_hex(res.get('version'),4) \ + + rev_hex(res.get('prev_block_hash')) \ + + rev_hex(res.get('merkle_root')) \ + + int_to_hex(int(res.get('timestamp')),4) \ + + int_to_hex(int(res.get('bits')),4) \ + + int_to_hex(int(res.get('nonce')),4) + return s + + + def header_from_string(self, s): + hex_to_int = lambda s: eval('0x' + s[::-1].encode('hex')) + h = {} + h['version'] = hex_to_int(s[0:4]) + h['prev_block_hash'] = hash_encode(s[4:36]) + h['merkle_root'] = hash_encode(s[36:68]) + h['timestamp'] = hex_to_int(s[68:72]) + h['bits'] = hex_to_int(s[72:76]) + h['nonce'] = hex_to_int(s[76:80]) + return h + + def hash_header(self, header): + return rev_hex(Hash(self.header_to_string(header).decode('hex')).encode('hex')) + + def path(self): + wdir = self.config.get('blockchain_headers_path', user_dir()) + if wdir and not os.path.exists( wdir ): os.mkdir(wdir) + return os.path.join( wdir, 'blockchain_headers') + + def init_headers_file(self): + filename = self.path() + if os.path.exists(filename): + return + + try: + import urllib, socket + socket.setdefaulttimeout(30) + print_error("downloading ", self.headers_url ) + urllib.urlretrieve(self.headers_url, filename) + except: + print_error( "download failed. creating file", filename ) + open(filename,'wb+').close() + + def save_chunk(self, index, chunk): + filename = self.path() + f = open(filename,'rb+') + f.seek(index*2016*80) + h = f.write(chunk) + f.close() + self.set_local_height() + + def save_header(self, header): + data = self.header_to_string(header).decode('hex') + assert len(data) == 80 + height = header.get('block_height') + filename = self.path() + f = open(filename,'rb+') + f.seek(height*80) + h = f.write(data) + f.close() + self.set_local_height() + + + def set_local_height(self): + name = self.path() + if os.path.exists(name): + h = os.path.getsize(name)/80 - 1 + if self.local_height != h: + self.local_height = h + + + def read_header(self, block_height): + name = self.path() + if os.path.exists(name): + f = open(name,'rb') + f.seek(block_height*80) + h = f.read(80) + f.close() + if len(h) == 80: + h = self.header_from_string(h) + return h + + + def get_target(self, index): + + max_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 + if index == 0: return 0x1d00ffff, max_target + + first = self.read_header((index-1)*2016) + last = self.read_header(index*2016-1) + + nActualTimespan = last.get('timestamp') - first.get('timestamp') + nTargetTimespan = 14*24*60*60 + nActualTimespan = max(nActualTimespan, nTargetTimespan/4) + nActualTimespan = min(nActualTimespan, nTargetTimespan*4) + + bits = last.get('bits') + # convert to bignum + MM = 256*256*256 + a = bits%MM + if a < 0x8000: + a *= 256 + target = (a) * pow(2, 8 * (bits/MM - 3)) + + # new target + new_target = min( max_target, (target * nActualTimespan)/nTargetTimespan ) + + # convert it to bits + c = ("%064X"%new_target)[2:] + i = 31 + while c[0:2]=="00": + c = c[2:] + i -= 1 + + c = eval('0x'+c[0:6]) + if c > 0x800000: + c /= 256 + i += 1 + + new_bits = c + MM * i + return new_bits, new_target + diff --git a/lib/interface.py b/lib/interface.py @@ -396,7 +396,7 @@ class Interface(threading.Thread): self.unanswered_requests[self.message_id] = method, params, channel ids.append(self.message_id) # uncomment to debug - # print "-->",request + # print "-->", request self.message_id += 1 out += request + '\n' while out: diff --git a/lib/simple_config.py b/lib/simple_config.py @@ -6,6 +6,9 @@ from version import ELECTRUM_VERSION, SEED_VERSION + + + class SimpleConfig: """ The SimpleConfig class is responsible for handling operations involving @@ -29,46 +32,44 @@ a SimpleConfig instance then reads the wallet file. # command-line options self.options_config = options - self.wallet_config = {} - self.wallet_file_exists = False - self.init_path(self.options_config.get('wallet_path')) - print_error( "path", self.path ) - if self.path: - self.read_wallet_config(self.path) + # init path + self.init_path(options) + + print_error( "user dir", self.user_dir) + + + def init_path(self, options): + + # Look for wallet file in the default data directory. + # Make wallet directory if it does not yet exist. + if not os.path.exists(self.user_dir): + os.mkdir(self.user_dir) + # portable wallet: use the same directory for wallet and headers file - if options.get('portable'): - self.wallet_config['blockchain_headers_path'] = os.path.dirname(self.path) + #if options.get('portable'): + # self.wallet_config['blockchain_headers_path'] = os.path.dirname(self.path) - - - - def set_key(self, key, value, save = False): + def set_key(self, key, value, save = True): # find where a setting comes from and save it there if self.options_config.get(key) is not None: print "Warning: not changing '%s' because it was passed as a command-line option"%key return - elif self.user_config.get(key) is not None: - self.user_config[key] = value - if save: self.save_user_config() - elif self.system_config.get(key) is not None: if str(self.system_config[key]) != str(value): print "Warning: not changing '%s' because it was set in the system configuration"%key - elif self.wallet_config.get(key) is not None: - self.wallet_config[key] = value - if save: self.save_wallet_config() - else: - # add key to wallet config - self.wallet_config[key] = value - if save: self.save_wallet_config() + self.user_config[key] = value + if save: self.save_user_config() + def get(self, key, default=None): - """Retrieve the filepath of the configuration file specified in the 'key' parameter.""" + + out = None + # 1. command-line options always override everything if self.options_config.has_key(key) and self.options_config.get(key) is not None: out = self.options_config.get(key) @@ -81,10 +82,6 @@ a SimpleConfig instance then reads the wallet file. elif self.system_config.has_key(key): out = self.system_config.get(key) - # 3. use the wallet file config - else: - out = self.wallet_config.get(key) - if out is None and default is not None: out = default @@ -135,85 +132,29 @@ a SimpleConfig instance then reads the wallet file. """Parse and store the user config settings in electrum.conf into user_config[].""" if not self.user_dir: return - name = os.path.join( self.user_dir, 'electrum.conf') - if os.path.exists(name): + path = os.path.join(self.user_dir, "config") + if os.path.exists(path): try: - import ConfigParser - except ImportError: - print "cannot parse electrum.conf. please install ConfigParser" + with open(path, "r") as f: + data = f.read() + except IOError: return - - p = ConfigParser.ConfigParser() - p.read(name) try: - for k, v in p.items('client'): - self.user_config[k] = v - except ConfigParser.NoSectionError: - pass - - def init_path(self, path): - """Set the path of the wallet.""" - - if not path: - path = self.get('default_wallet_path') - - if path is not None: - self.path = path - return + d = ast.literal_eval( data ) #parse raw data from reading wallet file + except: + raise IOError("Cannot read config file.") - # Look for wallet file in the default data directory. - # Make wallet directory if it does not yet exist. - if not os.path.exists(self.user_dir): - os.mkdir(self.user_dir) - self.path = os.path.join(self.user_dir, "electrum.dat") + self.user_config = d def save_user_config(self): if not self.user_dir: return - import ConfigParser - config = ConfigParser.RawConfigParser() - config.add_section('client') - for k,v in self.user_config.items(): - config.set('client', k, v) - - with open( os.path.join( self.user_dir, 'electrum.conf'), 'wb') as configfile: - config.write(configfile) - - - - - def read_wallet_config(self, path): - """Read the contents of the wallet file.""" - try: - with open(self.path, "r") as f: - data = f.read() - except IOError: - return - try: - d = ast.literal_eval( data ) #parse raw data from reading wallet file - except: - raise IOError("Cannot read wallet file.") - - self.wallet_config = d - self.wallet_file_exists = True - - - - def save(self, key=None): - self.save_wallet_config() - - - def save_wallet_config(self): - # prevent the creation of incomplete wallets - if self.wallet_config.get('master_public_keys') is None: - return - - s = repr(self.wallet_config) - f = open(self.path,"w") + path = os.path.join(self.user_dir, "config") + s = repr(self.user_config) + f = open(path,"w") f.write( s ) f.close() if self.get('gui') != 'android': import stat - os.chmod(self.path,stat.S_IREAD | stat.S_IWRITE) - + os.chmod(path, stat.S_IREAD | stat.S_IWRITE) diff --git a/lib/verifier.py b/lib/verifier.py @@ -24,34 +24,29 @@ from bitcoin import * -class WalletVerifier(threading.Thread): +class TxVerifier(threading.Thread): """ Simple Payment Verification """ - def __init__(self, interface, config): + def __init__(self, interface, blockchain, storage): threading.Thread.__init__(self) self.daemon = True - self.config = config + self.storage = storage + self.blockchain = blockchain self.interface = interface self.transactions = {} # requested verifications (with height sent by the requestor) - self.interface.register_channel('verifier') - - self.verified_tx = config.get('verified_tx3',{}) # height, timestamp of verified transactions - self.merkle_roots = config.get('merkle_roots',{}) # hashed by me - - self.targets = config.get('targets',{}) # compute targets + self.interface.register_channel('txverifier') + self.verified_tx = storage.get('verified_tx3',{}) # height, timestamp of verified transactions + self.merkle_roots = storage.get('merkle_roots',{}) # hashed by me self.lock = threading.Lock() - self.pending_headers = [] # headers that have not been verified - self.height = 0 - self.local_height = 0 self.running = False - self.headers_url = 'http://headers.electrum.org/blockchain_headers' + def get_confirmations(self, tx): """ return the number of confirmations of a monitored transaction. """ with self.lock: if tx in self.verified_tx: height, timestamp, pos = self.verified_tx[tx] - conf = (self.local_height - height + 1) + conf = (self.blockchain.local_height - height + 1) if conf <= 0: timestamp = None elif tx in self.transactions: @@ -101,67 +96,21 @@ class WalletVerifier(threading.Thread): with self.lock: return self.running def run(self): - - self.init_headers_file() - self.set_local_height() - with self.lock: self.running = True requested_merkle = [] - requested_chunks = [] - requested_headers = [] - all_chunks = False - - # subscribe to block headers - self.interface.send([ ('blockchain.headers.subscribe',[])], 'verifier') while self.is_running(): - # request missing chunks - if not all_chunks and self.height and not requested_chunks: - - if self.local_height + 50 < self.height: - min_index = (self.local_height + 1)/2016 - max_index = (self.height + 1)/2016 - for i in range(min_index, max_index + 1): - print_error( "requesting chunk", i ) - self.interface.send([ ('blockchain.block.get_chunk',[i])], 'verifier') - requested_chunks.append(i) - break - else: - all_chunks = True - print_error("downloaded all chunks") - # request missing tx - if all_chunks: - for tx_hash, tx_height in self.transactions.items(): - if tx_hash not in self.verified_tx: - if self.merkle_roots.get(tx_hash) is None and tx_hash not in requested_merkle: - print_error('requesting merkle', tx_hash) - self.interface.send([ ('blockchain.transaction.get_merkle',[tx_hash, tx_height]) ], 'verifier') - requested_merkle.append(tx_hash) - - # process pending headers - if self.pending_headers and all_chunks: - done = [] - for header in self.pending_headers: - if self.verify_header(header): - done.append(header) - else: - # request previous header - i = header.get('block_height') - 1 - if i not in requested_headers: - print_error("requesting header %d"%i) - self.interface.send([ ('blockchain.block.get_header',[i])], 'verifier') - requested_headers.append(i) - # no point continuing - break - if done: - self.interface.trigger_callback('updated') - for header in done: - self.pending_headers.remove(header) + for tx_hash, tx_height in self.transactions.items(): + if tx_hash not in self.verified_tx: + if self.merkle_roots.get(tx_hash) is None and tx_hash not in requested_merkle: + print_error('requesting merkle', tx_hash) + self.interface.send([ ('blockchain.transaction.get_merkle',[tx_hash, tx_height]) ], 'txverifier') + requested_merkle.append(tx_hash) try: - r = self.interface.get_response('verifier',timeout=1) + r = self.interface.get_response('txverifier',timeout=1) except Queue.Empty: continue if not r: continue @@ -180,138 +129,23 @@ class WalletVerifier(threading.Thread): self.verify_merkle(tx_hash, result) requested_merkle.remove(tx_hash) - elif method == 'blockchain.block.get_chunk': - index = params[0] - self.verify_chunk(index, result) - requested_chunks.remove(index) - - elif method in ['blockchain.headers.subscribe', 'blockchain.block.get_header']: - - self.pending_headers.append(result) - if method == 'blockchain.block.get_header': - requested_headers.remove(result.get('block_height')) - else: - self.height = result.get('block_height') - self.interface.poke('synchronizer') - - self.pending_headers.sort(key=lambda x: x.get('block_height')) - # print "pending headers", map(lambda x: x.get('block_height'), self.pending_headers) - - def verify_merkle(self, tx_hash, result): tx_height = result.get('block_height') pos = result.get('pos') self.merkle_roots[tx_hash] = self.hash_merkle_root(result['merkle'], tx_hash, pos) - header = self.read_header(tx_height) + header = self.blockchain.read_header(tx_height) if not header: return assert header.get('merkle_root') == self.merkle_roots[tx_hash] # we passed all the tests - header = self.read_header(tx_height) timestamp = header.get('timestamp') with self.lock: self.verified_tx[tx_hash] = (tx_height, timestamp, pos) print_error("verified %s"%tx_hash) - self.config.set_key('verified_tx3', self.verified_tx, True) + self.storage.put('verified_tx3', self.verified_tx, True) self.interface.trigger_callback('updated') - def verify_chunk(self, index, hexdata): - data = hexdata.decode('hex') - height = index*2016 - num = len(data)/80 - print_error("validating headers %d"%height) - - if index == 0: - previous_hash = ("0"*64) - else: - prev_header = self.read_header(index*2016-1) - if prev_header is None: raise - previous_hash = self.hash_header(prev_header) - - bits, target = self.get_target(index) - - for i in range(num): - height = index*2016 + i - raw_header = data[i*80:(i+1)*80] - header = self.header_from_string(raw_header) - _hash = self.hash_header(header) - assert previous_hash == header.get('prev_block_hash') - assert bits == header.get('bits') - assert eval('0x'+_hash) < target - - previous_header = header - previous_hash = _hash - - self.save_chunk(index, data) - - - def verify_header(self, header): - # add header to the blockchain file - # if there is a reorg, push it in a stack - - height = header.get('block_height') - - prev_header = self.read_header(height -1) - if not prev_header: - # return False to request previous header - return False - - prev_hash = self.hash_header(prev_header) - bits, target = self.get_target(height/2016) - _hash = self.hash_header(header) - try: - assert prev_hash == header.get('prev_block_hash') - assert bits == header.get('bits') - assert eval('0x'+_hash) < target - except: - # this can be caused by a reorg. - print_error("verify header failed"+ repr(header)) - # undo verifications - with self.lock: - items = self.verified_tx.items()[:] - for tx_hash, item in items: - tx_height, timestamp, pos = item - if tx_height >= height: - print_error("redoing", tx_hash) - with self.lock: - self.verified_tx.pop(tx_hash) - if tx_hash in self.merkle_roots: - self.merkle_roots.pop(tx_hash) - # return False to request previous header. - return False - - self.save_header(header) - print_error("verify header:", _hash, height) - return True - - - - - def header_to_string(self, res): - s = int_to_hex(res.get('version'),4) \ - + rev_hex(res.get('prev_block_hash')) \ - + rev_hex(res.get('merkle_root')) \ - + int_to_hex(int(res.get('timestamp')),4) \ - + int_to_hex(int(res.get('bits')),4) \ - + int_to_hex(int(res.get('nonce')),4) - return s - - - def header_from_string(self, s): - hex_to_int = lambda s: eval('0x' + s[::-1].encode('hex')) - h = {} - h['version'] = hex_to_int(s[0:4]) - h['prev_block_hash'] = hash_encode(s[4:36]) - h['merkle_root'] = hash_encode(s[36:68]) - h['timestamp'] = hex_to_int(s[68:72]) - h['bits'] = hex_to_int(s[72:76]) - h['nonce'] = hex_to_int(s[76:80]) - return h - - def hash_header(self, header): - return rev_hex(Hash(self.header_to_string(header).decode('hex')).encode('hex')) - def hash_merkle_root(self, merkle_s, target_hash, pos): h = hash_decode(target_hash) for i in range(len(merkle_s)): @@ -319,101 +153,6 @@ class WalletVerifier(threading.Thread): h = Hash( hash_decode(item) + h ) if ((pos >> i) & 1) else Hash( h + hash_decode(item) ) return hash_encode(h) - def path(self): - wdir = self.config.get('blockchain_headers_path', user_dir()) - if wdir and not os.path.exists( wdir ): os.mkdir(wdir) - return os.path.join( wdir, 'blockchain_headers') - - def init_headers_file(self): - filename = self.path() - if os.path.exists(filename): - return - - try: - import urllib, socket - socket.setdefaulttimeout(30) - print_error("downloading ", self.headers_url ) - urllib.urlretrieve(self.headers_url, filename) - except: - print_error( "download failed. creating file", filename ) - open(filename,'wb+').close() - - def save_chunk(self, index, chunk): - filename = self.path() - f = open(filename,'rb+') - f.seek(index*2016*80) - h = f.write(chunk) - f.close() - self.set_local_height() - - def save_header(self, header): - data = self.header_to_string(header).decode('hex') - assert len(data) == 80 - height = header.get('block_height') - filename = self.path() - f = open(filename,'rb+') - f.seek(height*80) - h = f.write(data) - f.close() - self.set_local_height() - - - def set_local_height(self): - name = self.path() - if os.path.exists(name): - h = os.path.getsize(name)/80 - 1 - if self.local_height != h: - self.local_height = h - - - def read_header(self, block_height): - name = self.path() - if os.path.exists(name): - f = open(name,'rb') - f.seek(block_height*80) - h = f.read(80) - f.close() - if len(h) == 80: - h = self.header_from_string(h) - return h - - - def get_target(self, index): - - max_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 - if index == 0: return 0x1d00ffff, max_target - - first = self.read_header((index-1)*2016) - last = self.read_header(index*2016-1) - - nActualTimespan = last.get('timestamp') - first.get('timestamp') - nTargetTimespan = 14*24*60*60 - nActualTimespan = max(nActualTimespan, nTargetTimespan/4) - nActualTimespan = min(nActualTimespan, nTargetTimespan*4) - - bits = last.get('bits') - # convert to bignum - MM = 256*256*256 - a = bits%MM - if a < 0x8000: - a *= 256 - target = (a) * pow(2, 8 * (bits/MM - 3)) - - # new target - new_target = min( max_target, (target * nActualTimespan)/nTargetTimespan ) - - # convert it to bits - c = ("%064X"%new_target)[2:] - i = 31 - while c[0:2]=="00": - c = c[2:] - i -= 1 - - c = eval('0x'+c[0:6]) - if c > 0x800000: - c /= 256 - i += 1 - - new_bits = c + MM * i - return new_bits, new_target + + diff --git a/lib/version.py b/lib/version.py @@ -1,4 +1,4 @@ ELECTRUM_VERSION = "1.9" # version of the client package PROTOCOL_VERSION = '0.6' # protocol version requested -SEED_VERSION = 4 # bump this every time the seed generation is modified +SEED_VERSION = 5 # bump this every time the seed generation is modified TRANSLATION_ID = 4101 # version of the wiki page diff --git a/lib/wallet.py b/lib/wallet.py @@ -63,39 +63,113 @@ def pw_decode(s, password): from version import ELECTRUM_VERSION, SEED_VERSION +class WalletStorage: + + def __init__(self, config): + self.data = {} + self.file_exists = False + self.init_path(config) + print_error( "wallet path", self.path ) + if self.path: + self.read(self.path) + + + def init_path(self, config): + """Set the path of the wallet.""" + + path = config.get('wallet_path') + if not path: + path = config.get('default_wallet_path') + if path is not None: + self.path = path + return + + # Look for wallet file in the default data directory. + # Make wallet directory if it does not yet exist. + if not os.path.exists(self.user_dir): + os.mkdir(self.user_dir) + + self.path = os.path.join(self.user_dir, "electrum.dat") + + + def read(self, path): + """Read the contents of the wallet file.""" + try: + with open(self.path, "r") as f: + data = f.read() + except IOError: + return + try: + d = ast.literal_eval( data ) #parse raw data from reading wallet file + except: + raise IOError("Cannot read wallet file.") + + self.data = d + self.file_exists = True + + + def get(self, key, default=None): + return self.data.get(key, default) + + def put(self, key, value, save = True): + + if self.data.get(key) is not None: + self.data[key] = value + else: + # add key to wallet config + self.data[key] = value + + if save: + self.write() + + + def write(self): + s = repr(self.data) + f = open(self.path,"w") + f.write( s ) + f.close() + if self.get('gui') != 'android': + import stat + os.chmod(self.path,stat.S_IREAD | stat.S_IWRITE) + + class Wallet: - def __init__(self, config={}): - self.config = config + def __init__(self, storage): + + self.storage = storage self.electrum_version = ELECTRUM_VERSION self.gap_limit_for_change = 3 # constant # saved fields - self.seed_version = config.get('seed_version', SEED_VERSION) - self.gap_limit = config.get('gap_limit', 5) - self.use_change = config.get('use_change',True) - self.fee = int(config.get('fee_per_kb',20000)) - self.num_zeros = int(config.get('num_zeros',0)) - self.use_encryption = config.get('use_encryption', False) - self.seed = config.get('seed', '') # encrypted - self.labels = config.get('labels', {}) - self.frozen_addresses = config.get('frozen_addresses',[]) - self.prioritized_addresses = config.get('prioritized_addresses',[]) - self.addressbook = config.get('contacts', []) + self.seed_version = storage.get('seed_version', SEED_VERSION) + + self.gap_limit = storage.get('gap_limit', 5) + self.use_change = storage.get('use_change',True) + self.use_encryption = storage.get('use_encryption', False) + self.seed = storage.get('seed', '') # encrypted + self.labels = storage.get('labels', {}) + self.frozen_addresses = storage.get('frozen_addresses',[]) + self.prioritized_addresses = storage.get('prioritized_addresses',[]) + self.addressbook = storage.get('contacts', []) - self.imported_keys = config.get('imported_keys',{}) - self.history = config.get('addr_history',{}) # address -> list(txid, height) + self.imported_keys = storage.get('imported_keys',{}) + self.history = storage.get('addr_history',{}) # address -> list(txid, height) + self.fee = int(storage.get('fee_per_kb',20000)) - self.master_public_keys = config.get('master_public_keys',{}) - self.master_private_keys = config.get('master_private_keys', {}) + self.master_public_keys = storage.get('master_public_keys',{}) + self.master_private_keys = storage.get('master_private_keys', {}) - self.first_addresses = config.get('first_addresses',{}) + self.first_addresses = storage.get('first_addresses',{}) - self.load_accounts(config) + #if self.seed_version != SEED_VERSION: + # raise ValueError("This wallet seed is deprecated. Please restore from seed.") + + self.load_accounts() self.transactions = {} - tx = config.get('transactions',{}) + tx = storage.get('transactions',{}) try: for k,v in tx.items(): self.transactions[k] = Transaction(v) except: @@ -117,9 +191,6 @@ class Wallet: self.transaction_lock = threading.Lock() self.tx_event = threading.Event() - if self.seed_version != SEED_VERSION: - raise ValueError("This wallet seed is deprecated. Please run upgrade.py for a diagnostic.") - for tx_hash, tx in self.transactions.items(): if self.check_new_tx(tx_hash, tx): self.update_tx_outputs(tx_hash) @@ -152,13 +223,13 @@ class Wallet: # store the originally requested keypair into the imported keys table self.imported_keys[address] = pw_encode(sec, password ) - self.config.set_key('imported_keys', self.imported_keys, True) + self.storage.put('imported_keys', self.imported_keys, True) return address def delete_imported_key(self, addr): if addr in self.imported_keys: self.imported_keys.pop(addr) - self.config.set_key('imported_keys', self.imported_keys, True) + self.storage.put('imported_keys', self.imported_keys, True) def init_seed(self, seed): @@ -169,8 +240,8 @@ class Wallet: def save_seed(self): - self.config.set_key('seed', self.seed, True) - self.config.set_key('seed_version', self.seed_version, True) + self.storage.put('seed', self.seed, True) + self.storage.put('seed_version', self.seed_version, True) master_k, master_c, master_K, master_cK = bip32_init(self.seed) @@ -202,8 +273,8 @@ class Wallet: "m/5'/": k5 } - self.config.set_key('master_public_keys', self.master_public_keys, True) - self.config.set_key('master_private_keys', self.master_private_keys, True) + self.storage.put('master_public_keys', self.master_public_keys, True) + self.storage.put('master_private_keys', self.master_private_keys, True) # create default account self.create_account('1','Main account') @@ -220,14 +291,14 @@ class Wallet: # for safety, we ask the user to enter their seed assert seed == self.decode_seed(password) self.seed = '' - self.config.set_key('seed', '', True) + self.storage.put('seed', '', True) def deseed_branch(self, k): # check that parent has no seed assert self.seed == '' self.master_private_keys.pop(k) - self.config.set_key('master_private_keys', self.master_private_keys, True) + self.storage.put('master_private_keys', self.master_private_keys, True) def account_id(self, account_type, i): @@ -260,7 +331,7 @@ class Wallet: account_id, account = self.next_account(account_type) addr = account.first_address() self.first_addresses[k] = addr - self.config.set_key('first_addresses',self.first_addresses) + self.storage.put('first_addresses',self.first_addresses) return addr @@ -294,26 +365,39 @@ class Wallet: return account_id, account - def create_account(self, account_type = '1', name = 'unnamed'): + def create_account(self, account_type = '1', name = None): account_id, account = self.next_account(account_type) self.accounts[account_id] = account self.save_accounts() - self.labels[account_id] = name - self.config.set_key('labels', self.labels, True) + if name: + self.labels[account_id] = name + self.storage.put('labels', self.labels, True) + + + def create_old_account(self): + print self.seed + mpk = OldAccount.mpk_from_seed(self.seed) + self.storage.put('master_public_key', mpk, True) + self.accounts[0] = OldAccount({'mpk':mpk, 0:[], 1:[]}) + self.save_accounts() def save_accounts(self): d = {} for k, v in self.accounts.items(): d[k] = v.dump() - self.config.set_key('accounts', d, True) + self.storage.put('accounts', d, True) + - def load_accounts(self, config): - d = config.get('accounts', {}) + def load_accounts(self): + d = self.storage.get('accounts', {}) self.accounts = {} for k, v in d.items(): - if '&' in k: + if k == 0: + v['mpk'] = self.storage.get('master_public_key') + self.accounts[k] = OldAccount(v) + elif '&' in k: self.accounts[k] = BIP32_Account_2of2(v) else: self.accounts[k] = BIP32_Account(v) @@ -339,7 +423,7 @@ class Wallet: def get_master_public_key(self): raise - return self.config.get("master_public_key") + return self.storage.get("master_public_key") def get_master_private_key(self, account, password): master_k = pw_decode( self.master_private_keys[account], password) @@ -379,6 +463,9 @@ class Wallet: def get_keyID(self, account, sequence): + if account == 0: + return 'old' + rs = self.rebase_sequence(account, sequence) dd = [] for root, public_sequence in rs: @@ -405,6 +492,12 @@ class Wallet: out.append( pw_decode( self.imported_keys[address], password ) ) else: account, sequence = self.get_address_index(address) + if account == 0: + seed = self.decode_seed(password) + pk = self.accounts[account].get_private_key(seed, sequence) + out.append(pk) + return out + # assert address == self.accounts[account].get_address(*sequence) rs = self.rebase_sequence( account, sequence) for root, public_sequence in rs: @@ -520,7 +613,7 @@ class Wallet: def change_gap_limit(self, value): if value >= self.gap_limit: self.gap_limit = value - self.config.set_key('gap_limit', self.gap_limit, True) + self.storage.put('gap_limit', self.gap_limit, True) self.interface.poke('synchronizer') return True @@ -533,7 +626,7 @@ class Wallet: self.accounts[key][0] = addresses self.gap_limit = value - self.config.set_key('gap_limit', self.gap_limit, True) + self.storage.put('gap_limit', self.gap_limit, True) self.save_accounts() return True else: @@ -616,13 +709,14 @@ class Wallet: def synchronize(self): - self.create_pending_accounts() + if self.master_public_keys: + self.create_pending_accounts() new = [] for account in self.accounts.values(): new += self.synchronize_account(account) if new: self.save_accounts() - self.config.set_key('addr_history', self.history, True) + self.storage.put('addr_history', self.history, True) return new @@ -632,15 +726,15 @@ class Wallet: def add_contact(self, address, label=None): self.addressbook.append(address) - self.config.set_key('contacts', self.addressbook, True) + self.storage.put('contacts', self.addressbook, True) if label: self.labels[address] = label - self.config.set_key('labels', self.labels, True) + self.storage.put('labels', self.labels, True) def delete_contact(self, addr): if addr in self.addressbook: self.addressbook.remove(addr) - self.config.set_key('addressbook', self.addressbook, True) + self.storage.put('addressbook', self.addressbook, True) def fill_addressbook(self): @@ -837,6 +931,11 @@ class Wallet: return inputs, total, fee + def set_fee(self, fee): + if self.fee != fee: + self.fee = fee + self.storage.put('fee_per_kb', self.fee, True) + def estimated_fee(self, inputs): estimated_size = len(inputs) * 180 + 80 # this assumes non-compressed keys fee = self.fee * int(round(estimated_size/1024.)) @@ -900,7 +999,7 @@ class Wallet: tx = {} for k,v in self.transactions.items(): tx[k] = str(v) - self.config.set_key('transactions', tx, True) + self.storage.put('transactions', tx, True) def receive_history_callback(self, addr, hist): @@ -909,7 +1008,7 @@ class Wallet: with self.lock: self.history[addr] = hist - self.config.set_key('addr_history', self.history, True) + self.storage.put('addr_history', self.history, True) if hist != ['*']: for tx_hash, tx_height in hist: @@ -1060,28 +1159,28 @@ class Wallet: if new_password == '': new_password = None # this will throw an exception if unicode cannot be converted self.seed = pw_encode( seed, new_password) - self.config.set_key('seed', self.seed, True) + self.storage.put('seed', self.seed, True) self.use_encryption = (new_password != None) - self.config.set_key('use_encryption', self.use_encryption,True) + self.storage.put('use_encryption', self.use_encryption,True) for k in self.imported_keys.keys(): a = self.imported_keys[k] b = pw_decode(a, old_password) c = pw_encode(b, new_password) self.imported_keys[k] = c - self.config.set_key('imported_keys', self.imported_keys, True) + self.storage.put('imported_keys', self.imported_keys, True) for k, v in self.master_private_keys.items(): b = pw_decode(v, old_password) c = pw_encode(b, new_password) self.master_private_keys[k] = c - self.config.set_key('master_private_keys', self.master_private_keys, True) + self.storage.put('master_private_keys', self.master_private_keys, True) def freeze(self,addr): if self.is_mine(addr) and addr not in self.frozen_addresses: self.unprioritize(addr) self.frozen_addresses.append(addr) - self.config.set_key('frozen_addresses', self.frozen_addresses, True) + self.storage.put('frozen_addresses', self.frozen_addresses, True) return True else: return False @@ -1089,7 +1188,7 @@ class Wallet: def unfreeze(self,addr): if self.is_mine(addr) and addr in self.frozen_addresses: self.frozen_addresses.remove(addr) - self.config.set_key('frozen_addresses', self.frozen_addresses, True) + self.storage.put('frozen_addresses', self.frozen_addresses, True) return True else: return False @@ -1098,7 +1197,7 @@ class Wallet: if self.is_mine(addr) and addr not in self.prioritized_addresses: self.unfreeze(addr) self.prioritized_addresses.append(addr) - self.config.set_key('prioritized_addresses', self.prioritized_addresses, True) + self.storage.put('prioritized_addresses', self.prioritized_addresses, True) return True else: return False @@ -1106,38 +1205,11 @@ class Wallet: def unprioritize(self,addr): if self.is_mine(addr) and addr in self.prioritized_addresses: self.prioritized_addresses.remove(addr) - self.config.set_key('prioritized_addresses', self.prioritized_addresses, True) + self.storage.put('prioritized_addresses', self.prioritized_addresses, True) return True else: return False - def set_fee(self, fee): - if self.fee != fee: - self.fee = fee - self.config.set_key('fee_per_kb', self.fee, True) - - - def save(self): - print_error("Warning: wallet.save() is deprecated") - tx = {} - for k,v in self.transactions.items(): - tx[k] = str(v) - - s = { - 'use_change': self.use_change, - 'fee_per_kb': self.fee, - 'addr_history': self.history, - 'labels': self.labels, - 'contacts': self.addressbook, - 'num_zeros': self.num_zeros, - 'frozen_addresses': self.frozen_addresses, - 'prioritized_addresses': self.prioritized_addresses, - 'gap_limit': self.gap_limit, - 'transactions': tx, - } - for k, v in s.items(): - self.config.set_key(k,v) - self.config.save() def set_verifier(self, verifier): self.verifier = verifier @@ -1239,12 +1311,26 @@ class Wallet: return True + def start_threads(self, interface, blockchain): + from verifier import TxVerifier + self.interface = interface + self.verifier = TxVerifier(interface, blockchain, self.storage) + self.verifier.start() + self.set_verifier(self.verifier) + self.synchronizer = WalletSynchronizer(self) + self.synchronizer.start() + + def stop_threads(self): + self.verifier.stop() + self.synchronizer.stop() + + class WalletSynchronizer(threading.Thread): - def __init__(self, wallet, config): + def __init__(self, wallet): threading.Thread.__init__(self) self.daemon = True self.wallet = wallet diff --git a/setup.py b/setup.py @@ -58,6 +58,7 @@ setup(name = "Electrum", 'electrum.wallet_bitkey', 'electrum.wallet_factory', 'electrum.interface', + 'electrum.blockchain', 'electrum.commands', 'electrum.mnemonic', 'electrum.simple_config',