#! /usr/bin/env python '''Creates wallets and yield generators in regtest. Provides seed for joinmarket-qt test. This should be run via pytest, even though it's NOT part of the test-suite, because that makes it much easier to handle start up and shut down of the environment. Run it like: PYTHONPATH=.:$PYTHONPATH pytest \ --btcroot=/path/to/bitcoin/bin/ \ --btcpwd=123456abcdef --btcconf=/blah/bitcoin.conf \ --nirc=2 -s test/ygrunner.py ''' from twisted.internet import task from common import make_wallets import pytest import random from datetime import datetime from jmbase import jmprint from jmclient import YieldGeneratorBasic, load_test_config, jm_single,\ JMClientProtocolFactory, start_reactor, SegwitWallet,\ SegwitLegacyWallet, cryptoengine, SNICKERClientProtocolFactory, SNICKERReceiver from jmclient.wallet_utils import wallet_gettimelockaddress # For quicker testing, restrict the range of timelock # addresses to avoid slow load of multiple bots. # Note: no need to revert this change as ygrunner runs # in isolation. from jmclient import FidelityBondMixin FidelityBondMixin.TIMELOCK_ERA_YEARS = 2 FidelityBondMixin.TIMELOCK_EPOCH_YEAR = datetime.now().year FidelityBondMixin.TIMENUMBERS_PER_PUBKEY = 12 class MaliciousYieldGenerator(YieldGeneratorBasic): """Overrides, randomly, some maker functions to prevent taker continuing successfully (unless they can complete-with-subset). """ def set_maliciousness(self, frac, mtype=None): self.authmal = False self.txmal = False if mtype == "tx": self.txmal = True elif mtype == "auth": self.authmal = True else: self.txmal = True self.authmal = True self.mfrac = frac def on_auth_received(self, nick, offer, commitment, cr, amount, kphex): if self.authmal: if random.randint(1, 100) < self.mfrac: jmprint("Counterparty commitment rejected maliciously", "debug") return (False,) return super().on_auth_received(nick, offer, commitment, cr, amount, kphex) def on_tx_received(self, nick, tx, offerinfo): if self.txmal: if random.randint(1, 100) < self.mfrac: jmprint("Counterparty tx rejected maliciously", "debug") return (False, "malicious tx rejection") return super().on_tx_received(nick, tx, offerinfo) class DeterministicMaliciousYieldGenerator(YieldGeneratorBasic): """Overrides, randomly chosen persistently, some maker functions to prevent taker continuing successfully (unless they can complete-with-subset). """ def set_maliciousness(self, frac, mtype=None): self.authmal = False self.txmal = False if mtype == "tx": if random.randint(1, 100) < frac: self.txmal = True elif mtype == "auth": if random.randint(1, 100) < frac: self.authmal = True else: if random.randint(1, 100) < frac: self.txmal = True self.authmal = True def on_auth_received(self, nick, offer, commitment, cr, amount, kphex): if self.authmal: jmprint("Counterparty commitment rejected maliciously", "debug") return (False,) return super().on_auth_received(nick, offer, commitment, cr, amount, kphex) def on_tx_received(self, nick, tx, offerinfo): if self.txmal: jmprint("Counterparty tx rejected maliciously", "debug") return (False, "malicious tx rejection") return super().on_tx_received(nick, tx, offerinfo) @pytest.mark.parametrize( "num_ygs, wallet_structures, fb_indices, mean_amt, malicious, deterministic", [ # 1sp 3yg, honest makers, one maker has FB: (3, [[1, 3, 0, 0, 0]] * 4, [], 2, 0, False), # 1sp 3yg, malicious makers reject on auth and on tx 30% of time #(3, [[1, 3, 0, 0, 0]] * 4, 2, 30, False), # 1 sp 9 ygs, deterministically malicious 50% of time #(9, [[1, 3, 0, 0, 0]] * 10, 2, 50, True), ]) def test_start_ygs(setup_ygrunner, num_ygs, wallet_structures, fb_indices, mean_amt, malicious, deterministic): """Set up some wallets, for the ygs and 1 sp. Then start the ygs in background and publish the seed of the sp wallet for easy import into -qt """ if jm_single().config.get("POLICY", "native") == "true": walletclass = SegwitWallet else: # TODO add Legacy walletclass = SegwitLegacyWallet wallet_services = make_wallets(num_ygs + 1, wallet_structures=wallet_structures, mean_amt=mean_amt, walletclass=walletclass, fb_indices=fb_indices) #the sendpayment bot uses the last wallet in the list wallet_service = wallet_services[num_ygs]['wallet'] jmprint("\n\nTaker wallet seed : " + wallet_services[num_ygs]['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(num_ygs): jmprint("Maker seed: " + wallet_services[i]['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 if malicious: if deterministic: ygclass = DeterministicMaliciousYieldGenerator else: ygclass = MaliciousYieldGenerator for i in range(num_ygs): cfg = [txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize, txfee_contribution_factor, cjfee_factor, size_factor] wallet_service_yg = wallet_services[i]["wallet"] wallet_service_yg.startService() yg = ygclass(wallet_service_yg, cfg) if i in fb_indices: # create a timelocked address and fund it; # must be done after sync, so deferred: wallet_service_yg.timelock_funded = False sync_wait_loop = task.LoopingCall(get_addr_and_fund, yg) sync_wait_loop.start(1.0, now=False) if malicious: yg.set_maliciousness(malicious, mtype="tx") clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER") if jm_single().config.get("SNICKER", "enabled") == "true": snicker_r = SNICKERReceiver(wallet_service_yg) servers = jm_single().config.get("SNICKER", "servers").split(",") snicker_factory = SNICKERClientProtocolFactory(snicker_r, servers) else: snicker_factory = None nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False 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, snickerfactory=snicker_factory, daemon=daemon, rs=rs) def get_addr_and_fund(yg): """ This function allows us to create and publish a fidelity bond for a particular yield generator object after the wallet has reached a synced state and is therefore ready to serve up timelock addresses. We create the TL address, fund it, refresh the wallet and then republish our offers, which will also publish the new FB. """ if not yg.wallet_service.synced: return if yg.wallet_service.timelock_funded: return addr = wallet_gettimelockaddress(yg.wallet_service.wallet, "2023-11") print("Got timelockaddress: {}".format(addr)) # pay into it; amount is randomized for now. # Note that grab_coins already mines 1 block. fb_amt = random.randint(1, 5) jm_single().bc_interface.grab_coins(addr, fb_amt) # we no longer have to run this loop (TODO kill with nonlocal) yg.wallet_service.timelock_funded = True # force wallet to check for the new coins so the new # yg offers will include them: yg.wallet_service.transaction_monitor() # FIXME to coroutine call # publish a new offer: yg.offerlist = yg.create_my_orders() yg.fidelity_bond = yg.get_fidelity_bond_template() jmprint('updated offerlist={}'.format(yg.offerlist)) @pytest.fixture(scope="module") def setup_ygrunner(setup_regtest_bitcoind, setup_miniircd): load_test_config() jm_single().bc_interface.tick_forward_chain_interval = 10 jm_single().bc_interface.simulate_blocks() # handles the custom regtest hrp for bech32 cryptoengine.BTC_P2WPKH.VBYTE = 100