Browse Source

fix existing tests

add_frost_channel_encryption
zebra-lucky 8 months ago
parent
commit
456fbfaef3
  1. 7
      pyproject.toml
  2. 2
      src/jmbase/__init__.py
  3. 3
      src/jmbase/support.py
  4. 6
      src/jmclient/wallet.py
  5. 6
      src/jmclient/wallet_service.py
  6. 4
      src/jmclient/wallet_utils.py
  7. 62
      test/jmclient/commontest.py
  8. 334
      test/jmclient/test_blockchaininterface.py
  9. 23
      test/jmclient/test_client_protocol.py
  10. 382
      test/jmclient/test_coinjoin.py
  11. 85
      test/jmclient/test_core_nohistory_sync.py
  12. 145
      test/jmclient/test_maker.py
  13. 109
      test/jmclient/test_payjoin.py
  14. 411
      test/jmclient/test_podle.py
  15. 789
      test/jmclient/test_psbt_wallet.py
  16. 211
      test/jmclient/test_snicker.py
  17. 1023
      test/jmclient/test_taker.py
  18. 475
      test/jmclient/test_tx_creation.py
  19. 232
      test/jmclient/test_utxomanager.py
  20. 2154
      test/jmclient/test_wallet.py
  21. 29
      test/jmclient/test_wallet_rpc.py
  22. 161
      test/jmclient/test_wallets.py
  23. 92
      test/jmclient/test_walletservice.py
  24. 200
      test/jmclient/test_walletutils.py
  25. 4
      test/jmclient/test_websocket.py
  26. 116
      test/jmclient/test_yieldgenerator.py

7
pyproject.toml

