From 470ceb9b4cb06f304dc28ab28220a1be48f2aadb Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Sun, 4 Dec 2016 16:41:47 +0200 Subject: [PATCH] Add more jmclient tests, wallets full coverage Some modifications to Wallet to remove user interaction from module. Removal of ElectrumWrapWallet, moved to the plugin files. Add tests for aes, mnemonics. Add harness for tests in jmclient, conftest.py and commontest Add .coveragerc for these tests locally. --- jmclient/jmclient/__init__.py | 7 +- jmclient/jmclient/blockchaininterface.py | 2 +- jmclient/jmclient/slowaes.py | 14 -- jmclient/jmclient/wallet.py | 140 ++----------- jmclient/test/.coveragerc | 5 + jmclient/test/commontest.py | 238 +++++++++++++++++++++ jmclient/test/conftest.py | 54 +++++ jmclient/test/test_aes.py | 29 +++ jmclient/test/test_mnemonic.py | 55 +++++ jmclient/test/test_taker.py | 79 +------ jmclient/test/test_wallets.py | 251 +++++++++++++++++++++++ 11 files changed, 660 insertions(+), 214 deletions(-) create mode 100644 jmclient/test/.coveragerc create mode 100644 jmclient/test/commontest.py create mode 100644 jmclient/test/conftest.py create mode 100644 jmclient/test/test_aes.py create mode 100644 jmclient/test/test_mnemonic.py create mode 100644 jmclient/test/test_wallets.py diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index bdca501..3132a39 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -10,13 +10,14 @@ from btc import * from .support import (calc_cj_fee, choose_sweep_orders, choose_orders, pick_order, cheapest_order_choose, weighted_order_choose, - rand_norm_array, rand_pow_array, rand_exp_array) + rand_norm_array, rand_pow_array, rand_exp_array, select, + select_gradual, select_greedy, select_greediest) from .jsonrpc import JsonRpcError, JsonRpcConnectionError, JsonRpc from .old_mnemonic import mn_decode, mn_encode from .slowaes import decryptData, encryptData from .taker import Taker from .wallet import AbstractWallet, BitcoinCoreInterface, Wallet, \ - BitcoinCoreWallet, estimate_tx_fee, ElectrumWrapWallet + BitcoinCoreWallet, estimate_tx_fee from .configure import load_program_config, jm_single, get_p2pk_vbyte, \ get_network, jm_single, get_network, validate_address, get_irc_mchannels, \ check_utxo_blacklist @@ -33,7 +34,7 @@ from .commitment_utils import get_utxo_info, validate_utxo_data, quit try: from logging import NullHandler -except ImportError: +except ImportError: #pragma: no cover class NullHandler(logging.Handler): def emit(self, record): pass diff --git a/jmclient/jmclient/blockchaininterface.py b/jmclient/jmclient/blockchaininterface.py index 1efc4bb..9a9fb6c 100644 --- a/jmclient/jmclient/blockchaininterface.py +++ b/jmclient/jmclient/blockchaininterface.py @@ -99,7 +99,7 @@ class BlockchainInterface(object): ''' -class ElectrumWalletInterface(BlockchainInterface): +class ElectrumWalletInterface(BlockchainInterface): #pragma: no cover """A pseudo-blockchain interface using the existing Electrum server connection in an Electrum wallet. Usage requires calling set_wallet with a valid Electrum diff --git a/jmclient/jmclient/slowaes.py b/jmclient/jmclient/slowaes.py index b516094..258694f 100644 --- a/jmclient/jmclient/slowaes.py +++ b/jmclient/jmclient/slowaes.py @@ -658,17 +658,3 @@ def decryptData(key, data, mode=AESModeOfOperation.modeOfOperation["CBC"]): decr = strip_PKCS7_padding(decr) return decr -if __name__ == "__main__": - moo = AESModeOfOperation() - cleartext = "This is a test!" - cypherkey = [143, 194, 34, 208, 145, 203, 230, 143, 177, 246, 97, 206, 145, - 92, 255, 84] - iv = [103, 35, 148, 239, 76, 213, 47, 118, 255, 222, 123, 176, 106, 134, 98, - 92] - mode, orig_len, ciph = moo.encrypt(cleartext, moo.modeOfOperation["CBC"], - cypherkey, moo.aes.keySize["SIZE_128"], - iv) - print 'm=%s, ol=%s (%s), ciph=%s' % (mode, orig_len, len(cleartext), ciph) - decr = moo.decrypt(ciph, orig_len, mode, cypherkey, - moo.aes.keySize["SIZE_128"], iv) - print decr diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index bacc7c6..e5aed89 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -109,98 +109,6 @@ class AbstractWallet(object): [addrval['value'] for addrval in utxos.values()]) return mix_balance -class ElectrumWrapWallet(AbstractWallet): - """A thin wrapper class over Electrum's own - wallet for joinmarket compatibility - """ - def __init__(self, ewallet): - self.ewallet = ewallet - #TODO: populate self.unspent with all utxos in Electrum wallet. - - # None is valid for unencrypted electrum wallets; - # calling functions must set the password otherwise - # for private key operations to work - self.password = None - super(ElectrumWrapWallet, self).__init__() - - def get_key_from_addr(self, addr): - if self.ewallet.has_password() and self.password is None: - raise Exception("Cannot extract private key without password") - key = self.ewallet.get_private_key(addr, self.password) - #Convert from wif compressed to hex compressed - #TODO check if compressed - hex_key = btc.from_wif_privkey(key[0], vbyte=get_p2pk_vbyte()) - return hex_key - - def get_external_addr(self, mixdepth): - addr = self.ewallet.get_unused_address() - return addr - - def get_internal_addr(self, mixdepth): - try: - addrs = self.ewallet.get_change_addresses()[ - -self.ewallet.gap_limit_for_change:] - except Exception as e: - log.debug("Failed get change addresses: " + repr(e)) - raise - #filter by unused - try: - change_addrs = [addr for addr in addrs if - self.ewallet.get_num_tx(addr) == 0] - except Exception as e: - log.debug("Failed to filter chadr: " + repr(e)) - raise - #if no unused Electrum re-uses randomly TODO consider - #(of course, all coins in same mixdepth are in principle linkable, - #so I suspect it is better to stick with Electrum's own model, considering - #gap limit issues) - if not change_addrs: - try: - change_addrs = [random.choice(addrs)] - except Exception as e: - log.debug("Failed random: " + repr(e)) - raise - return change_addrs[0] - - def sign_tx(self, tx, addrs): - """tx should be a serialized hex tx. - If self.password is correctly set, - will return the raw transaction with all - inputs from this wallet signed. - """ - if not self.password: - raise Exception("No password, cannot sign") - from electrum.transaction import Transaction - etx = Transaction(tx) - etx.deserialize() - for i in addrs.keys(): - del etx._inputs[i]['scriptSig'] - self.ewallet.add_input_sig_info(etx._inputs[i], addrs[i]) - etx._inputs[i]['address'] = addrs[i] - self.ewallet.sign_transaction(etx, self.password) - return etx.raw - - def sign_message(self, address, message): - #TODO: not currently used, can we use it for auth? - return self.ewallet.sign_message(address, message, self.password) - - def get_utxos_by_mixdepth(self): - """Initial version: all underlying utxos are mixdepth 0. - Format of return is therefore: {0: - {txid:n : {"address": addr, "value": value}, - txid:n: {"address": addr, "value": value},..}} - TODO this should use the account feature in Electrum, - which is exactly that from BIP32, to implement - multiple mixdepths. - """ - ubym = {0:{}} - coins = self.ewallet.get_spendable_coins() - log.debug(pprint.pformat(coins)) - for c in coins: - utxo = c["prevout_hash"] + ":" + str(c["prevout_n"]) - ubym[0][utxo] = {"address": c["address"], "value": c["value"]} - return ubym - class Wallet(AbstractWallet): def __init__(self, seedarg, @@ -218,6 +126,8 @@ class Wallet(AbstractWallet): self.spent_utxos = [] self.imported_privkeys = {} self.seed = self.read_wallet_file_data(seedarg) + if not self.seed: + raise ValueError("Failed to decrypt wallet") if extend_mixdepth and len(self.index_cache) > max_mix_depth: self.max_mix_depth = len(self.index_cache) self.gaplimit = gaplimit @@ -245,42 +155,34 @@ class Wallet(AbstractWallet): return filename else: raise IOError('wallet file not found') + if not pwd: + log.info("Password required for non-testnet seed wallet") + return None self.path = path fd = open(path, 'r') walletfile = fd.read() fd.close() walletdata = json.loads(walletfile) if walletdata['network'] != get_network(): - print ('wallet network(%s) does not match ' + raise ValueError('wallet network(%s) does not match ' 'joinmarket configured network(%s)' % ( walletdata['network'], get_network())) - sys.exit(0) if 'index_cache' in walletdata: self.index_cache = walletdata['index_cache'] - decrypted = False - while not decrypted: - if pwd: - password = pwd - else: - password = getpass('Enter wallet decryption passphrase: ') - password_key = btc.bin_dbl_sha256(password) - encrypted_seed = walletdata['encrypted_seed'] - try: - decrypted_seed = decryptData( - password_key, - encrypted_seed.decode('hex')).encode('hex') - # there is a small probability of getting a valid PKCS7 - # padding by chance from a wrong password; sanity check the - # seed length - if len(decrypted_seed) == 32: - decrypted = True - else: - raise ValueError - except ValueError: - print('Incorrect password') - if pwd: - raise - decrypted = False + password_key = btc.bin_dbl_sha256(pwd) + encrypted_seed = walletdata['encrypted_seed'] + try: + decrypted_seed = decryptData( + password_key, + encrypted_seed.decode('hex')).encode('hex') + # there is a small probability of getting a valid PKCS7 + # padding by chance from a wrong password; sanity check the + # seed length + if len(decrypted_seed) != 32: + raise ValueError + except ValueError: + log.info('Incorrect password') + return None if self.storepassword: self.password_key = password_key self.walletdata = walletdata @@ -402,7 +304,7 @@ class Wallet(AbstractWallet): return mix_utxo_list -class BitcoinCoreWallet(AbstractWallet): +class BitcoinCoreWallet(AbstractWallet): #pragma: no cover def __init__(self, fromaccount): super(BitcoinCoreWallet, self).__init__() if not isinstance(jm_single().bc_interface, diff --git a/jmclient/test/.coveragerc b/jmclient/test/.coveragerc new file mode 100644 index 0000000..3da71da --- /dev/null +++ b/jmclient/test/.coveragerc @@ -0,0 +1,5 @@ +# .coveragerc to control coverage.py +[run] +omit = + ../jmclient/jsonrpc.py + ../jmclient/btc.py diff --git a/jmclient/test/commontest.py b/jmclient/test/commontest.py new file mode 100644 index 0000000..c825dc6 --- /dev/null +++ b/jmclient/test/commontest.py @@ -0,0 +1,238 @@ +#! /usr/bin/env python +from __future__ import absolute_import +'''Some helper functions for testing''' + +import sys +import os +import time +import binascii +import pexpect +import random +import subprocess +import platform +from decimal import Decimal + +from jmclient import (jm_single, Wallet, get_log, estimate_tx_fee, + BlockchainInterface) +from jmbase.support import chunks +import jmbitcoin as btc + +log = get_log() +'''This code is intended to provide +subprocess startup cross-platform with +some useful options; it could do with +some simplification/improvement.''' +import platform +OS = platform.system() +PINL = '\r\n' if OS == 'Windows' else '\n' + +class DummyBlockchainInterface(BlockchainInterface): + def __init__(self): + self.fake_query_results = None + self.qusfail = False + + def sync_addresses(self, wallet): + pass + def sync_unspent(self, wallet): + pass + def add_tx_notify(self, + txd, + unconfirmfun, + confirmfun, + notifyaddr, + timeoutfun=None): + pass + + def pushtx(self, txhex): + print("pushing: " + str(txhex)) + return True + + def insert_fake_query_results(self, fqr): + self.fake_query_results = fqr + + def setQUSFail(self, state): + self.qusfail = state + + def query_utxo_set(self, txouts,includeconf=False): + if self.qusfail: + #simulate failure to find the utxo + return [None] + if self.fake_query_results: + result = [] + for x in self.fake_query_results: + for y in txouts: + if y == x['utxo']: + result.append(x) + return result + result = [] + #external maker utxos + known_outs = {"03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6:1": "03a2d1cbe977b1feaf8d0d5cc28c686859563d1520b28018be0c2661cf1ebe4857", + "498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3:0": "02b4b749d54e96b04066b0803e372a43d6ffa16e75a001ae0ed4b235674ab286be", + "3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c:1": "023bcbafb4f68455e0d1d117c178b0e82a84e66414f0987453d78da034b299c3a9"} + #our wallet utxos, faked, for podle tests: utxos are doctored (leading 'f'), + #and the lists are (amt, age) + wallet_outs = {'f34b635ed8891f16c4ec5b8236ae86164783903e8e8bb47fa9ef2ca31f3c2d7a:0': [10000000, 2], + 'f780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1': [20000000, 6], + 'fe574db96a4d43a99786b3ea653cda9e4388f377848f489332577e018380cff1:0': [50000000, 3], + 'fd9711a2ef340750db21efb761f5f7d665d94b312332dc354e252c77e9c48349:0': [50000000, 6]} + + if includeconf and set(txouts).issubset(set(wallet_outs)): + #includeconf used as a trigger for a podle check; + #here we simulate a variety of amount/age returns + results = [] + for to in txouts: + results.append({'value': wallet_outs[to][0], + 'confirms': wallet_outs[to][1]}) + return results + if txouts[0] in known_outs: + return [{'value': 200000000, + 'address': btc.pubkey_to_address(known_outs[txouts[0]], magicbyte=0x6f), + 'confirms': 20}] + for t in txouts: + result_dict = {'value': 10000000000, + 'address': "mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ"} + if includeconf: + result_dict['confirms'] = 20 + result.append(result_dict) + return result + + def estimate_fee_per_kb(self, N): + return 30000 + +class TestWallet(Wallet): + """Implementation of wallet + that allows passing in a password + for removal of command line interrupt. + """ + + def __init__(self, + seedarg, + max_mix_depth=2, + gaplimit=6, + extend_mixdepth=False, + storepassword=False, + pwd=None): + self.given_pwd = pwd + super(TestWallet, self).__init__(seedarg, + max_mix_depth, + gaplimit, + extend_mixdepth, + storepassword) + + def read_wallet_file_data(self, filename): + return super(TestWallet, self).read_wallet_file_data( + filename, self.given_pwd) + +def make_sign_and_push(ins_full, + wallet, + amount, + output_addr=None, + change_addr=None, + hashcode=btc.SIGHASH_ALL, + estimate_fee = False): + """Utility function for easily building transactions + from wallets + """ + total = sum(x['value'] for x in ins_full.values()) + ins = ins_full.keys() + #random output address and change addr + output_addr = wallet.get_new_addr(1, 1) if not output_addr else output_addr + change_addr = wallet.get_new_addr(1, 0) if not change_addr else change_addr + fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000 + outs = [{'value': amount, + 'address': output_addr}, {'value': total - amount - fee_est, + 'address': change_addr}] + + tx = btc.mktx(ins, outs) + de_tx = btc.deserialize(tx) + for index, ins in enumerate(de_tx['ins']): + utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) + addr = ins_full[utxo]['address'] + priv = wallet.get_key_from_addr(addr) + if index % 2: + priv = binascii.unhexlify(priv) + tx = btc.sign(tx, index, priv, hashcode=hashcode) + #pushtx returns False on any error + print btc.deserialize(tx) + push_succeed = jm_single().bc_interface.pushtx(tx) + if push_succeed: + return btc.txhash(tx) + else: + return False + +def local_command(command, bg=False, redirect=''): + if redirect == 'NULL': + if OS == 'Windows': + command.append(' > NUL 2>&1') + elif OS == 'Linux': + command.extend(['>', '/dev/null', '2>&1']) + else: + print "OS not recognised, quitting." + elif redirect: + command.extend(['>', redirect]) + + if bg: + #using subprocess.PIPE seems to cause problems + FNULL = open(os.devnull, 'w') + return subprocess.Popen(command, + stdout=FNULL, + stderr=subprocess.STDOUT, + close_fds=True) + else: + #in case of foreground execution, we can use the output; if not + #it doesn't matter + return subprocess.check_output(command) + + +def make_wallets(n, + wallet_structures=None, + mean_amt=1, + sdev_amt=0, + start_index=0, + fixed_seeds=None, + test_wallet=False, + passwords=None): + '''n: number of wallets to be created + wallet_structure: array of n arrays , each subarray + specifying the number of addresses to be populated with coins + at each depth (for now, this will only populate coins into 'receive' addresses) + mean_amt: the number of coins (in btc units) in each address as above + sdev_amt: if randomness in amouts is desired, specify here. + Returns: a dict of dicts of form {0:{'seed':seed,'wallet':Wallet object},1:..,} + Default Wallet constructor is joinmarket.Wallet, else use TestWallet, + which takes a password parameter as in the list passwords. + ''' + if len(wallet_structures) != n: + raise Exception("Number of wallets doesn't match wallet structures") + if not fixed_seeds: + seeds = chunks(binascii.hexlify(os.urandom(15 * n)), 15 * 2) + else: + seeds = fixed_seeds + wallets = {} + for i in range(n): + if test_wallet: + w = TestWallet(seeds[i], max_mix_depth=5, pwd=passwords[i]) + else: + w = Wallet(seeds[i], max_mix_depth=5) + wallets[i + start_index] = {'seed': seeds[i], + 'wallet': w} + for j in range(5): + for k in range(wallet_structures[i][j]): + deviation = sdev_amt * random.random() + amt = mean_amt - sdev_amt / 2.0 + deviation + if amt < 0: amt = 0.001 + amt = float(Decimal(amt).quantize(Decimal(10)**-8)) + jm_single().bc_interface.grab_coins( + wallets[i + start_index]['wallet'].get_external_addr(j), + amt) + #reset the index so the coins can be seen if running in same script + wallets[i + start_index]['wallet'].index[j][0] -= wallet_structures[i][j] + return wallets + + +def interact(process, inputs, expected): + if len(inputs) != len(expected): + raise Exception("Invalid inputs to interact()") + for i, inp in enumerate(inputs): + process.expect(expected[i]) + process.sendline(inp) diff --git a/jmclient/test/conftest.py b/jmclient/test/conftest.py new file mode 100644 index 0000000..004ba73 --- /dev/null +++ b/jmclient/test/conftest.py @@ -0,0 +1,54 @@ +import pytest +import os +import time +import subprocess +from commontest import local_command +from jmclient import load_program_config + +bitcoin_path = None +bitcoin_conf = None +bitcoin_rpcpassword = None +bitcoin_rpcusername = None + + +def pytest_addoption(parser): + parser.addoption("--btcroot", action="store", default='', + help="the fully qualified path to the directory containing "+\ + "the bitcoin binaries, e.g. /home/user/bitcoin/bin/") + parser.addoption("--btcconf", action="store", + help="the fully qualified path to the location of the "+\ + "bitcoin configuration file you use for testing, e.g. "+\ + "/home/user/.bitcoin/bitcoin.conf") + parser.addoption("--btcpwd", + action="store", + help="the RPC password for your test bitcoin instance") + parser.addoption("--btcuser", + action="store", + default='bitcoinrpc', + help="the RPC username for your test bitcoin instance (default=bitcoinrpc)") + +def teardown(): + #shut down bitcoin and remove the regtest dir + local_command([bitcoin_path + "bitcoin-cli", "-regtest", "-rpcuser=" + bitcoin_rpcusername, + "-rpcpassword=" + bitcoin_rpcpassword, "stop"]) + #note, it is better to clean out ~/.bitcoin/regtest but too + #dangerous to automate it here perhaps + + +@pytest.fixture(scope="session", autouse=True) +def setup(request): + request.addfinalizer(teardown) + + global bitcoin_conf, bitcoin_path, bitcoin_rpcpassword, bitcoin_rpcusername + bitcoin_path = request.config.getoption("--btcroot") + bitcoin_conf = request.config.getoption("--btcconf") + bitcoin_rpcpassword = request.config.getoption("--btcpwd") + bitcoin_rpcusername = request.config.getoption("--btcuser") + + #start up regtest blockchain + btc_proc = subprocess.call([bitcoin_path + "bitcoind", "-regtest", + "-daemon", "-conf=" + bitcoin_conf]) + time.sleep(3) + #generate blocks + local_command([bitcoin_path + "bitcoin-cli", "-regtest", "-rpcuser=" + bitcoin_rpcusername, + "-rpcpassword=" + bitcoin_rpcpassword, "generate", "101"]) diff --git a/jmclient/test/test_aes.py b/jmclient/test/test_aes.py new file mode 100644 index 0000000..e093b9e --- /dev/null +++ b/jmclient/test/test_aes.py @@ -0,0 +1,29 @@ +import jmclient.slowaes as sa +"""test general AES operation; probably not needed. + Not included in coverage, but should be included in suite.""" +import os +import pytest + +def test_pkcs7_bad_padding(): + #used in seed decryption; check that it throws + #if wrongly padded (this caused a REAL bug before!) + bad_padded = ['\x07'*14, '\x07'*31, '\x07'*31+'\x11', '\x07'*31+'\x00', + '\x07'*14+'\x01\x02'] + for b in bad_padded: + with pytest.raises(Exception) as e_info: + fake_unpadded = sa.strip_PKCS7_padding(b) + +def test_aes(): + cleartext = "This is a test!" + iv = [103, 35, 148, 239, 76, 213, 47, 118, 255, 222, 123, 176, 106, 134, 98, + 92] + for ks in [16,24,32]: + for mode in ["CFB", "CBC", "OFB"]: + cypherkey = map(ord, os.urandom(ks)) + moo = sa.AESModeOfOperation() + mode, orig_len, ciph = moo.encrypt(cleartext, moo.modeOfOperation[mode], + cypherkey, ks, + iv) + decr = moo.decrypt(ciph, orig_len, mode, cypherkey, + ks, iv) + assert decr==cleartext \ No newline at end of file diff --git a/jmclient/test/test_mnemonic.py b/jmclient/test/test_mnemonic.py new file mode 100644 index 0000000..2c703f5 --- /dev/null +++ b/jmclient/test/test_mnemonic.py @@ -0,0 +1,55 @@ +from jmclient import old_mnemonic + +import pytest + +@pytest.mark.parametrize( + "seedphrase, key, valid", + [ + (["spiral", "squeeze", "strain", "sunset", "suspend", "sympathy", + "thigh", "throne", "total", "unseen", "weapon", "weary"], + '0028644c0028644f0028645200286455', + True), + (["pair", "bury", "lung", "swim", "orange", "doctor", "numb", "interest", + "shock", "bloom", "fragile", "screen"], + 'fa92999d01431f961a26c876f55d3f6c', + True), + (["check", "squeeze", "strain", "sunset", "suspend", "sympathy", + "thigh", "throne", "total", "unseen", "weapon", "weary"], + '0028644c0028644f0028645200286455', + False), + (["qwerty", "check", "strain", "sunset", "suspend", "sympathy", + "thigh", "throne", "total", "unseen", "weapon", "weary"], + '', + False), + (["", "check", "strain", "sunset", "suspend", "sympathy", + "thigh", "throne", "total", "unseen", "weapon", "weary"], + '', + False), + (["strain", "sunset"], + '', + False), + ]) +def test_old_mnemonic(seedphrase, key, valid): + if valid: + assert old_mnemonic.mn_decode(seedphrase) == key + assert old_mnemonic.mn_encode(key) == seedphrase + else: + if len(key) > 0: + #test cases where the seedphrase is valid + #but must not match the provided master private key + assert old_mnemonic.mn_decode(seedphrase) != key + else: + #test cases where the seedphrase is intrinsically invalid + #Already known error condition: an incorrectly short + #word list will NOT throw an error; this is handled by calling code + if len(seedphrase) < 12: + print "For known failure case of seedphrase less than 12: " + print old_mnemonic.mn_decode(seedphrase) + else: + with pytest.raises(Exception) as e_info: + dummy = old_mnemonic.mn_decode(seedphrase) + print "Got this return value from mn_decode: " + str(dummy) + + + + diff --git a/jmclient/test/test_taker.py b/jmclient/test/test_taker.py index af51a13..5c52893 100644 --- a/jmclient/test/test_taker.py +++ b/jmclient/test/test_taker.py @@ -1,8 +1,6 @@ #!/usr/bin/env python from __future__ import print_function -from jmclient import AbstractWallet -from jmclient import BlockchainInterface -from jmclient import Taker +from commontest import DummyBlockchainInterface import jmbitcoin as bitcoin import binascii import os @@ -12,7 +10,7 @@ import pytest import json from base64 import b64encode from jmclient import (load_program_config, jm_single, set_commitment_file, - get_commitment_file) + get_commitment_file, AbstractWallet, Taker) from taker_test_data import (t_utxos_by_mixdepth, t_selected_utxos, t_orderbook, t_maker_response, t_chosen_orders, t_dummy_ext) @@ -58,79 +56,6 @@ class DummyWallet(AbstractWallet): return binascii.hexlify(p) raise ValueError("No such keypair") -class DummyBlockchainInterface(BlockchainInterface): - def __init__(self): - self.fake_query_results = None - self.qusfail = False - - def sync_addresses(self, wallet): - pass - def sync_unspent(self, wallet): - pass - def add_tx_notify(self, - txd, - unconfirmfun, - confirmfun, - notifyaddr, - timeoutfun=None): - pass - - def pushtx(self, txhex): - print("pushing: " + str(txhex)) - return True - - def insert_fake_query_results(self, fqr): - self.fake_query_results = fqr - - def setQUSFail(self, state): - self.qusfail = state - - def query_utxo_set(self, txouts,includeconf=False): - if self.qusfail: - #simulate failure to find the utxo - return [None] - if self.fake_query_results: - result = [] - for x in self.fake_query_results: - for y in txouts: - if y == x['utxo']: - result.append(x) - return result - result = [] - #external maker utxos - known_outs = {"03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6:1": "03a2d1cbe977b1feaf8d0d5cc28c686859563d1520b28018be0c2661cf1ebe4857", - "498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3:0": "02b4b749d54e96b04066b0803e372a43d6ffa16e75a001ae0ed4b235674ab286be", - "3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c:1": "023bcbafb4f68455e0d1d117c178b0e82a84e66414f0987453d78da034b299c3a9"} - #our wallet utxos, faked, for podle tests: utxos are doctored (leading 'f'), - #and the lists are (amt, age) - wallet_outs = {'f34b635ed8891f16c4ec5b8236ae86164783903e8e8bb47fa9ef2ca31f3c2d7a:0': [10000000, 2], - 'f780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1': [20000000, 6], - 'fe574db96a4d43a99786b3ea653cda9e4388f377848f489332577e018380cff1:0': [50000000, 3], - 'fd9711a2ef340750db21efb761f5f7d665d94b312332dc354e252c77e9c48349:0': [50000000, 6]} - - if includeconf and set(txouts).issubset(set(wallet_outs)): - #includeconf used as a trigger for a podle check; - #here we simulate a variety of amount/age returns - results = [] - for to in txouts: - results.append({'value': wallet_outs[to][0], - 'confirms': wallet_outs[to][1]}) - return results - if txouts[0] in known_outs: - return [{'value': 200000000, - 'address': bitcoin.pubkey_to_address(known_outs[txouts[0]], magicbyte=0x6f), - 'confirms': 20}] - for t in txouts: - result_dict = {'value': 10000000000, - 'address': "mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ"} - if includeconf: - result_dict['confirms'] = 20 - result.append(result_dict) - return result - - def estimate_fee_per_kb(self, N): - return 30000 - def dummy_order_chooser(): return t_chosen_orders diff --git a/jmclient/test/test_wallets.py b/jmclient/test/test_wallets.py new file mode 100644 index 0000000..f0307a7 --- /dev/null +++ b/jmclient/test/test_wallets.py @@ -0,0 +1,251 @@ +#! /usr/bin/env python +from __future__ import absolute_import +'''Wallet functionality tests.''' + +import sys +import os +import time +import binascii +import pexpect +import random +import subprocess +import datetime +import unittest +from ConfigParser import SafeConfigParser, NoSectionError +from decimal import Decimal +from commontest import (local_command, interact, make_wallets, make_sign_and_push, + DummyBlockchainInterface, TestWallet) +import json + +import jmbitcoin as bitcoin +import pytest +from jmclient import (load_program_config, jm_single, sync_wallet, AbstractWallet, + get_p2pk_vbyte, get_log, Wallet, select, select_gradual, + select_greedy, select_greediest, estimate_tx_fee, encryptData, + get_network) +from jmbase.support import chunks +from taker_test_data import t_obtained_tx + +log = get_log() + +def do_tx(wallet, amount): + ins_full = wallet.select_utxos(0, amount) + cj_addr = wallet.get_internal_addr(1) + change_addr = wallet.get_internal_addr(0) + wallet.update_cache_index() + txid = make_sign_and_push(ins_full, wallet, amount, + output_addr=cj_addr, + change_addr=change_addr, + estimate_fee=True) + assert txid + time.sleep(2) #blocks + jm_single().bc_interface.sync_unspent(wallet) + +def test_absurd_fee(setup_wallets): + jm_single().config.set("POLICY", "absurd_fee_per_kb", "1000") + with pytest.raises(ValueError) as e_info: + estimate_tx_fee(10,2) + load_program_config() + +def test_abstract_wallet(setup_wallets): + class DoNothingWallet(AbstractWallet): + pass + for algo in ["default", "gradual", "greedy", "greediest", "none"]: + jm_single().config.set("POLICY", "merge_algorithm", algo) + if algo == "none": + with pytest.raises(Exception) as e_info: + dnw = DoNothingWallet() + #also test if the config is blank + jm_single().config = SafeConfigParser() + dnw = DoNothingWallet() + assert dnw.utxo_selector == select + else: + dnw = DoNothingWallet() + assert not dnw.get_key_from_addr("a") + assert not dnw.get_utxos_by_mixdepth() + assert not dnw.get_external_addr(1) + assert not dnw.get_internal_addr(0) + dnw.update_cache_index() + dnw.remove_old_utxos("a") + dnw.add_new_utxos("b", "c") + load_program_config() + +def create_default_testnet_wallet(): + walletdir = "wallets" + testwalletname = "testwallet.json" + pathtowallet = os.path.join(walletdir, testwalletname) + if os.path.exists(pathtowallet): + os.remove(pathtowallet) + seed = "hello" + return (walletdir, pathtowallet, testwalletname, Wallet(seed, + 5, + 6, + extend_mixdepth=False, + storepassword=False)) + +@pytest.mark.parametrize( + "includecache, wrongnet, storepwd, extendmd, pwdnumtries", + [ + (False, False, False, False, 1000), + (True, False, False, True, 1), + (False, True, False, False, 1), + (False, False, True, False, 1) + ]) +def test_wallet_create(setup_wallets, includecache, wrongnet, storepwd, extendmd, + pwdnumtries): + walletdir, pathtowallet, testwalletname, wallet = create_default_testnet_wallet() + assert wallet.get_key(4,1,17) == "1289ca322f96673acef83f396a9735840e3ab69f0459cf9bfa8d9985a876534401" + assert wallet.get_addr(2,0,5) == "myWPu9QJWHGE79XAmuKkwKgNk8vsr5evpk" + jm_single().bc_interface.wallet_synced = True + assert wallet.get_new_addr(1, 0) == "mi88ZgDGPmarzcsU6S437h9CY9BLmgH5M6" + assert wallet.get_external_addr(3) == "mvChQuChnXVhqvH67wfMxrodPQ7xccdVJU" + addr3internal = wallet.get_internal_addr(3) + assert addr3internal == "mv26o79Bauf2miJMoxoSu1vXmfXnk85YPQ" + assert wallet.get_key_from_addr(addr3internal) == "2a283c9a2168a25509e2fb944939637228c50c8b4fecd9024650316c4584246501" + dummyaddr = "mvw1NazKDRbeNufFANqpYNAANafsMC2zVU" + assert not wallet.get_key_from_addr(dummyaddr) + #Make a new Wallet(), and prepare a testnet wallet file for this wallet + + password = "dummypassword" + password_key = bitcoin.bin_dbl_sha256(password) + seed = bitcoin.sha256("\xaa"*64)[:32] + encrypted_seed = encryptData(password_key, seed.decode('hex')) + timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") + net = get_network() if not wrongnet else 'mainnnet' + walletfilejson = {'creator': 'joinmarket project', + 'creation_time': timestamp, + 'encrypted_seed': encrypted_seed.encode('hex'), + 'network': net} + if includecache: + mmd = wallet.max_mix_depth if not extendmd else wallet.max_mix_depth + 5 + print("using mmd: " + str(mmd)) + walletfilejson.update({'index_cache': [[0,0]]*mmd}) + walletfile = json.dumps(walletfilejson) + if not os.path.exists(walletdir): + os.makedirs(walletdir) + with open(pathtowallet, "wb") as f: + f.write(walletfile) + if wrongnet: + with pytest.raises(ValueError) as e_info: + TestWallet(testwalletname, 5, 6, extend_mixdepth=extendmd, + storepassword=storepwd, pwd=password) + return + from string import ascii_letters + for i in range(pwdnumtries): #multiple tries to ensure pkcs7 error is triggered + with pytest.raises(ValueError) as e_info: + wrongpwd = "".join([random.choice(ascii_letters) for _ in range(20)]) + TestWallet(testwalletname, 5, 6, extend_mixdepth=extendmd, + storepassword=storepwd, pwd=wrongpwd) + + with pytest.raises(ValueError) as e_info: + TestWallet(testwalletname, 5, 6, extend_mixdepth=extendmd, + storepassword=storepwd, pwd=None) + newwallet = TestWallet(testwalletname, 5, 6, extend_mixdepth=extendmd, + storepassword=storepwd, pwd=password) + assert newwallet.seed == seed + #now we have a functional wallet + file, update the cache; first try + #with failed paths + oldpath = newwallet.path + newwallet.path = None + newwallet.update_cache_index() + newwallet.path = "fake-path-definitely-doesnt-exist" + newwallet.update_cache_index() + #with real path + newwallet.path = oldpath + newwallet.index = [[1,1]]*5 + newwallet.update_cache_index() + + #ensure we cannot find a mainnet wallet from seed + seed = "goodbye" + jm_single().config.set("BLOCKCHAIN", "network", "mainnet") + with pytest.raises(IOError) as e_info: + Wallet(seed, 5, 6, False, False) + load_program_config() + +def test_imported_privkey(setup_wallets): + jm_single().config.set("BLOCKCHAIN", "network", "mainnet") + password = "dummypassword" + password_key = bitcoin.bin_dbl_sha256(password) + privkey = "L1RrrnXkcKut5DEMwtDthjwRcTTwED36thyL1DebVrKuwvohjMNi" + #to verify use from_wif_privkey and privkey_to_address + iaddr = "1LDsjB43N2NAQ1Vbc2xyHca4iBBciN8iwC" + privkey_bin = bitcoin.from_wif_privkey(privkey, + vbyte=get_p2pk_vbyte()).decode('hex')[:-1] + encrypted_privkey = encryptData(password_key, privkey_bin) + encrypted_privkey_bad = encryptData(password_key, privkey_bin[:6]) + walletdir = "wallets" + testwalletname = "testreal" + pathtowallet = os.path.join(walletdir, testwalletname) + seed = bitcoin.sha256("\xaa"*64)[:32] + encrypted_seed = encryptData(password_key, seed.decode('hex')) + timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") + for ep in [encrypted_privkey, encrypted_privkey_bad]: + walletfilejson = {'creator': 'joinmarket project', + 'creation_time': timestamp, + 'encrypted_seed': encrypted_seed.encode('hex'), + 'network': get_network(), + 'index_cache': [[0,0]]*5, + 'imported_keys': [ + {'encrypted_privkey': ep.encode('hex'), + 'mixdepth': 0}]} + walletfile = json.dumps(walletfilejson) + if not os.path.exists(walletdir): + os.makedirs(walletdir) + with open(pathtowallet, "wb") as f: + f.write(walletfile) + if ep == encrypted_privkey_bad: + with pytest.raises(Exception) as e_info: + TestWallet(testwalletname, 5, 6, False, False, pwd=password) + continue + newwallet = TestWallet(testwalletname, 5, 6, False, False, pwd=password) + assert newwallet.seed == seed + #test accessing the key from the addr + assert newwallet.get_key_from_addr(iaddr) == bitcoin.from_wif_privkey(privkey) + load_program_config() + +def test_add_remove_utxos(setup_wallets): + #Make a fake wallet and inject and then remove fake utxos + walletdir, pathtowallet, testwalletname, wallet = create_default_testnet_wallet() + assert wallet.get_addr(2,0,5) == "myWPu9QJWHGE79XAmuKkwKgNk8vsr5evpk" + wallet.addr_cache["myWPu9QJWHGE79XAmuKkwKgNk8vsr5evpk"] = (2, 0, 5) + #'76a914c55738deaa9861b6022e53a129968cbf354898b488ac' + #these calls automatically update the addr_cache: + assert wallet.get_new_addr(1, 0) == "mi88ZgDGPmarzcsU6S437h9CY9BLmgH5M6" + #76a9141c9761f5fef73bef6aca378c930c59e7e795088488ac + assert wallet.get_external_addr(3) == "mvChQuChnXVhqvH67wfMxrodPQ7xccdVJU" + #76a914a115fa0394ce881437a96d443e236b39e07db1f988ac + #using the above pubkey scripts: + faketxforwallet = {'outs': + [{'script': '76a914c55738deaa9861b6022e53a129968cbf354898b488ac', + 'value': 110000000}, + {'script': '76a9141c9761f5fef73bef6aca378c930c59e7e795088488ac', + 'value': 89910900}, + {'script': '76a914a115fa0394ce881437a96d443e236b39e07db1f988ac', + 'value': 90021000}, + {'script': '76a9145ece2dac945c8ff5b2b6635360ca0478ade305d488ac', #not ours + 'value': 110000000}], + 'version': 1} + wallet.add_new_utxos(faketxforwallet, "aa"*32) + faketxforspending = {'ins': + [{'outpoint': {'hash': 'aa'*32, + 'index': 0}}, + {'outpoint': {'hash': 'aa'*32, + 'index': 1}}, + {'outpoint': {'hash': 'aa'*32, + 'index': 2}}, + {'outpoint': {'hash': '3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c', + 'index': 1}, + 'script': '', + 'sequence': 4294967295}]} + wallet.select_utxos(1, 100000) + with pytest.raises(Exception) as e_info: + wallet.select_utxos(0, 100000) + #ensure get_utxos_by_mixdepth can handle utxos outside of maxmixdepth + wallet.max_mix_depth = 2 + mul = wallet.get_utxos_by_mixdepth() + assert mul[3] != {} + wallet.remove_old_utxos(faketxforspending) + +@pytest.fixture(scope="module") +def setup_wallets(): + load_program_config()