electrum

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

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:
Melectrum | 24++++++++++++++++--------
Mlib/bitcoin.py | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mlib/deserialize.py | 2+-
Mlib/gui_qt.py | 4++--
Mlib/wallet.py | 200++++++++++++++++++++++++++++++++++---------------------------------------------
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()