@ -56,14 +56,15 @@ services = [
] ]
test = [ test = [
"joinmarket[services]", "joinmarket[services]",
"coverage==5.2.1", "coverage==7.8.2",
"flake8", "flake8",
"freezegun", "freezegun",
"mock", "mock",
"pexpect", "pexpect",
"pytest-cov>=2.4.0,<2.6", "pytest-cov==6.1.1",
"pytest==6.2.5", "pytest==7.4.4",
"python-coveralls", "python-coveralls",
"unittest-parametrize==1.6.0",
] ]
gui = [ gui = [
"joinmarket[services]", "joinmarket[services]",

2
src/jmbase/__init__.py

@ -10,7 +10,7 @@ from .support import (get_log, chunks, debug_silence, jmprint,
IndentedHelpFormatterWithNL, wrapped_urlparse, IndentedHelpFormatterWithNL, wrapped_urlparse,
bdict_sdict_convert, random_insert, dict_factory, bdict_sdict_convert, random_insert, dict_factory,
cli_prompt_user_value, cli_prompt_user_yesno, cli_prompt_user_value, cli_prompt_user_yesno,
async_hexbin, twisted_sys_exit) async_hexbin, twisted_sys_exit, is_running_from_pytest)
from .proof_of_work import get_pow, verify_pow from .proof_of_work import get_pow, verify_pow
from .twisted_utils import (stop_reactor, is_hs_uri, get_tor_agent, from .twisted_utils import (stop_reactor, is_hs_uri, get_tor_agent,
get_nontor_agent, JMHiddenService, get_nontor_agent, JMHiddenService,

3
src/jmbase/support.py

@ -220,6 +220,9 @@ def get_password(msg): #pragma: no cover
password = password.encode('utf-8') password = password.encode('utf-8')
return password return password
def is_running_from_pytest():
return bool(environ.get("PYTEST_CURRENT_TEST"))
def lookup_appdata_folder(appname): def lookup_appdata_folder(appname):
""" Given an appname as a string, """ Given an appname as a string,
return the correct directory for storing return the correct directory for storing

6
src/jmclient/wallet.py

@ -1659,8 +1659,10 @@ class BaseWallet(object):
async def _populate_maps(self, paths): async def _populate_maps(self, paths):
for path in paths: for path in paths:
self._script_map[await self.get_script_from_path(path)] = path script = await self.get_script_from_path(path)
self._addr_map[await self.get_address_from_path(path)] = path self._script_map[script] = path
addr = await self.get_address_from_path(path)
self._addr_map[addr] = path
def addr_to_path(self, addr): def addr_to_path(self, addr):
assert isinstance(addr, str) assert isinstance(addr, str)

6
src/jmclient/wallet_service.py

@ -20,7 +20,8 @@ from jmclient.blockchaininterface import (INF_HEIGHT, BitcoinCoreInterface,
from jmclient.wallet import (FidelityBondMixin, BaseWallet, TaprootWallet, from jmclient.wallet import (FidelityBondMixin, BaseWallet, TaprootWallet,
FrostWallet) FrostWallet)
from jmbase import (stop_reactor, hextobin, utxo_to_utxostr, from jmbase import (stop_reactor, hextobin, utxo_to_utxostr,
twisted_sys_exit, jmprint, EXIT_SUCCESS, EXIT_FAILURE) twisted_sys_exit, jmprint, EXIT_SUCCESS, EXIT_FAILURE,
is_running_from_pytest)
from .descriptor import descsum_create from .descriptor import descsum_create
"""Wallet service """Wallet service
@ -706,7 +707,8 @@ class WalletService(Service):
#theres also a sys.exit() in BitcoinCoreInterface.import_addresses() #theres also a sys.exit() in BitcoinCoreInterface.import_addresses()
#perhaps have sys.exit() placed inside the restart_cb that only #perhaps have sys.exit() placed inside the restart_cb that only
# CLI scripts will use # CLI scripts will use
if isinstance(self.bci, BitcoinCoreInterface): if (isinstance(self.bci, BitcoinCoreInterface)
and not is_running_from_pytest()):
#Exit conditions cannot be included in tests #Exit conditions cannot be included in tests
restart_msg = ("Use `bitcoin-cli rescanblockchain` if you're " restart_msg = ("Use `bitcoin-cli rescanblockchain` if you're "
"recovering an existing wallet from backup seed\n" "recovering an existing wallet from backup seed\n"

4
src/jmclient/wallet_utils.py

@ -1587,7 +1587,9 @@ async def open_test_wallet_maybe(
del kwargs['password'] del kwargs['password']
if 'read_only' in kwargs: if 'read_only' in kwargs:
del kwargs['read_only'] del kwargs['read_only']
return test_wallet_cls(storage, **kwargs) wallet = test_wallet_cls(storage, **kwargs)
await wallet.async_init(storage, **kwargs)
return wallet
if wallet_password_stdin is True: if wallet_password_stdin is True:
password = read_password_stdin() password = read_password_stdin()

62
test/jmclient/commontest.py

@ -5,6 +5,9 @@ import os
import random import random
from decimal import Decimal from decimal import Decimal
from typing import Callable, List, Optional, Set, Tuple, Union from typing import Callable, List, Optional, Set, Tuple, Union
from unittest import IsolatedAsyncioTestCase
from twisted.trial.unittest import TestCase as TrialTestCase
import jmbitcoin as btc import jmbitcoin as btc
from jmbase import (get_log, hextobin, bintohex, dictchanger) from jmbase import (get_log, hextobin, bintohex, dictchanger)
@ -33,6 +36,17 @@ def dummy_accept_callback(tx, destaddr, actual_amount, fee_est,
def dummy_info_callback(msg): def dummy_info_callback(msg):
pass pass
class TrialAsyncioTestCase(TrialTestCase, IsolatedAsyncioTestCase):
def __init__(self, methodName='runTest'):
IsolatedAsyncioTestCase.__init__(self, methodName)
TrialTestCase.__init__(self, methodName)
def __call__(self, *args, **kwds):
return IsolatedAsyncioTestCase.run(self, *args, **kwds)
class DummyBlockchainInterface(BlockchainInterface): class DummyBlockchainInterface(BlockchainInterface):
def __init__(self) -> None: def __init__(self) -> None:
@ -174,16 +188,18 @@ class DummyBlockchainInterface(BlockchainInterface):
return 30000 return 30000
def create_wallet_for_sync(wallet_structure, a, **kwargs): async def create_wallet_for_sync(wallet_structure, a, **kwargs):
#We need a distinct seed for each run so as not to step over each other; #We need a distinct seed for each run so as not to step over each other;
#make it through a deterministic hash of all parameters including optionals. #make it through a deterministic hash of all parameters including optionals.
preimage = "".join([str(x) for x in a] + [str(y) for y in kwargs.values()]).encode("utf-8") preimage = "".join([str(x) for x in a] + [str(y) for y in kwargs.values()]).encode("utf-8")
print("using preimage: ", preimage) print("using preimage: ", preimage)
seedh = bintohex(btc.Hash(preimage))[:32] seedh = bintohex(btc.Hash(preimage))[:32]
return make_wallets( wallets = await make_wallets(
1, [wallet_structure], fixed_seeds=[seedh], **kwargs)[0]['wallet'] 1, [wallet_structure], fixed_seeds=[seedh], **kwargs)
return wallets [0]['wallet']
def make_sign_and_push(ins_full,
async def make_sign_and_push(ins_full,
wallet_service, wallet_service,
amount, amount,
output_addr=None, output_addr=None,
@ -202,8 +218,10 @@ def make_sign_and_push(ins_full,
total = sum(x['value'] for x in ins_full.values()) total = sum(x['value'] for x in ins_full.values())
ins = list(ins_full.keys()) ins = list(ins_full.keys())
#random output address and change addr #random output address and change addr
output_addr = wallet_service.get_new_addr(1, BaseWallet.ADDRESS_TYPE_INTERNAL) if not output_addr else output_addr output_addr = await wallet_service.get_new_addr(
change_addr = wallet_service.get_new_addr(0, BaseWallet.ADDRESS_TYPE_INTERNAL) if not change_addr else change_addr 1, BaseWallet.ADDRESS_TYPE_INTERNAL) if not output_addr else output_addr
change_addr = await wallet_service.get_new_addr(
0, BaseWallet.ADDRESS_TYPE_INTERNAL) if not change_addr else change_addr
fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000 fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000
outs = [{'value': amount, outs = [{'value': amount,
'address': output_addr}, {'value': total - amount - fee_est, 'address': output_addr}, {'value': total - amount - fee_est,
@ -214,7 +232,7 @@ def make_sign_and_push(ins_full,
for i, j in enumerate(ins): for i, j in enumerate(ins):
scripts[i] = (ins_full[j]["script"], ins_full[j]["value"]) scripts[i] = (ins_full[j]["script"], ins_full[j]["value"])
success, msg = wallet_service.sign_tx(tx, scripts, hashcode=hashcode) success, msg = await wallet_service.sign_tx(tx, scripts, hashcode=hashcode)
if not success: if not success:
return False return False
#pushtx returns False on any error #pushtx returns False on any error
@ -222,20 +240,21 @@ def make_sign_and_push(ins_full,
if push_succeed: if push_succeed:
# in normal operation this happens automatically # in normal operation this happens automatically
# but in some tests there is no monitoring loop: # but in some tests there is no monitoring loop:
wallet_service.process_new_tx(tx) await wallet_service.process_new_tx(tx)
return tx.GetTxid()[::-1] return tx.GetTxid()[::-1]
else: else:
return False return False
def make_wallets(n,
wallet_structures=None, async def make_wallets(n,
mean_amt=1, wallet_structures=None,
sdev_amt=0, mean_amt=1,
start_index=0, sdev_amt=0,
fixed_seeds=None, start_index=0,
wallet_cls=SegwitWallet, fixed_seeds=None,
mixdepths=5, wallet_cls=SegwitWallet,
populate_internal=BaseWallet.ADDRESS_TYPE_EXTERNAL): mixdepths=5,
populate_internal=BaseWallet.ADDRESS_TYPE_EXTERNAL):
'''n: number of wallets to be created '''n: number of wallets to be created
wallet_structure: array of n arrays , each subarray wallet_structure: array of n arrays , each subarray
specifying the number of addresses to be populated with coins specifying the number of addresses to be populated with coins
@ -258,8 +277,8 @@ def make_wallets(n,
for i in range(n): for i in range(n):
assert len(seeds[i]) == BIP32Wallet.ENTROPY_BYTES * 2 assert len(seeds[i]) == BIP32Wallet.ENTROPY_BYTES * 2
w = open_test_wallet_maybe(seeds[i], seeds[i], mixdepths - 1, w = await open_test_wallet_maybe(seeds[i], seeds[i], mixdepths - 1,
test_wallet_cls=wallet_cls) test_wallet_cls=wallet_cls)
wallet_service = WalletService(w) wallet_service = WalletService(w)
wallets[i + start_index] = {'seed': seeds[i], wallets[i + start_index] = {'seed': seeds[i],
'wallet': wallet_service} 'wallet': wallet_service}
@ -270,8 +289,9 @@ def make_wallets(n,
amt = mean_amt - sdev_amt / 2.0 + deviation amt = mean_amt - sdev_amt / 2.0 + deviation
if amt < 0: amt = 0.001 if amt < 0: amt = 0.001
amt = float(Decimal(amt).quantize(Decimal(10)**-8)) amt = float(Decimal(amt).quantize(Decimal(10)**-8))
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr( addr = await wallet_service.get_new_addr(
j, populate_internal), amt) j, populate_internal)
jm_single().bc_interface.grab_coins(addr, amt)
return wallets return wallets

334
test/jmclient/test_blockchaininterface.py

@ -2,172 +2,212 @@
"""Blockchaininterface functionality tests.""" """Blockchaininterface functionality tests."""
import binascii import binascii
from commontest import create_wallet_for_sync
import pytest import pytest
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
from jmbase import get_log from jmbase import get_log
from jmclient import load_test_config, jm_single, BaseWallet from jmclient import load_test_config, jm_single, BaseWallet
from commontest import create_wallet_for_sync
log = get_log() log = get_log()
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
def sync_test_wallet(fast, wallet_service): async def sync_test_wallet(fast, wallet_service):
sync_count = 0 sync_count = 0
wallet_service.synced = False wallet_service.synced = False
while not wallet_service.synced: while not wallet_service.synced:
wallet_service.sync_wallet(fast=fast) await wallet_service.sync_wallet(fast=fast)
sync_count += 1 sync_count += 1
# avoid infinite loop # avoid infinite loop
assert sync_count < 10 assert sync_count < 10
log.debug("Tried " + str(sync_count) + " times") log.debug("Tried " + str(sync_count) + " times")
@pytest.mark.parametrize('fast', (False, True)) @pytest.mark.usefixtures("setup_wallets")
def test_empty_wallet_sync(setup_wallets, fast): class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
wallet_service = create_wallet_for_sync([0, 0, 0, 0, 0], ['test_empty_wallet_sync'])
@parametrize(
sync_test_wallet(fast, wallet_service) 'fast',
[
broken = True (False,),
for md in range(wallet_service.max_mixdepth + 1): (True,),
for internal in (BaseWallet.ADDRESS_TYPE_INTERNAL, ])
BaseWallet.ADDRESS_TYPE_EXTERNAL): async def test_empty_wallet_sync(self, fast):
wallet_service = await create_wallet_for_sync(
[0, 0, 0, 0, 0], ['test_empty_wallet_sync'])
await sync_test_wallet(fast, wallet_service)
broken = True
for md in range(wallet_service.max_mixdepth + 1):
for internal in (BaseWallet.ADDRESS_TYPE_INTERNAL,
BaseWallet.ADDRESS_TYPE_EXTERNAL):
broken = False
assert 0 == wallet_service.get_next_unused_index(md, internal)
assert not broken
@parametrize(
'fast,internal',
[
(False, BaseWallet.ADDRESS_TYPE_EXTERNAL),
(False, BaseWallet.ADDRESS_TYPE_INTERNAL),
(True, BaseWallet.ADDRESS_TYPE_EXTERNAL),
(True, BaseWallet.ADDRESS_TYPE_INTERNAL)
])
async def test_sequentially_used_wallet_sync(self, fast, internal):
used_count = [1, 3, 6, 2, 23]
wallet_service = await create_wallet_for_sync(
used_count, ['test_sequentially_used_wallet_sync'],
populate_internal=internal)
await sync_test_wallet(fast, wallet_service)
broken = True
for md in range(len(used_count)):
broken = False
assert used_count[md] == wallet_service.get_next_unused_index(md, internal)
assert not broken
@parametrize(
'fast',
[
(False,),
])
async def test_gap_used_wallet_sync(self, fast):
""" After careful examination this test now only includes the Recovery sync.
Note: pre-Aug 2019, because of a bug, this code was not in fact testing both
Fast and Recovery sync, but only Recovery (twice). Also, the scenario set
out in this test (where coins are funded to a wallet which has no index-cache,
and initially no imports) is only appropriate for recovery-mode sync, not for
fast-mode (the now default).
"""
used_count = [1, 3, 6, 2, 23]
wallet_service = await create_wallet_for_sync(
used_count, ['test_gap_used_wallet_sync'])
wallet_service.gap_limit = 20
for md in range(len(used_count)):
x = -1
for x in range(md):
assert x <= wallet_service.gap_limit, "test broken"
# create some unused addresses
await wallet_service.get_new_script(
md, BaseWallet.ADDRESS_TYPE_INTERNAL)
await wallet_service.get_new_script(
md, BaseWallet.ADDRESS_TYPE_EXTERNAL)
used_count[md] += x + 2
jm_single().bc_interface.grab_coins(
await wallet_service.get_new_addr(
md, BaseWallet.ADDRESS_TYPE_INTERNAL), 1)
jm_single().bc_interface.grab_coins(
await wallet_service.get_new_addr(
md, BaseWallet.ADDRESS_TYPE_EXTERNAL), 1)
# reset indices to simulate completely unsynced wallet
for md in range(wallet_service.max_mixdepth + 1):
wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_EXTERNAL, 0)
await sync_test_wallet(fast, wallet_service)
broken = True
for md in range(len(used_count)):
broken = False broken = False
assert 0 == wallet_service.get_next_unused_index(md, internal) assert md + 1 == wallet_service.get_next_unused_index(md,
assert not broken BaseWallet.ADDRESS_TYPE_INTERNAL)
assert used_count[md] == wallet_service.get_next_unused_index(md,
BaseWallet.ADDRESS_TYPE_EXTERNAL)
@pytest.mark.parametrize('fast,internal', ( assert not broken
(False, BaseWallet.ADDRESS_TYPE_EXTERNAL),
(False, BaseWallet.ADDRESS_TYPE_INTERNAL), @parametrize(
(True, BaseWallet.ADDRESS_TYPE_EXTERNAL), 'fast',
(True, BaseWallet.ADDRESS_TYPE_INTERNAL))) [
def test_sequentially_used_wallet_sync(setup_wallets, fast, internal): (False,),
used_count = [1, 3, 6, 2, 23] ])
wallet_service = create_wallet_for_sync( async def test_multigap_used_wallet_sync(self, fast):
used_count, ['test_sequentially_used_wallet_sync'], """ See docstring for test_gap_used_wallet_sync; exactly the
populate_internal=internal) same applies here.
"""
sync_test_wallet(fast, wallet_service) start_index = 5
used_count = [start_index, 0, 0, 0, 0]
broken = True wallet_service = await create_wallet_for_sync(
for md in range(len(used_count)): used_count, ['test_multigap_used_wallet_sync'])
broken = False wallet_service.gap_limit = 5
assert used_count[md] == wallet_service.get_next_unused_index(md, internal)
assert not broken mixdepth = 0
for w in range(5):
for x in range(int(wallet_service.gap_limit * 0.6)):
@pytest.mark.parametrize('fast', (False,)) assert x <= wallet_service.gap_limit, "test broken"
def test_gap_used_wallet_sync(setup_wallets, fast): # create some unused addresses
""" After careful examination this test now only includes the Recovery sync. await wallet_service.get_new_script(
Note: pre-Aug 2019, because of a bug, this code was not in fact testing both mixdepth, BaseWallet.ADDRESS_TYPE_INTERNAL)
Fast and Recovery sync, but only Recovery (twice). Also, the scenario set await wallet_service.get_new_script(
out in this test (where coins are funded to a wallet which has no index-cache, mixdepth, BaseWallet.ADDRESS_TYPE_EXTERNAL)
and initially no imports) is only appropriate for recovery-mode sync, not for used_count[mixdepth] += x + 2
fast-mode (the now default). jm_single().bc_interface.grab_coins(
""" await wallet_service.get_new_addr(
used_count = [1, 3, 6, 2, 23] mixdepth, BaseWallet.ADDRESS_TYPE_INTERNAL), 1)
wallet_service = create_wallet_for_sync(used_count, ['test_gap_used_wallet_sync']) jm_single().bc_interface.grab_coins(
wallet_service.gap_limit = 20 await wallet_service.get_new_addr(
mixdepth, BaseWallet.ADDRESS_TYPE_EXTERNAL), 1)
for md in range(len(used_count)):
x = -1 # reset indices to simulate completely unsynced wallet
for x in range(md): for md in range(wallet_service.max_mixdepth + 1):
assert x <= wallet_service.gap_limit, "test broken" wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
# create some unused addresses wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_EXTERNAL, 0)
wallet_service.get_new_script(md, BaseWallet.ADDRESS_TYPE_INTERNAL)
wallet_service.get_new_script(md, BaseWallet.ADDRESS_TYPE_EXTERNAL) await sync_test_wallet(fast, wallet_service)
used_count[md] += x + 2
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr(md, assert used_count[mixdepth] - start_index == \
BaseWallet.ADDRESS_TYPE_INTERNAL), 1) wallet_service.get_next_unused_index(
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr(md, mixdepth, BaseWallet.ADDRESS_TYPE_INTERNAL)
BaseWallet.ADDRESS_TYPE_EXTERNAL), 1) assert used_count[mixdepth] == wallet_service.get_next_unused_index(
mixdepth, BaseWallet.ADDRESS_TYPE_EXTERNAL)
# reset indices to simulate completely unsynced wallet
for md in range(wallet_service.max_mixdepth + 1): @parametrize(
wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_INTERNAL, 0) 'fast',
wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_EXTERNAL, 0) [
sync_test_wallet(fast, wallet_service) (False,),
(True,),
broken = True ])
for md in range(len(used_count)): async def test_retain_unused_indices_wallet_sync(self, fast):
broken = False used_count = [0, 0, 0, 0, 0]
assert md + 1 == wallet_service.get_next_unused_index(md, wallet_service = await create_wallet_for_sync(
BaseWallet.ADDRESS_TYPE_INTERNAL) used_count, ['test_retain_unused_indices_wallet_sync'])
assert used_count[md] == wallet_service.get_next_unused_index(md,
BaseWallet.ADDRESS_TYPE_EXTERNAL) for x in range(9):
assert not broken await wallet_service.get_new_script(
0, BaseWallet.ADDRESS_TYPE_INTERNAL)
@pytest.mark.parametrize('fast', (False,)) await sync_test_wallet(fast, wallet_service)
def test_multigap_used_wallet_sync(setup_wallets, fast):
""" See docstring for test_gap_used_wallet_sync; exactly the assert wallet_service.get_next_unused_index(0,
same applies here. BaseWallet.ADDRESS_TYPE_INTERNAL) == 9
"""
start_index = 5 @parametrize(
used_count = [start_index, 0, 0, 0, 0] 'fast',
wallet_service = create_wallet_for_sync(used_count, ['test_multigap_used_wallet_sync']) [
wallet_service.gap_limit = 5 (False,),
(True,),
mixdepth = 0 ])
for w in range(5): async def test_imported_wallet_sync(self, fast):
for x in range(int(wallet_service.gap_limit * 0.6)): used_count = [0, 0, 0, 0, 0]
assert x <= wallet_service.gap_limit, "test broken" wallet_service = await create_wallet_for_sync(
# create some unused addresses used_count, ['test_imported_wallet_sync'])
wallet_service.get_new_script(mixdepth, source_wallet_service = await create_wallet_for_sync(
BaseWallet.ADDRESS_TYPE_INTERNAL) used_count, ['test_imported_wallet_sync_origin'])
wallet_service.get_new_script(mixdepth,
BaseWallet.ADDRESS_TYPE_EXTERNAL) address = await source_wallet_service.get_internal_addr(0)
used_count[mixdepth] += x + 2 await wallet_service.import_private_key(0, source_wallet_service.get_wif(0, 1, 0))
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr( txid = binascii.unhexlify(jm_single().bc_interface.grab_coins(address, 1))
mixdepth, BaseWallet.ADDRESS_TYPE_INTERNAL), 1)
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr( await sync_test_wallet(fast, wallet_service)
mixdepth, BaseWallet.ADDRESS_TYPE_EXTERNAL), 1)
assert wallet_service._utxos.have_utxo(txid, 0) == 0
# reset indices to simulate completely unsynced wallet
for md in range(wallet_service.max_mixdepth + 1):
wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
wallet_service.set_next_index(md, BaseWallet.ADDRESS_TYPE_EXTERNAL, 0)
sync_test_wallet(fast, wallet_service)
assert used_count[mixdepth] - start_index == \
wallet_service.get_next_unused_index(mixdepth,
BaseWallet.ADDRESS_TYPE_INTERNAL)
assert used_count[mixdepth] == wallet_service.get_next_unused_index(
mixdepth, BaseWallet.ADDRESS_TYPE_EXTERNAL)
@pytest.mark.parametrize('fast', (False, True))
def test_retain_unused_indices_wallet_sync(setup_wallets, fast):
used_count = [0, 0, 0, 0, 0]
wallet_service = create_wallet_for_sync(used_count,
['test_retain_unused_indices_wallet_sync'])
for x in range(9):
wallet_service.get_new_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL)
sync_test_wallet(fast, wallet_service)
assert wallet_service.get_next_unused_index(0,
BaseWallet.ADDRESS_TYPE_INTERNAL) == 9
@pytest.mark.parametrize('fast', (False, True))
def test_imported_wallet_sync(setup_wallets, fast):
used_count = [0, 0, 0, 0, 0]
wallet_service = create_wallet_for_sync(used_count, ['test_imported_wallet_sync'])
source_wallet_service = create_wallet_for_sync(used_count, ['test_imported_wallet_sync_origin'])
address = source_wallet_service.get_internal_addr(0)
wallet_service.import_private_key(0, source_wallet_service.get_wif(0, 1, 0))
txid = binascii.unhexlify(jm_single().bc_interface.grab_coins(address, 1))
sync_test_wallet(fast, wallet_service)
assert wallet_service._utxos.have_utxo(txid, 0) == 0
@pytest.fixture(scope='module') @pytest.fixture(scope='module')

23
test/jmclient/test_client_protocol.py

@ -1,6 +1,16 @@
#! /usr/bin/env python #! /usr/bin/env python
'''test client-protocol interfacae.''' '''test client-protocol interfacae.'''
import json
import jmbitcoin as bitcoin
import twisted
import base64
import pytest
import jmclient # install asyncioreactor
from twisted.internet import reactor
from jmbase import get_log, bintohex from jmbase import get_log, bintohex
from jmbase.commands import * from jmbase.commands import *
from jmclient import load_test_config, Taker,\ from jmclient import load_test_config, Taker,\
@ -13,16 +23,9 @@ from twisted.internet.error import (ConnectionLost, ConnectionAborted,
ConnectionClosed, ConnectionDone) ConnectionClosed, ConnectionDone)
from twisted.protocols.amp import UnknownRemoteError from twisted.protocols.amp import UnknownRemoteError
from twisted.protocols import amp from twisted.protocols import amp
from twisted.trial import unittest
from twisted.test import proto_helpers from twisted.test import proto_helpers
from taker_test_data import t_raw_signed_tx from taker_test_data import t_raw_signed_tx
from commontest import default_max_cj_fee from commontest import default_max_cj_fee, TrialAsyncioTestCase
import json
import jmbitcoin as bitcoin
import twisted
import base64
import pytest
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
@ -274,7 +277,7 @@ class DummyClientProtocolFactory(JMClientProtocolFactory):
return JMTakerClientProtocol(self, self.client, nick_priv=b"\xaa"*32 + b"\x01") return JMTakerClientProtocol(self, self.client, nick_priv=b"\xaa"*32 + b"\x01")
class TrialTestJMClientProto(unittest.TestCase): class TrialTestJMClientProto(TrialAsyncioTestCase):
def setUp(self): def setUp(self):
global clientfactory global clientfactory
@ -319,7 +322,7 @@ class TrialTestJMClientProto(unittest.TestCase):
pass pass
class TestMakerClientProtocol(unittest.TestCase): class TestMakerClientProtocol(TrialAsyncioTestCase):
""" """
very basic test case for JMMakerClientProtocol very basic test case for JMMakerClientProtocol

382
test/jmclient/test_coinjoin.py

@ -5,10 +5,20 @@ Test doing full coinjoins, bypassing IRC
import os import os
import sys import sys
import pytest
import copy import copy
import shutil
import tempfile
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor from twisted.internet import reactor
import pytest
from _pytest.monkeypatch import MonkeyPatch
from jmbase import get_log from jmbase import get_log
from jmclient import load_test_config, jm_single,\ from jmclient import load_test_config, jm_single,\
YieldGeneratorBasic, Taker, LegacyWallet, SegwitLegacyWallet, SegwitWallet,\ YieldGeneratorBasic, Taker, LegacyWallet, SegwitLegacyWallet, SegwitWallet,\
@ -28,6 +38,7 @@ absoffer_type_map = {LegacyWallet: "absoffer",
SegwitLegacyWallet: "swabsoffer", SegwitLegacyWallet: "swabsoffer",
SegwitWallet: "sw0absoffer"} SegwitWallet: "sw0absoffer"}
def make_wallets_to_list(make_wallets_data): def make_wallets_to_list(make_wallets_data):
wallets = [None for x in range(len(make_wallets_data))] wallets = [None for x in range(len(make_wallets_data))]
for i in make_wallets_data: for i in make_wallets_data:
@ -35,14 +46,15 @@ def make_wallets_to_list(make_wallets_data):
assert all(wallets) assert all(wallets)
return wallets return wallets
def sync_wallets(wallet_services, fast=True):
async def sync_wallets(wallet_services, fast=True):
for wallet_service in wallet_services: for wallet_service in wallet_services:
wallet_service.synced = False wallet_service.synced = False
wallet_service.gap_limit = 0 wallet_service.gap_limit = 0
for x in range(20): for x in range(20):
if wallet_service.synced: if wallet_service.synced:
break break
wallet_service.sync_wallet(fast=fast) await wallet_service.sync_wallet(fast=fast)
else: else:
assert False, "Failed to sync wallet" assert False, "Failed to sync wallet"
# because we don't run the monitoring loops for the # because we don't run the monitoring loops for the
@ -51,6 +63,7 @@ def sync_wallets(wallet_services, fast=True):
for wallet_service in wallet_services: for wallet_service in wallet_services:
wallet_service.update_blockheight() wallet_service.update_blockheight()
def create_orderbook(makers): def create_orderbook(makers):
orderbook = [] orderbook = []
for i in range(len(makers)): for i in range(len(makers)):
@ -75,21 +88,23 @@ def create_taker(wallet, schedule, monkeypatch):
monkeypatch.setattr(taker, 'auth_counterparty', lambda *args: True) monkeypatch.setattr(taker, 'auth_counterparty', lambda *args: True)
return taker return taker
def create_orders(makers):
async def create_orders(makers):
# fire the order creation immediately (delayed 2s in prod, # fire the order creation immediately (delayed 2s in prod,
# but this is too slow for test): # but this is too slow for test):
for maker in makers: for maker in makers:
maker.try_to_create_my_orders() await maker.try_to_create_my_orders()
def init_coinjoin(taker, makers, orderbook, cj_amount):
init_data = taker.initialize(orderbook, []) async def init_coinjoin(taker, makers, orderbook, cj_amount):
init_data = await taker.initialize(orderbook, [])
assert init_data[0], "taker.initialize error" assert init_data[0], "taker.initialize error"
active_orders = init_data[4] active_orders = init_data[4]
maker_data = {} maker_data = {}
for mid in init_data[4]: for mid in init_data[4]:
m = makers[int(mid)] m = makers[int(mid)]
# note: '00' is kphex, usually set up by jmdaemon # note: '00' is kphex, usually set up by jmdaemon
response = m.on_auth_received( response = await m.on_auth_received(
'TAKER', init_data[4][mid], init_data[2][1:], 'TAKER', init_data[4][mid], init_data[2][1:],
init_data[3], init_data[1], '00') init_data[3], init_data[1], '00')
assert response[0], "maker.on_auth_received error" assert response[0], "maker.on_auth_received error"
@ -109,180 +124,209 @@ def init_coinjoin(taker, makers, orderbook, cj_amount):
return active_orders, maker_data return active_orders, maker_data
def do_tx_signing(taker, makers, active_orders, txdata): async def do_tx_signing(taker, makers, active_orders, txdata):
taker_final_result = 'not called' taker_final_result = 'not called'
maker_signatures = {} # left here for easier debugging maker_signatures = {} # left here for easier debugging
for mid in txdata[1]: for mid in txdata[1]:
m = makers[int(mid)] m = makers[int(mid)]
result = m.on_tx_received('TAKER', txdata[2], active_orders[mid]) result = await m.on_tx_received('TAKER', txdata[2], active_orders[mid])
assert result[0], "maker.on_tx_received error" assert result[0], "maker.on_tx_received error"
maker_signatures[mid] = result[1] maker_signatures[mid] = result[1]
for sig in result[1]: for sig in result[1]:
taker_final_result = taker.on_sig(mid, sig) taker_final_result = await taker.on_sig(mid, sig)
assert taker_final_result != 'not called' assert taker_final_result != 'not called'
return taker_final_result return taker_final_result
@pytest.mark.parametrize('wallet_cls', (LegacyWallet, SegwitLegacyWallet, SegwitWallet)) #@pytest.mark.usefixtures("setup_cj")
def test_simple_coinjoin(monkeypatch, tmpdir, setup_cj, wallet_cls): class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
def raise_exit(i):
raise Exception("sys.exit called") def setUp(self):
monkeypatch.setattr(sys, 'exit', raise_exit) self.tmpdir = tempfile.mkdtemp()
set_commitment_file(str(tmpdir.join('commitments.json'))) load_test_config()
jm_single().config.set('POLICY', 'tx_broadcast', 'self')
MAKER_NUM = 3 jm_single().bc_interface.tick_forward_chain_interval = 5
wallet_services = make_wallets_to_list(make_wallets( jm_single().bc_interface.simulate_blocks()
MAKER_NUM + 1, wallet_structures=[[4, 0, 0, 0, 0]] * (MAKER_NUM + 1), sys._exit_ = sys.exit
mean_amt=1, wallet_cls=wallet_cls))
def tearDown(self):
jm_single().bc_interface.tickchain() monkeypatch = MonkeyPatch()
jm_single().bc_interface.tickchain() monkeypatch.setattr(sys, 'exit', sys._exit_)
for dc in reactor.getDelayedCalls():
sync_wallets(wallet_services) dc.cancel()
shutil.rmtree(self.tmpdir)
makers = [YieldGeneratorBasic(
wallet_services[i], @parametrize(
[0, 2000, 0, absoffer_type_map[wallet_cls], 10**7, None, None, None]) for i in range(MAKER_NUM)] 'wallet_cls',
create_orders(makers) [
(LegacyWallet,),
orderbook = create_orderbook(makers) (SegwitLegacyWallet,),
assert len(orderbook) == MAKER_NUM (SegwitWallet,),
])
cj_amount = int(1.1 * 10**8) async def test_simple_coinjoin(self, wallet_cls):
# mixdepth, amount, counterparties, dest_addr, waittime, rounding def raise_exit(i):
schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)] raise Exception("sys.exit called")
taker = create_taker(wallet_services[-1], schedule, monkeypatch) monkeypatch = MonkeyPatch()
monkeypatch.setattr(sys, 'exit', raise_exit)
active_orders, maker_data = init_coinjoin(taker, makers, commitment_file = os.path.join(self.tmpdir, 'commitments.json')
orderbook, cj_amount) set_commitment_file(commitment_file)
txdata = taker.receive_utxos(maker_data) MAKER_NUM = 3
assert txdata[0], "taker.receive_utxos error" wallets = await make_wallets(
MAKER_NUM + 1, wallet_structures=[[4, 0, 0, 0, 0]] * (MAKER_NUM + 1),
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata) mean_amt=1, wallet_cls=wallet_cls)
assert taker_final_result is not False wallet_services = make_wallets_to_list(wallets)
assert taker.on_finished_callback.status is not False
jm_single().bc_interface.tickchain()
jm_single().bc_interface.tickchain()
def test_coinjoin_mixdepth_wrap_taker(monkeypatch, tmpdir, setup_cj):
def raise_exit(i): await sync_wallets(wallet_services)
raise Exception("sys.exit called")
monkeypatch.setattr(sys, 'exit', raise_exit) makers = [
set_commitment_file(str(tmpdir.join('commitments.json'))) YieldGeneratorBasic(
wallet_services[i],
MAKER_NUM = 3 [0, 2000, 0, absoffer_type_map[wallet_cls],
wallet_services = make_wallets_to_list(make_wallets( 10**7, None, None, None])
MAKER_NUM + 1, for i in range(MAKER_NUM)]
wallet_structures=[[4, 0, 0, 0, 0]] * MAKER_NUM + [[0, 0, 0, 0, 3]], await create_orders(makers)
mean_amt=1)) orderbook = create_orderbook(makers)
assert len(orderbook) == MAKER_NUM
for wallet_service in wallet_services:
assert wallet_service.max_mixdepth == 4 cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime, rounding
jm_single().bc_interface.tickchain() schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
jm_single().bc_interface.tickchain() taker = create_taker(wallet_services[-1], schedule, monkeypatch)
sync_wallets(wallet_services) active_orders, maker_data = await init_coinjoin(
taker, makers, orderbook, cj_amount)
cj_fee = 2000
makers = [YieldGeneratorBasic( txdata = await taker.receive_utxos(maker_data)
wallet_services[i], assert txdata[0], "taker.receive_utxos error"
[0, cj_fee, 0, absoffer_type_map[SegwitWallet], 10**7, None, None, None]) for i in range(MAKER_NUM)]
create_orders(makers) taker_final_result = await do_tx_signing(
taker, makers, active_orders, txdata)
orderbook = create_orderbook(makers) assert taker_final_result is not False
assert len(orderbook) == MAKER_NUM assert taker.on_finished_callback.status is not False
cj_amount = int(1.1 * 10**8) async def test_coinjoin_mixdepth_wrap_taker(self):
# mixdepth, amount, counterparties, dest_addr, waittime, rounding def raise_exit(i):
schedule = [(4, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)] raise Exception("sys.exit called")
taker = create_taker(wallet_services[-1], schedule, monkeypatch) monkeypatch = MonkeyPatch()
monkeypatch.setattr(sys, 'exit', raise_exit)
active_orders, maker_data = init_coinjoin(taker, makers, commitment_file = os.path.join(self.tmpdir, 'commitments.json')
orderbook, cj_amount) set_commitment_file(commitment_file)
txdata = taker.receive_utxos(maker_data) MAKER_NUM = 3
assert txdata[0], "taker.receive_utxos error" wallets = await make_wallets(
MAKER_NUM + 1,
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata) wallet_structures=[[4, 0, 0, 0, 0]] * MAKER_NUM + [[0, 0, 0, 0, 3]],
assert taker_final_result is not False mean_amt=1)
wallet_services = make_wallets_to_list(wallets)
tx = btc.CMutableTransaction.deserialize(txdata[2])
for wallet_service in wallet_services:
wallet_service = wallet_services[-1] assert wallet_service.max_mixdepth == 4
# TODO change for new tx monitoring:
wallet_service.remove_old_utxos(tx) jm_single().bc_interface.tickchain()
wallet_service.add_new_utxos(tx) jm_single().bc_interface.tickchain()
balances = wallet_service.get_balance_by_mixdepth() await sync_wallets(wallet_services)
assert balances[0] == cj_amount
# <= because of tx fee cj_fee = 2000
assert balances[4] <= 3 * 10**8 - cj_amount - (cj_fee * MAKER_NUM) makers = [
YieldGeneratorBasic(
wallet_services[i],
def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj): [0, cj_fee, 0, absoffer_type_map[SegwitWallet],
def raise_exit(i): 10**7, None, None, None])
raise Exception("sys.exit called") for i in range(MAKER_NUM)]
monkeypatch.setattr(sys, 'exit', raise_exit) await create_orders(makers)
set_commitment_file(str(tmpdir.join('commitments.json'))) orderbook = create_orderbook(makers)
assert len(orderbook) == MAKER_NUM
MAKER_NUM = 2
wallet_services = make_wallets_to_list(make_wallets( cj_amount = int(1.1 * 10**8)
MAKER_NUM + 1, # mixdepth, amount, counterparties, dest_addr, waittime, rounding
wallet_structures=[[0, 0, 0, 0, 4]] * MAKER_NUM + [[3, 0, 0, 0, 0]], schedule = [(4, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
mean_amt=1)) taker = create_taker(wallet_services[-1], schedule, monkeypatch)
for wallet_service in wallet_services: active_orders, maker_data = await init_coinjoin(
assert wallet_service.max_mixdepth == 4 taker, makers, orderbook, cj_amount)
jm_single().bc_interface.tickchain() txdata = await taker.receive_utxos(maker_data)
jm_single().bc_interface.tickchain() assert txdata[0], "taker.receive_utxos error"
sync_wallets(wallet_services) taker_final_result = await do_tx_signing(
taker, makers, active_orders, txdata)
cj_fee = 2000 assert taker_final_result is not False
makers = [YieldGeneratorBasic(
wallet_services[i], tx = btc.CMutableTransaction.deserialize(txdata[2])
[0, cj_fee, 0, absoffer_type_map[SegwitWallet], 10**7, None, None, None]) for i in range(MAKER_NUM)]
create_orders(makers) wallet_service = wallet_services[-1]
orderbook = create_orderbook(makers) # TODO change for new tx monitoring:
assert len(orderbook) == MAKER_NUM await wallet_service.remove_old_utxos(tx)
cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime, rounding
schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
taker = create_taker(wallet_services[-1], schedule, monkeypatch)
active_orders, maker_data = init_coinjoin(taker, makers,
orderbook, cj_amount)
txdata = taker.receive_utxos(maker_data)
assert txdata[0], "taker.receive_utxos error"
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata)
assert taker_final_result is not False
tx = btc.CMutableTransaction.deserialize(txdata[2])
for i in range(MAKER_NUM):
wallet_service = wallet_services[i]
# TODO as above re: monitoring
wallet_service.remove_old_utxos(tx)
wallet_service.add_new_utxos(tx) wallet_service.add_new_utxos(tx)
balances = wallet_service.get_balance_by_mixdepth() balances = wallet_service.get_balance_by_mixdepth()
assert balances[0] == cj_amount assert balances[0] == cj_amount
assert balances[4] == 4 * 10**8 - cj_amount + cj_fee # <= because of tx fee
assert balances[4] <= 3 * 10**8 - cj_amount - (cj_fee * MAKER_NUM)
@pytest.fixture(scope='module') async def test_coinjoin_mixdepth_wrap_maker(self):
def setup_cj(): def raise_exit(i):
load_test_config() raise Exception("sys.exit called")
jm_single().config.set('POLICY', 'tx_broadcast', 'self') monkeypatch = MonkeyPatch()
jm_single().bc_interface.tick_forward_chain_interval = 5 monkeypatch.setattr(sys, 'exit', raise_exit)
jm_single().bc_interface.simulate_blocks() commitment_file = os.path.join(self.tmpdir, 'commitments.json')
yield None set_commitment_file(commitment_file)
# teardown
for dc in reactor.getDelayedCalls(): MAKER_NUM = 2
dc.cancel() wallets = await make_wallets(
MAKER_NUM + 1,
wallet_structures=[[0, 0, 0, 0, 4]] * MAKER_NUM + [[3, 0, 0, 0, 0]],
mean_amt=1)
wallet_services = make_wallets_to_list(wallets)
for wallet_service in wallet_services:
assert wallet_service.max_mixdepth == 4
jm_single().bc_interface.tickchain()
jm_single().bc_interface.tickchain()
await sync_wallets(wallet_services)
cj_fee = 2000
makers = [
YieldGeneratorBasic(
wallet_services[i],
[0, cj_fee, 0, absoffer_type_map[SegwitWallet],
10**7, None, None, None])
for i in range(MAKER_NUM)]
await create_orders(makers)
orderbook = create_orderbook(makers)
assert len(orderbook) == MAKER_NUM
cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime, rounding
schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
taker = create_taker(wallet_services[-1], schedule, monkeypatch)
active_orders, maker_data = await init_coinjoin(
taker, makers, orderbook, cj_amount)
txdata = await taker.receive_utxos(maker_data)
assert txdata[0], "taker.receive_utxos error"
taker_final_result = await do_tx_signing(
taker, makers, active_orders, txdata)
assert taker_final_result is not False
tx = btc.CMutableTransaction.deserialize(txdata[2])
for i in range(MAKER_NUM):
wallet_service = wallet_services[i]
# TODO as above re: monitoring
await wallet_service.remove_old_utxos(tx)
wallet_service.add_new_utxos(tx)
balances = wallet_service.get_balance_by_mixdepth()
assert balances[0] == cj_amount
assert balances[4] == 4 * 10**8 - cj_amount + cj_fee

85
test/jmclient/test_core_nohistory_sync.py

@ -3,53 +3,66 @@
"""BitcoinCoreNoHistoryInterface functionality tests.""" """BitcoinCoreNoHistoryInterface functionality tests."""
from commontest import create_wallet_for_sync from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
import pytest import pytest
from jmbase import get_log from jmbase import get_log
from jmclient import (load_test_config, SegwitLegacyWallet, from jmclient import (load_test_config, SegwitLegacyWallet,
SegwitWallet, jm_single, BaseWallet) SegwitWallet, jm_single, BaseWallet)
from jmbitcoin import select_chain_params from jmbitcoin import select_chain_params
from commontest import create_wallet_for_sync
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log() log = get_log()
def test_fast_sync_unavailable(setup_sync): @pytest.mark.usefixtures("setup_sync")
wallet_service = create_wallet_for_sync([0, 0, 0, 0, 0], class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
['test_fast_sync_unavailable'])
with pytest.raises(RuntimeError) as e_info: async def test_fast_sync_unavailable(setup_sync):
wallet_service.sync_wallet(fast=True) wallet_service = await create_wallet_for_sync(
[0, 0, 0, 0, 0], ['test_fast_sync_unavailable'])
@pytest.mark.parametrize('internal, wallet_cls', with pytest.raises(RuntimeError) as e_info:
[(BaseWallet.ADDRESS_TYPE_EXTERNAL, SegwitLegacyWallet), await wallet_service.sync_wallet(fast=True)
(BaseWallet.ADDRESS_TYPE_INTERNAL, SegwitLegacyWallet),
(BaseWallet.ADDRESS_TYPE_EXTERNAL, SegwitWallet), @parametrize(
(BaseWallet.ADDRESS_TYPE_INTERNAL, SegwitWallet)]) 'internal, wallet_cls',
def test_sync(setup_sync, internal, wallet_cls): [
used_count = [1, 3, 6, 2, 23] (BaseWallet.ADDRESS_TYPE_EXTERNAL, SegwitLegacyWallet),
wallet_service = create_wallet_for_sync(used_count, ['test_sync'], (BaseWallet.ADDRESS_TYPE_INTERNAL, SegwitLegacyWallet),
populate_internal=internal, wallet_cls=wallet_cls) (BaseWallet.ADDRESS_TYPE_EXTERNAL, SegwitWallet),
##the gap limit should be not zero before sync (BaseWallet.ADDRESS_TYPE_INTERNAL, SegwitWallet),
assert wallet_service.gap_limit > 0 ])
for md in range(len(used_count)): async def test_sync(setup_sync, internal, wallet_cls):
##obtaining an address should be possible without error before sync used_count = [1, 3, 6, 2, 23]
wallet_service.get_new_script(md, internal) wallet_service = await create_wallet_for_sync(
used_count, ['test_sync'],
# TODO bci should probably not store this state globally, populate_internal=internal, wallet_cls=wallet_cls)
# in case syncing is needed for multiple wallets (as in this test): ##the gap limit should be not zero before sync
jm_single().bc_interface.import_addresses_call_count = 0 assert wallet_service.gap_limit > 0
wallet_service.sync_wallet(fast=False) for md in range(len(used_count)):
##obtaining an address should be possible without error before sync
for md in range(len(used_count)): await wallet_service.get_new_script(md, internal)
##plus one to take into account the one new script obtained above
assert used_count[md] + 1 == wallet_service.get_next_unused_index(md, # TODO bci should probably not store this state globally,
internal) # in case syncing is needed for multiple wallets (as in this test):
#gap limit is zero after sync jm_single().bc_interface.import_addresses_call_count = 0
assert wallet_service.gap_limit == 0 await wallet_service.sync_wallet(fast=False)
#obtaining an address leads to an error after sync
with pytest.raises(RuntimeError) as e_info: for md in range(len(used_count)):
wallet_service.get_new_script(0, internal) ##plus one to take into account the one new script obtained above
assert used_count[md] + 1 == wallet_service.get_next_unused_index(
md, internal)
#gap limit is zero after sync
assert wallet_service.gap_limit == 0
#obtaining an address leads to an error after sync
with pytest.raises(RuntimeError) as e_info:
await wallet_service.get_new_script(0, internal)
@pytest.fixture(scope='module') @pytest.fixture(scope='module')

145
test/jmclient/test_maker.py

@ -1,5 +1,10 @@
import datetime import datetime
from unittest import IsolatedAsyncioTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
import jmbitcoin as btc import jmbitcoin as btc
from jmclient import Maker, load_test_config, jm_single, WalletService, VolatileStorage, \ from jmclient import Maker, load_test_config, jm_single, WalletService, VolatileStorage, \
SegwitWalletFidelityBonds, get_network SegwitWalletFidelityBonds, get_network
@ -11,6 +16,7 @@ import struct
import binascii import binascii
from itertools import chain from itertools import chain
import pytest import pytest
from _pytest.monkeypatch import MonkeyPatch
class OfflineMaker(Maker): class OfflineMaker(Maker):
@ -101,91 +107,102 @@ def create_tx_and_offerlist(cj_addr, cj_change_addr, other_output_addrs,
return tx, offerlist return tx, offerlist
def test_verify_unsigned_tx_sw_valid(setup_env_nodeps): class AsyncioTestCase(IsolatedAsyncioTestCase):
jm_single().config.set("POLICY", "segwit", "true")
p2sh_gen = address_p2sh_generator()
p2pkh_gen = address_p2pkh_generator()
wallet = DummyWallet() def setUp(self):
maker = OfflineMaker(WalletService(wallet)) jmclient.configure._get_bc_interface_instance_ = \
jmclient.configure.get_blockchain_interface_instance
monkeypatch = MonkeyPatch()
monkeypatch.setattr(jmclient.configure,
'get_blockchain_interface_instance',
lambda x: DummyBlockchainInterface())
btc.select_chain_params("bitcoin/regtest")
load_test_config()
cj_addr, cj_script = next(p2sh_gen) def tearDown(self):
changeaddr, cj_change_script = next(p2sh_gen) monkeypatch = MonkeyPatch()
monkeypatch.setattr(jmclient.configure,
'get_blockchain_interface_instance',
jmclient.configure._get_bc_interface_instance_)
# test standard cj async def test_verify_unsigned_tx_sw_valid(self):
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, jm_single().config.set("POLICY", "segwit", "true")
[next(p2sh_gen)[0] for s in range(4)])
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "standard sw cj" p2sh_gen = address_p2sh_generator()
p2pkh_gen = address_p2pkh_generator()
# test cj with mixed outputs wallet = DummyWallet()
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, await wallet.async_init(wallet.storage)
list(chain((next(p2sh_gen)[0] for s in range(3)), maker = OfflineMaker(WalletService(wallet))
(next(p2pkh_gen)[0] for s in range(1)))))
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "sw cj with p2pkh output" cj_addr, cj_script = next(p2sh_gen)
changeaddr, cj_change_script = next(p2sh_gen)
# test cj with only p2pkh outputs # test standard cj
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr,
[next(p2pkh_gen)[0] for s in range(4)]) [next(p2sh_gen)[0] for s in range(4)])
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "sw cj with only p2pkh outputs" assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "standard sw cj"
# test cj with mixed outputs
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr,
list(chain((next(p2sh_gen)[0] for s in range(3)),
(next(p2pkh_gen)[0] for s in range(1)))))
def test_verify_unsigned_tx_nonsw_valid(setup_env_nodeps): assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "sw cj with p2pkh output"
jm_single().config.set("POLICY", "segwit", "false")
p2sh_gen = address_p2sh_generator() # test cj with only p2pkh outputs
p2pkh_gen = address_p2pkh_generator() tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr,
[next(p2pkh_gen)[0] for s in range(4)])
wallet = DummyWallet() assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "sw cj with only p2pkh outputs"
maker = OfflineMaker(WalletService(wallet))
cj_addr, cj_script = next(p2pkh_gen) async def test_verify_unsigned_tx_nonsw_valid(self):
changeaddr, cj_change_script = next(p2pkh_gen) jm_single().config.set("POLICY", "segwit", "false")
# test standard cj p2sh_gen = address_p2sh_generator()
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, p2pkh_gen = address_p2pkh_generator()
[next(p2pkh_gen)[0] for s in range(4)], offertype='reloffer')
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "standard nonsw cj" wallet = DummyWallet()
await wallet.async_init(wallet.storage)
maker = OfflineMaker(WalletService(wallet))
# test cj with mixed outputs cj_addr, cj_script = next(p2pkh_gen)
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, changeaddr, cj_change_script = next(p2pkh_gen)
list(chain((next(p2sh_gen)[0] for s in range(1)),
(next(p2pkh_gen)[0] for s in range(3)))), offertype='reloffer')
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with p2sh output" # test standard cj
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr,
[next(p2pkh_gen)[0] for s in range(4)], offertype='reloffer')
# test cj with only p2sh outputs assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "standard nonsw cj"
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr,
[next(p2sh_gen)[0] for s in range(4)], offertype='reloffer')
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with only p2sh outputs" # test cj with mixed outputs
tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr,
list(chain((next(p2sh_gen)[0] for s in range(1)),
(next(p2pkh_gen)[0] for s in range(3)))), offertype='reloffer')
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with p2sh output"
def test_freeze_timelocked_utxos(setup_env_nodeps): # test cj with only p2sh outputs
storage = VolatileStorage() tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr,
SegwitWalletFidelityBonds.initialize(storage, get_network()) [next(p2sh_gen)[0] for s in range(4)], offertype='reloffer')
wallet = SegwitWalletFidelityBonds(storage)
ts = wallet.datetime_to_time_number(
datetime.datetime.strptime("2021-07", "%Y-%m"))
tl_path = wallet.get_path(
wallet.FIDELITY_BOND_MIXDEPTH, wallet.BIP32_TIMELOCK_ID, ts)
tl_script = wallet.get_script_from_path(tl_path)
utxo = (b'a'*32, 0)
wallet.add_utxo(utxo[0], utxo[1], tl_script, 100000000)
assert not wallet._utxos.is_disabled(*utxo)
maker = OfflineMaker(WalletService(wallet)) assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with only p2sh outputs"
maker.freeze_timelocked_utxos()
assert wallet._utxos.is_disabled(*utxo)
async def test_freeze_timelocked_utxos(self):
storage = VolatileStorage()
SegwitWalletFidelityBonds.initialize(storage, get_network())
wallet = SegwitWalletFidelityBonds(storage)
await wallet.async_init(storage)
ts = wallet.datetime_to_time_number(
datetime.datetime.strptime("2021-07", "%Y-%m"))
tl_path = wallet.get_path(
wallet.FIDELITY_BOND_MIXDEPTH, wallet.BIP32_TIMELOCK_ID, ts)
tl_script = await wallet.get_script_from_path(tl_path)
utxo = (b'a'*32, 0)
wallet.add_utxo(utxo[0], utxo[1], tl_script, 100000000)
assert not wallet._utxos.is_disabled(*utxo)
@pytest.fixture maker = OfflineMaker(WalletService(wallet))
def setup_env_nodeps(monkeypatch): await maker.freeze_timelocked_utxos()
monkeypatch.setattr(jmclient.configure, 'get_blockchain_interface_instance', assert wallet._utxos.is_disabled(*utxo)
lambda x: DummyBlockchainInterface())
btc.select_chain_params("bitcoin/regtest")
load_test_config()

