Browse Source

Merge branch 'master' of gitorious.org:electrum/electrum

283
ThomasV 13 years ago
parent
commit
de41897226
  1. 230
      client/electrum.py
  2. 58
      client/gui.py
  3. 67
      client/upgrade.py
  4. 3
      client/version.py

230
client/electrum.py

@ -19,6 +19,7 @@
import sys, base64, os, re, hashlib, socket, getpass, copy, operator, ast
from decimal import Decimal
from ecdsa.util import string_to_number
try:
import ecdsa
@ -215,8 +216,7 @@ class InvalidPassword(Exception):
SEED_VERSION = 3 # bump this everytime the seed generation is modified
from version import ELECTRUM_VERSION
from version import ELECTRUM_VERSION, SEED_VERSION
class Wallet:
@ -230,13 +230,13 @@ class Wallet:
self.port = 50000
self.fee = 50000
self.servers = ['ecdsa.org','electrum.novit.ro'] # list of default servers
self.master_public_key = None
# saved fields
self.use_encryption = False
self.addresses = []
self.addresses = [] # receiving addresses visible for user
self.change_addresses = [] # addresses used as change
self.seed = '' # encrypted
self.private_keys = repr([]) # encrypted
self.change_addresses = [] # index of addresses used as change
self.status = {} # current status of addresses
self.history = {}
self.labels = {} # labels for addresses and transactions
@ -262,25 +262,31 @@ class Wallet:
elif "LOCALAPPDATA" in os.environ:
wallet_dir = os.path.join( os.environ["LOCALAPPDATA"], 'Electrum' )
elif "APPDATA" in os.environ:
wallet_dir = os.path.join( os.environ["APPDATA"], 'Electrum' )
wallet_dir = os.path.join( os.environ["APPDATA"], 'Electrum' )
else:
raise BaseException("No home directory found in environment variables.")
if not os.path.exists( wallet_dir ): os.mkdir( wallet_dir )
self.path = os.path.join( wallet_dir, 'electrum.dat')
self.path = os.path.join( wallet_dir, 'electrum.dat' )
def new_seed(self, password):
seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
self.seed = wallet.pw_encode( seed, password)
self.init_mpk(seed)
# encrypt
self.seed = wallet.pw_encode( seed, password )
def init_mpk(self,seed):
# public key
curve = SECP256k1
secexp = self.stretch_key(seed)
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
self.master_public_key = master_private_key.get_verifying_key().to_string()
def is_mine(self, address):
return address in self.addresses
return address in self.addresses or address in self.change_addresses
def is_change(self, address):
if not self.is_mine(address):
return False
k = self.addresses.index(address)
return k in self.change_addresses
return address in self.change_addresses
def is_valid(self,addr):
ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z')
@ -288,43 +294,61 @@ class Wallet:
h = bc_address_to_hash_160(addr)
return addr == hash_160_to_bc_address(h)
def create_new_address(self, for_change, password):
seed = self.pw_decode( self.seed, password)
# strenghtening
def stretch_key(self,seed):
oldseed = seed
for i in range(100000):
seed = hashlib.sha512(seed + oldseed).digest()
i = len( self.addresses ) - len(self.change_addresses) if not for_change else len(self.change_addresses)
seed = Hash( "%d:%d:"%(i,for_change) + seed )
seed = hashlib.sha256(seed + oldseed).digest()
return string_to_number( seed )
def get_sequence(self,n,for_change):
return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key ) )
def get_private_key2(self, address, password):
""" Privatekey(type,n) = Master_private_key + H(n|S|type) """
if address in self.addresses:
n = self.addresses.index(address)
for_change = False
elif address in self.change_addresses:
n = self.change_addresses.index(address)
for_change = True
else:
raise BaseException("unknown address")
seed = self.pw_decode( self.seed, password)
secexp = self.stretch_key(seed)
order = generator_secp256k1.order()
secexp = ecdsa.util.randrange_from_seed__trytryagain( seed, order )
secret = SecretToASecret( ('%064x' % secexp).decode('hex') )
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
address = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() )
try:
private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password) )
private_keys.append(secret)
except:
raise InvalidPassword("")
self.private_keys = self.pw_encode( repr(private_keys), password)
self.addresses.append(address)
if for_change: self.change_addresses.append( len(self.addresses) - 1 )
self.history[address] = []
self.status[address] = None
self.save()
return address
privkey_number = ( secexp + self.get_sequence(n,for_change) ) % order
private_key = ecdsa.SigningKey.from_secret_exponent( privkey_number, curve = SECP256k1 )
# sanity check
#public_key = private_key.get_verifying_key()
#assert address == public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() )
return private_key
def recover(self, password):
seed = self.pw_decode( self.seed, password)
def create_new_address2(self, for_change):
""" Publickey(type,n) = Master_public_key + H(n|S|type)*point """
curve = SECP256k1
n = len(self.change_addresses) if for_change else len(self.addresses)
z = self.get_sequence(n,for_change)
master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key, curve = SECP256k1 )
pubkey_point = master_public_key.pubkey.point + z*curve.generator
public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 )
address = public_key_to_bc_address( '04'.decode('hex') + public_key2.to_string() )
if for_change:
self.change_addresses.append(address)
else:
self.addresses.append(address)
self.save()
return address
def recover(self):
# todo: recover receiving addresses from tx
is_found = False
while True:
addr = self.create_new_address(True, password)
addr = self.create_new_address2(True)
self.history[addr] = h = self.retrieve_history(addr)
self.status[addr] = h[-1]['blk_hash'] if h else None
#print "recovering", addr
print "recovering", addr
if self.status[addr] is not None:
is_found = True
else:
@ -332,10 +356,10 @@ class Wallet:
num_gap = 0
while True:
addr = self.create_new_address(False, password)
addr = self.create_new_address2(False)
self.history[addr] = h = self.retrieve_history(addr)
self.status[addr] = h[-1]['blk_hash'] if h else None
#print "recovering", addr
print "recovering", addr
if self.status[addr] is None:
num_gap += 1
if num_gap == self.gap_limit: break
@ -345,12 +369,9 @@ class Wallet:
if not is_found: return False
# remove limit-1 addresses. [ this is ok, because change addresses are at the beginning of the list]
# remove limit-1 addresses.
n = self.gap_limit
self.addresses = self.addresses[:-n]
private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password))
private_keys = private_keys[:-n]
self.private_keys = self.pw_encode( repr(private_keys), password)
# history and addressbook
self.update_tx_history()
@ -364,12 +385,24 @@ class Wallet:
return True
def save(self):
s = repr( (self.seed_version, self.use_encryption, self.fee, self.host, self.port, self.blocks,
self.seed, self.addresses, self.private_keys,
self.change_addresses, self.status, self.history,
self.labels, self.addressbook) )
s = {
'seed_version':self.seed_version,
'use_encryption':self.use_encryption,
'master_public_key': self.master_public_key,
'fee':self.fee,
'host':self.host,
'port':self.port,
'blocks':self.blocks,
'seed':self.seed,
'addresses':self.addresses,
'change_addresses':self.change_addresses,
'status':self.status,
'history':self.history,
'labels':self.labels,
'contacts':self.addressbook
}
f = open(self.path,"w")
f.write(s)
f.write( repr(s) )
f.close()
def read(self):
@ -380,32 +413,41 @@ class Wallet:
except:
return False
try:
sequence = ast.literal_eval( data )
(self.seed_version, self.use_encryption, self.fee, self.host, self.port, self.blocks,
self.seed, self.addresses, self.private_keys,
self.change_addresses, self.status, self.history,
self.labels, self.addressbook) = sequence
self.fee = int(self.fee)
d = ast.literal_eval( data )
self.seed_version = d.get('seed_version')
self.master_public_key = d.get('master_public_key')
self.use_encryption = d.get('use_encryption')
self.fee = int( d.get('fee') )
self.host = d.get('host')
self.port = d.get('port')
self.blocks = d.get('blocks')
self.seed = d.get('seed')
self.addresses = d.get('addresses')
self.change_addresses = d.get('change_addresses')
self.status = d.get('status')
self.history = d.get('history')
self.labels = d.get('labels')
self.addressbook = d.get('contacts')
except:
# it is safer to exit immediately
print "Error; could not parse wallet."
exit(1)
raise BaseException("Error; could not parse wallet. If this is an old wallet format, please use upgrade.py.",0)
if self.seed_version != SEED_VERSION:
raise BaseException("Seed version mismatch.\nPlease move your balance to a new wallet.\nSee the release notes for more information.")
raise BaseException("""Seed version mismatch: your wallet seed is deprecated.
Please create a new wallet, and send your coins to the new wallet.
We apologize for the inconvenience. We try to keep this kind of upgrades as rare as possible.
See the release notes for more information.""",1)
self.update_tx_history()
return True
def get_new_address(self, password):
def get_new_address(self):
n = 0
for addr in self.addresses[-self.gap_limit:]:
if self.history[addr] == []:
if not self.history.get(addr):
n = n + 1
if n < self.gap_limit:
try:
new_address = self.create_new_address(False, password)
except InvalidPassword:
return False, "wrong password"
self.save()
new_address = self.create_new_address2(False)
self.history[new_address] = [] #get from server
return True, new_address
else:
return False, "The last %d addresses in your list have never been used. You should use them first, or increase the allowed gap size in your preferences. "%self.gap_limit
@ -516,19 +558,17 @@ class Wallet:
inputs = []
return inputs, total, fee
def choose_tx_outputs( self, to_addr, amount, fee, total, password ):
def choose_tx_outputs( self, to_addr, amount, fee, total ):
outputs = [ (to_addr, amount) ]
change_amount = total - ( amount + fee )
if change_amount != 0:
# first look for unused change addresses
for addr in self.addresses:
i = self.addresses.index(addr)
if i not in self.change_addresses: continue
for addr in self.change_addresses:
if self.history.get(addr): continue
change_address = addr
break
else:
change_address = self.create_new_address(True, password)
change_address = self.create_new_address2(True)
print "new change address", change_address
outputs.append( (change_address, change_amount) )
return outputs
@ -537,7 +577,7 @@ class Wallet:
s_inputs = []
for i in range(len(inputs)):
addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i]
private_key = self.get_private_key(addr, password)
private_key = self.get_private_key2(addr, password)
public_key = private_key.get_verifying_key()
pubkey = public_key.to_string()
tx = filter( raw_tx( inputs, outputs, for_sig = i ) )
@ -556,24 +596,15 @@ class Wallet:
def pw_decode(self, s, password):
if password:
secret = Hash(password)
return DecodeAES(secret, s)
d = DecodeAES(secret, s)
try:
d.decode('hex')
except:
raise InvalidPassword()
return d
else:
return s
def get_private_key( self, addr, password ):
try:
private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password ) )
except:
raise InvalidPassword("")
k = self.addresses.index(addr)
secret = private_keys[k]
b = ASecretToSecret(secret)
secexp = int( b.encode('hex'), 16)
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 )
public_key = private_key.get_verifying_key()
assert addr == public_key_to_bc_address( chr(4) + public_key.to_string() )
return private_key
def get_tx_history(self):
lines = self.tx_history.values()
lines = sorted(lines, key=operator.itemgetter("nTime"))
@ -623,7 +654,7 @@ class Wallet:
inputs, total, fee = wallet.choose_tx_inputs( amount, fee )
if not inputs: return False, "Not enough funds %d %d"%(total, fee)
try:
outputs = wallet.choose_tx_outputs( to_address, amount, fee, total, password )
outputs = wallet.choose_tx_outputs( to_address, amount, fee, total )
s_inputs = wallet.sign_inputs( inputs, outputs, password )
except InvalidPassword:
return False, "Wrong password"
@ -648,7 +679,7 @@ class Wallet:
from optparse import OptionParser
if __name__ == '__main__':
known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'payto', 'sendtx', 'password', 'newaddress', 'addresses', 'history', 'label', 'gui', 'mktx','seed']
known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'payto', 'sendtx', 'password', 'newaddress', 'addresses', 'history', 'label', 'gui', 'mktx','seed','t2']
usage = "usage: %prog [options] command args\nCommands: "+ (', '.join(known_commands))
@ -713,7 +744,7 @@ if __name__ == '__main__':
gap = raw_input("gap limit (default 5):")
if gap: wallet.gap_limit = int(gap)
print "recovering wallet..."
r = wallet.recover(password)
r = wallet.recover()
if r:
print "recovery successful"
wallet.save()
@ -724,7 +755,7 @@ if __name__ == '__main__':
print "Your seed is", wallet.seed
print "Please store it safely"
# generate first key
wallet.create_new_address(False, None)
wallet.create_new_address2(False)
# check syntax
if cmd in ['payto', 'mktx']:
@ -738,13 +769,13 @@ if __name__ == '__main__':
cmd = 'help'
# open session
if cmd not in ['password', 'mktx', 'history', 'label','contacts','help','validateaddress']:
if cmd not in ['password', 'mktx', 'history', 'label', 'contacts', 'help', 'validateaddress']:
wallet.new_session()
wallet.update()
wallet.save()
# commands needing password
if cmd in ['payto', 'password', 'newaddress','mktx','seed'] or ( cmd=='addresses' and options.show_keys):
if cmd in ['payto', 'password', 'mktx', 'seed' ] or ( cmd=='addresses' and options.show_keys):
password = getpass.getpass('Password:') if wallet.use_encryption else None
if cmd=='help':
@ -790,6 +821,9 @@ if __name__ == '__main__':
addr = args[1]
print wallet.is_valid(addr)
elif cmd == 't2':
wallet.create_t2_address(password)
elif cmd == 'balance':
c, u = wallet.get_balance()
if u:
@ -861,19 +895,18 @@ if __name__ == '__main__':
else:
print h
elif cmd=='sendtx':
elif cmd == 'sendtx':
tx = args[1]
r, h = wallet.sendtx( tx )
print h
elif cmd == 'newaddress':
s, a = wallet.get_new_address(password)
s, a = wallet.get_new_address()
print a
elif cmd == 'password':
try:
seed = wallet.pw_decode( wallet.seed, password)
private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
except:
print "sorry"
sys.exit(1)
@ -881,7 +914,6 @@ if __name__ == '__main__':
if new_password == getpass.getpass('Confirm new password:'):
wallet.use_encryption = (new_password != '')
wallet.seed = wallet.pw_encode( seed, new_password)
wallet.private_keys = wallet.pw_encode( repr( private_keys ), new_password)
wallet.save()
else:
print "error: mismatch"

