From 092ff46219e428d27633fcebe1c5e9ab6b285d5b Mon Sep 17 00:00:00 2001 From: zebra-lucky Date: Fri, 10 Oct 2025 07:06:33 +0300 Subject: [PATCH] fix test/unified/test_e2e_coinjoin.py --- test/unified/test_e2e_coinjoin.py | 281 ++++++++++++++++++------------ 1 file changed, 173 insertions(+), 108 deletions(-) diff --git a/test/unified/test_e2e_coinjoin.py b/test/unified/test_e2e_coinjoin.py index 109db88..0957f04 100644 --- a/test/unified/test_e2e_coinjoin.py +++ b/test/unified/test_e2e_coinjoin.py @@ -13,12 +13,19 @@ --btcpwd=123456abcdef --btcconf=/blah/bitcoin.conf \ -s test/e2e-coinjoin-test.py ''' -from twisted.internet import reactor, defer + +import asyncio + +import jmclient # install asyncioreactor +from twisted.internet import reactor, defer, task + from twisted.web.client import readBody, Headers -from common import make_wallets +from twisted import trial +from common import make_wallets, TrialTestCase import pytest import json from datetime import datetime +from _pytest.monkeypatch import MonkeyPatch from jmbase import (get_nontor_agent, BytesProducer, jmprint, get_log, stop_reactor) from jmclient import (YieldGeneratorBasic, load_test_config, jm_single, @@ -27,7 +34,7 @@ from jmclient import (YieldGeneratorBasic, load_test_config, jm_single, import jmclient from jmclient.wallet_rpc import api_version_string -pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") +pytestmark = pytest.mark.usefixtures("setup_miniircd", "setup_regtest_bitcoind") log = get_log() @@ -91,6 +98,7 @@ class RegtestJMClientProtocolFactory(JMClientProtocolFactory): hsd = "" for c in default_chans: if "type" in c and c["type"] == "onion": + continue # disable onion channel FIXME? onion_found = True if c["hidden_service_dir"] != "": hsd = c["hidden_service_dir"] @@ -166,99 +174,161 @@ class TWalletRPCManager(object): yield handler(body) return True -def test_start_yg_and_taker_setup(setup_onion_ygrunner): - """Set up some wallets, for the ygs and 1 taker. - Then start LN and the ygs in the background, then fire - a startup of a wallet daemon for the taker who then - makes a coinjoin payment. - """ - if jm_single().config.get("POLICY", "native") == "true": - walletclass = SegwitWallet - else: - # TODO add Legacy - walletclass = SegwitLegacyWallet - - start_bot_num, end_bot_num = [int(x) for x in jm_single().config.get( - "MESSAGING:onion", "regtest_count").split(",")] - num_ygs = end_bot_num - start_bot_num - # specify the number of wallets and bots of each type: - wallet_services = make_wallets(num_ygs + 1, - wallet_structures=[[1, 3, 0, 0, 0]] * (num_ygs + 1), - mean_amt=2.0, - walletclass=walletclass) - #the sendpayment bot uses the last wallet in the list - wallet_service = wallet_services[end_bot_num - 1]['wallet'] - jmprint("\n\nTaker wallet seed : " + wallet_services[end_bot_num - 1]['seed']) - # for manual audit if necessary, show the maker's wallet seeds - # also (note this audit should be automated in future) - jmprint("\n\nMaker wallet seeds: ") - for i in range(start_bot_num, end_bot_num): - jmprint("Maker seed: " + wallet_services[i - 1]['seed']) - jmprint("\n") - wallet_service.sync_wallet(fast=True) - ygclass = YieldGeneratorBasic - - # As per previous note, override non-default command line settings: - options = {} - for x in ["ordertype", "txfee_contribution", "txfee_contribution_factor", - "cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor"]: - options[x] = jm_single().config.get("YIELDGENERATOR", x) - ordertype = options["ordertype"] - txfee_contribution = int(options["txfee_contribution"]) - txfee_contribution_factor = float(options["txfee_contribution_factor"]) - cjfee_factor = float(options["cjfee_factor"]) - size_factor = float(options["size_factor"]) - if ordertype == 'reloffer': - cjfee_r = options["cjfee_r"] - # minimum size is such that you always net profit at least 20% - #of the miner fee - minsize = max(int(1.2 * txfee_contribution / float(cjfee_r)), - int(options["minsize"])) - cjfee_a = None - elif ordertype == 'absoffer': - cjfee_a = int(options["cjfee_a"]) - minsize = int(options["minsize"]) - cjfee_r = None - else: - assert False, "incorrect offertype config for yieldgenerator." - - txtype = wallet_service.get_txtype() - if txtype == "p2wpkh": - prefix = "sw0" - elif txtype == "p2sh-p2wpkh": - prefix = "sw" - elif txtype == "p2pkh": - prefix = "" - else: - assert False, "Unsupported wallet type for yieldgenerator: " + txtype - ordertype = prefix + ordertype +class E2ETCoinjoinTests(TrialTestCase): - for i in range(start_bot_num, end_bot_num): - cfg = [txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize, - txfee_contribution_factor, cjfee_factor, size_factor] - wallet_service_yg = wallet_services[i - 1]["wallet"] + def setUp(self): + # For quicker testing, restrict the range of timelock + # addresses to avoid slow load of multiple bots. + self._orig_TIMELOCK_ERA_YEARS = \ + jmclient.FidelityBondMixin.TIMELOCK_ERA_YEARS + self._orig_TIMELOCK_EPOCH_YEAR = \ + jmclient.FidelityBondMixin.TIMELOCK_EPOCH_YEAR + monkeypatch = MonkeyPatch() + monkeypatch.setattr( + jmclient.FidelityBondMixin, 'TIMELOCK_ERA_YEARS', 2) + monkeypatch.setattr( + jmclient.FidelityBondMixin, 'TIMELOCK_EPOCH_YEAR', + datetime.now().year) + # set doubled value of twisted.trial.util.DEFAULT_TIMEOUT_DURATION + self._orig_DEFAULT_TIMEOUT_DURATION = \ + trial.util.DEFAULT_TIMEOUT_DURATION + monkeypatch.setattr( + trial.util, 'DEFAULT_TIMEOUT_DURATION', + self._orig_DEFAULT_TIMEOUT_DURATION*2) - wallet_service_yg.startService() + load_test_config() + jm_single().bc_interface.tick_forward_chain_interval = 10 + jm_single().bc_interface.simulate_blocks() + self.mgr = None - yg = ygclass(wallet_service_yg, cfg) - clientfactory = RegtestJMClientProtocolFactory(yg, proto_type="MAKER") - # This ensures that the right rpc/port config is passed into the daemon, - # for this specific bot: - clientfactory.i = i - # This ensures that this bot knows which other bots are directory nodes: - clientfactory.set_directory_nodes(directory_node_indices) - nodaemon = jm_single().config.getint("DAEMON", "no_daemon") - daemon = bool(nodaemon) - #rs = True if i == num_ygs - 1 else False - start_reactor(jm_single().config.get("DAEMON", "daemon_host"), - jm_single().config.getint("DAEMON", "daemon_port"), - clientfactory, daemon=daemon, rs=False) - reactor.callLater(1.0, start_test_taker, wallet_services[end_bot_num - 1]['wallet'], end_bot_num, num_ygs) - reactor.run() - -@defer.inlineCallbacks -def start_test_taker(wallet_service, i, num_ygs): + def tearDown(self): + monkeypatch = MonkeyPatch() + monkeypatch.setattr( + jmclient.FidelityBondMixin, 'TIMELOCK_ERA_YEARS', + self._orig_TIMELOCK_ERA_YEARS) + monkeypatch.setattr( + jmclient.FidelityBondMixin, 'TIMELOCK_EPOCH_YEAR', + self._orig_TIMELOCK_EPOCH_YEAR) + monkeypatch.setattr( + trial.util, 'DEFAULT_TIMEOUT_DURATION', + self._orig_DEFAULT_TIMEOUT_DURATION) + reactor.disconnectAll() + for dc in reactor.getDelayedCalls(): + dc.cancel() + if self.mgr: + return self.mgr.stop() + + def test_start_yg_and_taker_setup(self): + d = defer.Deferred.fromFuture( + asyncio.ensure_future(self._start_yg_and_taker_setup())) + return d + + async def _start_yg_and_taker_setup(self): + """Set up some wallets, for the ygs and 1 taker. + Then start LN and the ygs in the background, then fire + a startup of a wallet daemon for the taker who then + makes a coinjoin payment. + """ + if jm_single().config.get("POLICY", "native") == "true": + walletclass = SegwitWallet + else: + # TODO add Legacy + walletclass = SegwitLegacyWallet + + start_bot_num, end_bot_num = [int(x) for x in jm_single().config.get( + "MESSAGING:onion", "regtest_count").split(",")] + num_ygs = end_bot_num - start_bot_num + # specify the number of wallets and bots of each type: + wallet_services = await make_wallets( + num_ygs + 1, + wallet_structures=[[1, 3, 0, 0, 0]] * (num_ygs + 1), + mean_amt=2.0, + walletclass=walletclass) + #the sendpayment bot uses the last wallet in the list + wallet_service = wallet_services[end_bot_num - 1]['wallet'] + jmprint("\n\nTaker wallet seed : " + wallet_services[end_bot_num - 1]['seed']) + # for manual audit if necessary, show the maker's wallet seeds + # also (note this audit should be automated in future) + jmprint("\n\nMaker wallet seeds: ") + for i in range(start_bot_num, end_bot_num): + jmprint("Maker seed: " + wallet_services[i - 1]['seed']) + jmprint("\n") + await wallet_service.sync_wallet(fast=True) + ygclass = YieldGeneratorBasic + + # As per previous note, override non-default command line settings: + options = {} + for x in ["ordertype", "txfee_contribution", "txfee_contribution_factor", + "cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor"]: + options[x] = jm_single().config.get("YIELDGENERATOR", x) + ordertype = options["ordertype"] + txfee_contribution = int(options["txfee_contribution"]) + txfee_contribution_factor = float(options["txfee_contribution_factor"]) + cjfee_factor = float(options["cjfee_factor"]) + size_factor = float(options["size_factor"]) + if ordertype == 'reloffer': + cjfee_r = options["cjfee_r"] + # minimum size is such that you always net profit at least 20% + #of the miner fee + minsize = max(int(1.2 * txfee_contribution / float(cjfee_r)), + int(options["minsize"])) + cjfee_a = None + elif ordertype == 'absoffer': + cjfee_a = int(options["cjfee_a"]) + minsize = int(options["minsize"]) + cjfee_r = None + else: + assert False, "incorrect offertype config for yieldgenerator." + + txtype = wallet_service.get_txtype() + if txtype == "p2wpkh": + prefix = "sw0" + elif txtype == "p2sh-p2wpkh": + prefix = "sw" + elif txtype == "p2pkh": + prefix = "" + else: + assert False, "Unsupported wallet type for yieldgenerator: " + txtype + + ordertype = prefix + ordertype + + for i in range(start_bot_num, end_bot_num): + cfg = [txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize, + txfee_contribution_factor, cjfee_factor, size_factor] + wallet_service_yg = wallet_services[i - 1]["wallet"] + + wallet_service_yg.startService() + + yg = ygclass(wallet_service_yg, cfg) + clientfactory = cf = RegtestJMClientProtocolFactory( + yg, proto_type="MAKER") + # This ensures that the right rpc/port config is passed into the daemon, + # for this specific bot: + clientfactory.i = i + # This ensures that this bot knows which other bots are directory nodes: + clientfactory.set_directory_nodes(directory_node_indices) + nodaemon = jm_single().config.getint("DAEMON", "no_daemon") + daemon = bool(nodaemon) + #rs = True if i == num_ygs - 1 else False + conn_pair = start_reactor( + jm_single().config.get("DAEMON", "daemon_host"), + jm_single().config.getint("DAEMON", "daemon_port"), + clientfactory, daemon=daemon, rs=False, gui=True) + wait_seconds = 60 + while wait_seconds > 0: + await asyncio.sleep(1) + wait_seconds -= 1 + if (cf.client and cf.proto_client + and getattr( + cf.proto_client, 'offers_ready_loop', None)): + if not cf.proto_client.offers_ready_loop.running: + break + await start_test_taker( + wallet_services[end_bot_num - 1]['wallet'], end_bot_num, num_ygs) + + +async def start_test_taker(wallet_service, i, num_ygs): # this rpc manager has auth disabled, # and the wallet_service is set manually, # so no unlock etc. @@ -270,7 +340,7 @@ def start_test_taker(wallet_service, i, num_ygs): # the auth token or start the websocket; so we must manually # sync the wallet, including bypassing any restart callback: def dummy_restart_callback(msg): - log.warn("Ignoring rescan request from backend wallet service: " + msg) + log.warning("Ignoring rescan request from backend wallet service: " + msg) mgr.daemon.services["wallet"].add_restart_callback(dummy_restart_callback) mgr.daemon.wallet_name = wallet_name mgr.daemon.services["wallet"].startService() @@ -286,7 +356,7 @@ def start_test_taker(wallet_service, i, num_ygs): # we decide a coinjoin destination, counterparty count and amount. # Choosing a destination in the wallet is a bit easier because # we can query the mixdepth balance at the end. - coinjoin_destination = mgr.daemon.services["wallet"].get_internal_addr(4) + coinjoin_destination = await mgr.daemon.services["wallet"].get_internal_addr(4) cj_amount = 22000000 def n_cps_from_n_ygs(n): if n > 4: @@ -297,6 +367,7 @@ def start_test_taker(wallet_service, i, num_ygs): n_cps = n_cps_from_n_ygs(num_ygs) # once the taker is finished we sanity check before # shutting down: + mgr.is_taker_finished = False def dummy_taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): jmprint("Taker is finished") @@ -304,7 +375,8 @@ def start_test_taker(wallet_service, i, num_ygs): mbal = mgr.daemon.services["wallet"].get_balance_by_mixdepth()[4] assert mbal == cj_amount jmprint("Funds: {} sats successfully arrived into mixdepth 4.".format(cj_amount)) - stop_reactor() + mgr.is_taker_finished = True + mgr.daemon.taker_finished = dummy_taker_finished mgr.start() agent = get_nontor_agent() @@ -317,19 +389,12 @@ def start_test_taker(wallet_service, i, num_ygs): "amount_sats": cj_amount, "counterparties": str(n_cps), "destination": coinjoin_destination}).encode()) - yield mgr.do_request(agent, b"POST", addr, body, - process_coinjoin_response) + d = defer.ensureDeferred(mgr.do_request(agent, b"POST", addr, body, process_coinjoin_response)) + + while not mgr.is_taker_finished: + await asyncio.sleep(1) + return d def process_coinjoin_response(response): json_body = json.loads(response.decode("utf-8")) - print("coinjoin response: {}".format(json_body)) - -@pytest.fixture -def setup_onion_ygrunner(monkeypatch): - # For quicker testing, restrict the range of timelock - # addresses to avoid slow load of multiple bots. - monkeypatch.setattr(jmclient.FidelityBondMixin, 'TIMELOCK_ERA_YEARS', 2) - monkeypatch.setattr(jmclient.FidelityBondMixin, 'TIMELOCK_EPOCH_YEAR', datetime.now().year) - load_test_config() - jm_single().bc_interface.tick_forward_chain_interval = 10 - jm_single().bc_interface.simulate_blocks() + log.warning("coinjoin response: {}".format(json_body))