109
test/jmclient/test_payjoin.py

@ -6,8 +6,8 @@ Test doing payjoins over tcp client/server
import os import os
import pytest import pytest
from twisted.internet import reactor from twisted.internet import reactor, defer
from twisted.web.server import Site from twisted.web.server import Site, NOT_DONE_YET
from twisted.web.client import readBody from twisted.web.client import readBody
from twisted.web.http_headers import Headers from twisted.web.http_headers import Headers
from twisted.trial import unittest from twisted.trial import unittest
@ -25,7 +25,7 @@ from jmclient import (load_test_config, jm_single,
JMBIP78ReceiverManager) JMBIP78ReceiverManager)
from jmclient.payjoin import make_payjoin_request_params, make_payment_psbt from jmclient.payjoin import make_payjoin_request_params, make_payment_psbt
from jmclient.payjoin import process_payjoin_proposal_from_server from jmclient.payjoin import process_payjoin_proposal_from_server
from commontest import make_wallets from commontest import make_wallets, TrialTestCase
from test_coinjoin import make_wallets_to_list, sync_wallets from test_coinjoin import make_wallets_to_list, sync_wallets
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
@ -47,12 +47,19 @@ class DummyBIP78ReceiverResource(JMHTTPResource):
def render_POST(self, request): def render_POST(self, request):
proposed_tx = request.content proposed_tx = request.content
payment_psbt_base64 = proposed_tx.read().decode("utf-8") payment_psbt_base64 = proposed_tx.read().decode("utf-8")
retval = self.bip78_receiver_manager.receive_proposal_from_sender( d = defer.Deferred.fromCoroutine(
payment_psbt_base64, request.args) self.bip78_receiver_manager.receive_proposal_from_sender(
assert retval[0] payment_psbt_base64, request.args))
content = retval[1].encode("utf-8")
request.setHeader(b"content-length", ("%d" % len(content))) def _delayedRender(result, request):
return content assert result[0]
content = result[1].encode("utf-8")
request.setHeader(b"content-length", ("%d" % len(content)))
request.write(content)
request.finish()
d.addCallback(_delayedRender, request)
return NOT_DONE_YET
class PayjoinTestBase(object): class PayjoinTestBase(object):
""" This tests that a payjoin invoice and """ This tests that a payjoin invoice and
@ -70,18 +77,20 @@ class PayjoinTestBase(object):
jm_single().bc_interface.tick_forward_chain_interval = 5 jm_single().bc_interface.tick_forward_chain_interval = 5
jm_single().bc_interface.simulate_blocks() jm_single().bc_interface.simulate_blocks()
def do_test_payment(self, wc1, wc2, amt=1.1): async def do_test_payment(self, wc1, wc2, amt=1.1):
wallet_structures = [self.wallet_structure] * 2 wallet_structures = [self.wallet_structure] * 2
wallet_cls = (wc1, wc2) wallet_cls = (wc1, wc2)
self.wallet_services = [] self.wallet_services = []
self.wallet_services.append(make_wallets_to_list(make_wallets( wallets = await make_wallets(
1, wallet_structures=[wallet_structures[0]], 1, wallet_structures=[wallet_structures[0]],
mean_amt=self.mean_amt, wallet_cls=wallet_cls[0]))[0]) mean_amt=self.mean_amt, wallet_cls=wallet_cls[0])
self.wallet_services.append(make_wallets_to_list(make_wallets( self.wallet_services.append(make_wallets_to_list(wallets)[0])
wallets = await make_wallets(
1, wallet_structures=[wallet_structures[1]], 1, wallet_structures=[wallet_structures[1]],
mean_amt=self.mean_amt, wallet_cls=wallet_cls[1]))[0]) mean_amt=self.mean_amt, wallet_cls=wallet_cls[1])
self.wallet_services.append(make_wallets_to_list(wallets)[0])
jm_single().bc_interface.tickchain() jm_single().bc_interface.tickchain()
sync_wallets(self.wallet_services) await sync_wallets(self.wallet_services)
# For accounting purposes, record the balances # For accounting purposes, record the balances
# at the start. # at the start.
@ -93,6 +102,7 @@ class PayjoinTestBase(object):
return self.port.stopListening() return self.port.stopListening()
b78rm = JMBIP78ReceiverManager(self.wallet_services[0], 0, b78rm = JMBIP78ReceiverManager(self.wallet_services[0], 0,
self.cj_amount, 47083) self.cj_amount, 47083)
await b78rm.async_init(self.wallet_services[0], 0, self.cj_amount)
resource = DummyBIP78ReceiverResource(jmprint, cbStopListening, b78rm) resource = DummyBIP78ReceiverResource(jmprint, cbStopListening, b78rm)
self.site = Site(resource) self.site = Site(resource)
self.site.displayTracebacks = False self.site.displayTracebacks = False
@ -109,7 +119,7 @@ class PayjoinTestBase(object):
safe=":/") safe=":/")
self.manager = parse_payjoin_setup(bip78_uri, self.wallet_services[1], 0) self.manager = parse_payjoin_setup(bip78_uri, self.wallet_services[1], 0)
self.manager.mode = "testing" self.manager.mode = "testing"
success, msg = make_payment_psbt(self.manager) success, msg = await make_payment_psbt(self.manager)
assert success, msg assert success, msg
params = make_payjoin_request_params(self.manager) params = make_payjoin_request_params(self.manager)
# avoiding backend daemon (testing only jmclient code here), # avoiding backend daemon (testing only jmclient code here),
@ -120,38 +130,52 @@ class PayjoinTestBase(object):
url_parts = list(wrapped_urlparse(serv)) url_parts = list(wrapped_urlparse(serv))
url_parts[4] = urlencode(params).encode("utf-8") url_parts[4] = urlencode(params).encode("utf-8")
destination_url = urlparse.urlunparse(url_parts) destination_url = urlparse.urlunparse(url_parts)
d = agent.request(b"POST", destination_url, response = await agent.request(
Headers({"Content-Type": ["text/plain"]}), b"POST", destination_url,
bodyProducer=body) Headers({"Content-Type": ["text/plain"]}),
d.addCallback(bip78_receiver_response, self.manager) bodyProducer=body)
return d await bip78_receiver_response(response, self.manager)
return response
def tearDown(self): def tearDown(self):
for dc in reactor.getDelayedCalls(): for dc in reactor.getDelayedCalls():
dc.cancel() dc.cancel()
res = final_checks(self.wallet_services, self.cj_amount, d = defer.ensureDeferred(
self.manager.final_psbt.get_fee(), final_checks(self.wallet_services, self.cj_amount,
self.ssb, self.rsb) self.manager.final_psbt.get_fee(),
assert res, "final checks failed" self.ssb, self.rsb))
assert d, "final checks failed"
class TrialTestPayjoin1(PayjoinTestBase, TrialTestCase):
class TrialTestPayjoin1(PayjoinTestBase, unittest.TestCase):
def test_payment(self): def test_payment(self):
return self.do_test_payment(SegwitLegacyWallet, SegwitLegacyWallet) coro = self.do_test_payment(SegwitLegacyWallet, SegwitLegacyWallet)
d = defer.Deferred.fromCoroutine(coro)
return d
class TrialTestPayjoin2(PayjoinTestBase, TrialTestCase):
class TrialTestPayjoin2(PayjoinTestBase, unittest.TestCase):
def test_bech32_payment(self): def test_bech32_payment(self):
return self.do_test_payment(SegwitWallet, SegwitWallet) coro = self.do_test_payment(SegwitWallet, SegwitWallet)
d = defer.Deferred.fromCoroutine(coro)
return d
class TrialTestPayjoin3(PayjoinTestBase, TrialTestCase):
class TrialTestPayjoin3(PayjoinTestBase, unittest.TestCase):
def test_multi_input(self): def test_multi_input(self):
# wallet structure and amt are chosen so that the sender # wallet structure and amt are chosen so that the sender
# will need 3 utxos rather than 1 (to pay 4.5 from 2,2,2). # will need 3 utxos rather than 1 (to pay 4.5 from 2,2,2).
self.wallet_structure = [3, 1, 0, 0, 0] self.wallet_structure = [3, 1, 0, 0, 0]
return self.do_test_payment(SegwitWallet, SegwitWallet, amt=4.5) coro = self.do_test_payment(SegwitWallet, SegwitWallet, amt=4.5)
d = defer.Deferred.fromCoroutine(coro)
return d
class TrialTestPayjoin4(PayjoinTestBase, TrialTestCase):
class TrialTestPayjoin4(PayjoinTestBase, unittest.TestCase):
def reset_fee(self, res): def reset_fee(self, res):
jm_single().config.set("POLICY", "txfees", self.old_txfees) jm_single().config.set("POLICY", "txfees", self.old_txfees)
def test_low_feerate(self): def test_low_feerate(self):
self.old_txfees = jm_single().config.get("POLICY", "tx_fees") self.old_txfees = jm_single().config.get("POLICY", "tx_fees")
# To set such that randomization cannot pull it below minfeerate # To set such that randomization cannot pull it below minfeerate
@ -160,25 +184,27 @@ class TrialTestPayjoin4(PayjoinTestBase, unittest.TestCase):
# as noted in https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/babad1963992965e933924b6c306ad9da89989e0/jmclient/jmclient/payjoin.py#L802-L809 # as noted in https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/babad1963992965e933924b6c306ad9da89989e0/jmclient/jmclient/payjoin.py#L802-L809
# , we increase from that by 2%. # , we increase from that by 2%.
jm_single().config.set("POLICY", "tx_fees", "1404") jm_single().config.set("POLICY", "tx_fees", "1404")
d = self.do_test_payment(SegwitWallet, SegwitWallet) coro = self.do_test_payment(SegwitWallet, SegwitWallet)
d = defer.Deferred.fromCoroutine(coro)
d.addCallback(self.reset_fee) d.addCallback(self.reset_fee)
return d return d
def bip78_receiver_response(response, manager): async def bip78_receiver_response(response, manager):
d = readBody(response) body = await readBody(response)
# if the response code is not 200 OK, we must assume payjoin # if the response code is not 200 OK, we must assume payjoin
# attempt has failed, and revert to standard payment. # attempt has failed, and revert to standard payment.
if int(response.code) != 200: if int(response.code) != 200:
d.addCallback(process_receiver_errormsg, response.code) await process_receiver_errormsg(body, response.code)
return return
d.addCallback(process_receiver_psbt, manager) await process_receiver_psbt(body, manager)
def process_receiver_errormsg(r, c): def process_receiver_errormsg(r, c):
print("Failed: r, c: ", r, c) print("Failed: r, c: ", r, c)
assert False assert False
def process_receiver_psbt(response, manager): async def process_receiver_psbt(response, manager):
process_payjoin_proposal_from_server(response.decode("utf-8"), manager) await process_payjoin_proposal_from_server(
response.decode("utf-8"), manager)
def getbals(wallet_service, mixdepth): def getbals(wallet_service, mixdepth):
""" Retrieves balances for a mixdepth and the 'next' """ Retrieves balances for a mixdepth and the 'next'
@ -186,7 +212,8 @@ def getbals(wallet_service, mixdepth):
bbm = wallet_service.get_balance_by_mixdepth() bbm = wallet_service.get_balance_by_mixdepth()
return (bbm[mixdepth], bbm[(mixdepth + 1) % (wallet_service.mixdepth + 1)]) return (bbm[mixdepth], bbm[(mixdepth + 1) % (wallet_service.mixdepth + 1)])
def final_checks(wallet_services, amount, txfee, ssb, rsb, source_mixdepth=0): async def final_checks(wallet_services, amount, txfee, ssb, rsb,
source_mixdepth=0):
"""We use this to check that the wallet contents are """We use this to check that the wallet contents are
as we've expected according to the test case. as we've expected according to the test case.
amount is the payment amount going from spender to receiver. amount is the payment amount going from spender to receiver.
@ -195,7 +222,7 @@ def final_checks(wallet_services, amount, txfee, ssb, rsb, source_mixdepth=0):
of two entries, source and destination mixdepth respectively. of two entries, source and destination mixdepth respectively.
""" """
jm_single().bc_interface.tickchain() jm_single().bc_interface.tickchain()
sync_wallets(wallet_services) await sync_wallets(wallet_services)
spenderbals = getbals(wallet_services[1], source_mixdepth) spenderbals = getbals(wallet_services[1], source_mixdepth)
receiverbals = getbals(wallet_services[0], source_mixdepth) receiverbals = getbals(wallet_services[0], source_mixdepth)
# is the payment received? # is the payment received?

411
test/jmclient/test_podle.py

@ -1,11 +1,14 @@
#! /usr/bin/env python #! /usr/bin/env python
'''Tests of Proof of discrete log equivalence commitments.''' '''Tests of Proof of discrete log equivalence commitments.'''
import os import os
import jmbitcoin as bitcoin
import struct import struct
import json import json
import pytest import pytest
import copy import copy
from unittest import IsolatedAsyncioTestCase
import jmbitcoin as bitcoin
from jmbase import get_log, bintohex from jmbase import get_log, bintohex
from jmclient import load_test_config, jm_single, generate_podle,\ from jmclient import load_test_config, jm_single, generate_podle,\
generate_podle_error_string, get_commitment_file, PoDLE,\ generate_podle_error_string, get_commitment_file, PoDLE,\
@ -17,31 +20,6 @@ pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log() log = get_log()
def test_commitments_empty(setup_podle):
"""Ensure that empty commitments file
results in {}
"""
assert get_podle_commitments() == ([], {})
def test_commitment_retries(setup_podle):
"""Assumes no external commitments available.
Generate pretend priv/utxo pairs and check that they can be used
taker_utxo_retries times.
"""
allowed = jm_single().config.getint("POLICY", "taker_utxo_retries")
#make some pretend commitments
dummy_priv_utxo_pairs = [(bitcoin.Hash(os.urandom(10)),
bitcoin.b2x(bitcoin.Hash(os.urandom(10)))+":0") for _ in range(10)]
#test a single commitment request of all 10
for x in dummy_priv_utxo_pairs:
p = generate_podle([x], allowed)
assert p
#At this point slot 0 has been taken by all 10.
for i in range(allowed-1):
p = generate_podle(dummy_priv_utxo_pairs[:1], allowed)
assert p
p = generate_podle(dummy_priv_utxo_pairs[:1], allowed)
assert p is None
def generate_single_podle_sig(priv, i): def generate_single_podle_sig(priv, i):
"""Make a podle entry for key priv at index i, using a dummy utxo value. """Make a podle entry for key priv at index i, using a dummy utxo value.
@ -54,184 +32,213 @@ def generate_single_podle_sig(priv, i):
return (r['P'], r['P2'], r['sig'], return (r['P'], r['P2'], r['sig'],
r['e'], r['commit']) r['e'], r['commit'])
def test_rand_commitments(setup_podle):
for i in range(20):
priv = os.urandom(32)+b"\x01"
Pser, P2ser, s, e, commitment = generate_single_podle_sig(priv, 1 + i%5)
assert verify_podle(Pser, P2ser, s, e, commitment)
#tweak commitments to verify failure
tweaked = [x[::-1] for x in [Pser, P2ser, s, e, commitment]]
for i in range(5):
#Check failure on garbling of each parameter
y = [Pser, P2ser, s, e, commitment]
y[i] = tweaked[i]
fail = False
try:
fail = verify_podle(*y)
except:
pass
finally:
assert not fail
def test_nums_verify(setup_podle):
"""Check that the NUMS precomputed values are
valid according to the code; assertion check
implicit.
"""
verify_all_NUMS(True)
def test_external_commitments(setup_podle): class AsyncioTestCase(IsolatedAsyncioTestCase):
"""Add this generated commitment to the external list
{txid:N:{'P':pubkey, 'reveal':{1:{'P2':P2,'s':s,'e':e}, 2:{..},..}}} async def asyncSetUp(self):
Note we do this *after* the sendpayment test so that the external load_test_config()
commitments will not erroneously used (they are fake). if not os.path.exists("cmtdata"):
""" os.mkdir("cmtdata")
#ensure the file exists even if empty self.prev_commits = False
update_commitments() #back up any existing commitments
ecs = {} pcf = get_commitment_file()
tries = jm_single().config.getint("POLICY","taker_utxo_retries") log.debug("Podle file: " + pcf)
for i in range(10): if os.path.exists(pcf):
priv = os.urandom(32) os.rename(pcf, pcf + ".bak")
dummy_utxo = (bitcoin.Hash(priv), 2) self.prev_commits = True
ecs[dummy_utxo] = {}
ecs[dummy_utxo]['reveal']={} async def asyncTearDown(self):
for j in range(tries): pcf = get_commitment_file()
P, P2, s, e, commit = generate_single_podle_sig(priv, j) if self.prev_commits:
if 'P' not in ecs[dummy_utxo]:
ecs[dummy_utxo]['P']=P
ecs[dummy_utxo]['reveal'][j] = {'P2':P2, 's':s, 'e':e}
add_external_commitments(ecs)
used, external = get_podle_commitments()
for u in external:
assert external[u]['P'] == ecs[u]['P']
for i in range(tries):
for x in ['P2', 's', 'e']:
assert external[u]['reveal'][i][x] == ecs[u]['reveal'][i][x]
#add a dummy used commitment, then try again
update_commitments(commitment=b"\xab"*32)
ecs = {}
known_commits = []
known_utxos = []
tries = 3
for i in range(1, 6):
u = (struct.pack(b'B', i)*32, i+3)
known_utxos.append(u)
priv = struct.pack(b'B', i)*32+b"\x01"
ecs[u] = {}
ecs[u]['reveal']={}
for j in range(tries):
P, P2, s, e, commit = generate_single_podle_sig(priv, j)
known_commits.append(commit)
if 'P' not in ecs[u]:
ecs[u]['P'] = P
ecs[u]['reveal'][j] = {'P2':P2, 's':s, 'e':e}
add_external_commitments(ecs)
#simulate most of those external being already used
for c in known_commits[:-1]:
update_commitments(commitment=c)
#this should find the remaining one utxo and return from it
assert generate_podle([], max_tries=tries, allow_external=known_utxos)
#test commitment removal
tru = (struct.pack(b"B", 3)*32, 3+3)
to_remove = {tru: ecs[tru]}
update_commitments(external_to_remove=to_remove)
#test that an incorrectly formatted file raises
with open(get_commitment_file(), "rb") as f:
validjson = json.loads(f.read().decode('utf-8'))
corruptjson = copy.deepcopy(validjson)
del corruptjson['used']
with open(get_commitment_file(), "wb") as f:
f.write(json.dumps(corruptjson, indent=4).encode('utf-8'))
with pytest.raises(PoDLEError) as e_info:
get_podle_commitments()
#clean up
with open(get_commitment_file(), "wb") as f:
f.write(json.dumps(validjson, indent=4).encode('utf-8'))
def test_podle_constructor(setup_podle):
"""Tests rules about construction of PoDLE object
are conformed to.
"""
priv = b"\xaa"*32
#pub and priv together not allowed
with pytest.raises(PoDLEError) as e_info:
p = PoDLE(priv=priv, P="dummypub")
#no pub or priv is allowed, i forget if this is useful for something
p = PoDLE()
#create from priv
p = PoDLE(priv=priv+b"\x01", u=(struct.pack(b"B", 7)*32, 4))
pdict = p.generate_podle(2)
assert all([k in pdict for k in ['used', 'utxo', 'P', 'P2', 'commit', 'sig', 'e']])
#using the valid data, serialize/deserialize test
deser = p.deserialize_revelation(p.serialize_revelation())
assert all([deser[x] == pdict[x] for x in ['utxo', 'P', 'P2', 'sig', 'e']])
#deserialization must fail for wrong number of items
with pytest.raises(PoDLEError) as e_info:
p.deserialize_revelation(':'.join([str(x) for x in range(4)]), separator=':')
#reveal() must work without pre-generated commitment
p.commitment = None
pdict2 = p.reveal()
assert pdict2 == pdict
#corrupt P2, cannot commit:
p.P2 = "blah"
with pytest.raises(PoDLEError) as e_info:
p.get_commitment()
#generation fails without a utxo
p = PoDLE(priv=priv)
with pytest.raises(PoDLEError) as e_info:
p.generate_podle(0)
#Test construction from pubkey
pub = bitcoin.privkey_to_pubkey(priv+b"\x01")
p = PoDLE(P=pub)
with pytest.raises(PoDLEError) as e_info:
p.get_commitment()
with pytest.raises(PoDLEError) as e_info:
p.verify("dummycommitment", range(3))
def test_podle_error_string(setup_podle):
example_utxos = [(b"\x00"*32, i) for i in range(6)]
priv_utxo_pairs = [('fakepriv1', example_utxos[0]),
('fakepriv2', example_utxos[1])]
to = example_utxos[2:4]
ts = example_utxos[4:6]
wallet_service = make_wallets(1, [[1, 0, 0, 0, 0]])[0]['wallet']
cjamt = 100
tua = "3"
tuamtper = "20"
errmgsheader, errmsg = generate_podle_error_string(priv_utxo_pairs,
to,
ts,
wallet_service,
cjamt,
tua,
tuamtper)
assert errmgsheader == ("Failed to source a commitment; this debugging information"
" may help:\n\n")
y = [bintohex(x[0]) for x in example_utxos]
assert all([errmsg.find(x) != -1 for x in y])
#ensure OK with nothing
errmgsheader, errmsg = generate_podle_error_string([], [], [], wallet_service,
cjamt, tua, tuamtper)
@pytest.fixture(scope="module")
def setup_podle(request):
load_test_config()
if not os.path.exists("cmtdata"):
os.mkdir("cmtdata")
prev_commits = False
#back up any existing commitments
pcf = get_commitment_file()
log.debug("Podle file: " + pcf)
if os.path.exists(pcf):
os.rename(pcf, pcf + ".bak")
prev_commits = True
def teardown():
if prev_commits:
os.rename(pcf + ".bak", pcf) os.rename(pcf + ".bak", pcf)
else: else:
if os.path.exists(pcf): if os.path.exists(pcf):
os.remove(pcf) os.remove(pcf)
request.addfinalizer(teardown)
async def test_commitments_empty(self):
"""Ensure that empty commitments file
results in {}
"""
assert get_podle_commitments() == ([], {})
async def test_commitment_retries(self):
"""Assumes no external commitments available.
Generate pretend priv/utxo pairs and check that they can be used
taker_utxo_retries times.
"""
allowed = jm_single().config.getint("POLICY", "taker_utxo_retries")
#make some pretend commitments
dummy_priv_utxo_pairs = [(bitcoin.Hash(os.urandom(10)),
bitcoin.b2x(bitcoin.Hash(os.urandom(10)))+":0") for _ in range(10)]
#test a single commitment request of all 10
for x in dummy_priv_utxo_pairs:
p = generate_podle([x], allowed)
assert p
#At this point slot 0 has been taken by all 10.
for i in range(allowed-1):
p = generate_podle(dummy_priv_utxo_pairs[:1], allowed)
assert p
p = generate_podle(dummy_priv_utxo_pairs[:1], allowed)
assert p is None
async def test_rand_commitments(self):
for i in range(20):
priv = os.urandom(32)+b"\x01"
Pser, P2ser, s, e, commitment = generate_single_podle_sig(priv, 1 + i%5)
assert verify_podle(Pser, P2ser, s, e, commitment)
#tweak commitments to verify failure
tweaked = [x[::-1] for x in [Pser, P2ser, s, e, commitment]]
for i in range(5):
#Check failure on garbling of each parameter
y = [Pser, P2ser, s, e, commitment]
y[i] = tweaked[i]
fail = False
try:
fail = verify_podle(*y)
except:
pass
finally:
assert not fail
async def test_nums_verify(self):
"""Check that the NUMS precomputed values are
valid according to the code; assertion check
implicit.
"""
verify_all_NUMS(True)
async def test_external_commitments(self):
"""Add this generated commitment to the external list
{txid:N:{'P':pubkey, 'reveal':{1:{'P2':P2,'s':s,'e':e}, 2:{..},..}}}
Note we do this *after* the sendpayment test so that the external
commitments will not erroneously used (they are fake).
"""
#ensure the file exists even if empty
update_commitments()
ecs = {}
tries = jm_single().config.getint("POLICY","taker_utxo_retries")
for i in range(10):
priv = os.urandom(32)
dummy_utxo = (bitcoin.Hash(priv), 2)
ecs[dummy_utxo] = {}
ecs[dummy_utxo]['reveal']={}
for j in range(tries):
P, P2, s, e, commit = generate_single_podle_sig(priv, j)
if 'P' not in ecs[dummy_utxo]:
ecs[dummy_utxo]['P']=P
ecs[dummy_utxo]['reveal'][j] = {'P2':P2, 's':s, 'e':e}
add_external_commitments(ecs)
used, external = get_podle_commitments()
for u in external:
assert external[u]['P'] == ecs[u]['P']
for i in range(tries):
for x in ['P2', 's', 'e']:
assert external[u]['reveal'][i][x] == ecs[u]['reveal'][i][x]
#add a dummy used commitment, then try again
update_commitments(commitment=b"\xab"*32)
ecs = {}
known_commits = []
known_utxos = []
tries = 3
for i in range(1, 6):
u = (struct.pack(b'B', i)*32, i+3)
known_utxos.append(u)
priv = struct.pack(b'B', i)*32+b"\x01"
ecs[u] = {}
ecs[u]['reveal']={}
for j in range(tries):
P, P2, s, e, commit = generate_single_podle_sig(priv, j)
known_commits.append(commit)
if 'P' not in ecs[u]:
ecs[u]['P'] = P
ecs[u]['reveal'][j] = {'P2':P2, 's':s, 'e':e}
add_external_commitments(ecs)
#simulate most of those external being already used
for c in known_commits[:-1]:
update_commitments(commitment=c)
#this should find the remaining one utxo and return from it
assert generate_podle([], max_tries=tries, allow_external=known_utxos)
#test commitment removal
tru = (struct.pack(b"B", 3)*32, 3+3)
to_remove = {tru: ecs[tru]}
update_commitments(external_to_remove=to_remove)
#test that an incorrectly formatted file raises
with open(get_commitment_file(), "rb") as f:
validjson = json.loads(f.read().decode('utf-8'))
corruptjson = copy.deepcopy(validjson)
del corruptjson['used']
with open(get_commitment_file(), "wb") as f:
f.write(json.dumps(corruptjson, indent=4).encode('utf-8'))
with pytest.raises(PoDLEError) as e_info:
get_podle_commitments()
#clean up
with open(get_commitment_file(), "wb") as f:
f.write(json.dumps(validjson, indent=4).encode('utf-8'))
async def test_podle_constructor(self):
"""Tests rules about construction of PoDLE object
are conformed to.
"""
priv = b"\xaa"*32
#pub and priv together not allowed
with pytest.raises(PoDLEError) as e_info:
p = PoDLE(priv=priv, P="dummypub")
#no pub or priv is allowed, i forget if this is useful for something
p = PoDLE()
#create from priv
p = PoDLE(priv=priv+b"\x01", u=(struct.pack(b"B", 7)*32, 4))
pdict = p.generate_podle(2)
assert all([k in pdict for k in ['used', 'utxo', 'P', 'P2', 'commit', 'sig', 'e']])
#using the valid data, serialize/deserialize test
deser = p.deserialize_revelation(p.serialize_revelation())
assert all([deser[x] == pdict[x] for x in ['utxo', 'P', 'P2', 'sig', 'e']])
#deserialization must fail for wrong number of items
with pytest.raises(PoDLEError) as e_info:
p.deserialize_revelation(':'.join([str(x) for x in range(4)]), separator=':')
#reveal() must work without pre-generated commitment
p.commitment = None
pdict2 = p.reveal()
assert pdict2 == pdict
#corrupt P2, cannot commit:
p.P2 = "blah"
with pytest.raises(PoDLEError) as e_info:
p.get_commitment()
#generation fails without a utxo
p = PoDLE(priv=priv)
with pytest.raises(PoDLEError) as e_info:
p.generate_podle(0)
#Test construction from pubkey
pub = bitcoin.privkey_to_pubkey(priv+b"\x01")
p = PoDLE(P=pub)
with pytest.raises(PoDLEError) as e_info:
p.get_commitment()
with pytest.raises(PoDLEError) as e_info:
p.verify("dummycommitment", range(3))
async def test_podle_error_string(self):
example_utxos = [(b"\x00"*32, i) for i in range(6)]
priv_utxo_pairs = [('fakepriv1', example_utxos[0]),
('fakepriv2', example_utxos[1])]
to = example_utxos[2:4]
ts = example_utxos[4:6]
wallets = await make_wallets(1, [[1, 0, 0, 0, 0]])
wallet_service = wallets[0]['wallet']
cjamt = 100
tua = "3"
tuamtper = "20"
errmgsheader, errmsg = await generate_podle_error_string(
priv_utxo_pairs,
to,
ts,
wallet_service,
cjamt,
tua,
tuamtper)
assert errmgsheader == ("Failed to source a commitment; this debugging information"
" may help:\n\n")
y = [bintohex(x[0]) for x in example_utxos]
assert all([errmsg.find(x) != -1 for x in y])
#ensure OK with nothing
errmgsheader, errmsg = await generate_podle_error_string(
[], [], [], wallet_service, cjamt, tua, tuamtper)

