electrum

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

commit 7267579fe0792fc7ff65724d1756520d943bc66e
parent bb859c244b219479be2ea0980ff552bd9df362e4
Author: ThomasV <thomasv@gitorious>
Date:   Thu, 27 Feb 2014 10:41:22 +0100

Merge branch '2.0'

Diffstat:
Melectrum | 125+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mgui/qt/installwizard.py | 23+++++++----------------
Mgui/qt/main_window.py | 15+++++++++++++++
Mlib/__init__.py | 2+-
Mlib/bitcoin.py | 218++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mlib/commands.py | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mlib/interface.py | 16++++++++++++++++
Mlib/network.py | 114+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mlib/version.py | 6+++---
Mlib/wallet.py | 358+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Dlib/wallet_factory.py | 11-----------
Mscripts/block_headers | 2+-
Mscripts/peers | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mscripts/servers | 4++--
14 files changed, 734 insertions(+), 315 deletions(-)

diff --git a/electrum b/electrum @@ -89,6 +89,7 @@ def arg_parser(): parser.add_option("-G", "--gap", dest="gap_limit", default=None, help="gap limit") parser.add_option("-W", "--password", dest="password", default=None, help="set password for usage with commands (currently only implemented for create command, do not use it for longrunning gui session since the password is visible in /proc)") parser.add_option("-1", "--oneserver", action="store_true", dest="oneserver", default=False, help="connect to one server only") + parser.add_option("--bip32", action="store_true", dest="bip32", default=False, help="bip32") return parser @@ -105,15 +106,34 @@ def print_help_cb(self, opt, value, parser): def run_command(cmd, password=None, args=[]): + import xmlrpclib, socket cmd_runner = Commands(wallet, network) - func = getattr(cmd_runner, cmd) + func = getattr(cmd_runner, cmd.name) cmd_runner.password = password + + if cmd.requires_network and not options.offline: + cmd_runner.network = xmlrpclib.ServerProxy('http://localhost:8000') + if wallet: + wallet.start_threads(cmd_runner.network) + wallet.update() + else: + cmd_runner.network = None + try: result = func(*args[1:]) + except socket.error: + print "Daemon not running" + sys.exit(1) except Exception: traceback.print_exc(file=sys.stdout) sys.exit(1) + + if cmd.requires_network and not options.offline: + if wallet: + wallet.stop_threads() + + if type(result) == str: util.print_msg(result) elif result is not None: @@ -191,15 +211,6 @@ if __name__ == '__main__': # instanciate wallet for command-line storage = WalletStorage(config) - if cmd.requires_wallet: - wallet = Wallet(storage) - else: - wallet = None - - if cmd.name not in ['create', 'restore'] and cmd.requires_wallet and not storage.file_exists: - 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) if cmd.name in ['create', 'restore']: if storage.file_exists: @@ -215,35 +226,26 @@ if __name__ == '__main__': if not config.get('server'): config.set_key('server', pick_random_server()) - fee = options.tx_fee if options.tx_fee else raw_input("fee (default:%s):" % (str(Decimal(wallet.fee)/100000000))) - gap = options.gap_limit if options.gap_limit else raw_input("gap limit (default 5):") - - if fee: - wallet.set_fee(float(fee)*100000000) - if gap: - wallet.change_gap_limit(int(gap)) + #fee = options.tx_fee if options.tx_fee else raw_input("fee (default:%s):" % (str(Decimal(wallet.fee)/100000000))) + #gap = options.gap_limit if options.gap_limit else raw_input("gap limit (default 5):") + #if fee: + # wallet.set_fee(float(fee)*100000000) + #if gap: + # wallet.change_gap_limit(int(gap)) if cmd.name == 'restore': import getpass seed = getpass.getpass(prompt="seed:", stream=None) if options.concealed else raw_input("seed:") - try: - seed.decode('hex') - except Exception: - print_error("Warning: Not hex, trying decode.") - seed = mnemonic_decode(seed.split(' ')) - if not seed: - sys.exit("Error: No seed") - - wallet.init_seed(str(seed)) + wallet = Wallet.from_seed(str(seed),storage) + if not wallet: + sys.exit("Error: Invalid seed") wallet.save_seed(password) if not options.offline: network = Network(config) network.start() wallet.start_threads(network) - print_msg("Recovering wallet...") wallet.restore(lambda x: x) - if wallet.is_found(): print_msg("Recovery successful") else: @@ -253,6 +255,7 @@ if __name__ == '__main__': print_msg("Warning: This wallet was restored offline. It may contain more addresses than displayed.") else: + wallet = Wallet(storage) wallet.init_seed(None) wallet.save_seed(password) wallet.synchronize() @@ -264,6 +267,19 @@ if __name__ == '__main__': # terminate sys.exit(0) + + if cmd.name not in ['create', 'restore'] and cmd.requires_wallet and not storage.file_exists: + 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) + + + if cmd.requires_wallet: + wallet = Wallet(storage) + else: + wallet = None + + # important warning if cmd.name in ['dumpprivkey', 'dumpprivkeys']: print_msg("WARNING: ALL your private keys are secret.") @@ -345,19 +361,37 @@ if __name__ == '__main__': print_msg("Warning: Final argument was reconstructed from several arguments:", repr(message)) args = args[0:cmd.min_args] + [message] - # open session - if cmd.requires_network and not options.offline: - network = Network(config) - if not network.start(wait=True): - print_msg("Not connected, aborting.") - sys.exit(1) - print_error("Connected to " + network.interface.connection_msg) - if wallet: - wallet.start_threads(network) - wallet.update() - else: - network = None + if cmd.name == 'daemon' and args[1] == 'start': + pid = os.fork() + if (pid == 0): # The first child. + os.chdir("/") + os.setsid() + os.umask(0) + pid2 = os.fork() + if (pid2 == 0): # Second child + from SimpleXMLRPCServer import SimpleXMLRPCServer + # start the daemon + network = Network(config) + if not network.start(wait=True): + print_msg("Not connected, aborting.") + sys.exit(1) + print_msg("Connected to " + network.interface.connection_msg) + server = SimpleXMLRPCServer(('localhost',8000), allow_none=True, logRequests=False) + server.register_function(network.synchronous_get, 'synchronous_get') + server.register_function(network.get_servers, 'get_servers') + server.register_function(network.main_server, 'main_server') + server.register_function(network.send, 'send') + server.register_function(network.subscribe, 'subscribe') + server.register_function(network.is_connected, 'is_connected') + server.register_function(network.is_up_to_date, 'is_up_to_date') + server.register_function(lambda: setattr(server,'running', False), 'stop') + server.running = True + while server.running: + server.handle_request() + print_msg("Daemon stopped") + + sys.exit(0) # run the command if cmd.name == 'deseed': @@ -394,11 +428,8 @@ if __name__ == '__main__': wallet.update_password(password, new_password) else: - run_command(cmd.name, password, args) + run_command(cmd, password, args) - if network: - if wallet: - wallet.stop_threads() - network.stop() - time.sleep(0.1) - sys.exit(0) + + time.sleep(0.1) + sys.exit(0) diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py @@ -3,7 +3,7 @@ from PyQt4.QtCore import * import PyQt4.QtCore as QtCore from electrum.i18n import _ -from electrum import Wallet, mnemonic +from electrum import Wallet from seed_dialog import SeedDialog from network_dialog import NetworkDialog @@ -259,13 +259,13 @@ class InstallWizard(QDialog): if not action: return - wallet = Wallet(self.storage) - gap = self.config.get('gap_limit', 5) - if gap != 5: - wallet.gap_limit = gap - wallet.storage.put('gap_limit', gap, True) + #gap = self.config.get('gap_limit', 5) + #if gap != 5: + # wallet.gap_limit = gap + # wallet.storage.put('gap_limit', gap, True) if action == 'create': + wallet = Wallet(self.storage) wallet.init_seed(None) if not self.show_seed(wallet): return @@ -277,23 +277,14 @@ class InstallWizard(QDialog): wallet.synchronize() # generate first addresses offline self.waiting_dialog(create) - elif action == 'restore': seed = self.seed_dialog() if not seed: return - try: - wallet.init_seed(seed) - except Exception: - import traceback - traceback.print_exc(file=sys.stdout) - QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK')) - return - + wallet = Wallet.from_seed(seed, self.storage) ok, old_password, password = self.password_dialog(wallet) wallet.save_seed(password) - elif action == 'watching': mpk = self.mpk_dialog() if not mpk: diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -426,6 +426,9 @@ class ElectrumWindow(QMainWindow): raw_transaction_text = raw_transaction_menu.addAction(_("&From text")) raw_transaction_text.triggered.connect(self.do_process_from_text) + raw_transaction_text = raw_transaction_menu.addAction(_("&From the blockchain")) + raw_transaction_text.triggered.connect(self.do_process_from_txid) + help_menu = menubar.addMenu(_("&Help")) show_about = help_menu.addAction(_("&About")) @@ -1887,6 +1890,18 @@ class ElectrumWindow(QMainWindow): if tx: self.show_transaction(tx) + def do_process_from_txid(self): + from electrum import transaction + txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':') + if ok and txid: + r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0] + if r: + tx = transaction.Transaction(r) + if tx: + self.show_transaction(tx) + else: + self.show_message("unknown transaction") + def do_process_from_csvReader(self, csvReader): outputs = [] try: diff --git a/lib/__init__.py b/lib/__init__.py @@ -1,7 +1,7 @@ from version import ELECTRUM_VERSION from util import format_satoshis, print_msg, print_json, print_error, set_verbosity from wallet import WalletSynchronizer, WalletStorage -from wallet_factory import WalletFactory as Wallet +from wallet import Wallet from verifier import TxVerifier from network import Network, DEFAULT_SERVERS, DEFAULT_PORTS, pick_random_server from interface import Interface diff --git a/lib/bitcoin.py b/lib/bitcoin.py @@ -53,14 +53,25 @@ def op_push(i): +def sha256(x): + return hashlib.sha256(x).digest() + def Hash(x): if type(x) is unicode: x=x.encode('utf-8') - return hashlib.sha256(hashlib.sha256(x).digest()).digest() + return sha256(sha256(x)) + hash_encode = lambda x: x[::-1].encode('hex') hash_decode = lambda x: x.decode('hex')[::-1] - hmac_sha_512 = lambda x,y: hmac.new(x, y, hashlib.sha512).digest() -mnemonic_hash = lambda x: hmac_sha_512("Bitcoin mnemonic", x).encode('hex') + +def mnemonic_to_seed(mnemonic, passphrase): + from pbkdf2 import PBKDF2 + import hmac + PBKDF2_ROUNDS = 2048 + return PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) + +from version import SEED_PREFIX +is_seed = lambda x: hmac_sha_512("Seed version", x).encode('hex')[0:2].startswith(SEED_PREFIX) # pywallet openssl private key implementation @@ -115,11 +126,11 @@ def i2o_ECPublicKey(pubkey, compressed=False): def hash_160(public_key): try: md = hashlib.new('ripemd160') - md.update(hashlib.sha256(public_key).digest()) + md.update(sha256(public_key)) return md.digest() except Exception: import ripemd - md = ripemd.new(hashlib.sha256(public_key).digest()) + md = ripemd.new(sha256(public_key)) return md.digest() @@ -137,15 +148,6 @@ def bc_address_to_hash_160(addr): bytes = b58decode(addr, 25) return ord(bytes[0]), bytes[1:21] -def encode_point(pubkey, compressed=False): - order = generator_secp256k1.order() - p = pubkey.pubkey.point - x_str = ecdsa.util.number_to_string(p.x(), order) - y_str = ecdsa.util.number_to_string(p.y(), order) - if compressed: - return chr(2 + (p.y() & 1)) + x_str - else: - return chr(4) + pubkey.to_string() #x_str + y_str __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' __b58base = len(__b58chars) @@ -233,8 +235,7 @@ def regenerate_key(sec): if not b: return False b = b[0:32] - secret = int('0x' + b.encode('hex'), 16) - return EC_KEY(secret) + return EC_KEY(b) def GetPubKey(pubkey, compressed=False): return i2o_ECPublicKey(pubkey, compressed) @@ -282,13 +283,14 @@ try: except Exception: print "cannot import ecdsa.curve_secp256k1. You probably need to upgrade ecdsa.\nTry: sudo pip install --upgrade ecdsa" exit() + from ecdsa.curves import SECP256k1 +from ecdsa.ellipticcurve import Point from ecdsa.util import string_to_number, number_to_string def msg_magic(message): varint = var_int(len(message)) encoded_varint = "".join([chr(int(varint[i:i+2], 16)) for i in xrange(0, len(varint), 2)]) - return "\x18Bitcoin Signed Message:\n" + encoded_varint + message @@ -301,9 +303,66 @@ def verify_message(address, signature, message): return False +def chunks(l, n): + return [l[i:i+n] for i in xrange(0, len(l), n)] + + +def ECC_YfromX(x,curved=curve_secp256k1, odd=True): + _p = curved.p() + _a = curved.a() + _b = curved.b() + for offset in range(128): + Mx = x + offset + My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p + My = pow(My2, (_p+1)/4, _p ) + + if curved.contains_point(Mx,My): + if odd == bool(My&1): + return [My,offset] + return [_p-My,offset] + raise Exception('ECC_YfromX: No Y found') + +def private_header(msg,v): + assert v<1, "Can't write version %d private header"%v + r = '' + if v==0: + r += ('%08x'%len(msg)).decode('hex') + r += sha256(msg)[:2] + return ('%02x'%v).decode('hex') + ('%04x'%len(r)).decode('hex') + r + +def public_header(pubkey,v): + assert v<1, "Can't write version %d public header"%v + r = '' + if v==0: + r = sha256(pubkey)[:2] + return '\x6a\x6a' + ('%02x'%v).decode('hex') + ('%04x'%len(r)).decode('hex') + r + + +def negative_point(P): + return Point( P.curve(), P.x(), -P.y(), P.order() ) + + +def point_to_ser(P, comp=True ): + if comp: + return ( ('%02x'%(2+(P.y()&1)))+('%064x'%P.x()) ).decode('hex') + return ( '04'+('%064x'%P.x())+('%064x'%P.y()) ).decode('hex') + + +def ser_to_point(Aser): + curve = curve_secp256k1 + generator = generator_secp256k1 + _r = generator.order() + assert Aser[0] in ['\x02','\x03','\x04'] + if Aser[0] == '\x04': + return Point( curve, str_to_long(Aser[1:33]), str_to_long(Aser[33:]), _r ) + Mx = string_to_number(Aser[1:]) + return Point( curve, Mx, ECC_YfromX(Mx, curve, Aser[0]=='\x03')[0], _r ) + + class EC_KEY(object): - def __init__( self, secret ): + def __init__( self, k ): + secret = string_to_number(k) self.pubkey = ecdsa.ecdsa.Public_key( generator_secp256k1, generator_secp256k1 * secret ) self.privkey = ecdsa.ecdsa.Private_key( self.pubkey, secret ) self.secret = secret @@ -323,10 +382,11 @@ class EC_KEY(object): else: raise Exception("error: cannot sign message") + @classmethod def verify_message(self, address, signature, message): """ See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """ - from ecdsa import numbertheory, ellipticcurve, util + from ecdsa import numbertheory, util import msqr curve = curve_secp256k1 G = generator_secp256k1 @@ -352,7 +412,7 @@ class EC_KEY(object): beta = msqr.modular_sqrt(alpha, curve.p()) y = beta if (beta - recid) % 2 == 0 else curve.p() - beta # 1.4 the constructor checks that nR is at infinity - R = ellipticcurve.Point(curve, x, y, order) + R = Point(curve, x, y, order) # 1.5 compute e from message: h = Hash( msg_magic(message) ) e = string_to_number(h) @@ -364,11 +424,94 @@ class EC_KEY(object): # check that Q is the public key public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string) # check that we get the original signing address - addr = public_key_to_bc_address( encode_point(public_key, compressed) ) + addr = public_key_to_bc_address( point_to_ser(public_key.pubkey.point, compressed) ) if address != addr: raise Exception("Bad signature") + # ecdsa encryption/decryption methods + # credits: jackjack, https://github.com/jackjack-jj/jeeq + + @classmethod + def encrypt_message(self, message, pubkey): + generator = generator_secp256k1 + curved = curve_secp256k1 + r = '' + msg = private_header(message,0) + message + msg = msg + ('\x00'*( 32-(len(msg)%32) )) + msgs = chunks(msg,32) + + _r = generator.order() + str_to_long = string_to_number + + P = generator + if len(pubkey)==33: #compressed + pk = Point( curve_secp256k1, str_to_long(pubkey[1:33]), ECC_YfromX(str_to_long(pubkey[1:33]), curve_secp256k1, pubkey[0]=='\x03')[0], _r ) + else: + pk = Point( curve_secp256k1, str_to_long(pubkey[1:33]), str_to_long(pubkey[33:65]), _r ) + + for i in range(len(msgs)): + n = ecdsa.util.randrange( pow(2,256) ) + Mx = str_to_long(msgs[i]) + My, xoffset = ECC_YfromX(Mx, curved) + M = Point( curved, Mx+xoffset, My, _r ) + T = P*n + U = pk*n + M + toadd = point_to_ser(T) + point_to_ser(U) + toadd = chr(ord(toadd[0])-2 + 2*xoffset) + toadd[1:] + r += toadd + + return base64.b64encode(public_header(pubkey,0) + r) + + + def decrypt_message(self, enc): + G = generator_secp256k1 + curved = curve_secp256k1 + pvk = self.secret + pubkeys = [point_to_ser(G*pvk,True), point_to_ser(G*pvk,False)] + enc = base64.b64decode(enc) + str_to_long = string_to_number + + assert enc[:2]=='\x6a\x6a' + + phv = str_to_long(enc[2]) + assert phv==0, "Can't read version %d public header"%phv + hs = str_to_long(enc[3:5]) + public_header=enc[5:5+hs] + checksum_pubkey=public_header[:2] + address=filter(lambda x:sha256(x)[:2]==checksum_pubkey, pubkeys) + assert len(address)>0, 'Bad private key' + address=address[0] + enc=enc[5+hs:] + r = '' + for Tser,User in map(lambda x:[x[:33],x[33:]], chunks(enc,66)): + ots = ord(Tser[0]) + xoffset = ots>>1 + Tser = chr(2+(ots&1))+Tser[1:] + T = ser_to_point(Tser) + U = ser_to_point(User) + V = T*pvk + Mcalc = U + negative_point(V) + r += ('%064x'%(Mcalc.x()-xoffset)).decode('hex') + + pvhv = str_to_long(r[0]) + assert pvhv==0, "Can't read version %d private header"%pvhv + phs = str_to_long(r[1:3]) + private_header = r[3:3+phs] + size = str_to_long(private_header[:4]) + checksum = private_header[4:6] + r = r[3+phs:] + + msg = r[:size] + hashmsg = sha256(msg)[:2] + checksumok = hashmsg==checksum + + return [msg, checksumok, address] + + + + + ###################################### BIP32 ############################## random_seed = lambda n: "%032x"%ecdsa.util.randrange( pow(2,n) ) @@ -408,7 +551,7 @@ def CKD(k, c, n): import hmac from ecdsa.util import string_to_number, number_to_string order = generator_secp256k1.order() - keypair = EC_KEY(string_to_number(k)) + keypair = EC_KEY(k) K = GetPubKey(keypair.pubkey,True) if n & BIP32_PRIME: # We want to make a "secret" address that can't be determined from K @@ -531,8 +674,35 @@ def test_bip32(seed, sequence): +def test_crypto(): + + G = generator_secp256k1 + _r = G.order() + pvk = ecdsa.util.randrange( pow(2,256) ) %_r + + Pub = pvk*G + pubkey_c = point_to_ser(Pub,True) + pubkey_u = point_to_ser(Pub,False) + addr_c = public_key_to_bc_address(pubkey_c) + addr_u = public_key_to_bc_address(pubkey_u) + + print "Private key ", '%064x'%pvk + print "Compressed public key ", pubkey_c.encode('hex') + print "Uncompressed public key", pubkey_u.encode('hex') + + message = "Chancellor on brink of second bailout for banks" + enc = EC_KEY.encrypt_message(message,pubkey_c) + eck = EC_KEY(number_to_string(pvk,_r)) + dec = eck.decrypt_message(enc) + print "decrypted", dec + + signature = eck.sign_message(message, True, addr_c) + print signature + EC_KEY.verify_message(addr_c, signature, message) + if __name__ == '__main__': - test_bip32("000102030405060708090a0b0c0d0e0f", "0'/1/2'/2/1000000000") - test_bip32("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542","0/2147483647'/1/2147483646'/2") + test_crypto() + #test_bip32("000102030405060708090a0b0c0d0e0f", "0'/1/2'/2/1000000000") + #test_bip32("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542","0/2147483647'/1/2147483646'/2") diff --git a/lib/commands.py b/lib/commands.py @@ -67,9 +67,9 @@ register_command('dumpprivkeys', 0, 0, False, True, True, 'dump all pr register_command('freeze', 1, 1, False, True, True, 'Freeze the funds at one of your wallet\'s addresses', 'freeze <address>') register_command('getbalance', 0, 1, True, True, False, 'Return the balance of your wallet, or of one account in your wallet', 'getbalance [<account>]') register_command('getservers', 0, 0, True, False, False, 'Return the list of available servers') -register_command('getversion', 0, 0, False, False, False, 'Return the version of your client', 'getversion') -register_command('getaddressbalance', 1, 1, True, True, False, 'Return the balance of an address', 'getaddressbalance <address>') -register_command('getaddresshistory', 1, 1, True, True, False, 'Return the transaction history of a wallet address', 'getaddresshistory <address>') +register_command('getversion', 0, 0, False, False, False, 'Return the version of your client', 'getversion') +register_command('getaddressbalance', 1, 1, True, False, False, 'Return the balance of an address', 'getaddressbalance <address>') +register_command('getaddresshistory', 1, 1, True, False, False, 'Return the transaction history of a wallet address', 'getaddresshistory <address>') register_command('getconfig', 1, 1, False, False, False, 'Return a configuration variable', 'getconfig <name>') register_command('getpubkeys', 1, 1, False, True, False, 'Return the public keys for a wallet address', 'getpubkeys <bitcoin address>') register_command('getrawtransaction', 1, 1, True, False, False, 'Retrieve a transaction', 'getrawtransaction <txhash>') @@ -79,7 +79,8 @@ register_command('help', 0, 1, False, False, False, 'Prints this register_command('history', 0, 0, True, True, False, 'Returns the transaction history of your wallet') register_command('importprivkey', 1, 1, False, True, True, 'Import a private key', 'importprivkey <privatekey>') register_command('listaddresses', 2, 2, False, True, False, 'Returns your list of addresses.', '', listaddr_options) -register_command('listunspent', 0, 0, True, True, False, 'Returns the list of unspent inputs in your wallet.') +register_command('listunspent', 0, 0, True, False, False, 'Returns the list of unspent inputs in your wallet.') +register_command('getaddressunspent', 1, 1, True, False, False, 'Returns the list of unspent inputs in your wallet.') register_command('mktx', 5, 5, False, True, True, 'Create a signed transaction', 'mktx <recipient> <amount> [label]', payto_options) register_command('mksendmanytx', 4, 4, False, True, True, 'Create a signed transaction', mksendmany_syntax, payto_options) register_command('payto', 5, 5, True, True, True, 'Create and broadcast a transaction.', payto_syntax, payto_options) @@ -95,6 +96,12 @@ register_command('unfreeze', 1, 1, False, True, False, 'Unfreeze th register_command('validateaddress', 1, 1, False, False, False, 'Check that the address is valid', 'validateaddress <address>') register_command('verifymessage', 3,-1, False, False, False, 'Verifies a signature', verifymessage_syntax) +register_command('encrypt', 2,-1, False, False, False, 'encrypt a message with pubkey','encrypt <pubkey> <message>') +register_command('decrypt', 2,-1, False, False, False, 'decrypt a message with privkey','decrypt <privkey> <message>') +register_command('daemon', 1, 1, True, False, False, 'start/stop daemon') +register_command('getproof', 1, 1, True, False, False, 'get merkle proof', 'getproof <address>') +register_command('getunspentaddress', 2, 2, True, False, False, 'get the address of an unspent','getunspentaddress <txid> <pos>') + @@ -106,6 +113,7 @@ class Commands: self._callback = callback self.password = None + def _run(self, method, args, password_getter): cmd = known_commands[method] if cmd.requires_password and self.wallet.use_encryption: @@ -117,11 +125,22 @@ class Commands: apply(self._callback, ()) return result + def getaddresshistory(self, addr): - assert self.wallet.is_mine(addr) - h = self.wallet.get_history(addr) - if h is None: h = self.network.synchronous_get([ ('blockchain.address.get_history',[addr]) ])[0] - return h + return self.network.synchronous_get([ ('blockchain.address.get_history',[addr]) ])[0] + + + def daemon(self, arg): + if arg=='stop': + return self.network.stop() + elif arg=='status': + return { + 'server':self.network.main_server(), + 'connected':self.network.is_connected() + } + else: + return "unknown command \"%s\""% arg + def listunspent(self): import copy @@ -129,6 +148,15 @@ class Commands: for i in l: i["value"] = str(Decimal(i["value"])/100000000) return l + + def getaddressunspent(self, addr): + return self.network.synchronous_get([ ('blockchain.address.listunspent',[addr]) ])[0] + + + def getunspentaddress(self, txid, num): + return self.network.synchronous_get([ ('blockchain.utxo.get_address',[txid, num]) ])[0] + + def createrawtransaction(self, inputs, outputs): # convert to own format for i in inputs: @@ -199,9 +227,14 @@ class Commands: return out def getaddressbalance(self, addr): - c, u = self.wallet.get_addr_balance(addr) - out = { "confirmed": str(Decimal(c)/100000000) } - if u: out["unconfirmed"] = str(Decimal(u)/100000000) + b = self.network.synchronous_get([ ('blockchain.address.get_balance',[addr]) ])[0] + return str(Decimal(b)/100000000) + + def getproof(self, addr): + p = self.network.synchronous_get([ ('blockchain.address.get_proof',[addr]) ])[0] + out = [] + for i,s in p: + out.append(i) return out def getservers(self): @@ -350,11 +383,27 @@ class Commands: if cmd.options: print_msg("options:\n" + cmd.options) return None + def getrawtransaction(self, tx_hash): + import transaction if self.wallet: tx = self.wallet.transactions.get(tx_hash) if tx: return tx - return self.network.retrieve_transaction(tx_hash) + + r = self.network.synchronous_get([ ('blockchain.transaction.get',[tx_hash]) ])[0] + if r: + return transaction.Transaction(r) + else: + return "unknown transaction" + + def encrypt(self, pubkey, message): + return EC_KEY.encrypt_message(message, pubkey.decode('hex')) + + def decrypt(self, secret, message): + ec = regenerate_key(secret) + decrypted = ec.decrypt_message(message) + return decrypted[0] + diff --git a/lib/interface.py b/lib/interface.py @@ -598,6 +598,22 @@ class Interface(threading.Thread): self.queue.put(self) + def synchronous_get(self, requests, timeout=100000000): + queue = Queue.Queue() + ids = self.send(requests, lambda i,r: queue.put(r)) + id2 = ids[:] + res = {} + while ids: + r = queue.get(True, timeout) + _id = r.get('id') + if _id in ids: + ids.remove(_id) + res[_id] = r.get('result') + out = [] + for _id in id2: + out.append(res[_id]) + return out + if __name__ == "__main__": diff --git a/lib/network.py b/lib/network.py @@ -22,6 +22,37 @@ DEFAULT_SERVERS = { } +def parse_servers(result): + """ parse servers list into dict format""" + from version import PROTOCOL_VERSION + servers = {} + for item in result: + host = item[1] + out = {} + version = None + pruning_level = '-' + if len(item) > 2: + for v in item[2]: + if re.match("[stgh]\d*", v): + protocol, port = v[0], v[1:] + if port == '': port = DEFAULT_PORTS[protocol] + out[protocol] = port + elif re.match("v(.?)+", v): + version = v[1:] + elif re.match("p\d*", v): + pruning_level = v[1:] + if pruning_level == '': pruning_level = '0' + try: + is_recent = float(version)>=float(PROTOCOL_VERSION) + except Exception: + is_recent = False + + if out and is_recent: + out['pruning'] = pruning_level + servers[host] = out + + return servers + def filter_protocol(servers, p): @@ -66,6 +97,8 @@ class Network(threading.Thread): self.interface = None self.proxy = self.config.get('proxy') self.heights = {} + self.merkle_roots = {} + self.utxo_roots = {} self.server_lag = 0 dir_path = os.path.join( self.config.path, 'certs') @@ -82,6 +115,14 @@ class Network(threading.Thread): return self.interface and self.interface.is_connected + def is_up_to_date(self): + return self.interface.is_up_to_date() + + + def main_server(self): + return self.interface.server + + def send_subscriptions(self): for cb, sub in self.subscriptions.items(): self.interface.send(sub, cb) @@ -327,6 +368,8 @@ class Network(threading.Thread): if not result: return height = result.get('block_height') self.heights[i.server] = height + self.merkle_roots[i.server] = result.get('merkle_root') + self.utxo_roots[i.server] = result.get('utxo_root') # notify blockchain about the new height self.blockchain.queue.put((i,result)) @@ -341,7 +384,7 @@ class Network(threading.Thread): def on_peers(self, i, r): if not r: return - self.irc_servers = self.parse_servers(r.get('result')) + self.irc_servers = parse_servers(r.get('result')) self.trigger_callback('peers') def on_banner(self, i, r): @@ -356,59 +399,26 @@ class Network(threading.Thread): def synchronous_get(self, requests, timeout=100000000): - queue = Queue.Queue() - ids = self.interface.send(requests, lambda i,r: queue.put(r)) - id2 = ids[:] - res = {} - while ids: - r = queue.get(True, timeout) - _id = r.get('id') - if _id in ids: - ids.remove(_id) - res[_id] = r.get('result') - out = [] - for _id in id2: - out.append(res[_id]) - return out + return self.interface.synchronous_get(requests) + + + + #def retrieve_transaction(self, tx_hash, tx_height=0): + # import transaction + # r = self.synchronous_get([ ('blockchain.transaction.get',[tx_hash, tx_height]) ])[0] + # if r: + # return transaction.Transaction(r) + + + - def retrieve_transaction(self, tx_hash, tx_height=0): - import transaction - r = self.synchronous_get([ ('blockchain.transaction.get',[tx_hash, tx_height]) ])[0] - if r: - return transaction.Transaction(r) - - - def parse_servers(self, result): - """ parse servers list into dict format""" - from version import PROTOCOL_VERSION - servers = {} - for item in result: - host = item[1] - out = {} - version = None - pruning_level = '-' - if len(item) > 2: - for v in item[2]: - if re.match("[stgh]\d*", v): - protocol, port = v[0], v[1:] - if port == '': port = DEFAULT_PORTS[protocol] - out[protocol] = port - elif re.match("v(.?)+", v): - version = v[1:] - elif re.match("p\d*", v): - pruning_level = v[1:] - if pruning_level == '': pruning_level = '0' - try: - is_recent = float(version)>=float(PROTOCOL_VERSION) - except Exception: - is_recent = False - - if out and is_recent: - out['pruning'] = pruning_level - servers[host] = out - - return servers +class NetworkProxy: + # interface to the network object. + # handle subscriptions and callbacks + # the network object can be jsonrpc server + def __init__(self, network): + self.network = network diff --git a/lib/version.py b/lib/version.py @@ -1,5 +1,5 @@ ELECTRUM_VERSION = "1.9.8" # version of the client package -PROTOCOL_VERSION = '0.6' # protocol version requested -SEED_VERSION = 4 # bump this every time the seed generation is modified +PROTOCOL_VERSION = '0.9' # protocol version requested +NEW_SEED_VERSION = 6 # bip32 wallets +OLD_SEED_VERSION = 4 # old electrum deterministic generation SEED_PREFIX = '01' # the hash of the mnemonic seed must begin with this - diff --git a/lib/wallet.py b/lib/wallet.py @@ -73,6 +73,7 @@ class WalletStorage: def __init__(self, config): self.lock = threading.Lock() + self.config = config self.data = {} self.file_exists = False self.path = self.init_path(config) @@ -151,7 +152,10 @@ class WalletStorage: os.chmod(self.path,stat.S_IREAD | stat.S_IWRITE) -class Wallet: + + + +class NewWallet: def __init__(self, storage): @@ -160,7 +164,7 @@ class Wallet: self.gap_limit_for_change = 3 # constant # saved fields - self.seed_version = storage.get('seed_version', SEED_VERSION) + self.seed_version = storage.get('seed_version', NEW_SEED_VERSION) self.gap_limit = storage.get('gap_limit', 5) self.use_change = storage.get('use_change',True) @@ -180,12 +184,6 @@ class Wallet: self.next_addresses = storage.get('next_addresses',{}) - if self.seed_version not in [4, 6]: - msg = "This wallet seed is not supported." - if self.seed_version in [5]: - msg += "\nTo open this wallet, try 'git checkout seed_v%d'"%self.seed_version - print msg - sys.exit(1) # This attribute is set when wallet.start_threads is called. self.synchronizer = None @@ -290,42 +288,27 @@ class Wallet: # we keep only 13 words, that's approximately 139 bits of entropy words = mnemonic.mn_encode(s)[0:13] seed = ' '.join(words) - if mnemonic_hash(seed).startswith(SEED_PREFIX): - break # this removes 12 bits of entropy + if is_seed(seed): + break # this will remove 8 bits of entropy nonce += 1 return seed def init_seed(self, seed): - import mnemonic + import mnemonic, unicodedata if self.seed: raise Exception("a seed exists") - if not seed: - self.seed = random_seed(128) - self.seed_version = 4 - return + self.seed_version = NEW_SEED_VERSION - # check if seed is hexadecimal - seed = seed.strip() - try: - assert seed - seed.decode('hex') - self.seed_version = 4 - self.seed = str(seed) + if not seed: + self.seed = self.make_seed() return - except Exception: - pass - words = seed.split() - self.seed_version = 4 - self.seed = mnemonic.mn_decode(words) + self.seed = unicodedata.normalize('NFC', unicode(seed.strip())) - if not self.seed: - raise Exception("Invalid seed") - def save_seed(self, password): @@ -338,41 +321,26 @@ class Wallet: self.create_accounts(password) - def create_watching_only_wallet(self, params): - K0, c0 = params - if not K0: - return - - if not c0: - self.seed_version = 4 - self.storage.put('seed_version', self.seed_version, True) - self.create_old_account(K0) - return - - cK0 = "" + def create_watching_only_wallet(self, K0, c0): + cK0 = "" #FIXME self.master_public_keys = { "m/0'/": (c0, K0, cK0), } self.storage.put('master_public_keys', self.master_public_keys, True) self.storage.put('seed_version', self.seed_version, True) - self.create_account('1','Main account') + self.create_account('1of1','Main account') def create_accounts(self, password): seed = pw_decode(self.seed, password) - - if self.seed_version == 4: - mpk = OldAccount.mpk_from_seed(seed) - self.create_old_account(mpk) - else: - # create default account - self.create_master_keys('1', password) - self.create_account('1','Main account') + # create default account + self.create_master_keys('1of1', password) + self.create_account('1of1','Main account') def create_master_keys(self, account_type, password): - master_k, master_c, master_K, master_cK = bip32_init(self.get_seed(None)) - if account_type == '1': + master_k, master_c, master_K, master_cK = bip32_init(self.get_seed(password)) + if account_type == '1of1': k0, c0, K0, cK0 = bip32_private_derivation(master_k, master_c, "m/", "m/0'/") self.master_public_keys["m/0'/"] = (c0, K0, cK0) self.master_private_keys["m/0'/"] = pw_encode(k0, password) @@ -398,7 +366,7 @@ class Wallet: self.storage.put('master_private_keys', self.master_private_keys, True) def has_master_public_keys(self, account_type): - if account_type == '1': + if account_type == '1of1': return "m/0'/" in self.master_public_keys elif account_type == '2of2': return set(["m/1'/", "m/2'/"]) <= set(self.master_public_keys.keys()) @@ -421,17 +389,18 @@ class Wallet: def deseed_branch(self, k): # check that parent has no seed - assert self.seed == '' + # assert self.seed == '' self.master_private_keys.pop(k) self.storage.put('master_private_keys', self.master_private_keys, True) + def is_watching_only(self): return (self.seed == '') and (self.master_private_keys == {}) def account_id(self, account_type, i): - if account_type == '1': + if account_type == '1of1': return "m/0'/%d"%i elif account_type == '2of2': return "m/1'/%d & m/2'/%d"%(i,i) @@ -451,7 +420,7 @@ class Wallet: return i - def new_account_address(self, account_type = '1'): + def new_account_address(self, account_type = '1of1'): i = self.num_accounts(account_type) k = self.account_id(account_type,i) @@ -465,12 +434,12 @@ class Wallet: return k, addr - def next_account(self, account_type = '1'): + def next_account(self, account_type = '1of1'): i = self.num_accounts(account_type) account_id = self.account_id(account_type,i) - if account_type is '1': + if account_type is '1of1': master_c0, master_K0, _ = self.master_public_keys["m/0'/"] c0, K0, cK0 = bip32_public_derivation(master_c0.decode('hex'), master_K0.decode('hex'), "m/0'/", "m/0'/%d"%i) account = BIP32_Account({ 'c':c0, 'K':K0, 'cK':cK0 }) @@ -514,7 +483,7 @@ class Wallet: - def create_account(self, account_type = '1', name = None): + def create_account(self, account_type = '1of1', name = None): k, account = self.next_account(account_type) if k in self.pending_accounts: self.pending_accounts.pop(k) @@ -526,12 +495,6 @@ class Wallet: self.set_label(k, name) - def create_old_account(self, mpk): - 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(): @@ -596,14 +559,13 @@ class Wallet: return s[0] == 1 def get_master_public_key(self): - if self.seed_version == 4: - return self.storage.get("master_public_key") - else: - c, K, cK = self.storage.get("master_public_keys")["m/0'/"] - return repr((c, K)) + c, K, cK = self.storage.get("master_public_keys")["m/0'/"] + return repr((c, K)) def get_master_private_key(self, account, password): - master_k = pw_decode( self.master_private_keys[account], password) + k = self.master_private_keys.get(account) + if not k: return + master_k = pw_decode( k, password) master_c, master_K, master_Kc = self.master_public_keys[account] try: K, Kc = get_pubkeys_from_secret(master_k.decode('hex')) @@ -675,25 +637,14 @@ class Wallet: return '&'.join(dd) - def get_seed(self, password): s = pw_decode(self.seed, password) - if self.seed_version == 4: - seed = s - self.accounts[0].check_seed(seed) - else: - seed = mnemonic_hash(s) + seed = mnemonic_to_seed(s,'').encode('hex') return seed - - def get_mnemonic(self, password): - import mnemonic - s = pw_decode(self.seed, password) - if self.seed_version == 4: - return ' '.join(mnemonic.mn_encode(s)) - else: - return s + def get_mnemonic(self, password): + return pw_decode(self.seed, password) def get_private_key(self, address, password): @@ -746,23 +697,6 @@ class Wallet: for txin in tx.inputs: keyid = txin.get('KeyID') if keyid: - - if self.seed_version == 4: - m = re.match("old\(([0-9a-f]+),(\d+),(\d+)", keyid) - if not m: continue - mpk = m.group(1) - if mpk != self.storage.get('master_public_key'): continue - for_change = int(m.group(2)) - num = int(m.group(3)) - account = self.accounts[0] - addr = account.get_address(for_change, num) - txin['address'] = addr # fixme: side effect - pk = account.get_private_key(seed, (for_change, num)) - pubkey = public_key_from_private_key(pk) - keypairs[pubkey] = pk - continue - - roots = [] for s in keyid.split('&'): m = re.match("bip32\(([0-9a-f]+),([0-9a-f]+),(/\d+/\d+/\d+)", s) @@ -921,7 +855,7 @@ class Wallet: def create_pending_accounts(self): - for account_type in ['1','2of2','2of3']: + for account_type in ['1of1','2of2','2of3']: if not self.has_master_public_keys(account_type): continue k, a = self.new_account_address(account_type) @@ -1050,29 +984,23 @@ class Wallet: def get_account_name(self, k): - if k == 0: - if self.seed_version == 4: - name = 'Main account' + default = "Unnamed account" + m = re.match("m/0'/(\d+)", k) + if m: + num = m.group(1) + if num == '0': + default = "Main account" else: - name = 'Old account' - else: - default = "Unnamed account" - m = re.match("m/0'/(\d+)", k) - if m: - num = m.group(1) - if num == '0': - default = "Main account" - else: - default = "Account %s"%num + default = "Account %s"%num - m = re.match("m/1'/(\d+) & m/2'/(\d+)", k) - if m: - num = m.group(1) - default = "2of2 account %s"%num - name = self.labels.get(k, default) - + m = re.match("m/1'/(\d+) & m/2'/(\d+)", k) + if m: + num = m.group(1) + default = "2of2 account %s"%num + name = self.labels.get(k, default) return name + def get_account_names(self): accounts = {} for k, account in self.accounts.items(): @@ -1081,6 +1009,7 @@ class Wallet: accounts[-1] = 'Imported keys' return accounts + def get_account_addresses(self, a, include_change=True): if a is None: o = self.addresses(True) @@ -1537,7 +1466,7 @@ class Wallet: def start_threads(self, network): from verifier import TxVerifier self.network = network - if self.network: + if self.network is not None: self.verifier = TxVerifier(self.network, self.storage) self.verifier.start() self.set_verifier(self.verifier) @@ -1622,12 +1551,12 @@ class WalletSynchronizer(threading.Thread): if not self.network.is_connected(): self.network.wait_until_connected() - self.run_interface(self.network.interface) + self.run_interface() - def run_interface(self, interface): + def run_interface(self): - print_error("synchronizer: connected to", interface.server) + print_error("synchronizer: connected to", self.network.main_server()) requested_tx = [] missing_tx = [] @@ -1657,12 +1586,12 @@ class WalletSynchronizer(threading.Thread): # request missing transactions for tx_hash, tx_height in missing_tx: if (tx_hash, tx_height) not in requested_tx: - interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], lambda i,r: self.queue.put(r)) + self.network.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], lambda i,r: self.queue.put(r)) requested_tx.append( (tx_hash, tx_height) ) missing_tx = [] # detect if situation has changed - if interface.is_up_to_date() and self.queue.empty(): + if self.network.is_up_to_date() and self.queue.empty(): if not self.wallet.is_up_to_date(): self.wallet.set_up_to_date(True) self.was_updated = True @@ -1672,7 +1601,7 @@ class WalletSynchronizer(threading.Thread): self.was_updated = True if self.was_updated: - self.wallet.network.trigger_callback('updated') + self.network.trigger_callback('updated') self.was_updated = False # 2. get a response @@ -1681,8 +1610,9 @@ class WalletSynchronizer(threading.Thread): except Queue.Empty: continue - if interface != self.network.interface: - break + # see if it changed + #if interface != self.network.interface: + # break if not r: continue @@ -1700,7 +1630,7 @@ class WalletSynchronizer(threading.Thread): addr = params[0] if self.wallet.get_status(self.wallet.get_history(addr)) != result: if requested_histories.get(addr) is None: - interface.send([('blockchain.address.get_history', [addr])], lambda i,r:self.queue.put(r)) + self.network.send([('blockchain.address.get_history', [addr])], lambda i,r:self.queue.put(r)) requested_histories[addr] = result elif method == 'blockchain.address.get_history': @@ -1750,7 +1680,163 @@ class WalletSynchronizer(threading.Thread): print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) ) if self.was_updated and not requested_tx: - self.wallet.network.trigger_callback('updated') - self.wallet.network.trigger_callback("new_transaction") # Updated gets called too many times from other places as well; if we use that signal we get the notification three times - + self.network.trigger_callback('updated') + # Updated gets called too many times from other places as well; if we use that signal we get the notification three times + self.network.trigger_callback("new_transaction") self.was_updated = False + + + + +class OldWallet(NewWallet): + + def init_seed(self, seed): + import mnemonic + + if self.seed: + raise Exception("a seed exists") + + if not seed: + seed = random_seed(128) + + self.seed_version = OLD_SEED_VERSION + + # see if seed was entered as hex + seed = seed.strip() + try: + assert seed + seed.decode('hex') + self.seed = str(seed) + return + except Exception: + pass + + words = seed.split() + try: + mnemonic.mn_decode(words) + except Exception: + raise + + self.seed = mnemonic.mn_decode(words) + + if not self.seed: + raise Exception("Invalid seed") + + + + def get_master_public_key(self): + return self.storage.get("master_public_key") + + def create_accounts(self, password): + seed = pw_decode(self.seed, password) + mpk = OldAccount.mpk_from_seed(seed) + self.create_account(mpk) + + def create_account(self, mpk): + self.storage.put('master_public_key', mpk, True) + self.accounts[0] = OldAccount({'mpk':mpk, 0:[], 1:[]}) + self.save_accounts() + + def create_watching_only_wallet(self, K0): + self.seed_version = OLD_SEED_VERSION + self.storage.put('seed_version', self.seed_version, True) + self.create_account(K0) + + def get_seed(self, password): + seed = pw_decode(self.seed, password) + self.accounts[0].check_seed(seed) + return seed + + def get_mnemonic(self, password): + import mnemonic + s = pw_decode(self.seed, password) + return ' '.join(mnemonic.mn_encode(s)) + + + def add_keypairs_from_KeyID(self, tx, keypairs, password): + # first check the provided password + seed = self.get_seed(password) + for txin in tx.inputs: + keyid = txin.get('KeyID') + if keyid: + m = re.match("old\(([0-9a-f]+),(\d+),(\d+)", keyid) + if not m: continue + mpk = m.group(1) + if mpk != self.storage.get('master_public_key'): continue + for_change = int(m.group(2)) + num = int(m.group(3)) + account = self.accounts[0] + addr = account.get_address(for_change, num) + txin['address'] = addr # fixme: side effect + pk = account.get_private_key(seed, (for_change, num)) + pubkey = public_key_from_private_key(pk) + keypairs[pubkey] = pk + + + def get_account_name(self, k): + assert k == 0 + return 'Main account' + + + + +# former WalletFactory +class Wallet(object): + + def __new__(self, storage): + config = storage.config + if config.get('bitkey', False): + # if user requested support for Bitkey device, + # import Bitkey driver + from wallet_bitkey import WalletBitkey + return WalletBitkey(config) + + if not storage.file_exists: + seed_version = NEW_SEED_VERSION if config.get('bip32') is True else OLD_SEED_VERSION + else: + seed_version = storage.get('seed_version') + + if seed_version == OLD_SEED_VERSION: + return OldWallet(storage) + elif seed_version == NEW_SEED_VERSION: + return NewWallet(storage) + else: + msg = "This wallet seed is not supported." + if seed_version in [5]: + msg += "\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version + print msg + sys.exit(1) + + + + + @classmethod + def from_seed(self, seed, storage): + import mnemonic + if not seed: + return + + words = seed.strip().split() + try: + mnemonic.mn_decode(words) + uses_electrum_words = True + except Exception: + uses_electrum_words = False + + try: + seed.decode('hex') + is_hex = True + except Exception: + is_hex = False + + if is_hex or (uses_electrum_words and len(words) != 13): + print "old style wallet", len(words), words + w = OldWallet(storage) + w.init_seed(seed) #hex + else: + #assert is_seed(seed) + w = Wallet(storage) + w.init_seed(seed) + + + return w diff --git a/lib/wallet_factory.py b/lib/wallet_factory.py @@ -1,11 +0,0 @@ -class WalletFactory(object): - def __new__(cls, config): - if config.get('bitkey', False): - # if user requested support for Bitkey device, - # import Bitkey driver - from wallet_bitkey import WalletBitkey - return WalletBitkey(config) - - # Load standard wallet - from wallet import Wallet - return Wallet(config) diff --git a/scripts/block_headers b/scripts/block_headers @@ -5,7 +5,7 @@ import time, electrum # 1. start the interface and wait for connection -interface = electrum.Interface('electrum.no-ip.org:50002:s') +interface = electrum.Interface('ecdsa.net:50002:s') interface.start(wait = True) if not interface.is_connected: print "not connected" diff --git a/scripts/peers b/scripts/peers @@ -1,17 +1,79 @@ #!/usr/bin/env python -import time, electrum +import time, electrum, Queue +from electrum import Interface, SimpleConfig +from electrum.network import filter_protocol, parse_servers +from collections import defaultdict -electrum.set_verbosity(False) # default is True -network = electrum.Network({'auto_cycle':True}) -network.register_callback('peers',lambda: electrum.print_json(network.irc_servers.keys())) +# 1. start interface and wait for connection +interface = electrum.Interface('ecdsa.net:50002:s') +interface.start(wait = True) +if not interface.is_connected: + print "not connected" + exit() -if not network.start(wait=True): - print "Not connected" - exit(1) +# 2. get list of peers +q = Queue.Queue() +interface.send([('server.peers.subscribe',[])], lambda i,x: q.put(x)) +r = q.get(timeout=10000) +peers = parse_servers(r.get('result')) +peers = filter_protocol(peers,'s') -print "Connected to", network.interface.server -while not network.irc_servers: - time.sleep(1) +# start interfaces +config = SimpleConfig() +interfaces = map ( lambda server: Interface(server, config), peers ) +results_queue = Queue.Queue() +reached_servers = [] +for i in interfaces: i.start(q) +while peers: + i = q.get(timeout=1) + peers.remove(i.server) + if i.is_connected: + i.send([('blockchain.headers.subscribe',[])], lambda i,x: results_queue.put((i,x))) + reached_servers.append(i.server) + +def analyze(results): + out = {} + dd = {} + for k, v in results.items(): + height = v.get('block_height') + merkle = v.get('merkle_root') + utxo = v.get('utxo_root') + d = dd.get(merkle, defaultdict(int)) + d[utxo] += 1 + dd[merkle] = d + + refs = {} + for merkle, d in dd.items(): + v = d.values() + m = max(v) + ref = d.keys()[v.index(m)] + refs[merkle] = ref, m + + for k, v in results.items(): + height = v.get('block_height') + merkle = v.get('merkle_root') + utxo = v.get('utxo_root') + ref_utxo, num = refs.get(merkle) + + if ref_utxo != utxo and num > 1: + out[k] = height, merkle, utxo + + return out + + +results = {} +while reached_servers: + i, r = results_queue.get(timeout=10000) + results[i.server] = r.get('result') + reached_servers.remove(i.server) + +electrum.print_json(results) +out = analyze(results) +if out: + print "faulty servers:" + electrum.print_json(out) +else: + print "ok" diff --git a/scripts/servers b/scripts/servers @@ -22,7 +22,7 @@ while servers: i = q.get(timeout=1000) servers.remove(i.server) if i.is_connected: - i.send([('blockchain.numblocks.subscribe',[])], lambda i,x: results_queue.put((i,x))) + i.send([('blockchain.headers.subscribe',[])], lambda i,x: results_queue.put((i,x))) reached_servers.append(i.server) i.status = "ok" else: @@ -32,7 +32,7 @@ d = defaultdict(int) while reached_servers: i, r = results_queue.get(timeout=1000) - i.blocks = r.get('result') + i.blocks = r.get('result').get('block_height') d[i.blocks] += 1 reached_servers.remove(i.server)