Browse Source

fix existing tests

add_frost_channel_encryption
zebra-lucky 7 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. 44
      test/jmclient/commontest.py
  8. 146
      test/jmclient/test_blockchaininterface.py
  9. 23
      test/jmclient/test_client_protocol.py
  10. 176
      test/jmclient/test_coinjoin.py
  11. 43
      test/jmclient/test_core_nohistory_sync.py
  12. 47
      test/jmclient/test_maker.py
  13. 101
      test/jmclient/test_payjoin.py
  14. 97
      test/jmclient/test_podle.py
  15. 179
      test/jmclient/test_psbt_wallet.py
  16. 57
      test/jmclient/test_snicker.py
  17. 203
      test/jmclient/test_taker.py
  18. 115
      test/jmclient/test_tx_creation.py
  19. 56
      test/jmclient/test_utxomanager.py
  20. 712
      test/jmclient/test_wallet.py
  21. 29
      test/jmclient/test_wallet_rpc.py
  22. 83
      test/jmclient/test_wallets.py
  23. 30
      test/jmclient/test_walletservice.py
  24. 42
      test/jmclient/test_walletutils.py
  25. 2
      test/jmclient/test_websocket.py
  26. 106
      test/jmclient/test_yieldgenerator.py

7
pyproject.toml

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

2
src/jmbase/__init__.py