789
test/jmclient/test_psbt_wallet.py

@ -8,6 +8,13 @@
import copy import copy
import base64 import base64
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
from commontest import make_wallets, dummy_accept_callback, dummy_info_callback from commontest import make_wallets, dummy_accept_callback, dummy_info_callback
import jmbitcoin as bitcoin import jmbitcoin as bitcoin
@ -22,384 +29,15 @@ pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log() log = get_log()
def create_volatile_wallet(seedphrase, wallet_cls=SegwitWallet): async def create_volatile_wallet(seedphrase, wallet_cls=SegwitWallet):
storage = VolatileStorage() storage = VolatileStorage()
wallet_cls.initialize(storage, get_network(), max_mixdepth=4, wallet_cls.initialize(storage, get_network(), max_mixdepth=4,
entropy=wallet_cls.entropy_from_mnemonic(seedphrase)) entropy=wallet_cls.entropy_from_mnemonic(seedphrase))
storage.save() storage.save()
return wallet_cls(storage) wallet = wallet_cls(storage)
await wallet.async_init(storage)
@pytest.mark.parametrize('walletseed, xpub, spktype_wallet, spktype_destn, partial, psbt', [ return wallet
("prosper diamond marriage spy across start shift elevator job lunar edge gallery",
"tpubDChjiEhsafnW2LcmK1C77XiEAgZddi6xZyxjMujBzUqZPTMRwsv3e5vSBYsdiPtCyc6TtoHTCjkxBjtF22tf8Z5ABRdeBUNwHCsqEyzR5wT",
"p2wpkh", "p2sh-p2wpkh", False,
"cHNidP8BAMQCAAAAA7uEliZeXLPfjeUiRBw6e5oZV1DtBrDmLthfDC4oaHQLAAAAAAD/////+X1Exketc4o5b9BPxsj70O+VlGvgiZz0KP1OMRtVLUQAAAAAAP////9r5ylMhQyxbJvCbU8aNE3NOPoXJwUaUZm4H3iT4RnaSwAAAAAA/////wKMz/AIAAAAABepFJwmRAefvZS7VQStD4k52Rn0k71Gh4zP8AgAAAAAFgAUA2shnTVftDXq+ssPwzml2UKdu1QAAAAAAAEBHwDh9QUAAAAAFgAUqw1Ifto4LztwcsxV6q+sQThIdloiBgMDZ5u3RN6Xum+OLkgAzwLFXGWFLwBUraMi7Oin4fYfrwzvYoLxAAAAAAAAAAAAAQEfAOH1BQAAAAAWABQpSCwoeMSghUoVflvtTPiqBPi+5yIGA/tAH4kVpqd3wzidaTNFxtwdpHTydkmB825us2w/3cAVDO9igvEAAAAAAQAAAAABAR8A4fUFAAAAABYAFEqM0KJ5FJ7ak2NL8PDqOPI0I1PaIgYD84aDwOqXKfGvEbre+bpNpuT0uZv6syESzz5PMu4RyLkM72KC8QAAAAACAAAAAAEAF6kUnCZEB5+9lLtVBK0PiTnZGfSTvUaHACICAw8k2gGGcF5sR8yKO5JeAkrkH15rmtCq8sCoDYbywTNzEO9igvEMAAAAIgAAAJQCAAAA"),
("prosper diamond marriage spy across start shift elevator job lunar edge gallery",
"tpubDChjiEhsafnW2LcmK1C77XiEAgZddi6xZyxjMujBzUqZPTMRwsv3e5vSBYsdiPtCyc6TtoHTCjkxBjtF22tf8Z5ABRdeBUNwHCsqEyzR5wT",
"p2wpkh", "p2wpkh", False,
"cHNidP8BAMMCAAAAA7uEliZeXLPfjeUiRBw6e5oZV1DtBrDmLthfDC4oaHQLAAAAAAD/////+X1Exketc4o5b9BPxsj70O+VlGvgiZz0KP1OMRtVLUQAAAAAAP////9r5ylMhQyxbJvCbU8aNE3NOPoXJwUaUZm4H3iT4RnaSwAAAAAA/////wKMz/AIAAAAABYAFBaOTObQIdtCaryiPxaDV5rsGYWUjM/wCAAAAAAWABR9TJm5rcSoIMW7bE1bnj7REL/eygAAAAAAAQEfAOH1BQAAAAAWABSrDUh+2jgvO3ByzFXqr6xBOEh2WiIGAwNnm7dE3pe6b44uSADPAsVcZYUvAFStoyLs6Kfh9h+vDO9igvEAAAAAAAAAAAABAR8A4fUFAAAAABYAFClILCh4xKCFShV+W+1M+KoE+L7nIgYD+0AfiRWmp3fDOJ1pM0XG3B2kdPJ2SYHzbm6zbD/dwBUM72KC8QAAAAABAAAAAAEBHwDh9QUAAAAAFgAUSozQonkUntqTY0vw8Oo48jQjU9oiBgPzhoPA6pcp8a8Rut75uk2m5PS5m/qzIRLPPk8y7hHIuQzvYoLxAAAAAAIAAAAAACICAuqCicVUfcM5IiVSiB/0ZemodybG5Im9Fu8MLorQSE4UEO9igvEMAAAAIgAAAOkAAAAA"),
("prosper diamond marriage spy across start shift elevator job lunar edge gallery",
"tpubDChjiEhsafnW2LcmK1C77XiEAgZddi6xZyxjMujBzUqZPTMRwsv3e5vSBYsdiPtCyc6TtoHTCjkxBjtF22tf8Z5ABRdeBUNwHCsqEyzR5wT",
"p2wpkh", "p2wpkh", True,
"cHNidP8BAMMCAAAAA7uEliZeXLPfjeUiRBw6e5oZV1DtBrDmLthfDC4oaHQLAAAAAAD/////+X1Exketc4o5b9BPxsj70O+VlGvgiZz0KP1OMRtVLUQAAAAAAP////9r5ylMhQyxbJvCbU8aNE3NOPoXJwUaUZm4H3iT4RnaSwAAAAAA/////wKMz/AIAAAAABYAFLH/IL11rTJ3wX1NcmUIsJ/T4j4jjM/wCAAAAAAWABR8GPNb1HUpCz8PKOc8aQXLD1wjcAAAAAAAAQEfAOH1BQAAAAAWABSrDUh+2jgvO3ByzFXqr6xBOEh2WiIGAwNnm7dE3pe6b44uSADPAsVcZYUvAFStoyLs6Kfh9h+vDE5vcGUAAAAAAAAAAAABAR8A4fUFAAAAABYAFClILCh4xKCFShV+W+1M+KoE+L7nIgYD+0AfiRWmp3fDOJ1pM0XG3B2kdPJ2SYHzbm6zbD/dwBUM72KC8QAAAAABAAAAAAEBHwDh9QUAAAAAFgAUSozQonkUntqTY0vw8Oo48jQjU9oiBgPzhoPA6pcp8a8Rut75uk2m5PS5m/qzIRLPPk8y7hHIuQzvYoLxAAAAAAIAAAAAACICAsQ7ZvU9tsbBoSje5rIJQBStlUkQaRCssKylEixre3AYEO9igvEMAAAAIgAAABcCAAAA"),
])
def test_sign_external_psbt(setup_psbt_wallet, walletseed, xpub,
spktype_wallet, spktype_destn, partial, psbt):
bitcoin.select_chain_params("bitcoin")
wallet_cls = SegwitWallet if spktype_wallet == "p2wpkh" else SegwitLegacyWallet
wallet = create_volatile_wallet(walletseed, wallet_cls=wallet_cls)
# if we want to actually sign, our wallet has to recognize the fake utxos
# as being in the wallet, so we inject them:
class DummyUtxoManager(object):
_utxo = {0:{}}
def add_utxo(self, utxo, path, value, height):
self._utxo[0][utxo] = (path, value, height)
wallet._index_cache[0][0] = 1000
wallet._utxos = DummyUtxoManager()
p0, p1, p2 = (wallet.get_path(0, 0, i) for i in range(3))
if not partial:
wallet._utxos.add_utxo(utxostr_to_utxo(
"0b7468282e0c5fd82ee6b006ed5057199a7b3a1c4422e58ddfb35c5e269684bb:0"),
p0, 10000, 1)
wallet._utxos.add_utxo(utxostr_to_utxo(
"442d551b314efd28f49c89e06b9495efd0fbc8c64fd06f398a73ad47c6447df9:0"),
p1, 10000, 1)
wallet._utxos.add_utxo(utxostr_to_utxo(
"4bda19e193781fb899511a052717fa38cd4d341a4f6dc29b6cb10c854c29e76b:0"),
p2, 10000, 1)
signresult_and_signedpsbt, err = wallet.sign_psbt(base64.b64decode(
psbt.encode("ascii")),with_sign_result=True)
assert not err
signresult, signedpsbt = signresult_and_signedpsbt
if partial:
assert not signresult.is_final
assert signresult.num_inputs_signed == 2
assert signresult.num_inputs_final == 2
else:
assert signresult.is_final
assert signresult.num_inputs_signed == 3
assert signresult.num_inputs_final == 3
print(PSBTWalletMixin.human_readable_psbt(signedpsbt))
bitcoin.select_chain_params("bitcoin/regtest")
def test_create_and_sign_psbt_with_legacy(setup_psbt_wallet):
""" The purpose of this test is to check that we can create and
then partially sign a PSBT where we own one input and the other input
is of legacy p2pkh type.
"""
wallet_service = make_wallets(1, [[1,0,0,0,0]], 1)[0]['wallet']
wallet_service.sync_wallet(fast=True)
utxos = wallet_service.select_utxos(0, bitcoin.coins_to_satoshi(0.5))
assert len(utxos) == 1
# create a legacy address and make a payment into it
legacy_addr = bitcoin.CCoinAddress.from_scriptPubKey(
bitcoin.pubkey_to_p2pkh_script(
bitcoin.privkey_to_pubkey(b"\x01"*33)))
tx = direct_send(wallet_service, 0,
[(str(legacy_addr), bitcoin.coins_to_satoshi(0.3))],
accept_callback=dummy_accept_callback,
info_callback=dummy_info_callback,
return_transaction=True)
assert tx
# this time we will have one utxo worth <~ 0.7
my_utxos = wallet_service.select_utxos(0, bitcoin.coins_to_satoshi(0.5))
assert len(my_utxos) == 1
# find the outpoint for the legacy address we're spending
n = -1
for i, t in enumerate(tx.vout):
if bitcoin.CCoinAddress.from_scriptPubKey(t.scriptPubKey) == legacy_addr:
n = i
assert n > -1
utxos = copy.deepcopy(my_utxos)
utxos[(tx.GetTxid()[::-1], n)] ={"script": legacy_addr.to_scriptPubKey(),
"value": bitcoin.coins_to_satoshi(0.3)}
outs = [{"value": bitcoin.coins_to_satoshi(0.998),
"address": wallet_service.get_addr(0,0,0)}]
tx2 = bitcoin.mktx(list(utxos.keys()), outs)
spent_outs = wallet_service.witness_utxos_to_psbt_utxos(my_utxos)
spent_outs.append(tx)
new_psbt = wallet_service.create_psbt_from_tx(tx2, spent_outs,
force_witness_utxo=False)
signed_psbt_and_signresult, err = wallet_service.sign_psbt(
new_psbt.serialize(), with_sign_result=True)
assert err is None
signresult, signed_psbt = signed_psbt_and_signresult
assert signresult.num_inputs_signed == 1
assert signresult.num_inputs_final == 1
assert not signresult.is_final
@pytest.mark.parametrize('unowned_utxo, wallet_cls', [
(True, SegwitLegacyWallet),
(False, SegwitLegacyWallet),
(True, SegwitWallet),
(False, SegwitWallet),
(True, LegacyWallet),
(False, LegacyWallet),
])
def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls):
""" Plan of test:
1. Create a wallet and source 3 destination addresses.
2. Make, and confirm, transactions that fund the 3 addrs.
3. Create a new tx spending 2 of those 3 utxos and spending
another utxo we don't own (extra is optional per `unowned_utxo`).
4. Create a psbt using the above transaction and corresponding
`spent_outs` field to fill in the redeem script.
5. Compare resulting PSBT with expected structure.
6. Use the wallet's sign_psbt method to sign the whole psbt, which
means signing each input we own.
7. Check that each input is finalized as per expected. Check that the whole
PSBT is or is not finalized as per whether there is an unowned utxo.
8. In case where whole psbt is finalized, attempt to broadcast the tx.
"""
# steps 1 and 2:
wallet_service = make_wallets(1, [[3,0,0,0,0]], 1,
wallet_cls=wallet_cls)[0]['wallet']
wallet_service.sync_wallet(fast=True)
utxos = wallet_service.select_utxos(0, bitcoin.coins_to_satoshi(1.5))
# for legacy wallets, psbt creation requires querying for the spending
# transaction:
if wallet_cls == LegacyWallet:
fulltxs = []
for utxo, v in utxos.items():
fulltxs.append(jm_single().bc_interface.get_deser_from_gettransaction(
jm_single().bc_interface.get_transaction(utxo[0])))
assert len(utxos) == 2
u_utxos = {}
if unowned_utxo:
# note: tx creation uses the key only; psbt creation uses the value,
# which can be fake here; we do not intend to attempt to fully
# finalize a psbt with an unowned input. See
# https://github.com/Simplexum/python-bitcointx/issues/30
# the redeem script creation (which is artificial) will be
# avoided in future.
priv = b"\xaa"*32 + b"\x01"
pub = bitcoin.privkey_to_pubkey(priv)
script = bitcoin.pubkey_to_p2sh_p2wpkh_script(pub)
redeem_script = bitcoin.pubkey_to_p2wpkh_script(pub)
u_utxos[(b"\xaa"*32, 12)] = {"value": 1000, "script": script}
utxos.update(u_utxos)
# outputs aren't interesting for this test (we selected 1.5 but will get 2):
outs = [{"value": bitcoin.coins_to_satoshi(1.999),
"address": wallet_service.get_addr(0,0,0)}]
tx = bitcoin.mktx(list(utxos.keys()), outs)
if wallet_cls != LegacyWallet:
spent_outs = wallet_service.witness_utxos_to_psbt_utxos(utxos)
force_witness_utxo=True
else:
spent_outs = fulltxs
# the extra input is segwit:
if unowned_utxo:
spent_outs.extend(
wallet_service.witness_utxos_to_psbt_utxos(u_utxos))
force_witness_utxo=False
newpsbt = wallet_service.create_psbt_from_tx(tx, spent_outs,
force_witness_utxo=force_witness_utxo)
# see note above
if unowned_utxo:
newpsbt.inputs[-1].redeem_script = redeem_script
print(bintohex(newpsbt.serialize()))
print("human readable: ")
print(wallet_service.human_readable_psbt(newpsbt))
# we cannot compare with a fixed expected result due to wallet randomization, but we can
# check psbt structure:
expected_inputs_length = 3 if unowned_utxo else 2
assert len(newpsbt.inputs) == expected_inputs_length
assert len(newpsbt.outputs) == 1
# note: redeem_script field is a CScript which is a bytes instance,
# so checking length is best way to check for existence (comparison
# with None does not work):
if wallet_cls == SegwitLegacyWallet:
assert len(newpsbt.inputs[0].redeem_script) != 0
assert len(newpsbt.inputs[1].redeem_script) != 0
if unowned_utxo:
assert newpsbt.inputs[2].redeem_script == redeem_script
signed_psbt_and_signresult, err = wallet_service.sign_psbt(
newpsbt.serialize(), with_sign_result=True)
assert err is None
signresult, signed_psbt = signed_psbt_and_signresult
expected_signed_inputs = len(utxos) if not unowned_utxo else len(utxos)-1
assert signresult.num_inputs_signed == expected_signed_inputs
assert signresult.num_inputs_final == expected_signed_inputs
if not unowned_utxo:
assert signresult.is_final
# only in case all signed do we try to broadcast:
extracted_tx = signed_psbt.extract_transaction().serialize()
assert jm_single().bc_interface.pushtx(extracted_tx)
else:
# transaction extraction must fail for not-fully-signed psbts:
with pytest.raises(ValueError) as e:
extracted_tx = signed_psbt.extract_transaction()
@pytest.mark.parametrize('payment_amt, wallet_cls_sender, wallet_cls_receiver', [
(0.05, SegwitLegacyWallet, SegwitLegacyWallet),
#(0.95, SegwitLegacyWallet, SegwitWallet),
#(0.05, SegwitWallet, SegwitLegacyWallet),
#(0.95, SegwitWallet, SegwitWallet),
])
def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
wallet_cls_receiver):
""" Workflow step 1:
Create a payment from a wallet, and create a finalized PSBT.
This step is fairly trivial as the functionality is built-in to
PSBTWalletMixin.
Note that only Segwit* wallets are supported for PayJoin.
Workflow step 2:
Receiver creates a new partially signed PSBT with the same amount
and at least one more utxo.
Workflow step 3:
Given a partially signed PSBT created by a receiver, here the sender
completes (co-signs) the PSBT they are given. Note this code is a PSBT
functionality check, and does NOT include the detailed checks that
the sender should perform before agreeing to sign (see:
https://github.com/btcpayserver/btcpayserver-doc/blob/eaac676866a4d871eda5fd7752b91b88fdf849ff/Payjoin-spec.md#receiver-side
).
"""
wallet_r = make_wallets(1, [[3,0,0,0,0]], 1,
wallet_cls=wallet_cls_receiver)[0]["wallet"]
wallet_s = make_wallets(1, [[3,0,0,0,0]], 1,
wallet_cls=wallet_cls_sender)[0]["wallet"]
for w in [wallet_r, wallet_s]:
w.sync_wallet(fast=True)
# destination address for payment:
destaddr = str(bitcoin.CCoinAddress.from_scriptPubKey(
bitcoin.pubkey_to_p2wpkh_script(bitcoin.privkey_to_pubkey(b"\x01"*33))))
payment_amt = bitcoin.coins_to_satoshi(payment_amt)
# *** STEP 1 ***
# **************
# create a normal tx from the sender wallet:
payment_psbt = direct_send(wallet_s, 0,
[(destaddr, payment_amt)],
accept_callback=dummy_accept_callback,
info_callback=dummy_info_callback,
with_final_psbt=True)
print("Initial payment PSBT created:\n{}".format(
wallet_s.human_readable_psbt(payment_psbt)))
# ensure that the payemnt amount is what was intended:
out_amts = [x.nValue for x in payment_psbt.unsigned_tx.vout]
# NOTE this would have to change for more than 2 outputs:
assert any([out_amts[i] == payment_amt for i in [0, 1]])
# ensure that we can actually broadcast the created tx:
# (note that 'extract_transaction' represents an implicit
# PSBT finality check).
extracted_tx = payment_psbt.extract_transaction().serialize()
# don't want to push the tx right now, because of test structure
# (in production code this isn't really needed, we will not
# produce invalid payment transactions).
assert jm_single().bc_interface.testmempoolaccept(bintohex(extracted_tx)),\
"Payment transaction was rejected from mempool."
# *** STEP 2 ***
# **************
# Simple receiver utxo choice heuristic.
# For more generality we test with two receiver-utxos, not one.
all_receiver_utxos = wallet_r.get_all_utxos()
# TODO is there a less verbose way to get any 2 utxos from the dict?
receiver_utxos_keys = list(all_receiver_utxos.keys())[:2]
receiver_utxos = {k: v for k, v in all_receiver_utxos.items(
) if k in receiver_utxos_keys}
# receiver will do other checks as discussed above, including payment
# amount; as discussed above, this is out of the scope of this PSBT test.
# construct unsigned tx for payjoin-psbt:
payjoin_tx_inputs = [(x.prevout.hash[::-1],
x.prevout.n) for x in payment_psbt.unsigned_tx.vin]
payjoin_tx_inputs.extend(receiver_utxos.keys())
# find payment output and change output
pay_out = None
change_out = None
for o in payment_psbt.unsigned_tx.vout:
jm_out_fmt = {"value": o.nValue,
"address": str(bitcoin.CCoinAddress.from_scriptPubKey(
o.scriptPubKey))}
if o.nValue == payment_amt:
assert pay_out is None
pay_out = jm_out_fmt
else:
assert change_out is None
change_out = jm_out_fmt
# we now know there were two outputs and know which is payment.
# bump payment output with our input:
outs = [pay_out, change_out]
our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()])
pay_out["value"] += our_inputs_val
print("we bumped the payment output value by: ", our_inputs_val)
print("It is now: ", pay_out["value"])
unsigned_payjoin_tx = bitcoin.make_shuffled_tx(payjoin_tx_inputs, outs,
version=payment_psbt.unsigned_tx.nVersion,
locktime=payment_psbt.unsigned_tx.nLockTime)
print("we created this unsigned tx: ")
print(bitcoin.human_readable_transaction(unsigned_payjoin_tx))
# to create the PSBT we need the spent_outs for each input,
# in the right order:
spent_outs = []
for i, inp in enumerate(unsigned_payjoin_tx.vin):
input_found = False
for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin):
if inp.prevout == inp2.prevout:
spent_outs.append(payment_psbt.inputs[j].utxo)
input_found = True
break
if input_found:
continue
# if we got here this input is ours, we must find
# it from our original utxo choice list:
for ru in receiver_utxos.keys():
if (inp.prevout.hash[::-1], inp.prevout.n) == ru:
spent_outs.append(
wallet_r.witness_utxos_to_psbt_utxos(
{ru: receiver_utxos[ru]})[0])
input_found = True
break
# there should be no other inputs:
assert input_found
r_payjoin_psbt = wallet_r.create_psbt_from_tx(unsigned_payjoin_tx,
spent_outs=spent_outs)
print("Receiver created payjoin PSBT:\n{}".format(
wallet_r.human_readable_psbt(r_payjoin_psbt)))
signresultandpsbt, err = wallet_r.sign_psbt(r_payjoin_psbt.serialize(),
with_sign_result=True)
assert not err, err
signresult, receiver_signed_psbt = signresultandpsbt
assert signresult.num_inputs_final == len(receiver_utxos)
assert not signresult.is_final
print("Receiver signing successful. Payjoin PSBT is now:\n{}".format(
wallet_r.human_readable_psbt(receiver_signed_psbt)))
# *** STEP 3 ***
# **************
# take the half-signed PSBT, validate and co-sign:
signresultandpsbt, err = wallet_s.sign_psbt(
receiver_signed_psbt.serialize(), with_sign_result=True)
assert not err, err
signresult, sender_signed_psbt = signresultandpsbt
print("Sender's final signed PSBT is:\n{}".format(
wallet_s.human_readable_psbt(sender_signed_psbt)))
assert signresult.is_final
# broadcast the tx
extracted_tx = sender_signed_psbt.extract_transaction().serialize()
assert jm_single().bc_interface.pushtx(extracted_tx)
""" test vector data for human readable parsing only, """ test vector data for human readable parsing only,
they are taken from bitcointx/tests/test_psbt.py and in turn they are taken from bitcointx/tests/test_psbt.py and in turn
@ -430,12 +68,403 @@ hr_test_vectors = {
"proprietary-values": '70736274ff0100550200000001ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff018e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000015fc0a676c6f62616c5f706678016d756c7469706c790563686965660001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb823080ffc06696e5f706678fde80377686174056672616d650afc00fe40420f0061736b077361746f7368690012fc076f75745f706678feffffff01636f726e05746967657217fc076f75745f706678ffffffffffffffffff707570707905647269766500' "proprietary-values": '70736274ff0100550200000001ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff018e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000015fc0a676c6f62616c5f706678016d756c7469706c790563686965660001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb823080ffc06696e5f706678fde80377686174056672616d650afc00fe40420f0061736b077361746f7368690012fc076f75745f706678feffffff01636f726e05746967657217fc076f75745f706678ffffffffffffffffff707570707905647269766500'
} }
def test_hr_psbt(setup_psbt_wallet): @pytest.mark.usefixtures("setup_psbt_wallet")
bitcoin.select_chain_params("bitcoin") class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
for k, v in hr_test_vectors.items():
print(PSBTWalletMixin.human_readable_psbt( @parametrize(
bitcoin.PartiallySignedTransaction.from_binary(hextobin(v)))) 'walletseed, xpub, spktype_wallet, spktype_destn, partial, psbt',
bitcoin.select_chain_params("bitcoin/regtest") [
("prosper diamond marriage spy across start shift elevator job lunar edge gallery",
"tpubDChjiEhsafnW2LcmK1C77XiEAgZddi6xZyxjMujBzUqZPTMRwsv3e5vSBYsdiPtCyc6TtoHTCjkxBjtF22tf8Z5ABRdeBUNwHCsqEyzR5wT",
"p2wpkh", "p2sh-p2wpkh", False,
"cHNidP8BAMQCAAAAA7uEliZeXLPfjeUiRBw6e5oZV1DtBrDmLthfDC4oaHQLAAAAAAD/////+X1Exketc4o5b9BPxsj70O+VlGvgiZz0KP1OMRtVLUQAAAAAAP////9r5ylMhQyxbJvCbU8aNE3NOPoXJwUaUZm4H3iT4RnaSwAAAAAA/////wKMz/AIAAAAABepFJwmRAefvZS7VQStD4k52Rn0k71Gh4zP8AgAAAAAFgAUA2shnTVftDXq+ssPwzml2UKdu1QAAAAAAAEBHwDh9QUAAAAAFgAUqw1Ifto4LztwcsxV6q+sQThIdloiBgMDZ5u3RN6Xum+OLkgAzwLFXGWFLwBUraMi7Oin4fYfrwzvYoLxAAAAAAAAAAAAAQEfAOH1BQAAAAAWABQpSCwoeMSghUoVflvtTPiqBPi+5yIGA/tAH4kVpqd3wzidaTNFxtwdpHTydkmB825us2w/3cAVDO9igvEAAAAAAQAAAAABAR8A4fUFAAAAABYAFEqM0KJ5FJ7ak2NL8PDqOPI0I1PaIgYD84aDwOqXKfGvEbre+bpNpuT0uZv6syESzz5PMu4RyLkM72KC8QAAAAACAAAAAAEAF6kUnCZEB5+9lLtVBK0PiTnZGfSTvUaHACICAw8k2gGGcF5sR8yKO5JeAkrkH15rmtCq8sCoDYbywTNzEO9igvEMAAAAIgAAAJQCAAAA"),
("prosper diamond marriage spy across start shift elevator job lunar edge gallery",
"tpubDChjiEhsafnW2LcmK1C77XiEAgZddi6xZyxjMujBzUqZPTMRwsv3e5vSBYsdiPtCyc6TtoHTCjkxBjtF22tf8Z5ABRdeBUNwHCsqEyzR5wT",
"p2wpkh", "p2wpkh", False,
"cHNidP8BAMMCAAAAA7uEliZeXLPfjeUiRBw6e5oZV1DtBrDmLthfDC4oaHQLAAAAAAD/////+X1Exketc4o5b9BPxsj70O+VlGvgiZz0KP1OMRtVLUQAAAAAAP////9r5ylMhQyxbJvCbU8aNE3NOPoXJwUaUZm4H3iT4RnaSwAAAAAA/////wKMz/AIAAAAABYAFBaOTObQIdtCaryiPxaDV5rsGYWUjM/wCAAAAAAWABR9TJm5rcSoIMW7bE1bnj7REL/eygAAAAAAAQEfAOH1BQAAAAAWABSrDUh+2jgvO3ByzFXqr6xBOEh2WiIGAwNnm7dE3pe6b44uSADPAsVcZYUvAFStoyLs6Kfh9h+vDO9igvEAAAAAAAAAAAABAR8A4fUFAAAAABYAFClILCh4xKCFShV+W+1M+KoE+L7nIgYD+0AfiRWmp3fDOJ1pM0XG3B2kdPJ2SYHzbm6zbD/dwBUM72KC8QAAAAABAAAAAAEBHwDh9QUAAAAAFgAUSozQonkUntqTY0vw8Oo48jQjU9oiBgPzhoPA6pcp8a8Rut75uk2m5PS5m/qzIRLPPk8y7hHIuQzvYoLxAAAAAAIAAAAAACICAuqCicVUfcM5IiVSiB/0ZemodybG5Im9Fu8MLorQSE4UEO9igvEMAAAAIgAAAOkAAAAA"),
("prosper diamond marriage spy across start shift elevator job lunar edge gallery",
"tpubDChjiEhsafnW2LcmK1C77XiEAgZddi6xZyxjMujBzUqZPTMRwsv3e5vSBYsdiPtCyc6TtoHTCjkxBjtF22tf8Z5ABRdeBUNwHCsqEyzR5wT",
"p2wpkh", "p2wpkh", True,
"cHNidP8BAMMCAAAAA7uEliZeXLPfjeUiRBw6e5oZV1DtBrDmLthfDC4oaHQLAAAAAAD/////+X1Exketc4o5b9BPxsj70O+VlGvgiZz0KP1OMRtVLUQAAAAAAP////9r5ylMhQyxbJvCbU8aNE3NOPoXJwUaUZm4H3iT4RnaSwAAAAAA/////wKMz/AIAAAAABYAFLH/IL11rTJ3wX1NcmUIsJ/T4j4jjM/wCAAAAAAWABR8GPNb1HUpCz8PKOc8aQXLD1wjcAAAAAAAAQEfAOH1BQAAAAAWABSrDUh+2jgvO3ByzFXqr6xBOEh2WiIGAwNnm7dE3pe6b44uSADPAsVcZYUvAFStoyLs6Kfh9h+vDE5vcGUAAAAAAAAAAAABAR8A4fUFAAAAABYAFClILCh4xKCFShV+W+1M+KoE+L7nIgYD+0AfiRWmp3fDOJ1pM0XG3B2kdPJ2SYHzbm6zbD/dwBUM72KC8QAAAAABAAAAAAEBHwDh9QUAAAAAFgAUSozQonkUntqTY0vw8Oo48jQjU9oiBgPzhoPA6pcp8a8Rut75uk2m5PS5m/qzIRLPPk8y7hHIuQzvYoLxAAAAAAIAAAAAACICAsQ7ZvU9tsbBoSje5rIJQBStlUkQaRCssKylEixre3AYEO9igvEMAAAAIgAAABcCAAAA"),
])
async def test_sign_external_psbt(self, walletseed, xpub, spktype_wallet,
spktype_destn, partial, psbt):
bitcoin.select_chain_params("bitcoin")
wallet_cls = SegwitWallet if spktype_wallet == "p2wpkh" else SegwitLegacyWallet
wallet = await create_volatile_wallet(
walletseed, wallet_cls=wallet_cls)
# if we want to actually sign, our wallet has to recognize the fake utxos
# as being in the wallet, so we inject them:
class DummyUtxoManager(object):
_utxo = {0:{}}
def add_utxo(self, utxo, path, value, height):
self._utxo[0][utxo] = (path, value, height)
wallet._index_cache[0][0] = 1000
wallet._utxos = DummyUtxoManager()
p0, p1, p2 = (wallet.get_path(0, 0, i) for i in range(3))
if not partial:
wallet._utxos.add_utxo(utxostr_to_utxo(
"0b7468282e0c5fd82ee6b006ed5057199a7b3a1c4422e58ddfb35c5e269684bb:0"),
p0, 10000, 1)
wallet._utxos.add_utxo(utxostr_to_utxo(
"442d551b314efd28f49c89e06b9495efd0fbc8c64fd06f398a73ad47c6447df9:0"),
p1, 10000, 1)
wallet._utxos.add_utxo(utxostr_to_utxo(
"4bda19e193781fb899511a052717fa38cd4d341a4f6dc29b6cb10c854c29e76b:0"),
p2, 10000, 1)
signresult_and_signedpsbt, err = await wallet.sign_psbt(
base64.b64decode(psbt.encode("ascii")), with_sign_result=True)
assert not err
signresult, signedpsbt = signresult_and_signedpsbt
if partial:
assert not signresult.is_final
assert signresult.num_inputs_signed == 2
assert signresult.num_inputs_final == 2
else:
assert signresult.is_final
assert signresult.num_inputs_signed == 3
assert signresult.num_inputs_final == 3
print(PSBTWalletMixin.human_readable_psbt(signedpsbt))
bitcoin.select_chain_params("bitcoin/regtest")
async def test_create_and_sign_psbt_with_legacy(self):
""" The purpose of this test is to check that we can create and
then partially sign a PSBT where we own one input and the other input
is of legacy p2pkh type.
"""
wallets = await make_wallets(1, [[1,0,0,0,0]], 1)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
utxos = await wallet_service.select_utxos(
0, bitcoin.coins_to_satoshi(0.5))
assert len(utxos) == 1
# create a legacy address and make a payment into it
legacy_addr = bitcoin.CCoinAddress.from_scriptPubKey(
bitcoin.pubkey_to_p2pkh_script(
bitcoin.privkey_to_pubkey(b"\x01"*33)))
tx = await direct_send(
wallet_service, 0,
[(str(legacy_addr), bitcoin.coins_to_satoshi(0.3))],
accept_callback=dummy_accept_callback,
info_callback=dummy_info_callback,
return_transaction=True)
assert tx
# this time we will have one utxo worth <~ 0.7
my_utxos = await wallet_service.select_utxos(
0, bitcoin.coins_to_satoshi(0.5))
assert len(my_utxos) == 1
# find the outpoint for the legacy address we're spending
n = -1
for i, t in enumerate(tx.vout):
if bitcoin.CCoinAddress.from_scriptPubKey(t.scriptPubKey) == legacy_addr:
n = i
assert n > -1
utxos = copy.deepcopy(my_utxos)
utxos[(tx.GetTxid()[::-1], n)] ={"script": legacy_addr.to_scriptPubKey(),
"value": bitcoin.coins_to_satoshi(0.3)}
outs = [{"value": bitcoin.coins_to_satoshi(0.998),
"address": await wallet_service.get_addr(0,0,0)}]
tx2 = bitcoin.mktx(list(utxos.keys()), outs)
spent_outs = wallet_service.witness_utxos_to_psbt_utxos(my_utxos)
spent_outs.append(tx)
new_psbt = await wallet_service.create_psbt_from_tx(
tx2, spent_outs, force_witness_utxo=False)
signed_psbt_and_signresult, err = await wallet_service.sign_psbt(
new_psbt.serialize(), with_sign_result=True)
assert err is None
signresult, signed_psbt = signed_psbt_and_signresult
assert signresult.num_inputs_signed == 1
assert signresult.num_inputs_final == 1
assert not signresult.is_final
@parametrize(
'unowned_utxo, wallet_cls',
[
(True, SegwitLegacyWallet),
(False, SegwitLegacyWallet),
(True, SegwitWallet),
(False, SegwitWallet),
(True, LegacyWallet),
(False, LegacyWallet),
])
async def test_create_psbt_and_sign(self, unowned_utxo, wallet_cls):
""" Plan of test:
1. Create a wallet and source 3 destination addresses.
2. Make, and confirm, transactions that fund the 3 addrs.
3. Create a new tx spending 2 of those 3 utxos and spending
another utxo we don't own (extra is optional per `unowned_utxo`).
4. Create a psbt using the above transaction and corresponding
`spent_outs` field to fill in the redeem script.
5. Compare resulting PSBT with expected structure.
6. Use the wallet's sign_psbt method to sign the whole psbt, which
means signing each input we own.
7. Check that each input is finalized as per expected. Check that the whole
PSBT is or is not finalized as per whether there is an unowned utxo.
8. In case where whole psbt is finalized, attempt to broadcast the tx.
"""
# steps 1 and 2:
wallets = await make_wallets(
1, [[3,0,0,0,0]], 1, wallet_cls=wallet_cls)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
utxos = await wallet_service.select_utxos(
0, bitcoin.coins_to_satoshi(1.5))
# for legacy wallets, psbt creation requires querying for the spending
# transaction:
if wallet_cls == LegacyWallet:
fulltxs = []
for utxo, v in utxos.items():
fulltxs.append(jm_single().bc_interface.get_deser_from_gettransaction(
jm_single().bc_interface.get_transaction(utxo[0])))
assert len(utxos) == 2
u_utxos = {}
if unowned_utxo:
# note: tx creation uses the key only; psbt creation uses the value,
# which can be fake here; we do not intend to attempt to fully
# finalize a psbt with an unowned input. See
# https://github.com/Simplexum/python-bitcointx/issues/30
# the redeem script creation (which is artificial) will be
# avoided in future.
priv = b"\xaa"*32 + b"\x01"
pub = bitcoin.privkey_to_pubkey(priv)
script = bitcoin.pubkey_to_p2sh_p2wpkh_script(pub)
redeem_script = bitcoin.pubkey_to_p2wpkh_script(pub)
u_utxos[(b"\xaa"*32, 12)] = {"value": 1000, "script": script}
utxos.update(u_utxos)
# outputs aren't interesting for this test (we selected 1.5 but will get 2):
outs = [{"value": bitcoin.coins_to_satoshi(1.999),
"address": await wallet_service.get_addr(0,0,0)}]
tx = bitcoin.mktx(list(utxos.keys()), outs)
if wallet_cls != LegacyWallet:
spent_outs = wallet_service.witness_utxos_to_psbt_utxos(utxos)
force_witness_utxo=True
else:
spent_outs = fulltxs
# the extra input is segwit:
if unowned_utxo:
spent_outs.extend(
wallet_service.witness_utxos_to_psbt_utxos(u_utxos))
force_witness_utxo=False
newpsbt = await wallet_service.create_psbt_from_tx(
tx, spent_outs, force_witness_utxo=force_witness_utxo)
# see note above
if unowned_utxo:
newpsbt.inputs[-1].redeem_script = redeem_script
print(bintohex(newpsbt.serialize()))
print("human readable: ")
print(wallet_service.human_readable_psbt(newpsbt))
# we cannot compare with a fixed expected result due to wallet randomization, but we can
# check psbt structure:
expected_inputs_length = 3 if unowned_utxo else 2
assert len(newpsbt.inputs) == expected_inputs_length
assert len(newpsbt.outputs) == 1
# note: redeem_script field is a CScript which is a bytes instance,
# so checking length is best way to check for existence (comparison
# with None does not work):
if wallet_cls == SegwitLegacyWallet:
assert len(newpsbt.inputs[0].redeem_script) != 0
assert len(newpsbt.inputs[1].redeem_script) != 0
if unowned_utxo:
assert newpsbt.inputs[2].redeem_script == redeem_script
signed_psbt_and_signresult, err = await wallet_service.sign_psbt(
newpsbt.serialize(), with_sign_result=True)
assert err is None
signresult, signed_psbt = signed_psbt_and_signresult
expected_signed_inputs = len(utxos) if not unowned_utxo else len(utxos)-1
assert signresult.num_inputs_signed == expected_signed_inputs
assert signresult.num_inputs_final == expected_signed_inputs
if not unowned_utxo:
assert signresult.is_final
# only in case all signed do we try to broadcast:
extracted_tx = signed_psbt.extract_transaction().serialize()
assert jm_single().bc_interface.pushtx(extracted_tx)
else:
# transaction extraction must fail for not-fully-signed psbts:
with pytest.raises(ValueError) as e:
extracted_tx = signed_psbt.extract_transaction()
@parametrize(
'payment_amt, wallet_cls_sender, wallet_cls_receiver',
[
(0.05, SegwitLegacyWallet, SegwitLegacyWallet),
#(0.95, SegwitLegacyWallet, SegwitWallet),
#(0.05, SegwitWallet, SegwitLegacyWallet),
#(0.95, SegwitWallet, SegwitWallet),
])
async def test_payjoin_workflow(self, payment_amt, wallet_cls_sender,
wallet_cls_receiver):
""" Workflow step 1:
Create a payment from a wallet, and create a finalized PSBT.
This step is fairly trivial as the functionality is built-in to
PSBTWalletMixin.
Note that only Segwit* wallets are supported for PayJoin.
Workflow step 2:
Receiver creates a new partially signed PSBT with the same amount
and at least one more utxo.
Workflow step 3:
Given a partially signed PSBT created by a receiver, here the sender
completes (co-signs) the PSBT they are given. Note this code is a PSBT
functionality check, and does NOT include the detailed checks that
the sender should perform before agreeing to sign (see:
https://github.com/btcpayserver/btcpayserver-doc/blob/eaac676866a4d871eda5fd7752b91b88fdf849ff/Payjoin-spec.md#receiver-side
).
"""
wallets = await make_wallets(
1, [[3,0,0,0,0]], 1, wallet_cls=wallet_cls_receiver)
wallet_r = wallets[0]["wallet"]
wallets = await make_wallets(
1, [[3,0,0,0,0]], 1, wallet_cls=wallet_cls_sender)
wallet_s = wallets[0]["wallet"]
for w in [wallet_r, wallet_s]:
await w.sync_wallet(fast=True)
# destination address for payment:
destaddr = str(bitcoin.CCoinAddress.from_scriptPubKey(
bitcoin.pubkey_to_p2wpkh_script(bitcoin.privkey_to_pubkey(b"\x01"*33))))
payment_amt = bitcoin.coins_to_satoshi(payment_amt)
# *** STEP 1 ***
# **************
# create a normal tx from the sender wallet:
payment_psbt = await direct_send(
wallet_s, 0,
[(destaddr, payment_amt)],
accept_callback=dummy_accept_callback,
info_callback=dummy_info_callback,
with_final_psbt=True)
print("Initial payment PSBT created:\n{}".format(
wallet_s.human_readable_psbt(payment_psbt)))
# ensure that the payemnt amount is what was intended:
out_amts = [x.nValue for x in payment_psbt.unsigned_tx.vout]
# NOTE this would have to change for more than 2 outputs:
assert any([out_amts[i] == payment_amt for i in [0, 1]])
# ensure that we can actually broadcast the created tx:
# (note that 'extract_transaction' represents an implicit
# PSBT finality check).
extracted_tx = payment_psbt.extract_transaction().serialize()
# don't want to push the tx right now, because of test structure
# (in production code this isn't really needed, we will not
# produce invalid payment transactions).
assert jm_single().bc_interface.testmempoolaccept(bintohex(extracted_tx)),\
"Payment transaction was rejected from mempool."
# *** STEP 2 ***
# **************
# Simple receiver utxo choice heuristic.
# For more generality we test with two receiver-utxos, not one.
all_receiver_utxos = await wallet_r.get_all_utxos()
# TODO is there a less verbose way to get any 2 utxos from the dict?
receiver_utxos_keys = list(all_receiver_utxos.keys())[:2]
receiver_utxos = {k: v for k, v in all_receiver_utxos.items(
) if k in receiver_utxos_keys}
# receiver will do other checks as discussed above, including payment
# amount; as discussed above, this is out of the scope of this PSBT test.
# construct unsigned tx for payjoin-psbt:
payjoin_tx_inputs = [(x.prevout.hash[::-1],
x.prevout.n) for x in payment_psbt.unsigned_tx.vin]
payjoin_tx_inputs.extend(receiver_utxos.keys())
# find payment output and change output
pay_out = None
change_out = None
for o in payment_psbt.unsigned_tx.vout:
jm_out_fmt = {"value": o.nValue,
"address": str(bitcoin.CCoinAddress.from_scriptPubKey(
o.scriptPubKey))}
if o.nValue == payment_amt:
assert pay_out is None
pay_out = jm_out_fmt
else:
assert change_out is None
change_out = jm_out_fmt
# we now know there were two outputs and know which is payment.
# bump payment output with our input:
outs = [pay_out, change_out]
our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()])
pay_out["value"] += our_inputs_val
print("we bumped the payment output value by: ", our_inputs_val)
print("It is now: ", pay_out["value"])
unsigned_payjoin_tx = bitcoin.make_shuffled_tx(payjoin_tx_inputs, outs,
version=payment_psbt.unsigned_tx.nVersion,
locktime=payment_psbt.unsigned_tx.nLockTime)
print("we created this unsigned tx: ")
print(bitcoin.human_readable_transaction(unsigned_payjoin_tx))
# to create the PSBT we need the spent_outs for each input,
# in the right order:
spent_outs = []
for i, inp in enumerate(unsigned_payjoin_tx.vin):
input_found = False
for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin):
if inp.prevout == inp2.prevout:
spent_outs.append(payment_psbt.inputs[j].utxo)
input_found = True
break
if input_found:
continue
# if we got here this input is ours, we must find
# it from our original utxo choice list:
for ru in receiver_utxos.keys():
if (inp.prevout.hash[::-1], inp.prevout.n) == ru:
spent_outs.append(
wallet_r.witness_utxos_to_psbt_utxos(
{ru: receiver_utxos[ru]})[0])
input_found = True
break
# there should be no other inputs:
assert input_found
r_payjoin_psbt = await wallet_r.create_psbt_from_tx(
unsigned_payjoin_tx, spent_outs=spent_outs)
print("Receiver created payjoin PSBT:\n{}".format(
wallet_r.human_readable_psbt(r_payjoin_psbt)))
signresultandpsbt, err = await wallet_r.sign_psbt(
r_payjoin_psbt.serialize(), with_sign_result=True)
assert not err, err
signresult, receiver_signed_psbt = signresultandpsbt
assert signresult.num_inputs_final == len(receiver_utxos)
assert not signresult.is_final
print("Receiver signing successful. Payjoin PSBT is now:\n{}".format(
wallet_r.human_readable_psbt(receiver_signed_psbt)))
# *** STEP 3 ***
# **************
# take the half-signed PSBT, validate and co-sign:
signresultandpsbt, err = await wallet_s.sign_psbt(
receiver_signed_psbt.serialize(), with_sign_result=True)
assert not err, err
signresult, sender_signed_psbt = signresultandpsbt
print("Sender's final signed PSBT is:\n{}".format(
wallet_s.human_readable_psbt(sender_signed_psbt)))
assert signresult.is_final
# broadcast the tx
extracted_tx = sender_signed_psbt.extract_transaction().serialize()
assert jm_single().bc_interface.pushtx(extracted_tx)
async def test_hr_psbt(self):
bitcoin.select_chain_params("bitcoin")
for k, v in hr_test_vectors.items():
print(PSBTWalletMixin.human_readable_psbt(
bitcoin.PartiallySignedTransaction.from_binary(hextobin(v))))
bitcoin.select_chain_params("bitcoin/regtest")
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def setup_psbt_wallet(): def setup_psbt_wallet():

