You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

558 lines
23 KiB

#! /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 (interact, make_wallets,
make_sign_and_push, DummyBlockchainInterface)
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, WalletError,
BitcoinCoreWallet, BitcoinCoreInterface, SegwitWallet)
from jmbase.support import chunks
from taker_test_data import t_obtained_tx, t_raw_signed_tx
testdir = os.path.dirname(os.path.realpath(__file__))
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)
return txid
def test_query_utxo_set(setup_wallets):
load_program_config()
jm_single().bc_interface.tick_forward_chain_interval = 1
wallet = create_wallet_for_sync("wallet4utxo.json", "4utxo",
[2, 3, 0, 0, 0],
["wallet4utxo.json", "4utxo", [2, 3]])
sync_wallet(wallet)
txid = do_tx(wallet, 90000000)
time.sleep(3)
txid2 = do_tx(wallet, 20000000)
time.sleep(3)
print("Got txs: ", txid, txid2)
res1 = jm_single().bc_interface.query_utxo_set(txid + ":0")
res2 = jm_single().bc_interface.query_utxo_set(
[txid + ":0", txid2 + ":1"],
includeconf=True)
assert len(res1) == 1
assert len(res2) == 2
assert all([x in res1[0] for x in ['script', 'address', 'value']])
assert not 'confirms' in res1[0]
assert 'confirms' in res2[0]
assert 'confirms' in res2[1]
res3 = jm_single().bc_interface.query_utxo_set("ee" * 32 + ":25")
assert res3 == [None]
def create_wallet_for_sync(wallet_file, password, wallet_structure, a):
#Prepare a testnet wallet file for this wallet
password_key = bitcoin.bin_dbl_sha256(password)
#We need a distinct seed for each run so as not to step over each other;
#make it through a deterministic hash
seedh = bitcoin.sha256("".join([str(x) for x in a]))[:32]
encrypted_seed = encryptData(password_key, seedh.decode('hex'))
timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
walletfilejson = {'creator': 'joinmarket project',
'creation_time': timestamp,
'encrypted_seed': encrypted_seed.encode('hex'),
'network': get_network()}
walletfile = json.dumps(walletfilejson)
if not os.path.exists('wallets'):
os.makedirs('wallets')
with open(os.path.join('wallets', wallet_file), "wb") as f:
f.write(walletfile)
#The call to Wallet() in make_wallets should now find the file
#and read from it:
return make_wallets(1,
[wallet_structure],
fixed_seeds=[wallet_file],
test_wallet=True,
passwords=[password])[0]['wallet']
@pytest.mark.parametrize(
"num_txs, fake_count, wallet_structure, amount, wallet_file, password",
[
(3, 13, [11, 3, 4, 5, 6], 150000000, 'test_import_wallet.json',
'import-pwd'),
#Uncomment all these for thorough tests. Passing currently.
#Lots of used addresses
#(7, 1, [51, 3, 4, 5, 6], 150000000, 'test_import_wallet.json',
# 'import-pwd'),
#(3, 1, [3, 1, 4, 5, 6], 50000000, 'test_import_wallet.json',
# 'import-pwd'),
#No spams/fakes
#(2, 0, [5, 20, 1, 1, 1], 50000000, 'test_import_wallet.json',
# 'import-pwd'),
#Lots of transactions and fakes
#(25, 30, [30, 20, 1, 1, 1], 50000000, 'test_import_wallet.json',
# 'import-pwd'),
])
def test_wallet_sync_with_fast(setup_wallets, num_txs, fake_count,
wallet_structure, amount, wallet_file, password):
jm_single().bc_interface.tick_forward_chain_interval = 1
wallet = create_wallet_for_sync(wallet_file, password, wallet_structure,
[num_txs, fake_count, wallet_structure,
amount, wallet_file, password])
sync_count = 0
jm_single().bc_interface.wallet_synced = False
while not jm_single().bc_interface.wallet_synced:
sync_wallet(wallet)
sync_count += 1
#avoid infinite loop
assert sync_count < 10
log.debug("Tried " + str(sync_count) + " times")
assert jm_single().bc_interface.wallet_synced
assert not jm_single().bc_interface.fast_sync_called
#do some transactions with the wallet, then close, then resync
for i in range(num_txs):
do_tx(wallet, amount)
log.debug("After doing a tx, index is now: " + str(wallet.index))
#simulate a spammer requesting a bunch of transactions. This
#mimics what happens in CoinJoinOrder.__init__()
for j in range(fake_count):
#Note that as in a real script run,
#the initial call to sync_wallet will
#have set wallet_synced to True, so these will
#trigger actual imports.
cj_addr = wallet.get_internal_addr(0)
change_addr = wallet.get_internal_addr(0)
wallet.update_cache_index()
log.debug("After doing a spam, index is now: " + str(wallet.index))
assert wallet.index[0][1] == num_txs + fake_count * 2 * num_txs
#Attempt re-sync, simulating a script restart.
jm_single().bc_interface.wallet_synced = False
sync_count = 0
#Probably should be fixed in main code:
#wallet.index_cache is only assigned in Wallet.__init__(),
#meaning a second sync in the same script, after some transactions,
#will not know about the latest index_cache value (see is_index_ahead_of_cache),
#whereas a real re-sync will involve reading the cache from disk.
#Hence, simulation of the fact that the cache index will
#be read from the file on restart:
wallet.index_cache = wallet.index
while not jm_single().bc_interface.wallet_synced:
#Wallet.__init__() resets index to zero.
wallet.index = []
for i in range(5):
wallet.index.append([0, 0])
#Wallet.__init__() also updates the cache index
#from file, but we can reuse from the above pre-loop setting,
#since nothing else in sync will overwrite the cache.
#for regtest add_watchonly_addresses does not exit(), so can
#just repeat as many times as possible. This might
#be usable for non-test code (i.e. no need to restart the
#script over and over again)?
sync_count += 1
log.debug("TRYING SYNC NUMBER: " + str(sync_count))
sync_wallet(wallet, fast=True)
assert jm_single().bc_interface.fast_sync_called
#avoid infinite loop on failure.
assert sync_count < 10
#Wallet should recognize index_cache on fast sync, so should not need to
#run sync process more than once.
assert sync_count == 1
#validate the wallet index values after sync
for i, ws in enumerate(wallet_structure):
assert wallet.index[i][0] == ws #spends into external only
#Same number as above; note it includes the spammer's extras.
assert wallet.index[0][1] == num_txs + fake_count * 2 * num_txs
assert wallet.index[1][1] == num_txs #one change per transaction
for i in range(2, 5):
assert wallet.index[i][1] == 0 #unused
#Now try to do more transactions as sanity check.
do_tx(wallet, 50000000)
@pytest.mark.parametrize(
"wallet_structure, wallet_file, password, ic",
[
#As usual, more test cases are preferable but time
#of build test is too long, so only one activated.
#([11,3,4,5,6], 'test_import_wallet.json', 'import-pwd',
# [(12,3),(100,99),(7, 40), (200, 201), (10,0)]
# ),
([1, 3, 0, 2, 9], 'test_import_wallet.json', 'import-pwd',
[(1, 7), (100, 99), (0, 0), (200, 201), (21, 41)]),
])
def test_wallet_sync_from_scratch(setup_wallets, wallet_structure, wallet_file,
password, ic):
"""Simulate a scenario in which we use a new bitcoind, thusly:
generate a new wallet and simply pretend that it has an existing
index_cache. This will force import of all addresses up to
the index_cache values.
"""
wallet = create_wallet_for_sync(wallet_file, password, wallet_structure,
[wallet_structure, wallet_file, password,
ic])
sync_count = 0
jm_single().bc_interface.wallet_synced = False
wallet.index_cache = ic
while not jm_single().bc_interface.wallet_synced:
wallet.index = []
for i in range(5):
wallet.index.append([0, 0])
#will call with fast=False but index_cache exists; should use slow-sync
sync_wallet(wallet)
sync_count += 1
#avoid infinite loop
assert sync_count < 10
log.debug("Tried " + str(sync_count) + " times")
#after #586 we expect to ALWAYS succeed within 2 rounds
assert sync_count <= 2
#for each external branch, the new index may be higher than
#the original index_cache if there was a higher used address
expected_wallet_index = []
for i, val in enumerate(wallet_structure):
if val > wallet.index_cache[i][0]:
expected_wallet_index.append([val, wallet.index_cache[i][1]])
else:
expected_wallet_index.append([wallet.index_cache[i][0],
wallet.index_cache[i][1]])
assert wallet.index == expected_wallet_index
log.debug("This is wallet unspent: ")
log.debug(json.dumps(wallet.unspent, indent=4))
"""Purely blockchaininterface related error condition tests"""
def test_index_ahead_cache(setup_wallets):
"""Artificial test; look into finding a sync mode that triggers this
"""
class NonWallet(object):
pass
wallet = NonWallet()
wallet.index_cache = [[0, 0], [0, 2]]
from jmclient.blockchaininterface import is_index_ahead_of_cache
assert is_index_ahead_of_cache(wallet, 3, 1)
def test_core_wallet_no_sync(setup_wallets):
"""Ensure BitcoinCoreWallet sync attempt does nothing
"""
wallet = BitcoinCoreWallet("")
#this will not trigger sync due to absence of non-zero index_cache, usually.
wallet.index_cache = [[1, 1]]
jm_single().bc_interface.wallet_synced = False
jm_single().bc_interface.sync_wallet(wallet, fast=True)
assert not jm_single().bc_interface.wallet_synced
jm_single().bc_interface.sync_wallet(wallet)
assert not jm_single().bc_interface.wallet_synced
def test_wrong_network_bci(setup_wallets):
rpc = jm_single().bc_interface.jsonRpc
with pytest.raises(Exception) as e_info:
x = BitcoinCoreInterface(rpc, 'mainnet')
def test_pushtx_errors(setup_wallets):
"""Ensure pushtx fails return False
"""
badtxhex = "aaaa"
assert not jm_single().bc_interface.pushtx(badtxhex)
#Break the authenticated jsonrpc and try again
jm_single().bc_interface.jsonRpc.port = 18333
assert not jm_single().bc_interface.pushtx(t_raw_signed_tx)
#rebuild a valid jsonrpc inside the bci
load_program_config()
"""Tests mainly for wallet.py"""
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 = "deadbeef"
return (walletdir, pathtowallet, testwalletname,
SegwitWallet(seed,
None,
5,
6,
extend_mixdepth=False,
storepassword=False))
@pytest.mark.parametrize(
"includecache, wrongnet, storepwd, extendmd, pwdnumtries", [
(False, False, False, False, 100),
(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) == "96095d7542e4e832c476b9df7e49ca9e5be61ad3bb8c8a3bdd8e141e2f4caf9101"
assert wallet.get_addr(2, 0, 5) == "2NBUxbEQrGPKrYCV6d4o7Y4AtJ34Uy6gZZg"
jm_single().bc_interface.wallet_synced = True
assert wallet.get_new_addr(1, 0) == "2Mz817RE6zqywgkG2h9cATUoiXwnFSxufk2"
assert wallet.get_external_addr(3) == "2N3gn65WXEzbLnjk5FLDZPc1pL6ebvZAmoA"
addr3internal = wallet.get_internal_addr(3)
assert addr3internal == "2N5NMTYogAyrGhDtWBnVQUp1kgwwFzcf7UM"
assert wallet.get_key_from_addr(
addr3internal) == "089a7173314d29f99e02a37e36da517ce41537a317c83284db1f33dda0af0cc201"
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:
SegwitWallet(testwalletname,
password,
5,
6,
extend_mixdepth=extendmd,
storepassword=storepwd)
return
from string import ascii_letters
for i in range(
pwdnumtries): #multiple tries to ensure pkcs7 error is triggered
with pytest.raises(WalletError) as e_info:
wrongpwd = "".join([random.choice(ascii_letters) for _ in range(20)
])
SegwitWallet(testwalletname,
wrongpwd,
5,
6,
extend_mixdepth=extendmd,
storepassword=storepwd)
with pytest.raises(WalletError) as e_info:
SegwitWallet(testwalletname,
None,
5,
6,
extend_mixdepth=extendmd,
storepassword=storepwd)
newwallet = SegwitWallet(testwalletname,
password,
5,
6,
extend_mixdepth=extendmd,
storepassword=storepwd)
assert newwallet.seed == wallet.entropy_to_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):
for n in ["mainnet", "testnet"]:
privkey = "7d998b45c219a1e38e99e7cbd312ef67f77a455a9b50c730c27f02c6f730dfb401"
jm_single().config.set("BLOCKCHAIN", "network", n)
password = "dummypassword"
password_key = bitcoin.bin_dbl_sha256(password)
wifprivkey = bitcoin.wif_compressed_privkey(privkey, get_p2pk_vbyte())
#mainnet is "L1RrrnXkcKut5DEMwtDthjwRcTTwED36thyL1DebVrKuwvohjMNi"
#to verify use from_wif_privkey and privkey_to_address
if n == "mainnet":
iaddr = "1LDsjB43N2NAQ1Vbc2xyHca4iBBciN8iwC"
else:
iaddr = "mzjq2E92B3oRB7yDKbwM7XnPaAnKfRERw2"
privkey_bin = bitcoin.from_wif_privkey(
wifprivkey,
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 = "test" + n
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': n,
'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:
Wallet(testwalletname, password, 5, 6, False, False)
continue
newwallet = Wallet(testwalletname, password, 5, 6, False, False)
assert newwallet.seed == seed
#test accessing the key from the addr
assert newwallet.get_key_from_addr(
iaddr) == bitcoin.from_wif_privkey(wifprivkey,
vbyte=get_p2pk_vbyte())
if n == "testnet":
jm_single().bc_interface.sync_wallet(newwallet)
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) == "2NBUxbEQrGPKrYCV6d4o7Y4AtJ34Uy6gZZg"
wallet.addr_cache["2NBUxbEQrGPKrYCV6d4o7Y4AtJ34Uy6gZZg"] = (2, 0, 5)
#'a914c80b3c03b96c0da5ef983942d9e541cb788aed8787'
#these calls automatically update the addr_cache:
assert wallet.get_new_addr(1, 0) == "2Mz817RE6zqywgkG2h9cATUoiXwnFSxufk2"
#a9144b6b3836a1708fd38d4728e41b86e69d5bb15d5187
assert wallet.get_external_addr(3) == "2N3gn65WXEzbLnjk5FLDZPc1pL6ebvZAmoA"
#a914728673d95ceafa892ed82f9cc23c8bf1700b6c6187
#using the above pubkey scripts:
faketxforwallet = {'outs': [
{'script': 'a914c80b3c03b96c0da5ef983942d9e541cb788aed8787',
'value': 110000000},
{'script': 'a9144b6b3836a1708fd38d4728e41b86e69d5bb15d5187',
'value': 89910900},
{'script': 'a914728673d95ceafa892ed82f9cc23c8bf1700b6c6187',
'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()
jm_single().bc_interface.tick_forward_chain_interval = 2