58
client/gui.py

@ -64,7 +64,6 @@ def show_seed_dialog(wallet, password, parent):
import mnemonic
try:
seed = wallet.pw_decode( wallet.seed, password)
private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
except:
show_message("Incorrect password")
return
@ -85,8 +84,13 @@ def init_wallet(wallet):
try:
found = wallet.read()
except BaseException, e:
show_message(e.message)
show_message(e.args[0])
if e.args[1] == 0: exit(1)
found = 1
except:
exit()
if not found:
# ask if the user wants to create a new wallet, or recover from a seed.
@ -114,7 +118,7 @@ def init_wallet(wallet):
run_settings_dialog(wallet, is_create=True, is_recovery=False, parent=None)
# generate first key
wallet.create_new_address(False, None)
wallet.create_new_address2(False)
# run a dialog indicating the seed, ask the user to remember it
show_seed_dialog(wallet, None, None)
@ -133,13 +137,14 @@ def init_wallet(wallet):
message_format = "Please wait..." )
dialog.show()
def recover_thread( wallet, dialog, password ):
wallet.is_found = wallet.recover( password )
def recover_thread( wallet, dialog ):
wallet.init_mpk( wallet.seed ) # not encrypted at this point
wallet.is_found = wallet.recover()
if wallet.is_found:
wallet.save()
gobject.idle_add( dialog.destroy )
thread.start_new_thread( recover_thread, ( wallet, dialog, None ) ) # no password
thread.start_new_thread( recover_thread, ( wallet, dialog ) )
r = dialog.run()
dialog.destroy()
if r==gtk.RESPONSE_CANCEL: sys.exit(1)
@ -354,7 +359,6 @@ def change_password_dialog(wallet, parent, icon):
try:
seed = wallet.pw_decode( wallet.seed, password)
private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
except:
show_message("Incorrect password")
return
@ -365,7 +369,6 @@ def change_password_dialog(wallet, parent, icon):
wallet.use_encryption = (new_password != '')
wallet.seed = wallet.pw_encode( seed, new_password)
wallet.private_keys = wallet.pw_encode( repr( private_keys ), new_password)
wallet.save()
if icon:
@ -487,30 +490,33 @@ class BitcoinGUI:
def update_wallet_thread():
while True:
try:
self.is_connected = False
self.wallet.new_session()
self.is_connected = True
self.update_session = False
self.info.set_text( self.wallet.message)
except:
self.is_connected = False
traceback.print_exc(file=sys.stdout)
time.sleep(self.period)
continue
get_servers_time = 0
while True:
if self.is_connected and self.update_session:
self.wallet.update_session()
self.update_session = False
try:
if self.is_connected and self.update_session:
self.wallet.update_session()
self.update_session = False
if time.time() - get_servers_time > 5*60:
wallet.get_servers()
if time.time() - get_servers_time > 5*60:
wallet.get_servers()
get_servers_time = time.time()
self.period = 15 if self.wallet.use_http() else 5
try:
self.period = 15 if self.wallet.use_http() else 5
u = self.wallet.update()
self.is_connected = True
if u:
self.wallet.save()
gobject.idle_add( self.update_history_tab )
time.sleep(self.period)
except BaseException:
print "starting new session"
break
@ -522,11 +528,7 @@ class BitcoinGUI:
print "error"
traceback.print_exc(file=sys.stdout)
break
self.error = '' if self.is_connected else "Not connected"
if u:
self.wallet.save()
gobject.idle_add( self.update_history_tab )
time.sleep(self.period)
self.error = '' if self.is_connected else "Not connected"
thread.start_new_thread(update_wallet_thread, ())
thread.start_new_thread(update_status_bar_thread, ())
@ -680,6 +682,12 @@ class BitcoinGUI:
self.show_message( msg )
def treeview_button_press(self, treeview, event):
if event.type == gtk.gdk._2BUTTON_PRESS:
c = treeview.get_cursor()[0]
tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
self.show_message(tx_details)
def treeview_key_press(self, treeview, event):
c = treeview.get_cursor()[0]
if event.keyval == gtk.keysyms.Up:
@ -699,6 +707,7 @@ class BitcoinGUI:
treeview.set_tooltip_column(7)
treeview.show()
treeview.connect('key-press-event', self.treeview_key_press)
treeview.connect('button-press-event', self.treeview_button_press)
tvcolumn = gtk.TreeViewColumn('')
treeview.append_column(tvcolumn)
@ -756,7 +765,7 @@ class BitcoinGUI:
tvcolumn.set_visible(False)
scroll = gtk.ScrolledWindow()
scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
scroll.add(treeview)
self.add_tab(scroll, 'History')
@ -1014,8 +1023,7 @@ class BitcoinGUI:
errorDialog.run()
errorDialog.destroy()
else:
password = password_dialog() if self.wallet.use_encryption else None
success, ret = self.wallet.get_new_address(password)
success, ret = self.wallet.get_new_address()
self.update_session = True # we created a new address
if success:
address = ret