211
test/jmclient/test_snicker.py

@ -3,6 +3,12 @@
wallets as defined in jmclient.wallet.''' wallets as defined in jmclient.wallet.'''
import pytest import pytest
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
from commontest import make_wallets, dummy_accept_callback, dummy_info_callback from commontest import make_wallets, dummy_accept_callback, dummy_info_callback
import jmbitcoin as btc import jmbitcoin as btc
@ -14,105 +20,112 @@ pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log() log = get_log()
@pytest.mark.parametrize(
"nw, wallet_structures, mean_amt, sdev_amt, amt, net_transfer", [ @pytest.mark.usefixtures("setup_snicker")
(2, [[1, 0, 0, 0, 0]] * 2, 4, 0, 20000000, 1000), class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
])
def test_snicker_e2e(setup_snicker, nw, wallet_structures, @parametrize(
mean_amt, sdev_amt, amt, net_transfer): "nw, wallet_structures, mean_amt, sdev_amt, amt, net_transfer",
""" Test strategy: [
1. create two wallets. (2, [[1, 0, 0, 0, 0]] * 2, 4, 0, 20000000, 1000),
2. with wallet 1 (Receiver), create a single transaction ])
tx1, from mixdepth 0 to 1. async def test_snicker_e2e(self, nw, wallet_structures,
3. with wallet 2 (Proposer), take pubkey of all inputs from tx1, and use mean_amt, sdev_amt, amt, net_transfer):
them to create snicker proposals to the non-change out of tx1, """ Test strategy:
in base64 and place in proposals.txt. 1. create two wallets.
4. Receiver polls for proposals in the file manually (instead of twisted 2. with wallet 1 (Receiver), create a single transaction
LoopingCall) and processes them. tx1, from mixdepth 0 to 1.
5. Check for valid final transaction with broadcast. 3. with wallet 2 (Proposer), take pubkey of all inputs from tx1, and use
""" them to create snicker proposals to the non-change out of tx1,
in base64 and place in proposals.txt.
# TODO: Make this test work with native segwit wallets 4. Receiver polls for proposals in the file manually (instead of twisted
wallets = make_wallets(nw, wallet_structures, mean_amt, sdev_amt) LoopingCall) and processes them.
for w in wallets.values(): 5. Check for valid final transaction with broadcast.
w['wallet'].sync_wallet(fast=True) """
print(wallets)
wallet_r = wallets[0]['wallet'] # TODO: Make this test work with native segwit wallets
wallet_p = wallets[1]['wallet'] wallets = await make_wallets(nw, wallet_structures, mean_amt, sdev_amt)
# next, create a tx from the receiver wallet for w in wallets.values():
our_destn_script = wallet_r.get_new_script(1, BaseWallet.ADDRESS_TYPE_INTERNAL) await w['wallet'].sync_wallet(fast=True)
tx = direct_send(wallet_r, 0, print(wallets)
[( wallet_r = wallets[0]['wallet']
wallet_r.script_to_addr(our_destn_script), wallet_p = wallets[1]['wallet']
btc.coins_to_satoshi(0.3) # next, create a tx from the receiver wallet
)], our_destn_script = await wallet_r.get_new_script(
accept_callback=dummy_accept_callback, 1, BaseWallet.ADDRESS_TYPE_INTERNAL)
info_callback=dummy_info_callback, tx = await direct_send(
return_transaction=True) wallet_r, 0,
[(await wallet_r.script_to_addr(our_destn_script),
assert tx, "Failed to spend from receiver wallet" btc.coins_to_satoshi(0.3))],
print("Parent transaction OK. It was: ") accept_callback=dummy_accept_callback,
print(btc.human_readable_transaction(tx)) info_callback=dummy_info_callback,
wallet_r.process_new_tx(tx) return_transaction=True)
# we must identify the receiver's output we're going to use;
# it can be destination or change, that's up to the proposer assert tx, "Failed to spend from receiver wallet"
# to guess successfully; here we'll just choose index 0. print("Parent transaction OK. It was: ")
txid1 = tx.GetTxid()[::-1] print(btc.human_readable_transaction(tx))
txid1_index = 0 await wallet_r.process_new_tx(tx)
# we must identify the receiver's output we're going to use;
receiver_start_bal = sum([x['value'] for x in wallet_r.get_all_utxos( # it can be destination or change, that's up to the proposer
).values()]) # to guess successfully; here we'll just choose index 0.
txid1 = tx.GetTxid()[::-1]
# Now create a proposal for every input index in tx1 txid1_index = 0
# (version 1 proposals mean we source keys from the/an
# ancestor transaction) receiver_start_bal = sum(
propose_keys = [] [x['value'] for x in (await wallet_r.get_all_utxos()).values()])
for i in range(len(tx.vin)):
# todo check access to pubkey # Now create a proposal for every input index in tx1
sig, pub = [a for a in iter(tx.wit.vtxinwit[i].scriptWitness)] # (version 1 proposals mean we source keys from the/an
propose_keys.append(pub) # ancestor transaction)
# the proposer wallet needs to choose a single propose_keys = []
# utxo that is bigger than the output amount of tx1 for i in range(len(tx.vin)):
prop_m_utxos = wallet_p.get_utxos_by_mixdepth()[0] # todo check access to pubkey
prop_utxo = prop_m_utxos[list(prop_m_utxos)[0]] sig, pub = [a for a in iter(tx.wit.vtxinwit[i].scriptWitness)]
# get the private key for that utxo propose_keys.append(pub)
priv = wallet_p.get_key_from_addr( # the proposer wallet needs to choose a single
wallet_p.script_to_addr(prop_utxo['script'])) # utxo that is bigger than the output amount of tx1
prop_input_amt = prop_utxo['value'] prop_m_utxos = (await wallet_p.get_utxos_by_mixdepth())[0]
# construct the arguments for the snicker proposal: prop_utxo = prop_m_utxos[list(prop_m_utxos)[0]]
our_input = list(prop_m_utxos)[0] # should be (txid, index) # get the private key for that utxo
their_input = (txid1, txid1_index) addr = await wallet_p.script_to_addr(prop_utxo['script'])
our_input_utxo = btc.CMutableTxOut(prop_utxo['value'], priv = wallet_p.get_key_from_addr(addr)
prop_utxo['script']) prop_input_amt = prop_utxo['value']
fee_est = estimate_tx_fee(len(tx.vin), 2, # construct the arguments for the snicker proposal:
txtype=wallet_p.get_txtype()) our_input = list(prop_m_utxos)[0] # should be (txid, index)
change_spk = wallet_p.get_new_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL) their_input = (txid1, txid1_index)
our_input_utxo = btc.CMutableTxOut(prop_utxo['value'],
encrypted_proposals = [] prop_utxo['script'])
fee_est = estimate_tx_fee(len(tx.vin), 2,
for p in propose_keys: txtype=wallet_p.get_txtype())
# TODO: this can be a loop over all outputs, change_spk = await wallet_p.get_new_script(
# not just one guessed output, if desired. 0, BaseWallet.ADDRESS_TYPE_INTERNAL)
encrypted_proposals.append(
wallet_p.create_snicker_proposal( encrypted_proposals = []
[our_input], their_input,
[our_input_utxo], for p in propose_keys:
tx.vout[txid1_index], # TODO: this can be a loop over all outputs,
net_transfer, # not just one guessed output, if desired.
fee_est, encrypted_proposals.append(
priv, await wallet_p.create_snicker_proposal(
p, [our_input], their_input,
prop_utxo['script'], [our_input_utxo],
change_spk, tx.vout[txid1_index],
version_byte=1) + b"," + bintohex(p).encode('utf-8')) net_transfer,
sR = SNICKERReceiver(wallet_r) fee_est,
sR.process_proposals([x.decode("utf-8") for x in encrypted_proposals]) priv,
assert len(sR.successful_txs) == 1 p,
wallet_r.process_new_tx(sR.successful_txs[0]) prop_utxo['script'],
end_utxos = wallet_r.get_all_utxos() change_spk,
print("At end the receiver has these utxos: ", end_utxos) version_byte=1) + b"," + bintohex(p).encode('utf-8'))
receiver_end_bal = sum([x['value'] for x in end_utxos.values()]) sR = SNICKERReceiver(wallet_r)
assert receiver_end_bal == receiver_start_bal + net_transfer await sR.process_proposals(
[x.decode("utf-8") for x in encrypted_proposals])
assert len(sR.successful_txs) == 1
await wallet_r.process_new_tx(sR.successful_txs[0])
end_utxos = await wallet_r.get_all_utxos()
print("At end the receiver has these utxos: ", end_utxos)
receiver_end_bal = sum([x['value'] for x in end_utxos.values()])
assert receiver_end_bal == receiver_start_bal + net_transfer
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def setup_snicker(request): def setup_snicker(request):