@ -10,7 +10,7 @@ from .support import (get_log, chunks, debug_silence, jmprint,
IndentedHelpFormatterWithNL, wrapped_urlparse,
bdict_sdict_convert, random_insert, dict_factory,
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 .twisted_utils import (stop_reactor, is_hs_uri, get_tor_agent,
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')
return password
def is_running_from_pytest():
return bool(environ.get("PYTEST_CURRENT_TEST"))
def lookup_appdata_folder(appname):
""" Given an appname as a string,
return the correct directory for storing

6
src/jmclient/wallet.py

@ -1659,8 +1659,10 @@ class BaseWallet(object):
async def _populate_maps(self, paths):
for path in paths:
self._script_map[await self.get_script_from_path(path)] = path
self._addr_map[await self.get_address_from_path(path)] = path
script = await self.get_script_from_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):
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,
FrostWallet)
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
"""Wallet service
@ -706,7 +707,8 @@ class WalletService(Service):
#theres also a sys.exit() in BitcoinCoreInterface.import_addresses()
#perhaps have sys.exit() placed inside the restart_cb that only
# 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
restart_msg = ("Use `bitcoin-cli rescanblockchain` if you're "
"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']
if 'read_only' in kwargs:
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:
password = read_password_stdin()

44
test/jmclient/commontest.py

@ -5,6 +5,9 @@ import os
import random
from decimal import Decimal
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
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):
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):
def __init__(self) -> None:
@ -174,16 +188,18 @@ class DummyBlockchainInterface(BlockchainInterface):
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;
#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")
print("using preimage: ", preimage)
seedh = bintohex(btc.Hash(preimage))[:32]
return make_wallets(
1, [wallet_structure], fixed_seeds=[seedh], **kwargs)[0]['wallet']
wallets = await make_wallets(
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,
amount,
output_addr=None,
@ -202,8 +218,10 @@ def make_sign_and_push(ins_full,
total = sum(x['value'] for x in ins_full.values())
ins = list(ins_full.keys())
#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
change_addr = wallet_service.get_new_addr(0, BaseWallet.ADDRESS_TYPE_INTERNAL) if not change_addr else change_addr
output_addr = await wallet_service.get_new_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
outs = [{'value': amount,
'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):
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:
return False
#pushtx returns False on any error
@ -222,12 +240,13 @@ def make_sign_and_push(ins_full,
if push_succeed:
# in normal operation this happens automatically
# 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]
else:
return False
def make_wallets(n,
async def make_wallets(n,
wallet_structures=None,
mean_amt=1,
sdev_amt=0,
@ -258,7 +277,7 @@ def make_wallets(n,
for i in range(n):
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)
wallet_service = WalletService(w)
wallets[i + start_index] = {'seed': seeds[i],
@ -270,8 +289,9 @@ def make_wallets(n,
amt = mean_amt - sdev_amt / 2.0 + deviation
if amt < 0: amt = 0.001
amt = float(Decimal(amt).quantize(Decimal(10)**-8))
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr(
j, populate_internal), amt)
addr = await wallet_service.get_new_addr(
j, populate_internal)
jm_single().bc_interface.grab_coins(addr, amt)
return wallets

146
test/jmclient/test_blockchaininterface.py

@ -2,33 +2,46 @@
"""Blockchaininterface functionality tests."""
import binascii
from commontest import create_wallet_for_sync
import pytest
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
from jmbase import get_log
from jmclient import load_test_config, jm_single, BaseWallet
from commontest import create_wallet_for_sync
log = get_log()
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
wallet_service.synced = False
while not wallet_service.synced:
wallet_service.sync_wallet(fast=fast)
await wallet_service.sync_wallet(fast=fast)
sync_count += 1
# avoid infinite loop
assert sync_count < 10
log.debug("Tried " + str(sync_count) + " times")
@pytest.mark.parametrize('fast', (False, True))
def test_empty_wallet_sync(setup_wallets, fast):
wallet_service = create_wallet_for_sync([0, 0, 0, 0, 0], ['test_empty_wallet_sync'])
@pytest.mark.usefixtures("setup_wallets")
class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
sync_test_wallet(fast, wallet_service)
@parametrize(
'fast',
[
(False,),
(True,),
])
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):
@ -38,19 +51,21 @@ def test_empty_wallet_sync(setup_wallets, fast):
assert 0 == wallet_service.get_next_unused_index(md, internal)
assert not broken
@pytest.mark.parametrize('fast,internal', (
@parametrize(
'fast,internal',
[
(False, BaseWallet.ADDRESS_TYPE_EXTERNAL),
(False, BaseWallet.ADDRESS_TYPE_INTERNAL),
(True, BaseWallet.ADDRESS_TYPE_EXTERNAL),
(True, BaseWallet.ADDRESS_TYPE_INTERNAL)))
def test_sequentially_used_wallet_sync(setup_wallets, fast, internal):
(True, BaseWallet.ADDRESS_TYPE_INTERNAL)
])
async def test_sequentially_used_wallet_sync(self, fast, internal):
used_count = [1, 3, 6, 2, 23]
wallet_service = create_wallet_for_sync(
wallet_service = await create_wallet_for_sync(
used_count, ['test_sequentially_used_wallet_sync'],
populate_internal=internal)
sync_test_wallet(fast, wallet_service)
await sync_test_wallet(fast, wallet_service)
broken = True
for md in range(len(used_count)):
@ -58,9 +73,12 @@ def test_sequentially_used_wallet_sync(setup_wallets, fast, internal):
assert used_count[md] == wallet_service.get_next_unused_index(md, internal)
assert not broken
@pytest.mark.parametrize('fast', (False,))
def test_gap_used_wallet_sync(setup_wallets, fast):
@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
@ -69,7 +87,8 @@ def test_gap_used_wallet_sync(setup_wallets, fast):
fast-mode (the now default).
"""
used_count = [1, 3, 6, 2, 23]
wallet_service = create_wallet_for_sync(used_count, ['test_gap_used_wallet_sync'])
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)):
@ -77,19 +96,23 @@ def test_gap_used_wallet_sync(setup_wallets, fast):
for x in range(md):
assert x <= wallet_service.gap_limit, "test broken"
# create some unused addresses
wallet_service.get_new_script(md, BaseWallet.ADDRESS_TYPE_INTERNAL)
wallet_service.get_new_script(md, BaseWallet.ADDRESS_TYPE_EXTERNAL)
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(wallet_service.get_new_addr(md,
BaseWallet.ADDRESS_TYPE_INTERNAL), 1)
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr(md,
BaseWallet.ADDRESS_TYPE_EXTERNAL), 1)
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)
sync_test_wallet(fast, wallet_service)
await sync_test_wallet(fast, wallet_service)
broken = True
for md in range(len(used_count)):
@ -100,15 +123,19 @@ def test_gap_used_wallet_sync(setup_wallets, fast):
BaseWallet.ADDRESS_TYPE_EXTERNAL)
assert not broken
@pytest.mark.parametrize('fast', (False,))
def test_multigap_used_wallet_sync(setup_wallets, fast):
@parametrize(
'fast',
[
(False,),
])
async def test_multigap_used_wallet_sync(self, fast):
""" See docstring for test_gap_used_wallet_sync; exactly the
same applies here.
"""
start_index = 5
used_count = [start_index, 0, 0, 0, 0]
wallet_service = create_wallet_for_sync(used_count, ['test_multigap_used_wallet_sync'])
wallet_service = await create_wallet_for_sync(
used_count, ['test_multigap_used_wallet_sync'])
wallet_service.gap_limit = 5
mixdepth = 0
@ -116,14 +143,16 @@ def test_multigap_used_wallet_sync(setup_wallets, fast):
for x in range(int(wallet_service.gap_limit * 0.6)):
assert x <= wallet_service.gap_limit, "test broken"
# create some unused addresses
wallet_service.get_new_script(mixdepth,
BaseWallet.ADDRESS_TYPE_INTERNAL)
wallet_service.get_new_script(mixdepth,
BaseWallet.ADDRESS_TYPE_EXTERNAL)
await wallet_service.get_new_script(
mixdepth, BaseWallet.ADDRESS_TYPE_INTERNAL)
await wallet_service.get_new_script(
mixdepth, BaseWallet.ADDRESS_TYPE_EXTERNAL)
used_count[mixdepth] += x + 2
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr(
jm_single().bc_interface.grab_coins(
await wallet_service.get_new_addr(
mixdepth, BaseWallet.ADDRESS_TYPE_INTERNAL), 1)
jm_single().bc_interface.grab_coins(wallet_service.get_new_addr(
jm_single().bc_interface.grab_coins(
await wallet_service.get_new_addr(
mixdepth, BaseWallet.ADDRESS_TYPE_EXTERNAL), 1)
# reset indices to simulate completely unsynced wallet
@ -131,41 +160,52 @@ def test_multigap_used_wallet_sync(setup_wallets, fast):
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)
await sync_test_wallet(fast, wallet_service)
assert used_count[mixdepth] - start_index == \
wallet_service.get_next_unused_index(mixdepth,
BaseWallet.ADDRESS_TYPE_INTERNAL)
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):
@parametrize(
'fast',
[
(False,),
(True,),
])
async def test_retain_unused_indices_wallet_sync(self, fast):
used_count = [0, 0, 0, 0, 0]
wallet_service = create_wallet_for_sync(used_count,
['test_retain_unused_indices_wallet_sync'])
wallet_service = await 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)
await wallet_service.get_new_script(
0, BaseWallet.ADDRESS_TYPE_INTERNAL)
sync_test_wallet(fast, wallet_service)
await 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):
@parametrize(
'fast',
[
(False,),
(True,),
])
async def test_imported_wallet_sync(self, 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'])
wallet_service = await create_wallet_for_sync(
used_count, ['test_imported_wallet_sync'])
source_wallet_service = await 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))
address = await source_wallet_service.get_internal_addr(0)
await 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)
await sync_test_wallet(fast, wallet_service)
assert wallet_service._utxos.have_utxo(txid, 0) == 0

23
test/jmclient/test_client_protocol.py

@ -1,6 +1,16 @@
#! /usr/bin/env python
'''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.commands import *
from jmclient import load_test_config, Taker,\
@ -13,16 +23,9 @@ from twisted.internet.error import (ConnectionLost, ConnectionAborted,
ConnectionClosed, ConnectionDone)
from twisted.protocols.amp import UnknownRemoteError
from twisted.protocols import amp
from twisted.trial import unittest
from twisted.test import proto_helpers
from taker_test_data import t_raw_signed_tx
from commontest import default_max_cj_fee
import json
import jmbitcoin as bitcoin
import twisted
import base64
import pytest
from commontest import default_max_cj_fee, TrialAsyncioTestCase
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")
class TrialTestJMClientProto(unittest.TestCase):
class TrialTestJMClientProto(TrialAsyncioTestCase):
def setUp(self):
global clientfactory
@ -319,7 +322,7 @@ class TrialTestJMClientProto(unittest.TestCase):
pass
class TestMakerClientProtocol(unittest.TestCase):
class TestMakerClientProtocol(TrialAsyncioTestCase):
"""
very basic test case for JMMakerClientProtocol

176
test/jmclient/test_coinjoin.py

@ -5,10 +5,20 @@ Test doing full coinjoins, bypassing IRC
import os
import sys
import pytest
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
import pytest
from _pytest.monkeypatch import MonkeyPatch
from jmbase import get_log
from jmclient import load_test_config, jm_single,\
YieldGeneratorBasic, Taker, LegacyWallet, SegwitLegacyWallet, SegwitWallet,\
@ -28,6 +38,7 @@ absoffer_type_map = {LegacyWallet: "absoffer",
SegwitLegacyWallet: "swabsoffer",
SegwitWallet: "sw0absoffer"}
def make_wallets_to_list(make_wallets_data):
wallets = [None for x in range(len(make_wallets_data))]
for i in make_wallets_data:
@ -35,14 +46,15 @@ def make_wallets_to_list(make_wallets_data):
assert all(wallets)
return wallets
def sync_wallets(wallet_services, fast=True):
async def sync_wallets(wallet_services, fast=True):
for wallet_service in wallet_services:
wallet_service.synced = False
wallet_service.gap_limit = 0
for x in range(20):
if wallet_service.synced:
break
wallet_service.sync_wallet(fast=fast)
await wallet_service.sync_wallet(fast=fast)
else:
assert False, "Failed to sync wallet"
# 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:
wallet_service.update_blockheight()
def create_orderbook(makers):
orderbook = []
for i in range(len(makers)):
@ -75,21 +88,23 @@ def create_taker(wallet, schedule, monkeypatch):
monkeypatch.setattr(taker, 'auth_counterparty', lambda *args: True)
return taker
def create_orders(makers):
async def create_orders(makers):
# fire the order creation immediately (delayed 2s in prod,
# but this is too slow for test):
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"
active_orders = init_data[4]
maker_data = {}
for mid in init_data[4]:
m = makers[int(mid)]
# 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:],
init_data[3], init_data[1], '00')
assert response[0], "maker.on_auth_received error"
@ -109,43 +124,72 @@ def init_coinjoin(taker, makers, orderbook, cj_amount):
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'
maker_signatures = {} # left here for easier debugging
for mid in txdata[1]:
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"
maker_signatures[mid] = 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'
return taker_final_result
@pytest.mark.parametrize('wallet_cls', (LegacyWallet, SegwitLegacyWallet, SegwitWallet))
def test_simple_coinjoin(monkeypatch, tmpdir, setup_cj, wallet_cls):
#@pytest.mark.usefixtures("setup_cj")
class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
load_test_config()
jm_single().config.set('POLICY', 'tx_broadcast', 'self')
jm_single().bc_interface.tick_forward_chain_interval = 5
jm_single().bc_interface.simulate_blocks()
sys._exit_ = sys.exit
def tearDown(self):
monkeypatch = MonkeyPatch()
monkeypatch.setattr(sys, 'exit', sys._exit_)
for dc in reactor.getDelayedCalls():
dc.cancel()
shutil.rmtree(self.tmpdir)
@parametrize(
'wallet_cls',
[
(LegacyWallet,),
(SegwitLegacyWallet,),
(SegwitWallet,),
])
async def test_simple_coinjoin(self, wallet_cls):
def raise_exit(i):
raise Exception("sys.exit called")
monkeypatch = MonkeyPatch()
monkeypatch.setattr(sys, 'exit', raise_exit)
set_commitment_file(str(tmpdir.join('commitments.json')))
commitment_file = os.path.join(self.tmpdir, 'commitments.json')
set_commitment_file(commitment_file)
MAKER_NUM = 3
wallet_services = make_wallets_to_list(make_wallets(
wallets = await make_wallets(
MAKER_NUM + 1, wallet_structures=[[4, 0, 0, 0, 0]] * (MAKER_NUM + 1),
mean_amt=1, wallet_cls=wallet_cls))
mean_amt=1, wallet_cls=wallet_cls)
wallet_services = make_wallets_to_list(wallets)
jm_single().bc_interface.tickchain()
jm_single().bc_interface.tickchain()
sync_wallets(wallet_services)
await sync_wallets(wallet_services)
makers = [YieldGeneratorBasic(
makers = [
YieldGeneratorBasic(
wallet_services[i],
[0, 2000, 0, absoffer_type_map[wallet_cls], 10**7, None, None, None]) for i in range(MAKER_NUM)]
create_orders(makers)
[0, 2000, 0, absoffer_type_map[wallet_cls],
10**7, None, None, None])
for i in range(MAKER_NUM)]
await create_orders(makers)
orderbook = create_orderbook(makers)
assert len(orderbook) == MAKER_NUM
@ -154,28 +198,31 @@ def test_simple_coinjoin(monkeypatch, tmpdir, setup_cj, wallet_cls):
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)
active_orders, maker_data = await init_coinjoin(
taker, makers, orderbook, cj_amount)
txdata = taker.receive_utxos(maker_data)
txdata = await taker.receive_utxos(maker_data)
assert txdata[0], "taker.receive_utxos error"
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata)
taker_final_result = await do_tx_signing(
taker, makers, active_orders, txdata)
assert taker_final_result is not False
assert taker.on_finished_callback.status is not False
def test_coinjoin_mixdepth_wrap_taker(monkeypatch, tmpdir, setup_cj):
async def test_coinjoin_mixdepth_wrap_taker(self):
def raise_exit(i):
raise Exception("sys.exit called")
monkeypatch = MonkeyPatch()
monkeypatch.setattr(sys, 'exit', raise_exit)
set_commitment_file(str(tmpdir.join('commitments.json')))
commitment_file = os.path.join(self.tmpdir, 'commitments.json')
set_commitment_file(commitment_file)
MAKER_NUM = 3
wallet_services = make_wallets_to_list(make_wallets(
wallets = await make_wallets(
MAKER_NUM + 1,
wallet_structures=[[4, 0, 0, 0, 0]] * MAKER_NUM + [[0, 0, 0, 0, 3]],
mean_amt=1))
mean_amt=1)
wallet_services = make_wallets_to_list(wallets)
for wallet_service in wallet_services:
assert wallet_service.max_mixdepth == 4
@ -183,14 +230,16 @@ def test_coinjoin_mixdepth_wrap_taker(monkeypatch, tmpdir, setup_cj):
jm_single().bc_interface.tickchain()
jm_single().bc_interface.tickchain()
sync_wallets(wallet_services)
await sync_wallets(wallet_services)
cj_fee = 2000
makers = [YieldGeneratorBasic(
makers = [
YieldGeneratorBasic(
wallet_services[i],
[0, cj_fee, 0, absoffer_type_map[SegwitWallet], 10**7, None, None, None]) for i in range(MAKER_NUM)]
create_orders(makers)
[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
@ -199,20 +248,21 @@ def test_coinjoin_mixdepth_wrap_taker(monkeypatch, tmpdir, setup_cj):
schedule = [(4, 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)
active_orders, maker_data = await init_coinjoin(
taker, makers, orderbook, cj_amount)
txdata = taker.receive_utxos(maker_data)
txdata = await taker.receive_utxos(maker_data)
assert txdata[0], "taker.receive_utxos error"
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata)
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])
wallet_service = wallet_services[-1]
# TODO change for new tx monitoring:
wallet_service.remove_old_utxos(tx)
await wallet_service.remove_old_utxos(tx)
wallet_service.add_new_utxos(tx)
balances = wallet_service.get_balance_by_mixdepth()
@ -220,18 +270,20 @@ def test_coinjoin_mixdepth_wrap_taker(monkeypatch, tmpdir, setup_cj):
# <= because of tx fee
assert balances[4] <= 3 * 10**8 - cj_amount - (cj_fee * MAKER_NUM)
def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj):
async def test_coinjoin_mixdepth_wrap_maker(self):
def raise_exit(i):
raise Exception("sys.exit called")
monkeypatch = MonkeyPatch()
monkeypatch.setattr(sys, 'exit', raise_exit)
set_commitment_file(str(tmpdir.join('commitments.json')))
commitment_file = os.path.join(self.tmpdir, 'commitments.json')
set_commitment_file(commitment_file)
MAKER_NUM = 2
wallet_services = make_wallets_to_list(make_wallets(
wallets = await make_wallets(
MAKER_NUM + 1,
wallet_structures=[[0, 0, 0, 0, 4]] * MAKER_NUM + [[3, 0, 0, 0, 0]],
mean_amt=1))
mean_amt=1)
wallet_services = make_wallets_to_list(wallets)
for wallet_service in wallet_services:
assert wallet_service.max_mixdepth == 4
@ -239,13 +291,16 @@ def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj):
jm_single().bc_interface.tickchain()
jm_single().bc_interface.tickchain()
sync_wallets(wallet_services)
await sync_wallets(wallet_services)
cj_fee = 2000
makers = [YieldGeneratorBasic(
makers = [
YieldGeneratorBasic(
wallet_services[i],
[0, cj_fee, 0, absoffer_type_map[SegwitWallet], 10**7, None, None, None]) for i in range(MAKER_NUM)]
create_orders(makers)
[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
@ -254,13 +309,14 @@ def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj):
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)
active_orders, maker_data = await init_coinjoin(
taker, makers, orderbook, cj_amount)
txdata = taker.receive_utxos(maker_data)
txdata = await taker.receive_utxos(maker_data)
assert txdata[0], "taker.receive_utxos error"
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata)
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])
@ -268,21 +324,9 @@ def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj):
for i in range(MAKER_NUM):
wallet_service = wallet_services[i]
# TODO as above re: monitoring
wallet_service.remove_old_utxos(tx)
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
@pytest.fixture(scope='module')
def setup_cj():
load_test_config()
jm_single().config.set('POLICY', 'tx_broadcast', 'self')
jm_single().bc_interface.tick_forward_chain_interval = 5
jm_single().bc_interface.simulate_blocks()
yield None
# teardown
for dc in reactor.getDelayedCalls():
dc.cancel()

43
test/jmclient/test_core_nohistory_sync.py

@ -3,53 +3,66 @@
"""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
from jmbase import get_log
from jmclient import (load_test_config, SegwitLegacyWallet,
SegwitWallet, jm_single, BaseWallet)
from jmbitcoin import select_chain_params
from commontest import create_wallet_for_sync
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log()
def test_fast_sync_unavailable(setup_sync):
wallet_service = create_wallet_for_sync([0, 0, 0, 0, 0],
['test_fast_sync_unavailable'])
@pytest.mark.usefixtures("setup_sync")
class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
async def test_fast_sync_unavailable(setup_sync):
wallet_service = await create_wallet_for_sync(
[0, 0, 0, 0, 0], ['test_fast_sync_unavailable'])
with pytest.raises(RuntimeError) as e_info:
wallet_service.sync_wallet(fast=True)
await wallet_service.sync_wallet(fast=True)
@pytest.mark.parametrize('internal, wallet_cls',
[(BaseWallet.ADDRESS_TYPE_EXTERNAL, SegwitLegacyWallet),
@parametrize(
'internal, wallet_cls',
[
(BaseWallet.ADDRESS_TYPE_EXTERNAL, SegwitLegacyWallet),
(BaseWallet.ADDRESS_TYPE_INTERNAL, SegwitLegacyWallet),
(BaseWallet.ADDRESS_TYPE_EXTERNAL, SegwitWallet),
(BaseWallet.ADDRESS_TYPE_INTERNAL, SegwitWallet)])
def test_sync(setup_sync, internal, wallet_cls):
(BaseWallet.ADDRESS_TYPE_INTERNAL, SegwitWallet),
])
async def test_sync(setup_sync, internal, wallet_cls):
used_count = [1, 3, 6, 2, 23]
wallet_service = create_wallet_for_sync(used_count, ['test_sync'],
wallet_service = await create_wallet_for_sync(
used_count, ['test_sync'],
populate_internal=internal, wallet_cls=wallet_cls)
##the gap limit should be not zero before sync
assert wallet_service.gap_limit > 0
for md in range(len(used_count)):
##obtaining an address should be possible without error before sync
wallet_service.get_new_script(md, internal)
await wallet_service.get_new_script(md, internal)
# TODO bci should probably not store this state globally,
# in case syncing is needed for multiple wallets (as in this test):
jm_single().bc_interface.import_addresses_call_count = 0
wallet_service.sync_wallet(fast=False)
await wallet_service.sync_wallet(fast=False)
for md in range(len(used_count)):
##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)
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:
wallet_service.get_new_script(0, internal)
await wallet_service.get_new_script(0, internal)
@pytest.fixture(scope='module')

47
test/jmclient/test_maker.py

@ -1,5 +1,10 @@
import datetime
from unittest import IsolatedAsyncioTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
import jmbitcoin as btc
from jmclient import Maker, load_test_config, jm_single, WalletService, VolatileStorage, \
SegwitWalletFidelityBonds, get_network
@ -11,6 +16,7 @@ import struct
import binascii
from itertools import chain
import pytest
from _pytest.monkeypatch import MonkeyPatch
class OfflineMaker(Maker):
@ -101,13 +107,32 @@ def create_tx_and_offerlist(cj_addr, cj_change_addr, other_output_addrs,
return tx, offerlist
def test_verify_unsigned_tx_sw_valid(setup_env_nodeps):
class AsyncioTestCase(IsolatedAsyncioTestCase):
def setUp(self):
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()
def tearDown(self):
monkeypatch = MonkeyPatch()
monkeypatch.setattr(jmclient.configure,
'get_blockchain_interface_instance',
jmclient.configure._get_bc_interface_instance_)
async def test_verify_unsigned_tx_sw_valid(self):
jm_single().config.set("POLICY", "segwit", "true")
p2sh_gen = address_p2sh_generator()
p2pkh_gen = address_p2pkh_generator()
wallet = DummyWallet()
await wallet.async_init(wallet.storage)
maker = OfflineMaker(WalletService(wallet))
cj_addr, cj_script = next(p2sh_gen)
@ -132,14 +157,14 @@ def test_verify_unsigned_tx_sw_valid(setup_env_nodeps):
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "sw cj with only p2pkh outputs"
def test_verify_unsigned_tx_nonsw_valid(setup_env_nodeps):
async def test_verify_unsigned_tx_nonsw_valid(self):
jm_single().config.set("POLICY", "segwit", "false")
p2sh_gen = address_p2sh_generator()
p2pkh_gen = address_p2pkh_generator()
wallet = DummyWallet()
await wallet.async_init(wallet.storage)
maker = OfflineMaker(WalletService(wallet))
cj_addr, cj_script = next(p2pkh_gen)
@ -164,28 +189,20 @@ def test_verify_unsigned_tx_nonsw_valid(setup_env_nodeps):
assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with only p2sh outputs"
def test_freeze_timelocked_utxos(setup_env_nodeps):
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 = wallet.get_script_from_path(tl_path)
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)
maker = OfflineMaker(WalletService(wallet))
maker.freeze_timelocked_utxos()
await maker.freeze_timelocked_utxos()
assert wallet._utxos.is_disabled(*utxo)
@pytest.fixture
def setup_env_nodeps(monkeypatch):
monkeypatch.setattr(jmclient.configure, 'get_blockchain_interface_instance',
lambda x: DummyBlockchainInterface())
btc.select_chain_params("bitcoin/regtest")
load_test_config()

101
test/jmclient/test_payjoin.py

@ -6,8 +6,8 @@ Test doing payjoins over tcp client/server
import os
import pytest
from twisted.internet import reactor
from twisted.web.server import Site
from twisted.internet import reactor, defer
from twisted.web.server import Site, NOT_DONE_YET
from twisted.web.client import readBody
from twisted.web.http_headers import Headers
from twisted.trial import unittest
@ -25,7 +25,7 @@ from jmclient import (load_test_config, jm_single,
JMBIP78ReceiverManager)
from jmclient.payjoin import make_payjoin_request_params, make_payment_psbt
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
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
@ -47,12 +47,19 @@ class DummyBIP78ReceiverResource(JMHTTPResource):
def render_POST(self, request):
proposed_tx = request.content
payment_psbt_base64 = proposed_tx.read().decode("utf-8")
retval = self.bip78_receiver_manager.receive_proposal_from_sender(
payment_psbt_base64, request.args)
assert retval[0]
content = retval[1].encode("utf-8")
d = defer.Deferred.fromCoroutine(
self.bip78_receiver_manager.receive_proposal_from_sender(
payment_psbt_base64, request.args))
def _delayedRender(result, request):
assert result[0]
content = result[1].encode("utf-8")
request.setHeader(b"content-length", ("%d" % len(content)))
return content
request.write(content)
request.finish()
d.addCallback(_delayedRender, request)
return NOT_DONE_YET
class PayjoinTestBase(object):
""" 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.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_cls = (wc1, wc2)
self.wallet_services = []
self.wallet_services.append(make_wallets_to_list(make_wallets(
wallets = await make_wallets(
1, wallet_structures=[wallet_structures[0]],
mean_amt=self.mean_amt, wallet_cls=wallet_cls[0]))[0])
self.wallet_services.append(make_wallets_to_list(make_wallets(
mean_amt=self.mean_amt, wallet_cls=wallet_cls[0])
self.wallet_services.append(make_wallets_to_list(wallets)[0])
wallets = await make_wallets(
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()
sync_wallets(self.wallet_services)
await sync_wallets(self.wallet_services)
# For accounting purposes, record the balances
# at the start.
@ -93,6 +102,7 @@ class PayjoinTestBase(object):
return self.port.stopListening()
b78rm = JMBIP78ReceiverManager(self.wallet_services[0], 0,
self.cj_amount, 47083)
await b78rm.async_init(self.wallet_services[0], 0, self.cj_amount)
resource = DummyBIP78ReceiverResource(jmprint, cbStopListening, b78rm)
self.site = Site(resource)
self.site.displayTracebacks = False
@ -109,7 +119,7 @@ class PayjoinTestBase(object):
safe=":/")
self.manager = parse_payjoin_setup(bip78_uri, self.wallet_services[1], 0)
self.manager.mode = "testing"
success, msg = make_payment_psbt(self.manager)
success, msg = await make_payment_psbt(self.manager)
assert success, msg
params = make_payjoin_request_params(self.manager)
# avoiding backend daemon (testing only jmclient code here),
@ -120,38 +130,52 @@ class PayjoinTestBase(object):
url_parts = list(wrapped_urlparse(serv))
url_parts[4] = urlencode(params).encode("utf-8")
destination_url = urlparse.urlunparse(url_parts)
d = agent.request(b"POST", destination_url,
response = await agent.request(
b"POST", destination_url,
Headers({"Content-Type": ["text/plain"]}),
bodyProducer=body)
d.addCallback(bip78_receiver_response, self.manager)
return d
await bip78_receiver_response(response, self.manager)
return response
def tearDown(self):
for dc in reactor.getDelayedCalls():
dc.cancel()
res = final_checks(self.wallet_services, self.cj_amount,
d = defer.ensureDeferred(
final_checks(self.wallet_services, self.cj_amount,
self.manager.final_psbt.get_fee(),
self.ssb, self.rsb)
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):
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):
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):
# 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).
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):
jm_single().config.set("POLICY", "txfees", self.old_txfees)
def test_low_feerate(self):
self.old_txfees = jm_single().config.get("POLICY", "tx_fees")
# 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
# , we increase from that by 2%.
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)
return d
def bip78_receiver_response(response, manager):
d = readBody(response)
async def bip78_receiver_response(response, manager):
body = await readBody(response)
# if the response code is not 200 OK, we must assume payjoin
# attempt has failed, and revert to standard payment.
if int(response.code) != 200:
d.addCallback(process_receiver_errormsg, response.code)
await process_receiver_errormsg(body, response.code)
return
d.addCallback(process_receiver_psbt, manager)
await process_receiver_psbt(body, manager)
def process_receiver_errormsg(r, c):
print("Failed: r, c: ", r, c)
assert False
def process_receiver_psbt(response, manager):
process_payjoin_proposal_from_server(response.decode("utf-8"), manager)
async def process_receiver_psbt(response, manager):
await process_payjoin_proposal_from_server(
response.decode("utf-8"), manager)
def getbals(wallet_service, mixdepth):
""" Retrieves balances for a mixdepth and the 'next'
@ -186,7 +212,8 @@ def getbals(wallet_service, mixdepth):
bbm = wallet_service.get_balance_by_mixdepth()
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
as we've expected according to the test case.
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.
"""
jm_single().bc_interface.tickchain()
sync_wallets(wallet_services)
await sync_wallets(wallet_services)
spenderbals = getbals(wallet_services[1], source_mixdepth)
receiverbals = getbals(wallet_services[0], source_mixdepth)
# is the payment received?

97
test/jmclient/test_podle.py

@ -1,11 +1,14 @@
#! /usr/bin/env python
'''Tests of Proof of discrete log equivalence commitments.'''
import os
import jmbitcoin as bitcoin
import struct
import json
import pytest
import copy
from unittest import IsolatedAsyncioTestCase
import jmbitcoin as bitcoin
from jmbase import get_log, bintohex
from jmclient import load_test_config, jm_single, generate_podle,\
generate_podle_error_string, get_commitment_file, PoDLE,\
@ -17,13 +20,48 @@ pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log()
def test_commitments_empty(setup_podle):
def generate_single_podle_sig(priv, i):
"""Make a podle entry for key priv at index i, using a dummy utxo value.
This calls the underlying 'raw' code based on the class PoDLE, not the
library 'generate_podle' which intelligently searches and updates commitments.
"""
dummy_utxo = bitcoin.b2x(bitcoin.Hash(priv)) + ":3"
podle = PoDLE(dummy_utxo, priv)
r = podle.generate_podle(i)
return (r['P'], r['P2'], r['sig'],
r['e'], r['commit'])
class AsyncioTestCase(IsolatedAsyncioTestCase):
async def asyncSetUp(self):
load_test_config()
if not os.path.exists("cmtdata"):
os.mkdir("cmtdata")
self.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")
self.prev_commits = True
async def asyncTearDown(self):
pcf = get_commitment_file()
if self.prev_commits:
os.rename(pcf + ".bak", pcf)
else:
if os.path.exists(pcf):
os.remove(pcf)
async def test_commitments_empty(self):
"""Ensure that empty commitments file
results in {}
"""
assert get_podle_commitments() == ([], {})
def test_commitment_retries(setup_podle):
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.
@ -43,18 +81,7 @@ def test_commitment_retries(setup_podle):
p = generate_podle(dummy_priv_utxo_pairs[:1], allowed)
assert p is None
def generate_single_podle_sig(priv, i):
"""Make a podle entry for key priv at index i, using a dummy utxo value.
This calls the underlying 'raw' code based on the class PoDLE, not the
library 'generate_podle' which intelligently searches and updates commitments.
"""
dummy_utxo = bitcoin.b2x(bitcoin.Hash(priv)) + ":3"
podle = PoDLE(dummy_utxo, priv)
r = podle.generate_podle(i)
return (r['P'], r['P2'], r['sig'],
r['e'], r['commit'])
def test_rand_commitments(setup_podle):
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)
@ -73,14 +100,14 @@ def test_rand_commitments(setup_podle):
finally:
assert not fail
def test_nums_verify(setup_podle):
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)
def test_external_commitments(setup_podle):
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
@ -149,9 +176,7 @@ def test_external_commitments(setup_podle):
with open(get_commitment_file(), "wb") as f:
f.write(json.dumps(validjson, indent=4).encode('utf-8'))
def test_podle_constructor(setup_podle):
async def test_podle_constructor(self):
"""Tests rules about construction of PoDLE object
are conformed to.
"""
@ -191,17 +216,19 @@ def test_podle_constructor(setup_podle):
with pytest.raises(PoDLEError) as e_info:
p.verify("dummycommitment", range(3))
def test_podle_error_string(setup_podle):
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]
wallet_service = make_wallets(1, [[1, 0, 0, 0, 0]])[0]['wallet']
wallets = await make_wallets(1, [[1, 0, 0, 0, 0]])
wallet_service = wallets[0]['wallet']
cjamt = 100
tua = "3"
tuamtper = "20"
errmgsheader, errmsg = generate_podle_error_string(priv_utxo_pairs,
errmgsheader, errmsg = await generate_podle_error_string(
priv_utxo_pairs,
to,
ts,
wallet_service,
@ -213,25 +240,5 @@ def test_podle_error_string(setup_podle):
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)
else:
if os.path.exists(pcf):
os.remove(pcf)
request.addfinalizer(teardown)
errmgsheader, errmsg = await generate_podle_error_string(
[], [], [], wallet_service, cjamt, tua, tuamtper)

179
test/jmclient/test_psbt_wallet.py

@ -8,6 +8,13 @@
import copy
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
import jmbitcoin as bitcoin
@ -22,14 +29,51 @@ pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log()
def create_volatile_wallet(seedphrase, wallet_cls=SegwitWallet):
async def create_volatile_wallet(seedphrase, wallet_cls=SegwitWallet):
storage = VolatileStorage()
wallet_cls.initialize(storage, get_network(), max_mixdepth=4,
entropy=wallet_cls.entropy_from_mnemonic(seedphrase))
storage.save()
return wallet_cls(storage)
wallet = wallet_cls(storage)
await wallet.async_init(storage)
return wallet
@pytest.mark.parametrize('walletseed, xpub, spktype_wallet, spktype_destn, partial, psbt', [
""" test vector data for human readable parsing only,
they are taken from bitcointx/tests/test_psbt.py and in turn
taken from BIP174 test vectors.
TODO add more, but note we are not testing functionality here.
"""
hr_test_vectors = {
# PSBT with one P2PKH input. Outputs are empty
"one-p2pkh": '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab300000000000000',
# PSBT with one P2PKH input and one P2SH-P2WPKH input.
# First input is signed and finalized. Outputs are empty
"first-input-signed": '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac000000000001076a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa882920001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000',
# PSBT with one P2PKH input which has a non-final scriptSig
# and has a sighash type specified. Outputs are empty
"nonfinal-scriptsig": '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001030401000000000000',
# PSBT with one P2PKH input and one P2SH-P2WPKH input both with
# non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available.
# Outputs filled.
"mixed-inputs-nonfinal": '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000100df0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e13000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000',
# PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript,
# witnessScript, and keypaths are available. Contains one signature.
"2-2-multisig-p2wsh": '70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000',
# PSBT with unknown types in the inputs
"unknown-input-types": '70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000',
# PSBT with `PSBT_GLOBAL_XPUB`
"global-xpub": '70736274ff01009d0100000002710ea76ab45c5cb6438e607e59cc037626981805ae9e0dfd9089012abb0be5350100000000ffffffff190994d6a8b3c8c82ccbcfb2fba4106aa06639b872a8d447465c0d42588d6d670000000000ffffffff0200e1f505000000001976a914b6bc2c0ee5655a843d79afedd0ccc3f7dd64340988ac605af405000000001600141188ef8e4ce0449eaac8fb141cbf5a1176e6a088000000004f010488b21e039e530cac800000003dbc8a5c9769f031b17e77fea1518603221a18fd18f2b9a54c6c8c1ac75cbc3502f230584b155d1c7f1cd45120a653c48d650b431b67c5b2c13f27d7142037c1691027569c503100008000000080000000800001011f00e1f5050000000016001433b982f91b28f160c920b4ab95e58ce50dda3a4a220203309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c47304402202d704ced830c56a909344bd742b6852dccd103e963bae92d38e75254d2bb424502202d86c437195df46c0ceda084f2a291c3da2d64070f76bf9b90b195e7ef28f77201220603309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c1827569c5031000080000000800000008000000000010000000001011f00e1f50500000000160014388fb944307eb77ef45197d0b0b245e079f011de220202c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b11047304402204cb1fb5f869c942e0e26100576125439179ae88dca8a9dc3ba08f7953988faa60220521f49ca791c27d70e273c9b14616985909361e25be274ea200d7e08827e514d01220602c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b1101827569c5031000080000000800000008000000000000000000000220202d20ca502ee289686d21815bd43a80637b0698e1fbcdbe4caed445f6c1a0a90ef1827569c50310000800000008000000080000000000400000000',
# PSBT with proprietary values
"proprietary-values": '70736274ff0100550200000001ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff018e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000015fc0a676c6f62616c5f706678016d756c7469706c790563686965660001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb823080ffc06696e5f706678fde80377686174056672616d650afc00fe40420f0061736b077361746f7368690012fc076f75745f706678feffffff01636f726e05746967657217fc076f75745f706678ffffffffffffffffff707570707905647269766500'
}
@pytest.mark.usefixtures("setup_psbt_wallet")
class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
@parametrize(
'walletseed, xpub, spktype_wallet, spktype_destn, partial, psbt',
[
("prosper diamond marriage spy across start shift elevator job lunar edge gallery",
"tpubDChjiEhsafnW2LcmK1C77XiEAgZddi6xZyxjMujBzUqZPTMRwsv3e5vSBYsdiPtCyc6TtoHTCjkxBjtF22tf8Z5ABRdeBUNwHCsqEyzR5wT",
"p2wpkh", "p2sh-p2wpkh", False,
@ -42,12 +86,13 @@ def create_volatile_wallet(seedphrase, wallet_cls=SegwitWallet):
"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):
])
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 = create_volatile_wallet(walletseed, wallet_cls=wallet_cls)
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):
@ -67,8 +112,8 @@ def test_sign_external_psbt(setup_psbt_wallet, walletseed, xpub,
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)
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:
@ -82,27 +127,31 @@ def test_sign_external_psbt(setup_psbt_wallet, walletseed, xpub,
print(PSBTWalletMixin.human_readable_psbt(signedpsbt))
bitcoin.select_chain_params("bitcoin/regtest")
def test_create_and_sign_psbt_with_legacy(setup_psbt_wallet):
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.
"""
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))
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 = direct_send(wallet_service, 0,
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 = wallet_service.select_utxos(0, bitcoin.coins_to_satoshi(0.5))
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
@ -114,13 +163,13 @@ def test_create_and_sign_psbt_with_legacy(setup_psbt_wallet):
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)}]
"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 = wallet_service.create_psbt_from_tx(tx2, spent_outs,
force_witness_utxo=False)
signed_psbt_and_signresult, err = wallet_service.sign_psbt(
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
@ -128,15 +177,17 @@ def test_create_and_sign_psbt_with_legacy(setup_psbt_wallet):
assert signresult.num_inputs_final == 1
assert not signresult.is_final
@pytest.mark.parametrize('unowned_utxo, wallet_cls', [
@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):
])
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.
@ -152,10 +203,12 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls):
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))
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:
@ -181,7 +234,7 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls):
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)}]
"address": await wallet_service.get_addr(0,0,0)}]
tx = bitcoin.mktx(list(utxos.keys()), outs)
if wallet_cls != LegacyWallet:
@ -194,8 +247,8 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls):
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)
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
@ -216,7 +269,7 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls):
if unowned_utxo:
assert newpsbt.inputs[2].redeem_script == redeem_script
signed_psbt_and_signresult, err = wallet_service.sign_psbt(
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
@ -234,13 +287,15 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls):
with pytest.raises(ValueError) as e:
extracted_tx = signed_psbt.extract_transaction()
@pytest.mark.parametrize('payment_amt, wallet_cls_sender, wallet_cls_receiver', [
@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,
])
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.
@ -261,12 +316,14 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
).
"""
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"]
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]:
w.sync_wallet(fast=True)
await w.sync_wallet(fast=True)
# destination address for payment:
destaddr = str(bitcoin.CCoinAddress.from_scriptPubKey(
@ -278,7 +335,8 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
# **************
# create a normal tx from the sender wallet:
payment_psbt = direct_send(wallet_s, 0,
payment_psbt = await direct_send(
wallet_s, 0,
[(destaddr, payment_amt)],
accept_callback=dummy_accept_callback,
info_callback=dummy_info_callback,
@ -306,7 +364,7 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
# Simple receiver utxo choice heuristic.
# For more generality we test with two receiver-utxos, not one.
all_receiver_utxos = wallet_r.get_all_utxos()
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(
@ -369,13 +427,13 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
# 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)
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 = wallet_r.sign_psbt(r_payjoin_psbt.serialize(),
with_sign_result=True)
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)
@ -389,7 +447,7 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
# take the half-signed PSBT, validate and co-sign:
signresultandpsbt, err = wallet_s.sign_psbt(
signresultandpsbt, err = await wallet_s.sign_psbt(
receiver_signed_psbt.serialize(), with_sign_result=True)
assert not err, err
signresult, sender_signed_psbt = signresultandpsbt
@ -401,36 +459,7 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
extracted_tx = sender_signed_psbt.extract_transaction().serialize()
assert jm_single().bc_interface.pushtx(extracted_tx)
""" test vector data for human readable parsing only,
they are taken from bitcointx/tests/test_psbt.py and in turn
taken from BIP174 test vectors.
TODO add more, but note we are not testing functionality here.
"""
hr_test_vectors = {
# PSBT with one P2PKH input. Outputs are empty
"one-p2pkh": '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab300000000000000',
# PSBT with one P2PKH input and one P2SH-P2WPKH input.
# First input is signed and finalized. Outputs are empty
"first-input-signed": '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac000000000001076a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa882920001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000',
# PSBT with one P2PKH input which has a non-final scriptSig
# and has a sighash type specified. Outputs are empty
"nonfinal-scriptsig": '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001030401000000000000',
# PSBT with one P2PKH input and one P2SH-P2WPKH input both with
# non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available.
# Outputs filled.
"mixed-inputs-nonfinal": '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000100df0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e13000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000',
# PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript,
# witnessScript, and keypaths are available. Contains one signature.
"2-2-multisig-p2wsh": '70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000',
# PSBT with unknown types in the inputs
"unknown-input-types": '70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000',
# PSBT with `PSBT_GLOBAL_XPUB`
"global-xpub": '70736274ff01009d0100000002710ea76ab45c5cb6438e607e59cc037626981805ae9e0dfd9089012abb0be5350100000000ffffffff190994d6a8b3c8c82ccbcfb2fba4106aa06639b872a8d447465c0d42588d6d670000000000ffffffff0200e1f505000000001976a914b6bc2c0ee5655a843d79afedd0ccc3f7dd64340988ac605af405000000001600141188ef8e4ce0449eaac8fb141cbf5a1176e6a088000000004f010488b21e039e530cac800000003dbc8a5c9769f031b17e77fea1518603221a18fd18f2b9a54c6c8c1ac75cbc3502f230584b155d1c7f1cd45120a653c48d650b431b67c5b2c13f27d7142037c1691027569c503100008000000080000000800001011f00e1f5050000000016001433b982f91b28f160c920b4ab95e58ce50dda3a4a220203309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c47304402202d704ced830c56a909344bd742b6852dccd103e963bae92d38e75254d2bb424502202d86c437195df46c0ceda084f2a291c3da2d64070f76bf9b90b195e7ef28f77201220603309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c1827569c5031000080000000800000008000000000010000000001011f00e1f50500000000160014388fb944307eb77ef45197d0b0b245e079f011de220202c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b11047304402204cb1fb5f869c942e0e26100576125439179ae88dca8a9dc3ba08f7953988faa60220521f49ca791c27d70e273c9b14616985909361e25be274ea200d7e08827e514d01220602c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b1101827569c5031000080000000800000008000000000000000000000220202d20ca502ee289686d21815bd43a80637b0698e1fbcdbe4caed445f6c1a0a90ef1827569c50310000800000008000000080000000000400000000',
# PSBT with proprietary values
"proprietary-values": '70736274ff0100550200000001ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff018e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000015fc0a676c6f62616c5f706678016d756c7469706c790563686965660001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb823080ffc06696e5f706678fde80377686174056672616d650afc00fe40420f0061736b077361746f7368690012fc076f75745f706678feffffff01636f726e05746967657217fc076f75745f706678ffffffffffffffffff707570707905647269766500'
}
def test_hr_psbt(setup_psbt_wallet):
async def test_hr_psbt(self):
bitcoin.select_chain_params("bitcoin")
for k, v in hr_test_vectors.items():
print(PSBTWalletMixin.human_readable_psbt(

57
test/jmclient/test_snicker.py

@ -3,6 +3,12 @@
wallets as defined in jmclient.wallet.'''
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
import jmbitcoin as btc
@ -14,11 +20,16 @@ pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
log = get_log()
@pytest.mark.parametrize(
"nw, wallet_structures, mean_amt, sdev_amt, amt, net_transfer", [
@pytest.mark.usefixtures("setup_snicker")
class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
@parametrize(
"nw, wallet_structures, mean_amt, sdev_amt, amt, net_transfer",
[
(2, [[1, 0, 0, 0, 0]] * 2, 4, 0, 20000000, 1000),
])
def test_snicker_e2e(setup_snicker, nw, wallet_structures,
async def test_snicker_e2e(self, nw, wallet_structures,
mean_amt, sdev_amt, amt, net_transfer):
""" Test strategy:
1. create two wallets.
@ -33,19 +44,19 @@ def test_snicker_e2e(setup_snicker, nw, wallet_structures,
"""
# TODO: Make this test work with native segwit wallets
wallets = make_wallets(nw, wallet_structures, mean_amt, sdev_amt)
wallets = await make_wallets(nw, wallet_structures, mean_amt, sdev_amt)
for w in wallets.values():
w['wallet'].sync_wallet(fast=True)
await w['wallet'].sync_wallet(fast=True)
print(wallets)
wallet_r = wallets[0]['wallet']
wallet_p = wallets[1]['wallet']
# next, create a tx from the receiver wallet
our_destn_script = wallet_r.get_new_script(1, BaseWallet.ADDRESS_TYPE_INTERNAL)
tx = direct_send(wallet_r, 0,
[(
wallet_r.script_to_addr(our_destn_script),
btc.coins_to_satoshi(0.3)
)],
our_destn_script = await wallet_r.get_new_script(
1, BaseWallet.ADDRESS_TYPE_INTERNAL)
tx = await direct_send(
wallet_r, 0,
[(await wallet_r.script_to_addr(our_destn_script),
btc.coins_to_satoshi(0.3))],
accept_callback=dummy_accept_callback,
info_callback=dummy_info_callback,
return_transaction=True)
@ -53,15 +64,15 @@ def test_snicker_e2e(setup_snicker, nw, wallet_structures,
assert tx, "Failed to spend from receiver wallet"
print("Parent transaction OK. It was: ")
print(btc.human_readable_transaction(tx))
wallet_r.process_new_tx(tx)
await wallet_r.process_new_tx(tx)
# we must identify the receiver's output we're going to use;
# it can be destination or change, that's up to the proposer
# to guess successfully; here we'll just choose index 0.
txid1 = tx.GetTxid()[::-1]
txid1_index = 0
receiver_start_bal = sum([x['value'] for x in wallet_r.get_all_utxos(
).values()])
receiver_start_bal = sum(
[x['value'] for x in (await wallet_r.get_all_utxos()).values()])
# Now create a proposal for every input index in tx1
# (version 1 proposals mean we source keys from the/an
@ -73,11 +84,11 @@ def test_snicker_e2e(setup_snicker, nw, wallet_structures,
propose_keys.append(pub)
# the proposer wallet needs to choose a single
# utxo that is bigger than the output amount of tx1
prop_m_utxos = wallet_p.get_utxos_by_mixdepth()[0]
prop_m_utxos = (await wallet_p.get_utxos_by_mixdepth())[0]
prop_utxo = prop_m_utxos[list(prop_m_utxos)[0]]
# get the private key for that utxo
priv = wallet_p.get_key_from_addr(
wallet_p.script_to_addr(prop_utxo['script']))
addr = await wallet_p.script_to_addr(prop_utxo['script'])
priv = wallet_p.get_key_from_addr(addr)
prop_input_amt = prop_utxo['value']
# construct the arguments for the snicker proposal:
our_input = list(prop_m_utxos)[0] # should be (txid, index)
@ -86,7 +97,8 @@ def test_snicker_e2e(setup_snicker, nw, wallet_structures,
prop_utxo['script'])
fee_est = estimate_tx_fee(len(tx.vin), 2,
txtype=wallet_p.get_txtype())
change_spk = wallet_p.get_new_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL)
change_spk = await wallet_p.get_new_script(
0, BaseWallet.ADDRESS_TYPE_INTERNAL)
encrypted_proposals = []
@ -94,7 +106,7 @@ def test_snicker_e2e(setup_snicker, nw, wallet_structures,
# TODO: this can be a loop over all outputs,
# not just one guessed output, if desired.
encrypted_proposals.append(
wallet_p.create_snicker_proposal(
await wallet_p.create_snicker_proposal(
[our_input], their_input,
[our_input_utxo],
tx.vout[txid1_index],
@ -106,10 +118,11 @@ def test_snicker_e2e(setup_snicker, nw, wallet_structures,
change_spk,
version_byte=1) + b"," + bintohex(p).encode('utf-8'))
sR = SNICKERReceiver(wallet_r)
sR.process_proposals([x.decode("utf-8") for x in encrypted_proposals])
await sR.process_proposals(
[x.decode("utf-8") for x in encrypted_proposals])
assert len(sR.successful_txs) == 1
wallet_r.process_new_tx(sR.successful_txs[0])
end_utxos = wallet_r.get_all_utxos()
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

203
test/jmclient/test_taker.py

@ -1,5 +1,3 @@
from commontest import DummyBlockchainInterface
import jmbitcoin as bitcoin
import binascii
import os
import copy
@ -10,6 +8,15 @@ import json
import struct
from base64 import b64encode
from typing import Optional
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
from commontest import DummyBlockchainInterface
import jmbitcoin as bitcoin
from jmbase import utxostr_to_utxo, hextobin
from jmclient import load_test_config, jm_single, set_commitment_file,\
get_commitment_file, LegacyWallet, Taker, VolatileStorage,\
@ -29,10 +36,14 @@ def convert_utxos(utxodict):
return return_dict
class DummyWallet(LegacyWallet):
def __init__(self):
storage = VolatileStorage()
super().initialize(storage, get_network(), max_mixdepth=5)
super().__init__(storage)
self.storage = VolatileStorage()
super().initialize(self.storage, get_network(), max_mixdepth=5)
super().__init__(self.storage)
async def async_init(self, storage, **kwargs):
await super().async_init(storage)
self._add_utxos()
self.ex_utxos = {}
self.inject_addr_get_failure = False
@ -72,7 +83,7 @@ class DummyWallet(LegacyWallet):
def remove_extra_utxo(self, txid, index, md):
del self.ex_utxos[(txid, index)]
def get_utxos_by_mixdepth(self, include_disabled: bool = False,
async def get_utxos_by_mixdepth(self, include_disabled: bool = False,
verbose: bool = True,
includeheight: bool = False,
limit_mixdepth: Optional[int] = None):
@ -90,8 +101,8 @@ class DummyWallet(LegacyWallet):
retval[md].update(u)
return retval
def select_utxos(self, mixdepth, amount, utxo_filter=None, select_fn=None,
maxheight=None, includeaddr=False,
async def select_utxos(self, mixdepth, amount, utxo_filter=None,
select_fn=None, maxheight=None, includeaddr=False,
require_auth_address=False):
if amount > self.get_balance_by_mixdepth()[mixdepth]:
raise NotEnoughFundsException(amount, self.get_balance_by_mixdepth()[mixdepth])
@ -104,16 +115,16 @@ class DummyWallet(LegacyWallet):
retval[u]["script"] = self.addr_to_script(retval[u]["address"])
return retval
def get_internal_addr(self, mixing_depth, bci=None):
async def get_internal_addr(self, mixing_depth, bci=None):
if self.inject_addr_get_failure:
raise Exception("address get failure")
return "mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf"
def sign_tx(self, tx, addrs):
async def sign_tx(self, tx, addrs):
print("Pretending to sign on addresses: " + str(addrs))
return True, None
def sign(self, tx, i, priv, amount):
async def sign(self, tx, i, priv, amount):
"""Sign a transaction; the amount field
triggers the segwit style signing.
"""
@ -153,17 +164,17 @@ class DummyWallet(LegacyWallet):
def get_path_repr(self, path):
return '/'.join(map(str, path))
def is_standard_wallet_script(self, path):
async def is_standard_wallet_script(self, path):
if path[0] == "nonstandard_path":
return False
return True
def script_to_addr(self, script,
async def script_to_addr(self, script,
validate_cache: bool = False):
if self.script_to_path(script)[0] == "nonstandard_path":
return "dummyaddr"
return super().script_to_addr(script,
validate_cache=validate_cache)
return await super().script_to_addr(
script, validate_cache=validate_cache)
def dummy_order_chooser():
@ -176,7 +187,7 @@ def dummy_filter_orderbook(orders_fees, cjamount):
print("calling dummy filter orderbook")
return True
def get_taker(schedule=None, schedule_len=0, on_finished=None,
async def get_taker(schedule=None, schedule_len=0, on_finished=None,
filter_orders=None, custom_change=None):
if not schedule:
#note, for taker.initalize() this will result in junk
@ -184,26 +195,43 @@ def get_taker(schedule=None, schedule_len=0, on_finished=None,
print("Using schedule: " + str(schedule))
on_finished_callback = on_finished if on_finished else taker_finished
filter_orders_callback = filter_orders if filter_orders else dummy_filter_orderbook
taker = Taker(WalletService(DummyWallet()), schedule, default_max_cj_fee,
wallet = DummyWallet()
await wallet.async_init(wallet.storage)
taker = Taker(WalletService(wallet), schedule, default_max_cj_fee,
callbacks=[filter_orders_callback, None, on_finished_callback],
custom_change_address=custom_change)
taker.wallet_service.current_blockheight = 10**6
return taker
def test_filter_rejection(setup_taker):
class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
def setUp(self):
if not os.path.exists("cmtdata"):
os.makedirs("cmtdata")
load_test_config()
jm_single().bc_interface = DummyBlockchainInterface()
jm_single().config.set("BLOCKCHAIN", "network", "testnet")
def tearDown(self):
from twisted.internet import reactor
for dc in reactor.getDelayedCalls():
dc.cancel()
shutil.rmtree("cmtdata")
async def test_filter_rejection(self):
def filter_orders_reject(orders_feesl, cjamount):
print("calling filter orders rejection")
return False
taker = get_taker(filter_orders=filter_orders_reject)
taker = await get_taker(filter_orders=filter_orders_reject)
taker.schedule = [[0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]]
res = taker.initialize(t_orderbook, [])
res = await taker.initialize(t_orderbook, [])
assert not res[0]
taker = get_taker(filter_orders=filter_orders_reject)
taker = await get_taker(filter_orders=filter_orders_reject)
taker.schedule = [[0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]]
res = taker.initialize(t_orderbook, [])
res = await taker.initialize(t_orderbook, [])
assert not res[0]
@pytest.mark.parametrize(
@parametrize(
"mixdepth, cjamt, failquery, external, expected_success, amtpercent, age, mixdepth_extras",
[
(0, 110000000, False, False, True, 0, 0, {}),
@ -228,8 +256,9 @@ def test_filter_rejection(setup_taker):
# the timelocked UTXO is big enough:
(0, 1110000000, False, False, False, 20, 5, {"custom-script": {0: [1000000000]}}),
])
def test_make_commitment(setup_taker, mixdepth, cjamt, failquery, external,
expected_success, amtpercent, age, mixdepth_extras):
async def test_make_commitment(self, mixdepth, cjamt, failquery, external,
expected_success, amtpercent, age,
mixdepth_extras):
def clean_up():
jm_single().config.set("POLICY", "taker_utxo_age", old_taker_utxo_age)
jm_single().config.set("POLICY", "taker_utxo_amtpercent", old_taker_utxo_amtpercent)
@ -256,14 +285,16 @@ def test_make_commitment(setup_taker, mixdepth, cjamt, failquery, external,
jm_single().config.set("POLICY", "taker_utxo_age", newtua)
jm_single().config.set("POLICY", "taker_utxo_amtpercent", newtuap)
taker = get_taker([(mixdepth, cjamt, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", NO_ROUNDING)])
taker = await get_taker(
[(mixdepth, cjamt, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", NO_ROUNDING)])
# modify or add any extra utxos for this run:
for k, v in mixdepth_extras.items():
if k == "confchange":
for k2, v2 in v.items():
# set the utxos in mixdepth k2 to have confs v2:
cdict = taker.wallet_service.get_utxos_by_mixdepth()[k2]
cdict = (
await taker.wallet_service.get_utxos_by_mixdepth())[k2]
jm_single().bc_interface.set_confs({utxo: v2 for utxo in cdict.keys()})
elif k == "custom-script":
# note: this is inspired by fidelity bonds, and currently
@ -287,11 +318,12 @@ def test_make_commitment(setup_taker, mixdepth, cjamt, failquery, external,
os.urandom(32), 0, value, k)
taker.cjamount = cjamt
taker.input_utxos = taker.wallet_service.get_utxos_by_mixdepth()[mixdepth]
taker.input_utxos = (
await taker.wallet_service.get_utxos_by_mixdepth())[mixdepth]
taker.mixdepth = mixdepth
if failquery:
jm_single().bc_interface.setQUSFail(True)
comm, revelation, msg = taker.make_commitment()
comm, revelation, msg = await taker.make_commitment()
if expected_success and failquery:
# for manual tests, show the error message:
print("Failure case due to QUS fail: ")
@ -308,22 +340,24 @@ def test_make_commitment(setup_taker, mixdepth, cjamt, failquery, external,
assert not comm, "podle was generated but should not have been."
clean_up()
def test_not_found_maker_utxos(setup_taker):
taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)])
async def test_not_found_maker_utxos(self):
taker = await get_taker(
[(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)])
orderbook = copy.deepcopy(t_orderbook)
res = taker.initialize(orderbook, [])
res = await taker.initialize(orderbook, [])
taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same
maker_response = copy.deepcopy(t_maker_response)
jm_single().bc_interface.setQUSFail(True)
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
assert not res[0]
assert res[1] == "Not enough counterparties responded to fill, giving up"
jm_single().bc_interface.setQUSFail(False)
def test_auth_pub_not_found(setup_taker):
taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)])
async def test_auth_pub_not_found(self):
taker = await get_taker(
[(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)])
orderbook = copy.deepcopy(t_orderbook)
res = taker.initialize(orderbook, [])
res = await taker.initialize(orderbook, [])
taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same
maker_response = copy.deepcopy(t_maker_response)
utxos = [utxostr_to_utxo(x)[1] for x in [
@ -336,12 +370,12 @@ def test_auth_pub_not_found(setup_taker):
'utxo': utxos[i],
'confirms': 20} for i in range(3)]
jm_single().bc_interface.insert_fake_query_results(fake_query_results)
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
assert not res[0]
assert res[1] == "Not enough counterparties responded to fill, giving up"
jm_single().bc_interface.insert_fake_query_results(None)
@pytest.mark.parametrize(
@parametrize(
"schedule, highfee, toomuchcoins, minmakers, notauthed, ignored, nocommit",
[
([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
@ -375,7 +409,7 @@ def test_auth_pub_not_found(setup_taker):
([(0, 0, 5, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
2, False, ["J659UPUSLLjHJpaB", "J65z23xdjxJjC7er", 0], None), #test inadequate for sweep
])
def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
async def test_taker_init(self, schedule, highfee, toomuchcoins, minmakers,
notauthed, ignored, nocommit):
#these tests do not trigger utxo_retries
oldtakerutxoretries = jm_single().config.get("POLICY", "taker_utxo_retries")
@ -393,7 +427,7 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
oldminmakers = jm_single().config.get("POLICY", "minimum_makers")
jm_single().config.set("POLICY", "minimum_makers", str(minmakers))
jm_single().config.set("POLICY", "max_sweep_fee_change", "3.0")
taker = get_taker(schedule)
taker = await get_taker(schedule)
orderbook = copy.deepcopy(t_orderbook)
if highfee:
for o in orderbook:
@ -406,11 +440,11 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
if schedule[0][1] == 0.2:
#triggers calc-ing amount based on a fraction
jm_single().mincjamount = 50000000 #bigger than 40m = 0.2 * 200m
res = taker.initialize(orderbook, [])
res = await taker.initialize(orderbook, [])
assert res[0]
assert res[1] == jm_single().mincjamount
return clean_up()
res = taker.initialize(orderbook, [])
res = await taker.initialize(orderbook, [])
if toomuchcoins or ignored:
assert not res[0]
return clean_up()
@ -433,7 +467,7 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
#simulate the effect of a maker giving us a lot more utxos
taker.utxos["dummy_for_negative_change"] = [(struct.pack(b"B", a) *32, a+1) for a in range(7,12)]
with pytest.raises(ValueError) as e_info:
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
return clean_up()
if schedule[0][1] == 199856001:
#our own change is greater than zero but less than dust
@ -441,7 +475,7 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
#(because we need tx creation to complete), but trigger case by
#bumping dust threshold
jm_single().BITCOIN_DUST_THRESHOLD = 14000
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
#should have succeeded to build tx
assert res[0]
#change should be none
@ -457,7 +491,7 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
#given that real_cjfee = -0.002*x
#change = 200000000 - x - 1000 - 0.002*x
#x*1.002 = 1999999000; x = 199599800
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
assert not res[0]
assert res[1] == "Not enough counterparties responded to fill, giving up"
return clean_up()
@ -470,7 +504,7 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
taker.input_utxos = copy.deepcopy(t_utxos_by_mixdepth)[0]
for k,v in taker.input_utxos.items():
v["value"] = int(0.999805228 * v["value"])
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
assert res[0]
return clean_up()
if schedule[0][3] == "mteaYsGsLCL9a4cftZFTpGEWXNwZyDt5KS":
@ -478,11 +512,11 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
taker.input_utxos = copy.deepcopy(t_utxos_by_mixdepth)[0]
for k,v in taker.input_utxos.items():
v["value"] = int(0.999805028 * v["value"])
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
assert res[0]
return clean_up()
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
if minmakers != 2:
assert not res[0]
assert res[1] == "Not enough counterparties responded to fill, giving up"
@ -490,24 +524,24 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
assert res[0]
#re-calling will trigger "finished" code, since schedule is "complete".
res = taker.initialize(orderbook, [])
res = await taker.initialize(orderbook, [])
assert not res[0]
#some exception cases: no coinjoin address, no change address:
#donations not yet implemented:
taker.my_cj_addr = None
with pytest.raises(NotImplementedError) as e_info:
taker.prepare_my_bitcoin_data()
await taker.prepare_my_bitcoin_data()
with pytest.raises(NotImplementedError) as e_info:
a = taker.coinjoin_address()
taker.wallet_service.wallet.inject_addr_get_failure = True
taker.my_cj_addr = "dummy"
taker.my_change_addr = None
assert not taker.prepare_my_bitcoin_data()
assert not await taker.prepare_my_bitcoin_data()
#clean up
return clean_up()
def test_custom_change(setup_taker):
async def test_custom_change(self):
# create three random custom change addresses, one of each
# known type in Joinmarket.
privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,4)]]
@ -515,12 +549,12 @@ def test_custom_change(setup_taker):
addrs = [a.privkey_to_address(i) for a, i in zip([BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH], privs)]
schedule = [(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)]
for script, addr in zip(scripts, addrs):
taker = get_taker(schedule, custom_change=addr)
taker = await get_taker(schedule, custom_change=addr)
orderbook = copy.deepcopy(t_orderbook)
res = taker.initialize(orderbook, [])
res = await taker.initialize(orderbook, [])
taker.orderbook = copy.deepcopy(t_chosen_orders)
maker_response = copy.deepcopy(t_maker_response)
res = taker.receive_utxos(maker_response)
res = await taker.receive_utxos(maker_response)
assert res[0]
# ensure that the transaction created for signing has
# the address we intended with the right amount:
@ -542,12 +576,12 @@ def test_custom_change(setup_taker):
custom_change_found = True
assert custom_change_found
@pytest.mark.parametrize(
@parametrize(
"schedule_len",
[
(7),
(7,),
])
def test_unconfirm_confirm(setup_taker, schedule_len):
async def test_unconfirm_confirm(self, schedule_len):
"""These functions are: do-nothing by default (unconfirm, for Taker),
and merely update schedule index for confirm (useful for schedules/tumbles).
This tests that the on_finished callback correctly reports the fromtx
@ -555,14 +589,17 @@ def test_unconfirm_confirm(setup_taker, schedule_len):
The exception to the above is that the txd passed in must match
self.latest_tx, so we use a dummy value here for that.
"""
test_unconfirm_confirm = self.test_unconfirm_confirm_0.__wrapped__
class DummyTx(object):
pass
test_unconfirm_confirm.txflag = True
def finished_for_confirms(res, fromtx=False, waittime=0, txdetails=None):
assert res #confirmed should always send true
test_unconfirm_confirm.txflag = fromtx
taker = get_taker(schedule_len=schedule_len, on_finished=finished_for_confirms)
taker = await get_taker(
schedule_len=schedule_len, on_finished=finished_for_confirms)
taker.latest_tx = DummyTx()
taker.latest_tx.vout = "blah"
fake_txd = DummyTx()
@ -571,19 +608,19 @@ def test_unconfirm_confirm(setup_taker, schedule_len):
taker.unconfirm_callback(fake_txd, "b")
for i in range(schedule_len-1):
taker.schedule_index += 1
fromtx = taker.confirm_callback(fake_txd, "b", 1)
fromtx = await taker.confirm_callback(fake_txd, "b", 1)
assert test_unconfirm_confirm.txflag
taker.schedule_index += 1
fromtx = taker.confirm_callback(fake_txd, "b", 1)
fromtx = await taker.confirm_callback(fake_txd, "b", 1)
assert not test_unconfirm_confirm.txflag
@pytest.mark.parametrize(
@parametrize(
"dummyaddr, schedule",
[
("mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ",
[(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)])
[(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)],),
])
def test_on_sig(setup_taker, dummyaddr, schedule):
async def test_on_sig(self, dummyaddr, schedule):
#plan: create a new transaction with known inputs and dummy outputs;
#then, create a signature with various inputs, pass in in b64 to on_sig.
#in order for it to verify, the DummyBlockchainInterface will have to
@ -609,7 +646,7 @@ def test_on_sig(setup_taker, dummyaddr, schedule):
tx2 = bitcoin.mktx(utxos, outs)
#prepare the Taker with the right intermediate data
taker = get_taker(schedule=schedule)
taker = await get_taker(schedule=schedule)
taker.nonrespondants=["cp1", "cp2", "cp3"]
taker.latest_tx = tx
#my inputs are the first 2 utxos
@ -632,15 +669,15 @@ def test_on_sig(setup_taker, dummyaddr, schedule):
sig, msg = bitcoin.sign(tx2, 2, privs[2])
assert sig, "Failed to sign: " + msg
sig3 = b64encode(tx2.vin[2].scriptSig)
taker.on_sig("cp1", sig3)
await taker.on_sig("cp1", sig3)
#try sending the same sig again; should be ignored
taker.on_sig("cp1", sig3)
await taker.on_sig("cp1", sig3)
sig, msg = bitcoin.sign(tx2, 3, privs[3])
assert sig, "Failed to sign: " + msg
sig4 = b64encode(tx2.vin[3].scriptSig)
#try sending junk instead of cp2's correct sig
assert not taker.on_sig("cp2", str("junk")), "incorrectly accepted junk signature"
taker.on_sig("cp2", sig4)
assert not await taker.on_sig("cp2", str("junk")), "incorrectly accepted junk signature"
await taker.on_sig("cp2", sig4)
sig, msg = bitcoin.sign(tx2, 4, privs[4])
assert sig, "Failed to sign: " + msg
#Before completing with the final signature, which will trigger our own
@ -648,19 +685,19 @@ def test_on_sig(setup_taker, dummyaddr, schedule):
#prevent this signature being accepted.
dbci.setQUSFail(True)
sig5 = b64encode(tx2.vin[4].scriptSig)
assert not taker.on_sig("cp3", sig5), "incorrectly accepted sig5"
assert not await taker.on_sig("cp3", sig5), "incorrectly accepted sig5"
#allow it to succeed, and try again
dbci.setQUSFail(False)
#this should succeed and trigger the we-sign code
taker.on_sig("cp3", sig5)
await taker.on_sig("cp3", sig5)
@pytest.mark.parametrize(
@parametrize(
"schedule",
[
([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw")]),
([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw")],),
])
def test_auth_counterparty(setup_taker, schedule):
taker = get_taker(schedule=schedule)
async def test_auth_counterparty(self, schedule):
taker = await get_taker(schedule=schedule)
first_maker_response = t_maker_response["J659UPUSLLjHJpaB"]
utxo, auth_pub, cjaddr, changeaddr, sig, maker_pub = first_maker_response
auth_pub_tweaked = auth_pub[:8] + auth_pub[6:8] + auth_pub[10:]
@ -668,19 +705,3 @@ def test_auth_counterparty(setup_taker, schedule):
assert taker.auth_counterparty(sig, auth_pub, maker_pub)
assert not taker.auth_counterparty(sig, auth_pub_tweaked, maker_pub)
assert not taker.auth_counterparty(sig_tweaked, auth_pub, maker_pub)
@pytest.fixture(scope="module")
def setup_taker(request):
def clean():
from twisted.internet import reactor
for dc in reactor.getDelayedCalls():
dc.cancel()
request.addfinalizer(clean)
def cmtdatateardown():
shutil.rmtree("cmtdata")
request.addfinalizer(cmtdatateardown)
if not os.path.exists("cmtdata"):
os.makedirs("cmtdata")
load_test_config()
jm_single().bc_interface = DummyBlockchainInterface()
jm_single().config.set("BLOCKCHAIN", "network", "testnet")

115
test/jmclient/test_tx_creation.py

@ -5,11 +5,16 @@
p2(w)sh tests, these have been removed since Joinmarket
does not use this feature.'''
import pytest
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
import jmbitcoin as bitcoin
import pytest
from jmbase import get_log
from jmclient import load_test_config, jm_single, direct_send, estimate_tx_fee, compute_tx_locktime
@ -25,40 +30,46 @@ vpubs = ["03e9a06e539d6bf5cf1ca5c41b59121fa3df07a338322405a312c67b6349a707e9",
"028a2f126e3999ff66d01dcb101ab526d3aa1bf5cbdc4bde14950a4cead95f6fcb",
"02bea84d70e74f7603746b62d79bf035e16d982b56e6a1ee07dfd3b9130e8a2ad9"]
def test_all_same_priv(setup_tx_creation):
@pytest.mark.usefixtures("setup_tx_creation")
class AsyncioTestCase(IsolatedAsyncioTestCase):
async def test_all_same_priv(self):
#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']
wallets = await make_wallets(1, [[1,0,0,0,0]], 1)
wallet_service = wallets[0]['wallet']
#make another utxo on the same address
addrinwallet = wallet_service.get_addr(0,0,0)
addrinwallet = await 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)
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 = {}
for i, j in enumerate(ins):
scripts[i] = (insfull[j]["script"], insfull[j]["value"])
success, msg = wallet_service.sign_tx(tx, scripts)
success, msg = await wallet_service.sign_tx(tx, scripts)
assert success, msg
def test_verify_tx_input(setup_tx_creation):
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))
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)
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 = wallet_service.sign_tx(tx, scripts)
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,
@ -75,37 +86,41 @@ def test_verify_tx_input(setup_tx_creation):
witness = bitcoin.CScriptWitness([sig, pub]))
assert res
def test_absurd_fees(setup_tx_creation):
async def test_absurd_fees(self):
"""Test triggering of ValueError exception
if the transaction fees calculated from the blockchain
interface exceed the limit set in the config.
"""
jm_single().bc_interface.absurd_fees = True
#pay into it
wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
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 = wallet_service.select_utxos(0, amount)
ins_full = await wallet_service.select_utxos(0, amount)
with pytest.raises(ValueError) as e_info:
txid = make_sign_and_push(ins_full, wallet_service, amount, estimate_fee=True)
txid = await make_sign_and_push(
ins_full, wallet_service, amount, estimate_fee=True)
jm_single().bc_interface.absurd_fees = False
def test_create_sighash_txs(setup_tx_creation):
async def test_create_sighash_txs(self):
#non-standard hash codes:
for sighash in [bitcoin.SIGHASH_ANYONECANPAY + bitcoin.SIGHASH_SINGLE,
bitcoin.SIGHASH_NONE, bitcoin.SIGHASH_SINGLE]:
wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
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 = wallet_service.select_utxos(0, amount)
txid = make_sign_and_push(ins_full, wallet_service, amount, hashcode=sighash)
ins_full = await wallet_service.select_utxos(0, amount)
txid = await make_sign_and_push(
ins_full, wallet_service, amount, hashcode=sighash)
assert txid
#trigger insufficient funds
with pytest.raises(Exception) as e_info:
fake_utxos = wallet_service.select_utxos(4, 1000000000)
fake_utxos = await wallet_service.select_utxos(4, 1000000000)
def test_spend_p2wpkh(setup_tx_creation):
async def test_spend_p2wpkh(self):
#make 3 p2wpkh outputs from 3 privs
privs = [struct.pack(b'B', x) * 32 + b'\x01' for x in range(1, 4)]
pubs = [bitcoin.privkey_to_pubkey(priv) for priv in privs]
@ -113,20 +128,22 @@ def test_spend_p2wpkh(setup_tx_creation):
addresses = [str(bitcoin.CCoinAddress.from_scriptPubKey(
spk)) for spk in scriptPubKeys]
#pay into it
wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
wallets = await make_wallets(1, [[3, 0, 0, 0, 0]], 3)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
amount = 35000000
p2wpkh_ins = []
for i, addr in enumerate(addresses):
ins_full = wallet_service.select_utxos(0, amount)
txid = make_sign_and_push(ins_full, wallet_service, amount, output_addr=addr)
ins_full = await wallet_service.select_utxos(0, amount)
txid = await make_sign_and_push(
ins_full, wallet_service, amount, output_addr=addr)
assert txid
p2wpkh_ins.append((txid, 0))
txhex = jm_single().bc_interface.get_transaction(txid)
#wait for mining
jm_single().bc_interface.tick_forward_chain(1)
#random output address
output_addr = wallet_service.get_internal_addr(1)
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)
@ -140,7 +157,7 @@ def test_spend_p2wpkh(setup_tx_creation):
txid = jm_single().bc_interface.pushtx(tx.serialize())
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
rbf enabled in direct_send, then broadcast but
do not mine a block. Then create a re-spend of
@ -153,17 +170,18 @@ def test_spend_then_rbf(setup_tx_creation):
old_feerate = jm_single().config.get("POLICY", "tx_fees")
jm_single().config.set("POLICY", "tx_fees", "20000")
# set up a single wallet with some coins:
wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
wallets = await make_wallets(1, [[2, 0, 0, 0, 1]], 3)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
# ensure selection of two utxos, doesn't really matter
# but a more general case than only one:
amount = 350000000
# destination doesn't matter; this is easiest:
destn = wallet_service.get_internal_addr(1)
destn = await wallet_service.get_internal_addr(1)
# While `direct_send` usually encapsulates utxo selection
# for user, here we need to know what was chosen, hence
# we return the transaction object, not directly broadcast.
tx1 = direct_send(wallet_service, 0, [(destn, amount)],
tx1 = await direct_send(wallet_service, 0, [(destn, amount)],
answeryes=True,
return_transaction=True)
assert tx1
@ -174,7 +192,8 @@ def test_spend_then_rbf(setup_tx_creation):
# in order to sign on those utxos, we need their script and value.
scrs = {}
vals = {}
for u, details in wallet_service.get_utxos_by_mixdepth()[0].items():
for u, details in (
await wallet_service.get_utxos_by_mixdepth())[0].items():
if u in utxos:
scrs[u] = details["script"]
vals[u] = details["value"]
@ -186,7 +205,7 @@ def test_spend_then_rbf(setup_tx_creation):
push_succeed = jm_single().bc_interface.pushtx(tx1.serialize())
if push_succeed:
# mimics real operations with transaction monitor:
wallet_service.process_new_tx(tx1)
await wallet_service.process_new_tx(tx1)
else:
assert False
@ -195,7 +214,7 @@ def test_spend_then_rbf(setup_tx_creation):
# we set a larger fee rate.
jm_single().config.set("POLICY", "tx_fees", "30000")
# 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
# (modulo tiny 1 byte differences in signatures).
# Ordinary wallet operations would remove the first-spent utxos,
@ -210,7 +229,7 @@ def test_spend_then_rbf(setup_tx_creation):
total_input_val = sum(vals.values())
jm_single().config.set("POLICY", "tx_fees", old_feerate)
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}]
tx2 = bitcoin.mktx(utxos, outs, version=2,
locktime=compute_tx_locktime())
@ -218,9 +237,9 @@ def test_spend_then_rbf(setup_tx_creation):
for u in utxos:
spent_outs.append(bitcoin.CTxOut(nValue=vals[u],
scriptPubKey=scrs[u]))
psbt_unsigned = wallet_service.create_psbt_from_tx(tx2,
spent_outs=spent_outs)
signresultandpsbt, err = wallet_service.sign_psbt(
psbt_unsigned = await wallet_service.create_psbt_from_tx(
tx2, spent_outs=spent_outs)
signresultandpsbt, err = await wallet_service.sign_psbt(
psbt_unsigned.serialize(), with_sign_result=True)
assert not err
signresult, psbt_signed = signresultandpsbt
@ -230,11 +249,12 @@ def test_spend_then_rbf(setup_tx_creation):
# not allowed by Core:
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()
wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
wallets = await make_wallets(1, [[3, 0, 0, 0, 0]], 3)
wallet_service = wallets[0]['wallet']
await wallet_service.sync_wallet(fast=True)
mediantime = jm_single().bc_interface.get_best_block_median_time()
@ -253,13 +273,14 @@ def test_spend_freeze_script(setup_tx_creation):
#fund frozen funds address
amount = 100000000
funding_ins_full = wallet_service.select_utxos(0, amount)
funding_txid = make_sign_and_push(funding_ins_full, wallet_service, amount, output_addr=addr)
funding_ins_full = await wallet_service.select_utxos(0, amount)
funding_txid = await make_sign_and_push(
funding_ins_full, wallet_service, amount, output_addr=addr)
assert funding_txid
#spend frozen funds
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
outs = [{'value': amount - miner_fee, 'address': output_addr}]
tx = bitcoin.mktx([frozen_in], outs, locktime=addr_locktime+1)

56
test/jmclient/test_utxomanager.py

@ -1,8 +1,13 @@
from jmclient.wallet import UTXOManager
from test_storage import MockStorage
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
import jmclient
from commontest import DummyBlockchainInterface
@ -12,7 +17,24 @@ def select(unspent, value):
return unspent
def test_utxomanager_persist(setup_env_nodeps):
class AsyncioTestCase(IsolatedAsyncioTestCase):
def setUp(self):
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())
load_test_config()
def tearDown(self):
monkeypatch = MonkeyPatch()
monkeypatch.setattr(jmclient.configure,
'get_blockchain_interface_instance',
jmclient.configure._get_bc_interface_instance_)
async def test_utxomanager_persist(self):
""" Tests that the utxo manager's data is correctly
persisted and can be recreated from storage.
This persistence is currently only used for metadata
@ -56,9 +78,9 @@ def test_utxomanager_persist(setup_env_nodeps):
assert not um.is_disabled(txid, index+2)
um.disable_utxo(txid, index+2)
assert len(um.get_utxos_at_mixdepth(mixdepth)) == 1
assert len(um.get_utxos_at_mixdepth(mixdepth+1)) == 2
assert len(um.get_utxos_at_mixdepth(mixdepth+2)) == 0
assert len(await um.get_utxos_at_mixdepth(mixdepth)) == 1
assert len(await um.get_utxos_at_mixdepth(mixdepth+1)) == 2
assert len(await um.get_utxos_at_mixdepth(mixdepth+2)) == 0
assert um.get_balance_at_mixdepth(mixdepth) == value
assert um.get_balance_at_mixdepth(mixdepth+1) == value * 2
@ -77,15 +99,14 @@ def test_utxomanager_persist(setup_env_nodeps):
assert um.have_utxo(txid, index) == False
assert um.have_utxo(txid, index+1) == mixdepth + 1
assert len(um.get_utxos_at_mixdepth(mixdepth)) == 0
assert len(um.get_utxos_at_mixdepth(mixdepth+1)) == 1
assert len(await um.get_utxos_at_mixdepth(mixdepth)) == 0
assert len(await um.get_utxos_at_mixdepth(mixdepth+1)) == 1
assert um.get_balance_at_mixdepth(mixdepth) == 0
assert um.get_balance_at_mixdepth(mixdepth+1) == value
assert um.get_balance_at_mixdepth(mixdepth+2) == 0
def test_utxomanager_select(setup_env_nodeps):
async def test_utxomanager_select(self):
storage = MockStorage(None, 'wallet.jmdat', None, create=True)
UTXOManager.initialize(storage)
um = UTXOManager(storage, select)
@ -98,25 +119,18 @@ def test_utxomanager_select(setup_env_nodeps):
um.add_utxo(txid, index, path, value, mixdepth, 100)
assert len(um.select_utxos(mixdepth, value)) == 1
assert len(um.select_utxos(mixdepth+1, value)) == 0
assert len(await um.select_utxos(mixdepth, value)) == 1
assert len(await um.select_utxos(mixdepth+1, value)) == 0
um.add_utxo(txid, index+1, path, value, mixdepth, None)
assert len(um.select_utxos(mixdepth, value)) == 2
assert len(await um.select_utxos(mixdepth, value)) == 2
# 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(um.select_utxos(mixdepth, value)) == 2
assert len(await um.select_utxos(mixdepth, value)) == 2
# ensure that unconfirmed coins are not selected if
# dis-requested:
assert len(um.select_utxos(mixdepth, value, maxheight=105)) == 1
@pytest.fixture
def setup_env_nodeps(monkeypatch):
monkeypatch.setattr(jmclient.configure, 'get_blockchain_interface_instance',
lambda x: DummyBlockchainInterface())
load_test_config()
assert len(await um.select_utxos(mixdepth, value, maxheight=105)) == 1

712
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 os
import jmclient # install asyncioreactor
from twisted.internet import reactor
import jwt
import pytest
from twisted.internet import reactor, defer, task
from twisted.internet import defer, task
from twisted.web.client import readBody, Headers
from twisted.trial import unittest
from autobahn.twisted.websocket import WebSocketClientFactory, \
connectWS
@ -26,7 +28,7 @@ from jmclient import (
storage,
)
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_websocket import ClientTProtocol, test_tx_hex_1, test_tx_hex_txid
@ -45,7 +47,8 @@ class JMWalletDaemonT(JMWalletDaemon):
return True
return super().check_cookie(request, *args, **kwargs)
class WalletRPCTestBase(object):
class WalletRPCTestBase(TrialAsyncioTestCase):
""" Base class for set up of tests of the
Wallet RPC calls using the wallet_rpc.JMWalletDaemon service.
"""
@ -62,7 +65,7 @@ class WalletRPCTestBase(object):
# wallet type
wallet_cls = SegwitWallet
def setUp(self):
async def asyncSetUp(self):
load_test_config()
self.clean_out_wallet_files()
jm_single().bc_interface.tick_forward_chain_interval = 5
@ -94,11 +97,12 @@ class WalletRPCTestBase(object):
self.listener_rpc = r
self.listener_ws = s
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]],
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()
sync_wallets([self.daemon.services["wallet"]])
await sync_wallets([self.daemon.services["wallet"]])
# dummy tx example to force a notification event:
self.test_tx = CTransaction.deserialize(hextobin(test_tx_hex_1))
# auth token is not set at the start
@ -168,6 +172,7 @@ class WalletRPCTestBase(object):
def tearDown(self):
self.clean_out_wallet_files()
reactor.disconnectAll()
for dc in reactor.getDelayedCalls():
if not dc.cancelled:
dc.cancel()
@ -198,7 +203,7 @@ class ClientNotifTestFactory(WebSocketClientFactory):
self.callbackfn = kwargs.pop("callbackfn", None)
super().__init__(*args, **kwargs)
class TrialTestWRPC_WS(WalletRPCTestBase, unittest.TestCase):
class TrialTestWRPC_WS(WalletRPCTestBase):
""" 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,
test_tx_hex_txid)
class TrialTestWRPC_FB(WalletRPCTestBaseFB, unittest.TestCase):
class TrialTestWRPC_FB(WalletRPCTestBaseFB):
@defer.inlineCallbacks
def test_gettimelockaddress(self):
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:
assert self.daemon.coinjoin_state == CJ_NOT_RUNNING
class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase):
class TrialTestWRPC_DisplayWallet(WalletRPCTestBase):
@defer.inlineCallbacks
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"]
class TrialTestWRPC_JWT(WalletRPCTestBase, unittest.TestCase):
class TrialTestWRPC_JWT(WalletRPCTestBase):
@defer.inlineCallbacks
def do_request(self, agent, method, addr, body, handler, token):
headers = Headers({"Authorization": ["Bearer " + token]})

83
test/jmclient/test_wallets.py

@ -7,6 +7,8 @@ import binascii
from commontest import create_wallet_for_sync, make_sign_and_push
import json
from unittest import IsolatedAsyncioTestCase
import pytest
from jmbase import get_log, hextobin
from jmclient import (
@ -20,12 +22,12 @@ testdir = os.path.dirname(os.path.realpath(__file__))
log = get_log()
def do_tx(wallet_service, amount):
ins_full = wallet_service.select_utxos(0, amount)
cj_addr = wallet_service.get_internal_addr(1)
change_addr = wallet_service.get_internal_addr(0)
async def do_tx(wallet_service, amount):
ins_full = await wallet_service.select_utxos(0, amount)
cj_addr = await wallet_service.get_internal_addr(1)
change_addr = await wallet_service.get_internal_addr(0)
wallet_service.save_wallet()
txid = make_sign_and_push(ins_full,
txid = await make_sign_and_push(ins_full,
wallet_service,
amount,
output_addr=cj_addr,
@ -37,14 +39,28 @@ def do_tx(wallet_service, amount):
return txid
def test_query_utxo_set(setup_wallets):
def check_bip39_case(vectors, language="english"):
mnemo = Mnemonic(language)
for v in vectors:
code = mnemo.to_mnemonic(binascii.unhexlify(v[0]))
seed = binascii.hexlify(Mnemonic.to_seed(code, passphrase=v[4])).decode('ascii')
print('checking this phrase: ' + v[1])
assert mnemo.check(v[1])
assert v[1] == code
assert v[2] == seed
@pytest.mark.usefixtures("setup_wallets")
class AsyncioTestCase(IsolatedAsyncioTestCase):
async def test_query_utxo_set(self):
load_test_config()
jm_single().bc_interface.tick_forward_chain_interval = 1
wallet_service = create_wallet_for_sync([2, 3, 0, 0, 0],
wallet_service = await 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)
await wallet_service.sync_wallet(fast=True)
txid = await do_tx(wallet_service, 90000000)
txid2 = await do_tx(wallet_service, 20000000)
print("Got txs: ", txid, txid2)
res1 = jm_single().bc_interface.query_utxo_set(
(txid, 0), include_mempool=True)
@ -60,17 +76,13 @@ def test_query_utxo_set(setup_wallets):
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):
"""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')
def test_pushtx_errors(setup_wallets):
async def test_pushtx_errors(self):
"""Ensure pushtx fails return False
"""
badtx = b"\xaa\xaa"
@ -81,39 +93,28 @@ def test_pushtx_errors(setup_wallets):
#rebuild a valid jsonrpc inside the bci
load_test_config()
"""Tests mainly for wallet.py"""
def test_absurd_fee(setup_wallets):
"""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()
def check_bip39_case(vectors, language="english"):
mnemo = Mnemonic(language)
for v in vectors:
code = mnemo.to_mnemonic(binascii.unhexlify(v[0]))
seed = binascii.hexlify(Mnemonic.to_seed(code, passphrase=v[4])).decode('ascii')
print('checking this phrase: ' + v[1])
assert mnemo.check(v[1])
assert v[1] == code
assert v[2] == seed
"""
Sanity check of basic bip39 functionality for 12 words seed, copied from
https://github.com/trezor/python-mnemonic/blob/master/tests/test_mnemonic.py
"""
def test_bip39_vectors(setup_wallets):
"""
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)
vectors = vectors_full['english']
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 v in vectors:
v.append("TREZOR")
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)

30
test/jmclient/test_walletservice.py

@ -2,6 +2,9 @@
import os
import pytest
from unittest import IsolatedAsyncioTestCase
from jmbase import get_log
from jmclient import load_test_config, jm_single, \
WalletService
@ -16,17 +19,22 @@ log = get_log()
def set_freeze_reuse_config(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):
set_freeze_reuse_config(config_threshold)
# 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)
wallet_service.transaction_monitor()
await wallet_service.transaction_monitor()
balances = wallet_service.get_balance_by_mixdepth()
assert balances[0] == expected_final_balance
def test_address_reuse_freezing(setup_walletservice):
@pytest.mark.usefixtures("setup_walletservice")
class AsyncioTestCase(IsolatedAsyncioTestCase):
async def test_address_reuse_freezing(self):
""" 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
@ -45,26 +53,26 @@ def test_address_reuse_freezing(setup_walletservice):
context['cb_called'] += 1
# we must fund after initial sync (for imports), hence
# "populated" with no coins
wallet = get_populated_wallet(num=0)
wallet = await get_populated_wallet(num=0)
wallet_service = WalletService(wallet)
wallet_service.set_autofreeze_warning_cb(reuse_callback)
sync_test_wallet(True, wallet_service)
await sync_test_wallet(True, wallet_service)
for i in range(3):
fund_wallet_addr(wallet_service.wallet,
wallet_service.get_addr(0, 1, i))
await wallet_service.get_addr(0, 1, i))
# manually force the wallet service to see the new utxos:
wallet_service.transaction_monitor()
await wallet_service.transaction_monitor()
# check that with default status any reuse is blocked:
try_address_reuse(wallet_service, 0, 1, -1, 3 * 10**8)
await try_address_reuse(wallet_service, 0, 1, -1, 3 * 10**8)
assert context['cb_called'] == 1, "Failed to trigger freeze callback"
# check that above the threshold is allowed (1 sat less than funding)
try_address_reuse(wallet_service, 1, 1, 99999999, 4 * 10**8)
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:
try_address_reuse(wallet_service, 1, 0.99999998, 99999999, 4 * 10**8)
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"

42
test/jmclient/test_walletutils.py

@ -1,4 +1,8 @@
import pytest
from unittest import IsolatedAsyncioTestCase
from unittest_parametrize import parametrize, ParametrizedTestCase
from jmbitcoin import select_chain_params
from jmclient import (SegwitLegacyWallet, SegwitWallet, get_network,
jm_single, VolatileStorage, load_test_config)
@ -9,22 +13,26 @@ from jmclient.wallet_utils import (bip32pathparse, WalletView,
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
# The below signatures have all been verified against Electrum 4.0.9:
@pytest.mark.parametrize('seed, hdpath, walletcls, message, sig, addr', [
[b"\x01"*16, "m/84'/0'/0'/0/0", SegwitWallet, "hello",
class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
# The below signatures have all been verified against Electrum 4.0.9:
@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",
"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",
"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",
"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):
"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")
@ -32,20 +40,20 @@ def test_signmessage(seed, hdpath, walletcls, message, sig, addr):
walletcls.initialize(
storage, get_network(), entropy=seed, max_mixdepth=3)
wallet = walletcls(storage)
s, m, a = wallet_signmessage(wallet, hdpath, message,
out_str=False)
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")
def test_bip32_pathparse():
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"
walletbranch = 0
accounts = range(3)

2
test/jmclient/test_websocket.py

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

106
test/jmclient/test_yieldgenerator.py

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

Loading…
Cancel
Save