commit bbb7d6be92ab8cdc4cff1c86e3cbd3fc1491cf4b
parent 75cb5ddf2fabd9685be9c9d5c0fee8cf1e2c765f
Author: ThomasV <thomasv@gitorious>
Date: Sat, 23 Feb 2013 16:09:13 +0100
Merge branch 'master' of git://github.com/spesmilo/electrum
Diffstat:
5 files changed, 190 insertions(+), 147 deletions(-)
diff --git a/electrum b/electrum
@@ -247,7 +247,7 @@ if __name__ == '__main__':
wallet.gap_limit = gap
if len(seed) == 128:
wallet.seed = ''
- wallet.master_public_key = seed
+ wallet.sequence.master_public_key = seed
else:
wallet.init_seed(str(seed))
@@ -332,7 +332,7 @@ if __name__ == '__main__':
if len(seed) == 128:
wallet.seed = None
- wallet.master_public_key = seed
+ wallet.sequence.master_public_key = seed
else:
wallet.seed = str(seed)
wallet.init_mpk( wallet.seed )
@@ -488,12 +488,12 @@ if __name__ == '__main__':
except:
sys.exit("Error: Error with seed file")
- mpk = wallet.master_public_key
+ mpk = wallet.get_master_public_key()
wallet.seed = seed
wallet.imported_keys = imported_keys
wallet.use_encryption = False
wallet.init_mpk(seed)
- if mpk == wallet.master_public_key:
+ if mpk == wallet.get_master_public_key():
wallet.save()
print_msg("Done: " + wallet.config.path)
else:
@@ -501,7 +501,16 @@ if __name__ == '__main__':
elif cmd == 'validateaddress':
addr = args[1]
- print_msg(wallet.is_valid(addr))
+ is_valid = wallet.is_valid(addr)
+ out = { 'isvalid':is_valid }
+ if is_valid:
+ is_mine = wallet.is_mine(addr)
+ out['address'] = addr
+ out['ismine'] = is_mine
+ if is_mine:
+ out['pubkey'] = wallet.get_public_key(addr)
+
+ print_json(out)
elif cmd == 'balance':
try:
@@ -779,12 +788,11 @@ if __name__ == '__main__':
private_keys = pk
tx.sign( private_keys )
- print_msg(tx)
+ print_json({ "hex":str(tx),"complete":tx.is_complete})
elif cmd == 'listunspent':
- unspent = map(lambda x: {"txid":x[0].split(':')[0],"vout":x[0].split(':')[1],"amount":x[1]*1.e-8}, wallet.prevout_values.items() )
- print_json(unspent)
+ print_json(wallet.get_unspent_coins())
if cmd not in offline_commands and not options.offline:
diff --git a/lib/bitcoin.py b/lib/bitcoin.py
@@ -398,7 +398,57 @@ def CKD_prime(K, c, n):
+class DeterministicSequence:
+ """ Privatekey(type,n) = Master_private_key + H(n|S|type) """
+ def __init__(self, master_public_key):
+ self.master_public_key = master_public_key
+
+ @classmethod
+ def from_seed(klass, seed):
+ curve = SECP256k1
+ secexp = klass.stretch_key(seed)
+ master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
+ master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
+ self = klass(master_public_key)
+ return self
+
+ @classmethod
+ def stretch_key(self,seed):
+ oldseed = seed
+ for i in range(100000):
+ 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.decode('hex') ) )
+
+ def get_pubkey(self, n, for_change):
+ curve = SECP256k1
+ z = self.get_sequence(n, for_change)
+ master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key.decode('hex'), curve = SECP256k1 )
+ pubkey_point = master_public_key.pubkey.point + z*curve.generator
+ public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 )
+ return '04' + public_key2.to_string().encode('hex')
+
+ def get_private_key(self, n, for_change, seed):
+ order = generator_secp256k1.order()
+ secexp = self.stretch_key(seed)
+ secexp = ( secexp + self.get_sequence(n,for_change) ) % order
+ pk = number_to_string( secexp, generator_secp256k1.order() )
+ compressed = False
+ return SecretToASecret( pk, compressed )
+
+ def check_seed(self, seed):
+ curve = SECP256k1
+ secexp = self.stretch_key(seed)
+ master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
+ master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
+ if master_public_key != self.master_public_key:
+ print_error('invalid password (mpk)')
+ raise BaseException('Invalid password')
+
+ return True
################################## transactions
@@ -535,6 +585,7 @@ class Transaction:
for i in range(len(self.inputs)):
txin = self.inputs[i]
+ tx_for_sig = raw_tx( self.inputs, self.outputs, for_sig = i )
if txin.get('redeemScript'):
# 1 parse the redeem script
@@ -549,26 +600,43 @@ class Transaction:
pubkey = GetPubKey(pkey.pubkey, compressed)
keypairs[ pubkey.encode('hex') ] = sec
- # list of signatures
+ # list of already existing signatures
signatures = txin.get("signatures",[])
-
+ found = False
+ complete = True
+
# check if we have a key corresponding to the redeem script
- for pubkey, privkey in keypairs.items():
- if pubkey in redeem_pubkeys:
- # add signature
- compressed = is_compressed(sec)
- pkey = regenerate_key(sec)
- secexp = pkey.secret
- private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
- public_key = private_key.get_verifying_key()
-
- tx = raw_tx( self.inputs, self.outputs, for_sig = i )
- sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
- assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
- signatures.append( sig.encode('hex') )
+ for pubkey in redeem_pubkeys:
+ public_key = ecdsa.VerifyingKey.from_string(pubkey[2:].decode('hex'), curve = SECP256k1)
+
+ for s in signatures:
+ try:
+ public_key.verify_digest( s.decode('hex')[:-1], Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
+ break
+ except ecdsa.keys.BadSignatureError:
+ continue
+ else:
+ if pubkey in keypairs.keys():
+ # add signature
+ sec = keypairs[pubkey]
+ compressed = is_compressed(sec)
+ pkey = regenerate_key(sec)
+ secexp = pkey.secret
+ private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
+ public_key = private_key.get_verifying_key()
+ sig = private_key.sign_digest( Hash( tx_for_sig.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
+ assert public_key.verify_digest( sig, Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
+ signatures.append( sig.encode('hex') )
+ found = True
+ else:
+ complete = False
+
+ if not found:
+ raise BaseException("public key not found", keypairs.keys(), redeem_pubkeys)
# for p2sh, pubkeysig is a tuple (may be incomplete)
self.inputs[i]["signatures"] = signatures
+ self.is_complete = complete
else:
sec = private_keys[txin['address']]
@@ -580,11 +648,11 @@ class Transaction:
public_key = private_key.get_verifying_key()
pkey = EC_KEY(secexp)
pubkey = GetPubKey(pkey.pubkey, compressed)
- tx = raw_tx( self.inputs, self.outputs, for_sig = i )
- sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
- assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
+ sig = private_key.sign_digest( Hash( tx_for_sig.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
+ assert public_key.verify_digest( sig, Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
self.inputs[i]["pubkeysig"] = [(pubkey, sig)]
+ self.is_complete = True
self.raw = raw_tx( self.inputs, self.outputs )
@@ -598,9 +666,6 @@ class Transaction:
def has_address(self, addr):
- print self.inputs
- print self.outputs
-
found = False
for txin in self.inputs:
if addr == txin.get('address'):
diff --git a/lib/deserialize.py b/lib/deserialize.py
@@ -315,7 +315,7 @@ def match_decoded(decoded, to_match):
if len(decoded) != len(to_match):
return False;
for i in range(len(decoded)):
- if to_match[i] == opcodes.OP_PUSHDATA4 and decoded[i][0] <= opcodes.OP_PUSHDATA4:
+ if to_match[i] == opcodes.OP_PUSHDATA4 and decoded[i][0] <= opcodes.OP_PUSHDATA4 and decoded[i][0]>0:
continue # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent.
if to_match[i] != decoded[i][0]:
return False
diff --git a/lib/gui_qt.py b/lib/gui_qt.py
@@ -1319,10 +1319,10 @@ class ElectrumWindow(QMainWindow):
dialog.setWindowTitle(_("Master Public Key"))
main_text = QTextEdit()
- main_text.setText(self.wallet.master_public_key)
+ main_text.setText(self.wallet.get_master_public_key())
main_text.setReadOnly(True)
main_text.setMaximumHeight(170)
- qrw = QRCodeWidget(self.wallet.master_public_key, 6)
+ qrw = QRCodeWidget(self.wallet.get_master_public_key(), 6)
ok_button = QPushButton(_("OK"))
ok_button.setDefault(True)
diff --git a/lib/wallet.py b/lib/wallet.py
@@ -43,6 +43,27 @@ urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s))
DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
+def pw_encode(s, password):
+ if password:
+ secret = Hash(password)
+ return EncodeAES(secret, s)
+ else:
+ return s
+
+def pw_decode(s, password):
+ if password is not None:
+ secret = Hash(password)
+ try:
+ d = DecodeAES(secret, s)
+ except:
+ raise BaseException('Invalid password')
+ return d
+ else:
+ return s
+
+
+
+
from version import ELECTRUM_VERSION, SEED_VERSION
@@ -60,7 +81,6 @@ class Wallet:
self.use_change = config.get('use_change',True)
self.fee = int(config.get('fee',100000))
self.num_zeros = int(config.get('num_zeros',0))
- self.master_public_key = config.get('master_public_key','')
self.use_encryption = config.get('use_encryption', False)
self.addresses = config.get('addresses', []) # receiving addresses visible for user
self.change_addresses = config.get('change_addresses', []) # addresses used as change
@@ -76,6 +96,9 @@ class Wallet:
self.history = config.get('addr_history',{}) # address -> list(txid, height)
self.tx_height = config.get('tx_height',{})
+ master_public_key = config.get('master_public_key','')
+ self.sequence = DeterministicSequence(master_public_key)
+
self.transactions = {}
tx = config.get('transactions',{})
try:
@@ -122,19 +145,15 @@ class Wallet:
while not self.is_up_to_date(): time.sleep(0.1)
def import_key(self, sec, password):
- # try password
- try:
- seed = self.decode_seed(password)
- except:
- raise BaseException("Invalid password")
-
+ # check password
+ seed = self.decode_seed(password)
address = address_from_private_key(sec)
if address in self.all_addresses():
raise BaseException('Address already in wallet')
# store the originally requested keypair into the imported keys table
- self.imported_keys[address] = self.pw_encode(sec, password )
+ self.imported_keys[address] = pw_encode(sec, password )
return address
@@ -149,11 +168,8 @@ class Wallet:
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().encode('hex')
- self.config.set_key('master_public_key', self.master_public_key, True)
+ self.sequence = DeterministicSequence.from_seed(seed)
+ self.config.set_key('master_public_key', self.sequence.master_public_key, True)
def all_addresses(self):
return self.addresses + self.change_addresses + self.imported_keys.keys()
@@ -173,23 +189,35 @@ class Wallet:
return False
return addr == hash_160_to_bc_address(h, addrtype)
- def stretch_key(self,seed):
- oldseed = seed
- for i in range(100000):
- seed = hashlib.sha256(seed + oldseed).digest()
- return string_to_number( seed )
+ def get_master_public_key(self):
+ return self.sequence.master_public_key
+
+ def get_public_key(self, address):
+ if address in self.imported_keys.keys():
+ raise BaseException("imported key")
- def get_sequence(self,n,for_change):
- return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key.decode('hex') ) )
+ 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
+ return self.sequence.get_pubkey(n, for_change)
+
+
+ def decode_seed(self, password):
+ seed = pw_decode(self.seed, password)
+ self.sequence.check_seed(seed)
+ return seed
+
def get_private_key(self, address, password):
- """ Privatekey(type,n) = Master_private_key + H(n|S|type) """
- # decode seed in any case, in order to make test the password
+ # decode seed in any case, in order to test the password
seed = self.decode_seed(password)
if address in self.imported_keys.keys():
- return self.pw_decode( self.imported_keys[address], password )
+ return pw_decode( self.imported_keys[address], password )
else:
if address in self.addresses:
n = self.addresses.index(address)
@@ -199,13 +227,8 @@ class Wallet:
for_change = True
else:
raise BaseException("unknown address", address)
-
- order = generator_secp256k1.order()
- secexp = self.stretch_key(seed)
- secexp = ( secexp + self.get_sequence(n,for_change) ) % order
- pk = number_to_string( secexp, generator_secp256k1.order() )
- compressed = False
- return SecretToASecret( pk, compressed )
+
+ return self.sequence.get_private_key(n, for_change, seed)
def sign_message(self, address, message, password):
@@ -225,16 +248,10 @@ class Wallet:
return address
def get_new_address(self, n, for_change):
- """ Publickey(type,n) = Master_public_key + H(n|S|type)*point """
- curve = SECP256k1
- z = self.get_sequence(n, for_change)
- master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key.decode('hex'), 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() )
- print address
+ pubkey = self.sequence.get_pubkey(n, for_change)
+ address = public_key_to_bc_address( pubkey.decode('hex') )
+ print_msg( address )
return address
-
def change_gap_limit(self, value):
if value >= self.gap_limit:
@@ -303,7 +320,7 @@ class Wallet:
def synchronize(self):
- if not self.master_public_key:
+ if not self.sequence.master_public_key:
return []
new_addresses = []
new_addresses += self.synchronize_sequence(self.addresses, self.gap_limit, False)
@@ -341,7 +358,7 @@ class Wallet:
import datetime
if not tx_hash: return ''
tx = self.transactions.get(tx_hash)
- is_mine, v, fee = self.get_tx_value(tx_hash)
+ is_mine, v, fee = self.get_tx_value(tx)
conf, timestamp = self.verifier.get_confirmations(tx_hash)
if timestamp:
@@ -349,8 +366,8 @@ class Wallet:
else:
time_str = 'pending'
- inputs = map(lambda x: x.get('address'), tx['inputs'])
- outputs = map(lambda x: x.get('address'), tx['outputs'])
+ inputs = map(lambda x: x.get('address'), tx.inputs)
+ outputs = map(lambda x: x.get('address'), tx.d['outputs'])
tx_details = "Transaction Details" +"\n\n" \
+ "Transaction ID:\n" + tx_hash + "\n\n" \
+ "Status: %d confirmations\n"%conf
@@ -448,45 +465,40 @@ class Wallet:
return conf, unconf
- def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ):
- """ todo: minimize tx size """
- total = 0
- fee = self.fee if fixed_fee is None else fixed_fee
-
+ def get_unspent_coins(self, domain=None):
coins = []
- prioritized_coins = []
- domain = [from_addr] if from_addr else self.all_addresses()
- for i in self.frozen_addresses:
- if i in domain: domain.remove(i)
-
- for i in self.prioritized_addresses:
- if i in domain: domain.remove(i)
-
+ if domain is None: domain = self.all_addresses()
for addr in domain:
h = self.history.get(addr, [])
if h == ['*']: continue
for tx_hash, tx_height in h:
tx = self.transactions.get(tx_hash)
- for output in tx.get('outputs'):
+ for output in tx.d.get('outputs'):
if output.get('address') != addr: continue
key = tx_hash + ":%d" % output.get('index')
if key in self.spent_outputs: continue
output['tx_hash'] = tx_hash
coins.append(output)
+ return coins
- for addr in self.prioritized_addresses:
- h = self.history.get(addr, [])
- if h == ['*']: continue
- for tx_hash, tx_height in h:
- tx = self.transactions.get(tx_hash)
- for output in tx.get('outputs'):
- if output.get('address') != addr: continue
- key = tx_hash + ":%d" % output.get('index')
- if key in self.spent_outputs: continue
- output['tx_hash'] = tx_hash
- prioritized_coins.append(output)
+ def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ):
+ """ todo: minimize tx size """
+ total = 0
+ fee = self.fee if fixed_fee is None else fixed_fee
+
+ coins = []
+ prioritized_coins = []
+ domain = [from_addr] if from_addr else self.all_addresses()
+ for i in self.frozen_addresses:
+ if i in domain: domain.remove(i)
+
+ for i in self.prioritized_addresses:
+ if i in domain: domain.remove(i)
+
+ coins = self.get_unspent_coins(domain)
+ prioritized_coins = self.get_unspent_coins(self.prioritized_addresses)
inputs = []
coins = prioritized_coins + coins
@@ -516,39 +528,6 @@ class Wallet:
return outputs
- def pw_encode(self, s, password):
- if password:
- secret = Hash(password)
- return EncodeAES(secret, s)
- else:
- return s
-
- def pw_decode(self, s, password):
- if password is not None:
- secret = Hash(password)
- try:
- d = DecodeAES(secret, s)
- except:
- raise BaseException('Invalid password')
- return d
- else:
- return s
-
- def decode_seed(self, password):
- seed = self.pw_decode(self.seed, password)
-
- # check decoded seed with master public key
- curve = SECP256k1
- secexp = self.stretch_key(seed)
- master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
- master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
- if master_public_key != self.master_public_key:
- print_error('invalid password (mpk)')
- raise BaseException('Invalid password')
-
- return seed
-
-
def get_history(self, address):
with self.lock:
return self.history.get(address)
@@ -605,7 +584,7 @@ class Wallet:
def get_tx_history(self):
with self.lock:
history = self.transactions.items()
- history.sort(key = lambda x: self.tx_height.get(x[0],1e12) )
+ history.sort(key = lambda x: self.tx_height.get(x[0]) if self.tx_height.get(x[0]) else 1e12)
result = []
balance = 0
@@ -629,16 +608,6 @@ class Wallet:
return result
- def get_transactions_at_height(self, height):
- with self.lock:
- values = self.transactions.values()[:]
-
- out = []
- for tx in values:
- if tx['height'] == height:
- out.append(tx['tx_hash'])
- return out
-
def get_label(self, tx_hash):
label = self.labels.get(tx_hash)
@@ -646,6 +615,7 @@ class Wallet:
if is_default: label = self.get_default_label(tx_hash)
return label, is_default
+
def get_default_label(self, tx_hash):
tx = self.transactions.get(tx_hash)
default_label = ''
@@ -791,12 +761,12 @@ class Wallet:
def update_password(self, seed, old_password, new_password):
if new_password == '': new_password = None
self.use_encryption = (new_password != None)
- self.seed = self.pw_encode( seed, new_password)
+ self.seed = pw_encode( seed, new_password)
self.config.set_key('seed', self.seed, True)
for k in self.imported_keys.keys():
a = self.imported_keys[k]
- b = self.pw_decode(a, old_password)
- c = self.pw_encode(b, new_password)
+ b = pw_decode(a, old_password)
+ c = pw_encode(b, new_password)
self.imported_keys[k] = c
self.save()