1023
test/jmclient/test_taker.py

File diff suppressed because it is too large Load Diff

475
test/jmclient/test_tx_creation.py

@ -5,11 +5,16 @@
p2(w)sh tests, these have been removed since Joinmarket p2(w)sh tests, these have been removed since Joinmarket
does not use this feature.''' does not use this feature.'''
import pytest
import struct import struct
from unittest import IsolatedAsyncioTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
from commontest import make_wallets, make_sign_and_push, ensure_bip65_activated from commontest import make_wallets, make_sign_and_push, ensure_bip65_activated
import jmbitcoin as bitcoin import jmbitcoin as bitcoin
import pytest
from jmbase import get_log from jmbase import get_log
from jmclient import load_test_config, jm_single, direct_send, estimate_tx_fee, compute_tx_locktime from jmclient import load_test_config, jm_single, direct_send, estimate_tx_fee, compute_tx_locktime
@ -25,250 +30,266 @@ vpubs = ["03e9a06e539d6bf5cf1ca5c41b59121fa3df07a338322405a312c67b6349a707e9",
"028a2f126e3999ff66d01dcb101ab526d3aa1bf5cbdc4bde14950a4cead95f6fcb", "028a2f126e3999ff66d01dcb101ab526d3aa1bf5cbdc4bde14950a4cead95f6fcb",
"02bea84d70e74f7603746b62d79bf035e16d982b56e6a1ee07dfd3b9130e8a2ad9"] "02bea84d70e74f7603746b62d79bf035e16d982b56e6a1ee07dfd3b9130e8a2ad9"]
def test_all_same_priv(setup_tx_creation):
#recipient
priv = b"\xaa"*32 + b"\x01"
pub = bitcoin.privkey_to_pubkey(priv)
addr = str(bitcoin.CCoinAddress.from_scriptPubKey(
bitcoin.CScript([bitcoin.OP_0, bitcoin.Hash160(pub)])))
wallet_service = make_wallets(1, [[1,0,0,0,0]], 1)[0]['wallet']
#make another utxo on the same address
addrinwallet = wallet_service.get_addr(0,0,0)
jm_single().bc_interface.grab_coins(addrinwallet, 1)
wallet_service.sync_wallet(fast=True)
insfull = wallet_service.select_utxos(0, 110000000)
outs = [{"address": addr, "value": 1000000}]
ins = list(insfull.keys())
tx = bitcoin.mktx(ins, outs)
scripts = {}
for i, j in enumerate(ins):
scripts[i] = (insfull[j]["script"], insfull[j]["value"])
success, msg = wallet_service.sign_tx(tx, scripts)
assert success, msg
def test_verify_tx_input(setup_tx_creation): @pytest.mark.usefixtures("setup_tx_creation")
priv = b"\xaa"*32 + b"\x01" class AsyncioTestCase(IsolatedAsyncioTestCase):
pub = bitcoin.privkey_to_pubkey(priv)
script = bitcoin.pubkey_to_p2sh_p2wpkh_script(pub)
addr = str(bitcoin.CCoinAddress.from_scriptPubKey(script))
wallet_service = make_wallets(1, [[2,0,0,0,0]], 1)[0]['wallet']
wallet_service.sync_wallet(fast=True)
insfull = wallet_service.select_utxos(0, 110000000)
outs = [{"address": addr, "value": 1000000}]
ins = list(insfull.keys())
tx = bitcoin.mktx(ins, outs)
scripts = {0: (insfull[ins[0]]["script"], bitcoin.coins_to_satoshi(1))}
success, msg = wallet_service.sign_tx(tx, scripts)
assert success, msg
# testing Joinmarket's ability to verify transaction inputs
# of others: pretend we don't have a wallet owning the transaction,
# and instead verify an input using the (sig, pub, scriptCode) data
# that is sent by counterparties:
cScrWit = tx.wit.vtxinwit[0].scriptWitness
sig = cScrWit.stack[0]
pub = cScrWit.stack[1]
scriptSig = tx.vin[0].scriptSig
tx2 = bitcoin.mktx(ins, outs)
res = bitcoin.verify_tx_input(tx2, 0, scriptSig,
bitcoin.pubkey_to_p2wpkh_script(pub),
amount = bitcoin.coins_to_satoshi(1),
witness = bitcoin.CScriptWitness([sig, pub]))
assert res
def test_absurd_fees(setup_tx_creation): async def test_all_same_priv(self):
"""Test triggering of ValueError exception #recipient
if the transaction fees calculated from the blockchain priv = b"\xaa"*32 + b"\x01"
interface exceed the limit set in the config. pub = bitcoin.privkey_to_pubkey(priv)
""" addr = str(bitcoin.CCoinAddress.from_scriptPubKey(
jm_single().bc_interface.absurd_fees = True bitcoin.CScript([bitcoin.OP_0, bitcoin.Hash160(pub)])))
#pay into it wallets = await make_wallets(1, [[1,0,0,0,0]], 1)
wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet'] wallet_service = wallets[0]['wallet']
wallet_service.sync_wallet(fast=True) #make another utxo on the same address
amount = 350000000 addrinwallet = await wallet_service.get_addr(0,0,0)
ins_full = wallet_service.select_utxos(0, amount) jm_single().bc_interface.grab_coins(addrinwallet, 1)
with pytest.raises(ValueError) as e_info: await wallet_service.sync_wallet(fast=True)
txid = make_sign_and_push(ins_full, wallet_service, amount, estimate_fee=True) insfull = await wallet_service.select_utxos(0, 110000000)
jm_single().bc_interface.absurd_fees = False outs = [{"address": addr, "value": 1000000}]
ins = list(insfull.keys())
tx = bitcoin.mktx(ins, outs)
scripts = {}
for i, j in enumerate(ins):
scripts[i] = (insfull[j]["script"], insfull[j]["value"])
success, msg = await wallet_service.sign_tx(tx, scripts)
assert success, msg
async def test_verify_tx_input(self):
priv = b"\xaa"*32 + b"\x01"
pub = bitcoin.privkey_to_pubkey(priv)
script = bitcoin.pubkey_to_p2sh_p2wpkh_script(pub)
addr = str(bitcoin.CCoinAddress.from_scriptPubKey(script))
wallets = await make_wallets(1, [[2,0,0,0,0]], 1)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
insfull = await wallet_service.select_utxos(0, 110000000)
outs = [{"address": addr, "value": 1000000}]
ins = list(insfull.keys())
tx = bitcoin.mktx(ins, outs)
scripts = {0: (insfull[ins[0]]["script"], bitcoin.coins_to_satoshi(1))}
success, msg = await wallet_service.sign_tx(tx, scripts)
assert success, msg
# testing Joinmarket's ability to verify transaction inputs
# of others: pretend we don't have a wallet owning the transaction,
# and instead verify an input using the (sig, pub, scriptCode) data
# that is sent by counterparties:
cScrWit = tx.wit.vtxinwit[0].scriptWitness
sig = cScrWit.stack[0]
pub = cScrWit.stack[1]
scriptSig = tx.vin[0].scriptSig
tx2 = bitcoin.mktx(ins, outs)
res = bitcoin.verify_tx_input(tx2, 0, scriptSig,
bitcoin.pubkey_to_p2wpkh_script(pub),
amount = bitcoin.coins_to_satoshi(1),
witness = bitcoin.CScriptWitness([sig, pub]))
assert res
def test_create_sighash_txs(setup_tx_creation): async def test_absurd_fees(self):
#non-standard hash codes: """Test triggering of ValueError exception
for sighash in [bitcoin.SIGHASH_ANYONECANPAY + bitcoin.SIGHASH_SINGLE, if the transaction fees calculated from the blockchain
bitcoin.SIGHASH_NONE, bitcoin.SIGHASH_SINGLE]: interface exceed the limit set in the config.
wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet'] """
wallet_service.sync_wallet(fast=True) jm_single().bc_interface.absurd_fees = True
#pay into it
wallets = await make_wallets(1, [[2, 0, 0, 0, 1]], 3)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
amount = 350000000 amount = 350000000
ins_full = wallet_service.select_utxos(0, amount) ins_full = await wallet_service.select_utxos(0, amount)
txid = make_sign_and_push(ins_full, wallet_service, amount, hashcode=sighash) with pytest.raises(ValueError) as e_info:
assert txid txid = await make_sign_and_push(
ins_full, wallet_service, amount, estimate_fee=True)
jm_single().bc_interface.absurd_fees = False
#trigger insufficient funds async def test_create_sighash_txs(self):
with pytest.raises(Exception) as e_info: #non-standard hash codes:
fake_utxos = wallet_service.select_utxos(4, 1000000000) for sighash in [bitcoin.SIGHASH_ANYONECANPAY + bitcoin.SIGHASH_SINGLE,
bitcoin.SIGHASH_NONE, bitcoin.SIGHASH_SINGLE]:
wallets = await make_wallets(1, [[2, 0, 0, 0, 1]], 3)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
amount = 350000000
ins_full = await wallet_service.select_utxos(0, amount)
txid = await make_sign_and_push(
ins_full, wallet_service, amount, hashcode=sighash)
assert txid
def test_spend_p2wpkh(setup_tx_creation): #trigger insufficient funds
#make 3 p2wpkh outputs from 3 privs with pytest.raises(Exception) as e_info:
privs = [struct.pack(b'B', x) * 32 + b'\x01' for x in range(1, 4)] fake_utxos = await wallet_service.select_utxos(4, 1000000000)
pubs = [bitcoin.privkey_to_pubkey(priv) for priv in privs]
scriptPubKeys = [bitcoin.pubkey_to_p2wpkh_script(pub) for pub in pubs] async def test_spend_p2wpkh(self):
addresses = [str(bitcoin.CCoinAddress.from_scriptPubKey( #make 3 p2wpkh outputs from 3 privs
spk)) for spk in scriptPubKeys] privs = [struct.pack(b'B', x) * 32 + b'\x01' for x in range(1, 4)]
#pay into it pubs = [bitcoin.privkey_to_pubkey(priv) for priv in privs]
wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet'] scriptPubKeys = [bitcoin.pubkey_to_p2wpkh_script(pub) for pub in pubs]
wallet_service.sync_wallet(fast=True) addresses = [str(bitcoin.CCoinAddress.from_scriptPubKey(
amount = 35000000 spk)) for spk in scriptPubKeys]
p2wpkh_ins = [] #pay into it
for i, addr in enumerate(addresses): wallets = await make_wallets(1, [[3, 0, 0, 0, 0]], 3)
ins_full = wallet_service.select_utxos(0, amount) wallet_service = wallets[0]['wallet']
txid = make_sign_and_push(ins_full, wallet_service, amount, output_addr=addr) await wallet_service.sync_wallet(fast=True)
assert txid amount = 35000000
p2wpkh_ins.append((txid, 0)) p2wpkh_ins = []
txhex = jm_single().bc_interface.get_transaction(txid) for i, addr in enumerate(addresses):
#wait for mining ins_full = await wallet_service.select_utxos(0, amount)
jm_single().bc_interface.tick_forward_chain(1) txid = await make_sign_and_push(
#random output address ins_full, wallet_service, amount, output_addr=addr)
output_addr = wallet_service.get_internal_addr(1) assert txid
amount2 = amount*3 - 50000 p2wpkh_ins.append((txid, 0))
outs = [{'value': amount2, 'address': output_addr}] txhex = jm_single().bc_interface.get_transaction(txid)
tx = bitcoin.mktx(p2wpkh_ins, outs) #wait for mining
jm_single().bc_interface.tick_forward_chain(1)
#random output address
output_addr = await wallet_service.get_internal_addr(1)
amount2 = amount*3 - 50000
outs = [{'value': amount2, 'address': output_addr}]
tx = bitcoin.mktx(p2wpkh_ins, outs)
for i, priv in enumerate(privs): for i, priv in enumerate(privs):
# sign each of 3 inputs; note that bitcoin.sign # sign each of 3 inputs; note that bitcoin.sign
# automatically validates each signature it creates. # automatically validates each signature it creates.
sig, msg = bitcoin.sign(tx, i, priv, amount=amount, native="p2wpkh") sig, msg = bitcoin.sign(tx, i, priv, amount=amount, native="p2wpkh")
if not sig: if not sig:
assert False, msg assert False, msg
txid = jm_single().bc_interface.pushtx(tx.serialize()) txid = jm_single().bc_interface.pushtx(tx.serialize())
assert txid assert txid
def test_spend_then_rbf(setup_tx_creation): async def test_spend_then_rbf(self):
""" Test plan: first, create a normal spend with """ Test plan: first, create a normal spend with
rbf enabled in direct_send, then broadcast but rbf enabled in direct_send, then broadcast but
do not mine a block. Then create a re-spend of do not mine a block. Then create a re-spend of
the same utxos with a higher fee and check the same utxos with a higher fee and check
that broadcast succeeds. that broadcast succeeds.
""" """
# First phase: broadcast with RBF enabled. # First phase: broadcast with RBF enabled.
# #
# set a baseline feerate: # set a baseline feerate:
old_feerate = jm_single().config.get("POLICY", "tx_fees") old_feerate = jm_single().config.get("POLICY", "tx_fees")
jm_single().config.set("POLICY", "tx_fees", "20000") jm_single().config.set("POLICY", "tx_fees", "20000")
# set up a single wallet with some coins: # set up a single wallet with some coins:
wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet'] wallets = await make_wallets(1, [[2, 0, 0, 0, 1]], 3)
wallet_service.sync_wallet(fast=True) wallet_service = wallets[0]['wallet']
# ensure selection of two utxos, doesn't really matter await wallet_service.sync_wallet(fast=True)
# but a more general case than only one: # ensure selection of two utxos, doesn't really matter
amount = 350000000 # but a more general case than only one:
# destination doesn't matter; this is easiest: amount = 350000000
destn = wallet_service.get_internal_addr(1) # destination doesn't matter; this is easiest:
# While `direct_send` usually encapsulates utxo selection destn = await wallet_service.get_internal_addr(1)
# for user, here we need to know what was chosen, hence # While `direct_send` usually encapsulates utxo selection
# we return the transaction object, not directly broadcast. # for user, here we need to know what was chosen, hence
tx1 = direct_send(wallet_service, 0, [(destn, amount)], # we return the transaction object, not directly broadcast.
answeryes=True, tx1 = await direct_send(wallet_service, 0, [(destn, amount)],
return_transaction=True) answeryes=True,
assert tx1 return_transaction=True)
# record the utxos for reuse: assert tx1
assert isinstance(tx1, bitcoin.CTransaction) # record the utxos for reuse:
utxos_objs = (x.prevout for x in tx1.vin) assert isinstance(tx1, bitcoin.CTransaction)
utxos = [(x.hash[::-1], x.n) for x in utxos_objs] utxos_objs = (x.prevout for x in tx1.vin)
# in order to sign on those utxos, we need their script and value. utxos = [(x.hash[::-1], x.n) for x in utxos_objs]
scrs = {} # in order to sign on those utxos, we need their script and value.
vals = {} scrs = {}
for u, details in wallet_service.get_utxos_by_mixdepth()[0].items(): vals = {}
if u in utxos: for u, details in (
scrs[u] = details["script"] await wallet_service.get_utxos_by_mixdepth())[0].items():
vals[u] = details["value"] if u in utxos:
assert len(scrs.keys()) == 2 scrs[u] = details["script"]
assert len(vals.keys()) == 2 vals[u] = details["value"]
assert len(scrs.keys()) == 2
assert len(vals.keys()) == 2
# This will go to mempool but not get mined because # This will go to mempool but not get mined because
# we don't call `tick_forward_chain`. # we don't call `tick_forward_chain`.
push_succeed = jm_single().bc_interface.pushtx(tx1.serialize()) push_succeed = jm_single().bc_interface.pushtx(tx1.serialize())
if push_succeed: if push_succeed:
# mimics real operations with transaction monitor: # mimics real operations with transaction monitor:
wallet_service.process_new_tx(tx1) await wallet_service.process_new_tx(tx1)
else: else:
assert False assert False
# Second phase: bump fee. # Second phase: bump fee.
# #
# we set a larger fee rate. # we set a larger fee rate.
jm_single().config.set("POLICY", "tx_fees", "30000") jm_single().config.set("POLICY", "tx_fees", "30000")
# just a different destination to avoid confusion: # just a different destination to avoid confusion:
destn2 = wallet_service.get_internal_addr(2) destn2 = await wallet_service.get_internal_addr(2)
# We reuse *both* utxos so total fees are comparable # We reuse *both* utxos so total fees are comparable
# (modulo tiny 1 byte differences in signatures). # (modulo tiny 1 byte differences in signatures).
# Ordinary wallet operations would remove the first-spent utxos, # Ordinary wallet operations would remove the first-spent utxos,
# so for now we build a PSBT using the code from #921 to select # so for now we build a PSBT using the code from #921 to select
# the same utxos (it could be done other ways). # the same utxos (it could be done other ways).
# Then we broadcast the PSBT and check it is allowed # Then we broadcast the PSBT and check it is allowed
# before constructing the outputs, we need a good fee estimate, # before constructing the outputs, we need a good fee estimate,
# using the bumped feerate: # using the bumped feerate:
fee = estimate_tx_fee(2, 2, wallet_service.get_txtype()) fee = estimate_tx_fee(2, 2, wallet_service.get_txtype())
# reset the feerate: # reset the feerate:
total_input_val = sum(vals.values()) total_input_val = sum(vals.values())
jm_single().config.set("POLICY", "tx_fees", old_feerate) jm_single().config.set("POLICY", "tx_fees", old_feerate)
outs = [{"address": destn2, "value": 1000000}, outs = [{"address": destn2, "value": 1000000},
{"address": wallet_service.get_internal_addr(0), {"address": await wallet_service.get_internal_addr(0),
"value": total_input_val - 1000000 - fee}] "value": total_input_val - 1000000 - fee}]
tx2 = bitcoin.mktx(utxos, outs, version=2, tx2 = bitcoin.mktx(utxos, outs, version=2,
locktime=compute_tx_locktime()) locktime=compute_tx_locktime())
spent_outs = [] spent_outs = []
for u in utxos: for u in utxos:
spent_outs.append(bitcoin.CTxOut(nValue=vals[u], spent_outs.append(bitcoin.CTxOut(nValue=vals[u],
scriptPubKey=scrs[u])) scriptPubKey=scrs[u]))
psbt_unsigned = wallet_service.create_psbt_from_tx(tx2, psbt_unsigned = await wallet_service.create_psbt_from_tx(
spent_outs=spent_outs) tx2, spent_outs=spent_outs)
signresultandpsbt, err = wallet_service.sign_psbt( signresultandpsbt, err = await wallet_service.sign_psbt(
psbt_unsigned.serialize(), with_sign_result=True) psbt_unsigned.serialize(), with_sign_result=True)
assert not err assert not err
signresult, psbt_signed = signresultandpsbt signresult, psbt_signed = signresultandpsbt
tx2_signed = psbt_signed.extract_transaction() tx2_signed = psbt_signed.extract_transaction()
# the following assertion is sufficient, because # the following assertion is sufficient, because
# tx broadcast would fail if the replacement were # tx broadcast would fail if the replacement were
# not allowed by Core: # not allowed by Core:
assert jm_single().bc_interface.pushtx(tx2_signed.serialize()) assert jm_single().bc_interface.pushtx(tx2_signed.serialize())
def test_spend_freeze_script(setup_tx_creation): async def test_spend_freeze_script(self):
ensure_bip65_activated() ensure_bip65_activated()
wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet'] wallets = await make_wallets(1, [[3, 0, 0, 0, 0]], 3)
wallet_service.sync_wallet(fast=True) wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
mediantime = jm_single().bc_interface.get_best_block_median_time() mediantime = jm_single().bc_interface.get_best_block_median_time()
timeoffset_success_tests = [(2, False), (-60*60*24*30, True), (60*60*24*30, False)] timeoffset_success_tests = [(2, False), (-60*60*24*30, True), (60*60*24*30, False)]
for timeoffset, required_success in timeoffset_success_tests: for timeoffset, required_success in timeoffset_success_tests:
#generate keypair #generate keypair
priv = b"\xaa"*32 + b"\x01" priv = b"\xaa"*32 + b"\x01"
pub = bitcoin.privkey_to_pubkey(priv) pub = bitcoin.privkey_to_pubkey(priv)
addr_locktime = mediantime + timeoffset addr_locktime = mediantime + timeoffset
redeem_script = bitcoin.mk_freeze_script(pub, addr_locktime) redeem_script = bitcoin.mk_freeze_script(pub, addr_locktime)
script_pub_key = bitcoin.redeem_script_to_p2wsh_script(redeem_script) script_pub_key = bitcoin.redeem_script_to_p2wsh_script(redeem_script)
# cannot convert to address within wallet service, as not known # cannot convert to address within wallet service, as not known
# to wallet; use engine directly: # to wallet; use engine directly:
addr = wallet_service._ENGINE.script_to_address(script_pub_key) addr = wallet_service._ENGINE.script_to_address(script_pub_key)
#fund frozen funds address #fund frozen funds address
amount = 100000000 amount = 100000000
funding_ins_full = wallet_service.select_utxos(0, amount) funding_ins_full = await wallet_service.select_utxos(0, amount)
funding_txid = make_sign_and_push(funding_ins_full, wallet_service, amount, output_addr=addr) funding_txid = await make_sign_and_push(
assert funding_txid funding_ins_full, wallet_service, amount, output_addr=addr)
assert funding_txid
#spend frozen funds #spend frozen funds
frozen_in = (funding_txid, 0) frozen_in = (funding_txid, 0)
output_addr = wallet_service.get_internal_addr(1) output_addr = await wallet_service.get_internal_addr(1)
miner_fee = 5000 miner_fee = 5000
outs = [{'value': amount - miner_fee, 'address': output_addr}] outs = [{'value': amount - miner_fee, 'address': output_addr}]
tx = bitcoin.mktx([frozen_in], outs, locktime=addr_locktime+1) tx = bitcoin.mktx([frozen_in], outs, locktime=addr_locktime+1)
i = 0 i = 0
sig, success = bitcoin.sign(tx, i, priv, amount=amount, sig, success = bitcoin.sign(tx, i, priv, amount=amount,
native=redeem_script) native=redeem_script)
assert success assert success
push_success = jm_single().bc_interface.pushtx(tx.serialize()) push_success = jm_single().bc_interface.pushtx(tx.serialize())
assert push_success == required_success assert push_success == required_success
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def setup_tx_creation(): def setup_tx_creation():

232
test/jmclient/test_utxomanager.py

@ -1,8 +1,13 @@
from jmclient.wallet import UTXOManager
from test_storage import MockStorage from test_storage import MockStorage
import pytest import pytest
from _pytest.monkeypatch import MonkeyPatch
from unittest import IsolatedAsyncioTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
from jmclient.wallet import UTXOManager
from jmclient import load_test_config from jmclient import load_test_config
import jmclient import jmclient
from commontest import DummyBlockchainInterface from commontest import DummyBlockchainInterface
@ -12,111 +17,120 @@ def select(unspent, value):
return unspent return unspent
def test_utxomanager_persist(setup_env_nodeps): class AsyncioTestCase(IsolatedAsyncioTestCase):
""" Tests that the utxo manager's data is correctly
persisted and can be recreated from storage. def setUp(self):
This persistence is currently only used for metadata jmclient.configure._get_bc_interface_instance_ = \
(specifically, disabling coins for coin control). jmclient.configure.get_blockchain_interface_instance
""" monkeypatch = MonkeyPatch()
monkeypatch.setattr(jmclient.configure,
storage = MockStorage(None, 'wallet.jmdat', None, create=True) 'get_blockchain_interface_instance',
UTXOManager.initialize(storage) lambda x: DummyBlockchainInterface())
um = UTXOManager(storage, select) load_test_config()
txid = b'\x00' * UTXOManager.TXID_LEN def tearDown(self):
index = 0 monkeypatch = MonkeyPatch()
path = (0,) monkeypatch.setattr(jmclient.configure,
mixdepth = 0 'get_blockchain_interface_instance',
value = 500 jmclient.configure._get_bc_interface_instance_)
um.add_utxo(txid, index, path, value, mixdepth, 1) async def test_utxomanager_persist(self):
um.add_utxo(txid, index+1, path, value, mixdepth+1, 2) """ Tests that the utxo manager's data is correctly
# the third utxo will be disabled and we'll check if persisted and can be recreated from storage.
# the disablement persists in the storage across UM instances This persistence is currently only used for metadata
um.add_utxo(txid, index+2, path, value, mixdepth+1, 3) (specifically, disabling coins for coin control).
um.disable_utxo(txid, index+2) """
um.save()
storage = MockStorage(None, 'wallet.jmdat', None, create=True)
# Remove and recreate the UM from the same storage. UTXOManager.initialize(storage)
um = UTXOManager(storage, select)
del um
txid = b'\x00' * UTXOManager.TXID_LEN
um = UTXOManager(storage, select) index = 0
path = (0,)
assert um.have_utxo(txid, index) == mixdepth mixdepth = 0
assert um.have_utxo(txid, index+1) == mixdepth + 1 value = 500
# The third should not be registered as present given flag:
assert um.have_utxo(txid, index+2, include_disabled=False) == False um.add_utxo(txid, index, path, value, mixdepth, 1)
# check is_disabled works: um.add_utxo(txid, index+1, path, value, mixdepth+1, 2)
assert not um.is_disabled(txid, index) # the third utxo will be disabled and we'll check if
assert not um.is_disabled(txid, index+1) # the disablement persists in the storage across UM instances
assert um.is_disabled(txid, index+2) um.add_utxo(txid, index+2, path, value, mixdepth+1, 3)
# check re-enabling works um.disable_utxo(txid, index+2)
um.enable_utxo(txid, index+2) um.save()
assert not um.is_disabled(txid, index+2)
um.disable_utxo(txid, index+2) # Remove and recreate the UM from the same storage.
assert len(um.get_utxos_at_mixdepth(mixdepth)) == 1 del um
assert len(um.get_utxos_at_mixdepth(mixdepth+1)) == 2
assert len(um.get_utxos_at_mixdepth(mixdepth+2)) == 0 um = UTXOManager(storage, select)
assert um.get_balance_at_mixdepth(mixdepth) == value assert um.have_utxo(txid, index) == mixdepth
assert um.get_balance_at_mixdepth(mixdepth+1) == value * 2 assert um.have_utxo(txid, index+1) == mixdepth + 1
# The third should not be registered as present given flag:
um.remove_utxo(txid, index, mixdepth) assert um.have_utxo(txid, index+2, include_disabled=False) == False
assert um.have_utxo(txid, index) == False # check is_disabled works:
# check that removing a utxo does not remove the metadata assert not um.is_disabled(txid, index)
um.remove_utxo(txid, index+2, mixdepth+1) assert not um.is_disabled(txid, index+1)
assert um.is_disabled(txid, index+2) assert um.is_disabled(txid, index+2)
# check re-enabling works
um.save() um.enable_utxo(txid, index+2)
del um assert not um.is_disabled(txid, index+2)
um.disable_utxo(txid, index+2)
um = UTXOManager(storage, select)
assert len(await um.get_utxos_at_mixdepth(mixdepth)) == 1
assert um.have_utxo(txid, index) == False assert len(await um.get_utxos_at_mixdepth(mixdepth+1)) == 2
assert um.have_utxo(txid, index+1) == mixdepth + 1 assert len(await um.get_utxos_at_mixdepth(mixdepth+2)) == 0
assert len(um.get_utxos_at_mixdepth(mixdepth)) == 0 assert um.get_balance_at_mixdepth(mixdepth) == value
assert len(um.get_utxos_at_mixdepth(mixdepth+1)) == 1 assert um.get_balance_at_mixdepth(mixdepth+1) == value * 2
assert um.get_balance_at_mixdepth(mixdepth) == 0 um.remove_utxo(txid, index, mixdepth)
assert um.get_balance_at_mixdepth(mixdepth+1) == value assert um.have_utxo(txid, index) == False
assert um.get_balance_at_mixdepth(mixdepth+2) == 0 # check that removing a utxo does not remove the metadata
um.remove_utxo(txid, index+2, mixdepth+1)
assert um.is_disabled(txid, index+2)
def test_utxomanager_select(setup_env_nodeps):
storage = MockStorage(None, 'wallet.jmdat', None, create=True) um.save()
UTXOManager.initialize(storage) del um
um = UTXOManager(storage, select)
um = UTXOManager(storage, select)
txid = b'\x00' * UTXOManager.TXID_LEN
index = 0 assert um.have_utxo(txid, index) == False
path = (0,) assert um.have_utxo(txid, index+1) == mixdepth + 1
mixdepth = 0
value = 500 assert len(await um.get_utxos_at_mixdepth(mixdepth)) == 0
assert len(await um.get_utxos_at_mixdepth(mixdepth+1)) == 1
um.add_utxo(txid, index, path, value, mixdepth, 100)
assert um.get_balance_at_mixdepth(mixdepth) == 0
assert len(um.select_utxos(mixdepth, value)) == 1 assert um.get_balance_at_mixdepth(mixdepth+1) == value
assert len(um.select_utxos(mixdepth+1, value)) == 0 assert um.get_balance_at_mixdepth(mixdepth+2) == 0
um.add_utxo(txid, index+1, path, value, mixdepth, None) async def test_utxomanager_select(self):
assert len(um.select_utxos(mixdepth, value)) == 2 storage = MockStorage(None, 'wallet.jmdat', None, create=True)
UTXOManager.initialize(storage)
# ensure that added utxos that are disabled do not um = UTXOManager(storage, select)
# get used by the selector
um.add_utxo(txid, index+2, path, value, mixdepth, 101) txid = b'\x00' * UTXOManager.TXID_LEN
um.disable_utxo(txid, index+2) index = 0
assert len(um.select_utxos(mixdepth, value)) == 2 path = (0,)
mixdepth = 0
# ensure that unconfirmed coins are not selected if value = 500
# dis-requested:
assert len(um.select_utxos(mixdepth, value, maxheight=105)) == 1 um.add_utxo(txid, index, path, value, mixdepth, 100)
assert len(await um.select_utxos(mixdepth, value)) == 1
@pytest.fixture assert len(await um.select_utxos(mixdepth+1, value)) == 0
def setup_env_nodeps(monkeypatch):
monkeypatch.setattr(jmclient.configure, 'get_blockchain_interface_instance', um.add_utxo(txid, index+1, path, value, mixdepth, None)
lambda x: DummyBlockchainInterface()) assert len(await um.select_utxos(mixdepth, value)) == 2
load_test_config()
# ensure that added utxos that are disabled do not
# get used by the selector
um.add_utxo(txid, index+2, path, value, mixdepth, 101)
um.disable_utxo(txid, index+2)
assert len(await um.select_utxos(mixdepth, value)) == 2
# ensure that unconfirmed coins are not selected if
# dis-requested:
assert len(await um.select_utxos(mixdepth, value, maxheight=105)) == 1

2154
test/jmclient/test_wallet.py

File diff suppressed because it is too large Load Diff

29
test/jmclient/test_wallet_rpc.py

@ -4,11 +4,13 @@ import functools
import json import json
import os import os
import jmclient # install asyncioreactor
from twisted.internet import reactor
import jwt import jwt
import pytest import pytest
from twisted.internet import reactor, defer, task from twisted.internet import defer, task
from twisted.web.client import readBody, Headers from twisted.web.client import readBody, Headers
from twisted.trial import unittest
from autobahn.twisted.websocket import WebSocketClientFactory, \ from autobahn.twisted.websocket import WebSocketClientFactory, \
connectWS connectWS
@ -26,7 +28,7 @@ from jmclient import (
storage, storage,
) )
from jmclient.wallet_rpc import api_version_string, CJ_MAKER_RUNNING, CJ_NOT_RUNNING from jmclient.wallet_rpc import api_version_string, CJ_MAKER_RUNNING, CJ_NOT_RUNNING
from commontest import make_wallets from commontest import make_wallets, TrialAsyncioTestCase
from test_coinjoin import make_wallets_to_list, sync_wallets from test_coinjoin import make_wallets_to_list, sync_wallets
from test_websocket import ClientTProtocol, test_tx_hex_1, test_tx_hex_txid from test_websocket import ClientTProtocol, test_tx_hex_1, test_tx_hex_txid
@ -45,7 +47,8 @@ class JMWalletDaemonT(JMWalletDaemon):
return True return True
return super().check_cookie(request, *args, **kwargs) return super().check_cookie(request, *args, **kwargs)
class WalletRPCTestBase(object):
class WalletRPCTestBase(TrialAsyncioTestCase):
""" Base class for set up of tests of the """ Base class for set up of tests of the
Wallet RPC calls using the wallet_rpc.JMWalletDaemon service. Wallet RPC calls using the wallet_rpc.JMWalletDaemon service.
""" """
@ -62,7 +65,7 @@ class WalletRPCTestBase(object):
# wallet type # wallet type
wallet_cls = SegwitWallet wallet_cls = SegwitWallet
def setUp(self): async def asyncSetUp(self):
load_test_config() load_test_config()
self.clean_out_wallet_files() self.clean_out_wallet_files()
jm_single().bc_interface.tick_forward_chain_interval = 5 jm_single().bc_interface.tick_forward_chain_interval = 5
@ -94,11 +97,12 @@ class WalletRPCTestBase(object):
self.listener_rpc = r self.listener_rpc = r
self.listener_ws = s self.listener_ws = s
wallet_structures = [self.wallet_structure] * 2 wallet_structures = [self.wallet_structure] * 2
self.daemon.services["wallet"] = make_wallets_to_list(make_wallets( wallets = await make_wallets(
1, wallet_structures=[wallet_structures[0]], 1, wallet_structures=[wallet_structures[0]],
mean_amt=self.mean_amt, wallet_cls=self.wallet_cls))[0] mean_amt=self.mean_amt, wallet_cls=self.wallet_cls)
self.daemon.services["wallet"] = make_wallets_to_list(wallets)[0]
jm_single().bc_interface.tickchain() jm_single().bc_interface.tickchain()
sync_wallets([self.daemon.services["wallet"]]) await sync_wallets([self.daemon.services["wallet"]])
# dummy tx example to force a notification event: # dummy tx example to force a notification event:
self.test_tx = CTransaction.deserialize(hextobin(test_tx_hex_1)) self.test_tx = CTransaction.deserialize(hextobin(test_tx_hex_1))
# auth token is not set at the start # auth token is not set at the start
@ -168,6 +172,7 @@ class WalletRPCTestBase(object):
def tearDown(self): def tearDown(self):
self.clean_out_wallet_files() self.clean_out_wallet_files()
reactor.disconnectAll()
for dc in reactor.getDelayedCalls(): for dc in reactor.getDelayedCalls():
if not dc.cancelled: if not dc.cancelled:
dc.cancel() dc.cancel()
@ -198,7 +203,7 @@ class ClientNotifTestFactory(WebSocketClientFactory):
self.callbackfn = kwargs.pop("callbackfn", None) self.callbackfn = kwargs.pop("callbackfn", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class TrialTestWRPC_WS(WalletRPCTestBase, unittest.TestCase): class TrialTestWRPC_WS(WalletRPCTestBase):
""" class for testing websocket subscriptions/events etc. """ class for testing websocket subscriptions/events etc.
""" """
@ -240,7 +245,7 @@ class TrialTestWRPC_WS(WalletRPCTestBase, unittest.TestCase):
self.daemon.wss_factory.sendTxNotification(self.test_tx, self.daemon.wss_factory.sendTxNotification(self.test_tx,
test_tx_hex_txid) test_tx_hex_txid)
class TrialTestWRPC_FB(WalletRPCTestBaseFB, unittest.TestCase): class TrialTestWRPC_FB(WalletRPCTestBaseFB):
@defer.inlineCallbacks @defer.inlineCallbacks
def test_gettimelockaddress(self): def test_gettimelockaddress(self):
self.daemon.auth_disabled = True self.daemon.auth_disabled = True
@ -294,7 +299,7 @@ class TrialTestWRPC_FB(WalletRPCTestBaseFB, unittest.TestCase):
# be MAKER_RUNNING since no non-TL-type coin existed: # be MAKER_RUNNING since no non-TL-type coin existed:
assert self.daemon.coinjoin_state == CJ_NOT_RUNNING assert self.daemon.coinjoin_state == CJ_NOT_RUNNING
class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase): class TrialTestWRPC_DisplayWallet(WalletRPCTestBase):
@defer.inlineCallbacks @defer.inlineCallbacks
def do_session_request(self, agent, addr, handler=None, token=None): def do_session_request(self, agent, addr, handler=None, token=None):
@ -749,7 +754,7 @@ class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase):
assert json_body["seedphrase"] assert json_body["seedphrase"]
class TrialTestWRPC_JWT(WalletRPCTestBase, unittest.TestCase): class TrialTestWRPC_JWT(WalletRPCTestBase):
@defer.inlineCallbacks @defer.inlineCallbacks
def do_request(self, agent, method, addr, body, handler, token): def do_request(self, agent, method, addr, body, handler, token):
headers = Headers({"Authorization": ["Bearer " + token]}) headers = Headers({"Authorization": ["Bearer " + token]})