67
client/upgrade.py

@ -1,9 +1,12 @@
import electrum, getpass, base64,ast,sys
import electrum, getpass, base64,ast,sys,os
from version import SEED_VERSION
def upgrade_wallet(wallet):
if wallet.version == 1 and wallet.use_encryption:
print "walet path:",wallet.path
print "seed version:", wallet.seed_version
if wallet.seed_version == 1 and wallet.use_encryption:
# version 1 used pycrypto for wallet encryption
import Crypto
from Crypto.Cipher import AES
@ -27,21 +30,65 @@ def upgrade_wallet(wallet):
wallet.private_keys = wallet.pw_encode( repr( private_keys ), password)
wallet.save()
print "upgraded to version 2"
if wallet.version < 3:
print """
Your wallet is deprecated; its generation seed will not work with versions 0.31 and above.
In order to upgrade, you need to create a new wallet (you may use your current seed), and
to send your bitcoins to the new wallet.
exit(1)
We apologize for the inconvenience. We try to keep this kind of upgrades as rare as possible.
"""
if wallet.seed_version < SEED_VERSION:
print """Note: your wallet seed is deprecated. Please create a new wallet, and move your coins to the new wallet."""
if __name__ == "__main__":
try:
path = sys.argv[1]
except:
path = None
# backward compatibility: look for wallet file in the default data directory
if "HOME" in os.environ:
wallet_dir = os.path.join( os.environ["HOME"], '.electrum')
elif "LOCALAPPDATA" in os.environ:
wallet_dir = os.path.join( os.environ["LOCALAPPDATA"], 'Electrum' )
elif "APPDATA" in os.environ:
wallet_dir = os.path.join( os.environ["APPDATA"], 'Electrum' )
else:
raise BaseException("No home directory found in environment variables.")
path = os.path.join( wallet_dir, 'electrum.dat')
try:
f = open(path,"r")
data = f.read()
f.close()
except:
print "file not found", path
exit(1)
try:
x = ast.literal_eval(data)
except:
print "error: could not parse wallet"
exit(1)
if type(x) == tuple:
seed_version, use_encryption, fee, host, port, blocks, seed, addresses, private_keys, change_addresses, status, history, labels, addressbook = x
s = {
'seed_version':seed_version,
'use_encryption':use_encryption,
'master_public_key':None,
'fee':fee,
'host':host,
'port':port,
'blocks':blocks,
'seed':seed,
'addresses':addresses,
'change_addresses':change_addresses,
'status':status,
'history':history,
'labels':labels,
'contacts':addressbook
}
f = open(path,"w")
f.write( repr(s) )
f.close()
print "wallet format was upgraded."
exit(1)
wallet = electrum.Wallet(path)
try:
found = wallet.read()

3
client/version.py

@ -1 +1,2 @@
ELECTRUM_VERSION = "0.33"
ELECTRUM_VERSION = "0.34"
SEED_VERSION = 4 # bump this everytime the seed generation is modified

Loading…
Cancel
Save