commit ca32d29a9c7665daa54395622b7277728722e684
parent deaa5745a493000c6012d315230a3132caf85854
Author: thomasv <thomasv@gitorious>
Date: Thu, 15 Dec 2011 15:41:50 +0100
upgrade to type 2 wallet
Diffstat:
4 files changed, 202 insertions(+), 120 deletions(-)
diff --git a/client/electrum.py b/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"
diff --git a/client/gui.py b/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:
@@ -1020,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
diff --git a/client/upgrade.py b/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()
diff --git a/client/version.py b/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