161
test/jmclient/test_wallets.py

@ -7,6 +7,8 @@ import binascii
from commontest import create_wallet_for_sync, make_sign_and_push from commontest import create_wallet_for_sync, make_sign_and_push
import json import json
from unittest import IsolatedAsyncioTestCase
import pytest import pytest
from jmbase import get_log, hextobin from jmbase import get_log, hextobin
from jmclient import ( from jmclient import (
@ -20,78 +22,23 @@ testdir = os.path.dirname(os.path.realpath(__file__))
log = get_log() log = get_log()
def do_tx(wallet_service, amount): async def do_tx(wallet_service, amount):
ins_full = wallet_service.select_utxos(0, amount) ins_full = await wallet_service.select_utxos(0, amount)
cj_addr = wallet_service.get_internal_addr(1) cj_addr = await wallet_service.get_internal_addr(1)
change_addr = wallet_service.get_internal_addr(0) change_addr = await wallet_service.get_internal_addr(0)
wallet_service.save_wallet() wallet_service.save_wallet()
txid = make_sign_and_push(ins_full, txid = await make_sign_and_push(ins_full,
wallet_service, wallet_service,
amount, amount,
output_addr=cj_addr, output_addr=cj_addr,
change_addr=change_addr, change_addr=change_addr,
estimate_fee=True) estimate_fee=True)
assert txid assert txid
time.sleep(2) #blocks time.sleep(2) #blocks
wallet_service.sync_unspent() wallet_service.sync_unspent()
return txid return txid
def test_query_utxo_set(setup_wallets):
load_test_config()
jm_single().bc_interface.tick_forward_chain_interval = 1
wallet_service = create_wallet_for_sync([2, 3, 0, 0, 0],
["wallet4utxo.json", "4utxo", [2, 3]])
wallet_service.sync_wallet(fast=True)
txid = do_tx(wallet_service, 90000000)
txid2 = do_tx(wallet_service, 20000000)
print("Got txs: ", txid, txid2)
res1 = jm_single().bc_interface.query_utxo_set(
(txid, 0), include_mempool=True)
res2 = jm_single().bc_interface.query_utxo_set(
[(txid, 0), (txid2, 1)],
includeconfs=True, include_mempool=True)
assert len(res1) == 1
assert len(res2) == 2
assert all([x in res1[0] for x in ['script', '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((b"\xee" * 32, 25))
assert res3 == [None]
"""Purely blockchaininterface related error condition tests"""
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
"""
badtx = b"\xaa\xaa"
assert not jm_single().bc_interface.pushtx(badtx)
#Break the authenticated jsonrpc and try again
jm_single().bc_interface.jsonRpc.port = 18333
assert not jm_single().bc_interface.pushtx(hextobin(t_raw_signed_tx))
#rebuild a valid jsonrpc inside the bci
load_test_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_test_config()
def check_bip39_case(vectors, language="english"): def check_bip39_case(vectors, language="english"):
mnemo = Mnemonic(language) mnemo = Mnemonic(language)
for v in vectors: for v in vectors:
@ -102,21 +49,75 @@ def check_bip39_case(vectors, language="english"):
assert v[1] == code assert v[1] == code
assert v[2] == seed assert v[2] == seed
"""
Sanity check of basic bip39 functionality for 12 words seed, copied from @pytest.mark.usefixtures("setup_wallets")
https://github.com/trezor/python-mnemonic/blob/master/tests/test_mnemonic.py class AsyncioTestCase(IsolatedAsyncioTestCase):
"""
def test_bip39_vectors(setup_wallets): async def test_query_utxo_set(self):
with open(os.path.join(testdir, 'bip39vectors.json'), 'r') as f: load_test_config()
vectors_full = json.load(f) jm_single().bc_interface.tick_forward_chain_interval = 1
vectors = vectors_full['english'] wallet_service = await create_wallet_for_sync([2, 3, 0, 0, 0],
#default from-file cases use passphrase 'TREZOR'; TODO add other ["wallet4utxo.json", "4utxo", [2, 3]])
#extensions, but note there is coverage of that in the below test await wallet_service.sync_wallet(fast=True)
for v in vectors: txid = await do_tx(wallet_service, 90000000)
v.append("TREZOR") txid2 = await do_tx(wallet_service, 20000000)
#12 word seeds only print("Got txs: ", txid, txid2)
vectors = filter(lambda x: len(x[1].split())==12, vectors) res1 = jm_single().bc_interface.query_utxo_set(
check_bip39_case(vectors) (txid, 0), include_mempool=True)
res2 = jm_single().bc_interface.query_utxo_set(
[(txid, 0), (txid2, 1)],
includeconfs=True, include_mempool=True)
assert len(res1) == 1
assert len(res2) == 2
assert all([x in res1[0] for x in ['script', '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((b"\xee" * 32, 25))
assert res3 == [None]
"""Purely blockchaininterface related error condition tests"""
async def test_wrong_network_bci(self):
rpc = jm_single().bc_interface.jsonRpc
with pytest.raises(Exception) as e_info:
x = BitcoinCoreInterface(rpc, 'mainnet')
async def test_pushtx_errors(self):
"""Ensure pushtx fails return False
"""
badtx = b"\xaa\xaa"
assert not jm_single().bc_interface.pushtx(badtx)
#Break the authenticated jsonrpc and try again
jm_single().bc_interface.jsonRpc.port = 18333
assert not jm_single().bc_interface.pushtx(hextobin(t_raw_signed_tx))
#rebuild a valid jsonrpc inside the bci
load_test_config()
"""Tests mainly for wallet.py"""
async def test_absurd_fee(self):
jm_single().config.set("POLICY", "absurd_fee_per_kb", "1000")
with pytest.raises(ValueError) as e_info:
estimate_tx_fee(10, 2)
load_test_config()
"""
Sanity check of basic bip39 functionality for 12 words seed, copied from
https://github.com/trezor/python-mnemonic/blob/master/tests/test_mnemonic.py
"""
async def test_bip39_vectors(self):
with open(os.path.join(testdir, 'bip39vectors.json'), 'r') as f:
vectors_full = json.load(f)
data = vectors_full['english']
#default from-file cases use passphrase 'TREZOR'; TODO add other
#extensions, but note there is coverage of that in the below test
for d in data:
d.append("TREZOR")
vectors = []
for d in data:
vectors.append(tuple(d))
#12 word seeds only
vectors = filter(lambda x: len(x[1].split())==12, vectors)
check_bip39_case(vectors)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")

92
test/jmclient/test_walletservice.py

@ -2,6 +2,9 @@
import os import os
import pytest import pytest
from unittest import IsolatedAsyncioTestCase
from jmbase import get_log from jmbase import get_log
from jmclient import load_test_config, jm_single, \ from jmclient import load_test_config, jm_single, \
WalletService WalletService
@ -16,57 +19,62 @@ log = get_log()
def set_freeze_reuse_config(x): def set_freeze_reuse_config(x):
jm_single().config.set('POLICY', 'max_sats_freeze_reuse', str(x)) jm_single().config.set('POLICY', 'max_sats_freeze_reuse', str(x))
def try_address_reuse(wallet_service, idx, funding_amt, config_threshold, async def try_address_reuse(wallet_service, idx, funding_amt, config_threshold,
expected_final_balance): expected_final_balance):
set_freeze_reuse_config(config_threshold) set_freeze_reuse_config(config_threshold)
# check that below the threshold on the same address is not allowed: # check that below the threshold on the same address is not allowed:
fund_wallet_addr(wallet_service.wallet, wallet_service.get_addr(0, 1, idx), fund_wallet_addr(wallet_service.wallet,
await wallet_service.get_addr(0, 1, idx),
value_btc=funding_amt) value_btc=funding_amt)
wallet_service.transaction_monitor() await wallet_service.transaction_monitor()
balances = wallet_service.get_balance_by_mixdepth() balances = wallet_service.get_balance_by_mixdepth()
assert balances[0] == expected_final_balance assert balances[0] == expected_final_balance
def test_address_reuse_freezing(setup_walletservice):
""" Creates a WalletService on a pre-populated wallet,
and sets different values of the config var
'max_sats_freeze_reuse' then adds utxos to different
already used addresses to check that they are frozen or
not as appropriate.
Note that to avoid a twisted main loop the WalletService is
not actually started, but the transaction_monitor is triggered
manually (which executes the same code).
A custom test version of the reuse warning callback is added
and to check correct function, we check that this callback is
called, and that the balance available in the mixdepth correctly
reflects the usage pattern and freeze policy.
"""
context = {'cb_called': 0}
def reuse_callback(utxostr):
context['cb_called'] += 1
# we must fund after initial sync (for imports), hence
# "populated" with no coins
wallet = get_populated_wallet(num=0)
wallet_service = WalletService(wallet)
wallet_service.set_autofreeze_warning_cb(reuse_callback)
sync_test_wallet(True, wallet_service)
for i in range(3):
fund_wallet_addr(wallet_service.wallet,
wallet_service.get_addr(0, 1, i))
# manually force the wallet service to see the new utxos:
wallet_service.transaction_monitor()
# check that with default status any reuse is blocked: @pytest.mark.usefixtures("setup_walletservice")
try_address_reuse(wallet_service, 0, 1, -1, 3 * 10**8) class AsyncioTestCase(IsolatedAsyncioTestCase):
assert context['cb_called'] == 1, "Failed to trigger freeze callback"
# check that above the threshold is allowed (1 sat less than funding) async def test_address_reuse_freezing(self):
try_address_reuse(wallet_service, 1, 1, 99999999, 4 * 10**8) """ Creates a WalletService on a pre-populated wallet,
assert context['cb_called'] == 1, "Incorrectly triggered freeze callback" and sets different values of the config var
'max_sats_freeze_reuse' then adds utxos to different
already used addresses to check that they are frozen or
not as appropriate.
Note that to avoid a twisted main loop the WalletService is
not actually started, but the transaction_monitor is triggered
manually (which executes the same code).
A custom test version of the reuse warning callback is added
and to check correct function, we check that this callback is
called, and that the balance available in the mixdepth correctly
reflects the usage pattern and freeze policy.
"""
context = {'cb_called': 0}
def reuse_callback(utxostr):
context['cb_called'] += 1
# we must fund after initial sync (for imports), hence
# "populated" with no coins
wallet = await get_populated_wallet(num=0)
wallet_service = WalletService(wallet)
wallet_service.set_autofreeze_warning_cb(reuse_callback)
await sync_test_wallet(True, wallet_service)
for i in range(3):
fund_wallet_addr(wallet_service.wallet,
await wallet_service.get_addr(0, 1, i))
# manually force the wallet service to see the new utxos:
await wallet_service.transaction_monitor()
# check that below the threshold on the same address is not allowed: # check that with default status any reuse is blocked:
try_address_reuse(wallet_service, 1, 0.99999998, 99999999, 4 * 10**8) await try_address_reuse(wallet_service, 0, 1, -1, 3 * 10**8)
# note can be more than 1 extra call here, somewhat suboptimal: assert context['cb_called'] == 1, "Failed to trigger freeze callback"
assert context['cb_called'] > 1, "Failed to trigger freeze callback"
# check that above the threshold is allowed (1 sat less than funding)
await try_address_reuse(wallet_service, 1, 1, 99999999, 4 * 10**8)
assert context['cb_called'] == 1, "Incorrectly triggered freeze callback"
# check that below the threshold on the same address is not allowed:
await try_address_reuse(wallet_service, 1, 0.99999998, 99999999, 4 * 10**8)
# note can be more than 1 extra call here, somewhat suboptimal:
assert context['cb_called'] > 1, "Failed to trigger freeze callback"
@pytest.fixture(scope='module') @pytest.fixture(scope='module')

200
test/jmclient/test_walletutils.py

@ -1,4 +1,8 @@
import pytest import pytest
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
from jmbitcoin import select_chain_params from jmbitcoin import select_chain_params
from jmclient import (SegwitLegacyWallet, SegwitWallet, get_network, from jmclient import (SegwitLegacyWallet, SegwitWallet, get_network,
jm_single, VolatileStorage, load_test_config) jm_single, VolatileStorage, load_test_config)
@ -9,103 +13,107 @@ from jmclient.wallet_utils import (bip32pathparse, WalletView,
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
# The below signatures have all been verified against Electrum 4.0.9: class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
@pytest.mark.parametrize('seed, hdpath, walletcls, message, sig, addr', [
[b"\x01"*16, "m/84'/0'/0'/0/0", SegwitWallet, "hello",
"IOLk6ct/8aKtvTNnEAc+xojIWKv5FOwnzHGcnHkTJJwRBAyhrZ2ZyB0Re+dKS4SEav3qgjQeqMYRm+7mHi4sFKA=",
"bc1qq53d9372u8d50jfd5agq9zv7m7zdnzwducuqgz"],
[b"\x01"*16, "m/49'/0'/0'/0/0", SegwitLegacyWallet, "hello",
"HxVaQuXyBpl1UKutiusJjeLfKHwJYBzUiWuu6hEbmNFeSZGt/mbXKJ071ANR1gvdICbS/AnEa2RKDq9xMd/nU8s=",
"3AdTcqdoLHFGNq6znkahJDT41u65HAwiRv"],
[b"\x02"*16, "m/84'/0'/2'/1/0", SegwitWallet, "sign me",
"IA/V5DG7u108aNzCnpNPHqfrJAL8pF4GQ0sSqpf4Vlg5UWizauXzh2KskoD6Usl13hzqXBi4XDXl7Xxo5z6M298=",
"bc1q8mm69xs740sr0l2umrhmpl4ewhxfudxg2zvjw5"],
[b"\x02"*16, "m/49'/0'/2'/1/0", SegwitLegacyWallet, "sign me",
"H4cAtoE+zL+Mr+U8jm9DiYxZlym5xeZM3mcgymLz+TF4YYr4lgnM8qTZhFwlK4izcPaLuF27LFEoGJ/ltleIHUI=",
"3Qan1D4Vcy1yMGHfR9j7szDuC8QxSFVScA"],
])
def test_signmessage(seed, hdpath, walletcls, message, sig, addr):
load_test_config()
jm_single().config.set('BLOCKCHAIN', 'network', 'mainnet')
select_chain_params("bitcoin/mainnet")
storage = VolatileStorage()
walletcls.initialize(
storage, get_network(), entropy=seed, max_mixdepth=3)
wallet = walletcls(storage)
s, m, a = wallet_signmessage(wallet, hdpath, message,
out_str=False)
assert (s, m, a) == (sig, message, addr)
jm_single().config.set("BLOCKCHAIN", "network", "testnet")
select_chain_params("bitcoin/regtest")
def test_bip32_pathparse(): # The below signatures have all been verified against Electrum 4.0.9:
assert bip32pathparse("m/2/1/0017") @parametrize(
assert not bip32pathparse("n/1/1/1/1") 'seed, hdpath, walletcls, message, sig, addr',
assert bip32pathparse("m/0/1'/100'/3'/2/2/21/004/005") [
assert not bip32pathparse("m/0/0/00k") (b"\x01"*16, "m/84'/0'/0'/0/0", SegwitWallet, "hello",
"IOLk6ct/8aKtvTNnEAc+xojIWKv5FOwnzHGcnHkTJJwRBAyhrZ2ZyB0Re+dKS4SEav3qgjQeqMYRm+7mHi4sFKA=",
"bc1qq53d9372u8d50jfd5agq9zv7m7zdnzwducuqgz"),
(b"\x01"*16, "m/49'/0'/0'/0/0", SegwitLegacyWallet, "hello",
"HxVaQuXyBpl1UKutiusJjeLfKHwJYBzUiWuu6hEbmNFeSZGt/mbXKJ071ANR1gvdICbS/AnEa2RKDq9xMd/nU8s=",
"3AdTcqdoLHFGNq6znkahJDT41u65HAwiRv"),
(b"\x02"*16, "m/84'/0'/2'/1/0", SegwitWallet, "sign me",
"IA/V5DG7u108aNzCnpNPHqfrJAL8pF4GQ0sSqpf4Vlg5UWizauXzh2KskoD6Usl13hzqXBi4XDXl7Xxo5z6M298=",
"bc1q8mm69xs740sr0l2umrhmpl4ewhxfudxg2zvjw5"),
(b"\x02"*16, "m/49'/0'/2'/1/0", SegwitLegacyWallet, "sign me",
"H4cAtoE+zL+Mr+U8jm9DiYxZlym5xeZM3mcgymLz+TF4YYr4lgnM8qTZhFwlK4izcPaLuF27LFEoGJ/ltleIHUI=",
"3Qan1D4Vcy1yMGHfR9j7szDuC8QxSFVScA"),
])
async def test_signmessage(self, seed, hdpath, walletcls, message, sig, addr):
load_test_config()
jm_single().config.set('BLOCKCHAIN', 'network', 'mainnet')
select_chain_params("bitcoin/mainnet")
storage = VolatileStorage()
walletcls.initialize(
storage, get_network(), entropy=seed, max_mixdepth=3)
wallet = walletcls(storage)
await wallet.async_init(storage)
s, m, a = await wallet_signmessage(
wallet, hdpath, message, out_str=False)
assert (s, m, a) == (sig, message, addr)
jm_single().config.set("BLOCKCHAIN", "network", "testnet")
select_chain_params("bitcoin/regtest")
async def test_bip32_pathparse(self):
assert bip32pathparse("m/2/1/0017")
assert not bip32pathparse("n/1/1/1/1")
assert bip32pathparse("m/0/1'/100'/3'/2/2/21/004/005")
assert not bip32pathparse("m/0/0/00k")
def test_walletview(): async def test_walletview(self):
rootpath = "m/0" rootpath = "m/0"
walletbranch = 0 walletbranch = 0
accounts = range(3) accounts = range(3)
acctlist = [] acctlist = []
for a in accounts: for a in accounts:
branches = [] branches = []
for address_type in range(2): for address_type in range(2):
entries = [] entries = []
for i in range(4): for i in range(4):
entries.append(WalletViewEntry(rootpath, a, address_type, entries.append(WalletViewEntry(rootpath, a, address_type,
i, "DUMMYADDRESS" + str(i+a), [i*10000000, i*10000000])) i, "DUMMYADDRESS" + str(i+a), [i*10000000, i*10000000]))
branches.append(WalletViewBranch(rootpath, a, address_type, branches.append(WalletViewBranch(rootpath, a, address_type,
branchentries=entries, branchentries=entries,
xpub="xpubDUMMYXPUB" + str(a + address_type))) xpub="xpubDUMMYXPUB" + str(a + address_type)))
acctlist.append(WalletViewAccount(rootpath, a, branches=branches)) acctlist.append(WalletViewAccount(rootpath, a, branches=branches))
wallet = WalletView(rootpath + "/" + str(walletbranch), wallet = WalletView(rootpath + "/" + str(walletbranch),
accounts=acctlist) accounts=acctlist)
assert(wallet.serialize() == ( assert(wallet.serialize() == (
'JM wallet\n' 'JM wallet\n'
'mixdepth\t0\n' 'mixdepth\t0\n'
'external addresses\tm/0\txpubDUMMYXPUB0\n' 'external addresses\tm/0\txpubDUMMYXPUB0\n'
'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'internal addresses\tm/0\txpubDUMMYXPUB1\n' 'internal addresses\tm/0\txpubDUMMYXPUB1\n'
'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'Balance for mixdepth 0:\t1.20000000\n' 'Balance for mixdepth 0:\t1.20000000\n'
'mixdepth\t1\n' 'mixdepth\t1\n'
'external addresses\tm/0\txpubDUMMYXPUB1\n' 'external addresses\tm/0\txpubDUMMYXPUB1\n'
'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'internal addresses\tm/0\txpubDUMMYXPUB2\n' 'internal addresses\tm/0\txpubDUMMYXPUB2\n'
'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'Balance for mixdepth 1:\t1.20000000\n' 'Balance for mixdepth 1:\t1.20000000\n'
'mixdepth\t2\n' 'mixdepth\t2\n'
'external addresses\tm/0\txpubDUMMYXPUB2\n' 'external addresses\tm/0\txpubDUMMYXPUB2\n'
'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'internal addresses\tm/0\txpubDUMMYXPUB3\n' 'internal addresses\tm/0\txpubDUMMYXPUB3\n'
'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\t\t\n' 'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'Balance for mixdepth 2:\t1.20000000\n' 'Balance for mixdepth 2:\t1.20000000\n'
'Total balance:\t3.60000000')) 'Total balance:\t3.60000000'))

4
test/jmclient/test_websocket.py

@ -99,8 +99,10 @@ class WebsocketTestBase(object):
test_tx_hex_txid) test_tx_hex_txid)
def tearDown(self): def tearDown(self):
reactor.disconnectAll()
for dc in reactor.getDelayedCalls(): for dc in reactor.getDelayedCalls():
dc.cancel() if not dc.cancelled:
dc.cancel()
self.client_connector.disconnect() self.client_connector.disconnect()
return self.stopListening() return self.stopListening()

116
test/jmclient/test_yieldgenerator.py

@ -6,6 +6,7 @@ from jmbitcoin import CMutableTxOut, CMutableTransaction
from jmclient import load_test_config, jm_single,\ from jmclient import load_test_config, jm_single,\
SegwitLegacyWallet, VolatileStorage, YieldGeneratorBasic, \ SegwitLegacyWallet, VolatileStorage, YieldGeneratorBasic, \
get_network, WalletService get_network, WalletService
from commontest import TrialAsyncioTestCase
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
@ -23,15 +24,19 @@ class CustomUtxoWallet(SegwitLegacyWallet):
load_test_config() load_test_config()
storage = VolatileStorage() self._storage = storage = VolatileStorage()
self.balances = balances
super().initialize(storage, get_network(), max_mixdepth=len(balances)-1) super().initialize(storage, get_network(), max_mixdepth=len(balances)-1)
super().__init__(storage) super().__init__(storage)
for m, b in enumerate(balances): async def async_init(self, storage, **kwargs):
self.add_utxo_at_mixdepth(m, b) await super().async_init(storage)
for m, b in enumerate(self.balances):
await self.add_utxo_at_mixdepth(m, b)
def add_utxo_at_mixdepth(self, mixdepth, balance): async def add_utxo_at_mixdepth(self, mixdepth, balance):
txout = CMutableTxOut(balance, self.get_internal_script(mixdepth)) txout = CMutableTxOut(
balance, await self.get_internal_script(mixdepth))
tx = CMutableTransaction() tx = CMutableTransaction()
tx.vout = [txout] tx.vout = [txout]
# (note: earlier requirement that txid be generated uniquely is now # (note: earlier requirement that txid be generated uniquely is now
@ -46,13 +51,14 @@ class CustomUtxoWallet(SegwitLegacyWallet):
assert self.get_addr_mixdepth(u['address']) == expected assert self.get_addr_mixdepth(u['address']) == expected
def create_yg_basic(balances, txfee_contribution=0, cjfee_a=0, cjfee_r=0, async def create_yg_basic(balances, txfee_contribution=0, cjfee_a=0, cjfee_r=0,
ordertype='swabsoffer', minsize=0): ordertype='swabsoffer', minsize=0):
"""Constructs a YieldGeneratorBasic instance with a fake wallet. The """Constructs a YieldGeneratorBasic instance with a fake wallet. The
wallet will have the given balances at mixdepths, and the offer params wallet will have the given balances at mixdepths, and the offer params
will be set as given here.""" will be set as given here."""
wallet = CustomUtxoWallet(balances) wallet = CustomUtxoWallet(balances)
await wallet.async_init(wallet._storage)
offerconfig = (txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize, offerconfig = (txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize,
None, None, None) None, None, None)
@ -67,17 +73,18 @@ def create_yg_basic(balances, txfee_contribution=0, cjfee_a=0, cjfee_r=0,
return yg return yg
class CreateMyOrdersTests(unittest.TestCase): class CreateMyOrdersTests(TrialAsyncioTestCase):
"""Unit tests for YieldGeneratorBasic.create_my_orders.""" """Unit tests for YieldGeneratorBasic.create_my_orders."""
def test_no_coins(self): async def test_no_coins(self):
yg = create_yg_basic([0] * 3) yg = await create_yg_basic([0] * 3)
self.assertEqual(yg.create_my_orders(), []) self.assertEqual(yg.create_my_orders(), [])
def test_abs_fee(self): async def test_abs_fee(self):
jm_single().DUST_THRESHOLD = 10 jm_single().DUST_THRESHOLD = 10
yg = create_yg_basic([0, 2000000, 1000000], txfee_contribution=1000, yg = await create_yg_basic(
cjfee_a=10, ordertype='swabsoffer', minsize=100000) [0, 2000000, 1000000], txfee_contribution=1000,
cjfee_a=10, ordertype='swabsoffer', minsize=100000)
self.assertEqual(yg.create_my_orders(), [ self.assertEqual(yg.create_my_orders(), [
{'oid': 0, {'oid': 0,
'ordertype': 'swabsoffer', 'ordertype': 'swabsoffer',
@ -87,10 +94,11 @@ class CreateMyOrdersTests(unittest.TestCase):
'cjfee': '1010'}, 'cjfee': '1010'},
]) ])
def test_rel_fee(self): async def test_rel_fee(self):
jm_single().DUST_THRESHOLD = 10 jm_single().DUST_THRESHOLD = 10
yg = create_yg_basic([0, 2000000, 1000000], txfee_contribution=1000, yg = await create_yg_basic(
cjfee_r=0.1, ordertype='sw0reloffer', minsize=10) [0, 2000000, 1000000], txfee_contribution=1000,
cjfee_r=0.1, ordertype='sw0reloffer', minsize=10)
self.assertEqual(yg.create_my_orders(), [ self.assertEqual(yg.create_my_orders(), [
{'oid': 0, {'oid': 0,
'ordertype': 'sw0reloffer', 'ordertype': 'sw0reloffer',
@ -100,10 +108,11 @@ class CreateMyOrdersTests(unittest.TestCase):
'cjfee': 0.1}, 'cjfee': 0.1},
]) ])
def test_dust_threshold(self): async def test_dust_threshold(self):
jm_single().DUST_THRESHOLD = 1000 jm_single().DUST_THRESHOLD = 1000
yg = create_yg_basic([0, 2000000, 1000000], txfee_contribution=10, yg = await create_yg_basic(
cjfee_a=10, ordertype='swabsoffer', minsize=100000) [0, 2000000, 1000000], txfee_contribution=10,
cjfee_a=10, ordertype='swabsoffer', minsize=100000)
self.assertEqual(yg.create_my_orders(), [ self.assertEqual(yg.create_my_orders(), [
{'oid': 0, {'oid': 0,
'ordertype': 'swabsoffer', 'ordertype': 'swabsoffer',
@ -113,95 +122,98 @@ class CreateMyOrdersTests(unittest.TestCase):
'cjfee': '20'}, 'cjfee': '20'},
]) ])
def test_minsize_above_maxsize(self): async def test_minsize_above_maxsize(self):
jm_single().DUST_THRESHOLD = 10 jm_single().DUST_THRESHOLD = 10
yg = create_yg_basic([0, 20000, 10000], txfee_contribution=1000, yg = await create_yg_basic(
cjfee_a=10, ordertype='swabsoffer', minsize=100000) [0, 20000, 10000], txfee_contribution=1000,
cjfee_a=10, ordertype='swabsoffer', minsize=100000)
self.assertEqual(yg.create_my_orders(), []) self.assertEqual(yg.create_my_orders(), [])
class OidToOrderTests(unittest.TestCase): class OidToOrderTests(TrialAsyncioTestCase):
"""Tests YieldGeneratorBasic.oid_to_order.""" """Tests YieldGeneratorBasic.oid_to_order."""
def call_oid_to_order(self, yg, amount): async def call_oid_to_order(self, yg, amount):
"""Calls oid_to_order on the given yg instance. It passes the """Calls oid_to_order on the given yg instance. It passes the
txfee and abs fee from yg as offer.""" txfee and abs fee from yg as offer."""
offer = {'txfee': yg.txfee_contribution, offer = {'txfee': yg.txfee_contribution,
'cjfee': str(yg.cjfee_a), 'cjfee': str(yg.cjfee_a),
'ordertype': 'swabsoffer'} 'ordertype': 'swabsoffer'}
return yg.oid_to_order(offer, amount) return await yg.oid_to_order(offer, amount)
def test_not_enough_balance(self): async def test_not_enough_balance(self):
yg = create_yg_basic([100], txfee_contribution=0, cjfee_a=10) yg = await create_yg_basic([100], txfee_contribution=0, cjfee_a=10)
self.assertEqual(self.call_oid_to_order(yg, 1000), (None, None, None)) self.assertEqual(
await self.call_oid_to_order(yg, 1000), (None, None, None))
def test_chooses_single_utxo(self): async def test_chooses_single_utxo(self):
jm_single().DUST_THRESHOLD = 10 jm_single().DUST_THRESHOLD = 10
yg = create_yg_basic([10, 1000, 2000]) yg = await create_yg_basic([10, 1000, 2000])
utxos, cj_addr, change_addr = self.call_oid_to_order(yg, 500) utxos, cj_addr, change_addr = await self.call_oid_to_order(yg, 500)
self.assertEqual(len(utxos), 1) self.assertEqual(len(utxos), 1)
yg.wallet_service.wallet.assert_utxos_from_mixdepth(utxos, 1) yg.wallet_service.wallet.assert_utxos_from_mixdepth(utxos, 1)
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(cj_addr), 2) self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(cj_addr), 2)
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(change_addr), 1) self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(change_addr), 1)
def test_not_enough_balance_with_dust_threshold(self): async def test_not_enough_balance_with_dust_threshold(self):
# 410 is exactly the size of the change output. So it will be # 410 is exactly the size of the change output. So it will be
# right at the dust threshold. The wallet won't be able to find # right at the dust threshold. The wallet won't be able to find
# any extra inputs, though. # any extra inputs, though.
jm_single().DUST_THRESHOLD = 410 jm_single().DUST_THRESHOLD = 410
yg = create_yg_basic([10, 1000, 10], txfee_contribution=100, yg = await create_yg_basic(
cjfee_a=10) [10, 1000, 10], txfee_contribution=100, cjfee_a=10)
self.assertEqual(self.call_oid_to_order(yg, 500), (None, None, None)) self.assertEqual(
await self.call_oid_to_order(yg, 500), (None, None, None))
def test_extra_with_dust_threshold(self): async def test_extra_with_dust_threshold(self):
# The output will be right at the dust threshold, so that we will # The output will be right at the dust threshold, so that we will
# need to include the extra_utxo from the wallet as well to get # need to include the extra_utxo from the wallet as well to get
# over the threshold. # over the threshold.
jm_single().DUST_THRESHOLD = 410 jm_single().DUST_THRESHOLD = 410
yg = create_yg_basic([10, 1000, 10], txfee_contribution=100, yg = await create_yg_basic(
cjfee_a=10) [10, 1000, 10], txfee_contribution=100, cjfee_a=10)
yg.wallet_service.wallet.add_utxo_at_mixdepth(1, 500) await yg.wallet_service.wallet.add_utxo_at_mixdepth(1, 500)
utxos, cj_addr, change_addr = self.call_oid_to_order(yg, 500) utxos, cj_addr, change_addr = await self.call_oid_to_order(yg, 500)
self.assertEqual(len(utxos), 2) self.assertEqual(len(utxos), 2)
yg.wallet_service.wallet.assert_utxos_from_mixdepth(utxos, 1) yg.wallet_service.wallet.assert_utxos_from_mixdepth(utxos, 1)
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(cj_addr), 2) self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(cj_addr), 2)
self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(change_addr), 1) self.assertEqual(yg.wallet_service.wallet.get_addr_mixdepth(change_addr), 1)
class OfferReannouncementTests(unittest.TestCase): class OfferReannouncementTests(TrialAsyncioTestCase):
"""Tests offer reannouncement logic from on_tx_unconfirmed.""" """Tests offer reannouncement logic from on_tx_unconfirmed."""
def call_on_tx_unconfirmed(self, yg): def call_on_tx_unconfirmed(self, yg):
"""Calls yg.on_tx_unconfirmed with fake arguments.""" """Calls yg.on_tx_unconfirmed with fake arguments."""
return yg.on_tx_unconfirmed({'cjaddr': 'addr'}, 'txid') return yg.on_tx_unconfirmed({'cjaddr': 'addr'}, 'txid')
def create_yg_and_offer(self, maxsize): async def create_yg_and_offer(self, maxsize):
"""Constructs a fake yg instance that has an offer with the given """Constructs a fake yg instance that has an offer with the given
maxsize. Returns it together with the offer.""" maxsize. Returns it together with the offer."""
jm_single().DUST_THRESHOLD = 10 jm_single().DUST_THRESHOLD = 10
yg = create_yg_basic([100 + maxsize], txfee_contribution=100, yg = await create_yg_basic(
ordertype='swabsoffer') [100 + maxsize], txfee_contribution=100, ordertype='swabsoffer')
offers = yg.create_my_orders() offers = yg.create_my_orders()
self.assertEqual(len(offers), 1) self.assertEqual(len(offers), 1)
self.assertEqual(offers[0]['maxsize'], maxsize) self.assertEqual(offers[0]['maxsize'], maxsize)
return yg, offers[0] return yg, offers[0]
def test_no_new_offers(self): async def test_no_new_offers(self):
yg = create_yg_basic([0] * 3) yg = await create_yg_basic([0] * 3)
yg.offerlist = [{'oid': 0}] yg.offerlist = [{'oid': 0}]
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([0], [])) self.assertEqual(self.call_on_tx_unconfirmed(yg), ([0], []))
def test_no_old_offers(self): async def test_no_old_offers(self):
yg, offer = self.create_yg_and_offer(100) yg, offer = await self.create_yg_and_offer(100)
yg.offerlist = [] yg.offerlist = []
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [offer])) self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [offer]))
def test_offer_unchanged(self): async def test_offer_unchanged(self):
yg, offer = self.create_yg_and_offer(100) yg, offer = await self.create_yg_and_offer(100)
yg.offerlist = [offer] yg.offerlist = [offer]
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [])) self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], []))
def test_offer_changed(self): async def test_offer_changed(self):
yg, offer = self.create_yg_and_offer(100) yg, offer = await self.create_yg_and_offer(100)
yg.offerlist = [{'oid': 0, 'maxsize': 10}] yg.offerlist = [{'oid': 0, 'maxsize': 10}]
self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [offer])) self.assertEqual(self.call_on_tx_unconfirmed(yg), ([], [offer]))

Loading…
Cancel
Save