def ygmain(ygclass, nickserv_password='', gaplimit=6): import sys parser = OptionParser(usage='usage: %prog [options] [wallet file]') add_base_options(parser) # A note about defaults: # We want command line settings to override config settings. # This would naturally mean setting `default=` arguments here, to the # values in the config. # However, we cannot load the config until we know the datadir. # The datadir is a setting in the command line options, so we have to # call parser.parse_args() before we know the datadir. # Hence we do the following: set all modifyable-by-config arguments to # default "None" initially; call parse_args(); then call load_program_config # and override values of "None" with what is set in the config. # (remember, the joinmarket defaultconfig always sets every value, even if # the user doesn't). parser.add_option('-o', '--ordertype', action='store', type='string', dest='ordertype', default=None, help='type of order; can be either reloffer or absoffer') parser.add_option('-t', '--txfee', action='store', type='int', dest='txfee', default=None, help='minimum miner fee in satoshis') parser.add_option('-f', '--txfee-factor', action='store', type='float', dest='txfee_factor', default=None, help='variance around the average fee, decimal fraction') parser.add_option('-a', '--cjfee-a', action='store', type='string', dest='cjfee_a', default=None, help='requested coinjoin fee (absolute) in satoshis') parser.add_option('-r', '--cjfee-r', action='store', type='string', dest='cjfee_r', default=None, help='requested coinjoin fee (relative) as a decimal') parser.add_option('-j', '--cjfee-factor', action='store', type='float', dest='cjfee_factor', default=None, help='variance around the average fee, decimal fraction') parser.add_option('-p', '--password', action='store', type='string', dest='password', default=nickserv_password, help='irc nickserv password') parser.add_option('-s', '--minsize', action='store', type='int', dest='minsize', default=None, help='minimum coinjoin size in satoshis') parser.add_option('-z', '--size-factor', action='store', type='float', dest='size_factor', default=None, help='variance around all offer sizes, decimal fraction') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=gaplimit, help='gap limit for wallet, default=' + str(gaplimit)) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=None, help="highest mixdepth to use") (options, args) = parser.parse_args() # for string access, convert to dict: options = vars(options) if len(args) < 1: parser.error('Needs a wallet') sys.exit(EXIT_ARGERROR) load_program_config(config_path=options["datadir"]) # As per previous note, override non-default command line settings: for x in [ "ordertype", "txfee", "txfee_factor", "cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor" ]: if options[x] is None: options[x] = jm_single().config.get("YIELDGENERATOR", x) wallet_name = args[0] ordertype = options["ordertype"] txfee = int(options["txfee"]) txfee_factor = float(options["txfee_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 / 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: parser.error('You specified an incorrect offer type which ' +\ 'can be either reloffer or absoffer') sys.exit(EXIT_ARGERROR) nickserv_password = options["password"] if jm_single().bc_interface is None: jlog.error("Running yield generator requires configured " + "blockchain source.") sys.exit(EXIT_FAILURE) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, options["mixdepth"], wallet_password_stdin=options["wallet_password_stdin"], gap_limit=options["gaplimit"]) wallet_service = WalletService(wallet) while not wallet_service.synced: wallet_service.sync_wallet(fast=not options["recoversync"]) wallet_service.startService() txtype = wallet_service.get_txtype() if txtype == "p2wpkh": prefix = "sw0" elif txtype == "p2sh-p2wpkh": prefix = "sw" elif txtype == "p2pkh": prefix = "" else: jlog.error("Unsupported wallet type for yieldgenerator: " + txtype) sys.exit(EXIT_ARGERROR) ordertype = prefix + ordertype jlog.debug("Set the offer type string to: " + ordertype) maker = ygclass(wallet_service, [ txfee, cjfee_a, cjfee_r, ordertype, minsize, txfee_factor, cjfee_factor, size_factor ]) jlog.info('starting yield generator') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") if jm_single().config.get("SNICKER", "enabled") == "true": if jm_single().config.get("BLOCKCHAIN", "network") == "mainnet": jlog.error("You have enabled SNICKER on mainnet, this is not " "yet supported for yieldgenerators; either use " "signet/regtest/testnet, or run SNICKER manually " "with snicker/receive-snicker.py.") sys.exit(EXIT_ARGERROR) snicker_r = SNICKERReceiver(wallet_service) 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 if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet", "signet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, snickerfactory=snicker_factory, daemon=daemon)
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, see # test_full_coinjoin.py in this directory) 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", "txfee_factor", "cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor" ]: options[x] = jm_single().config.get("YIELDGENERATOR", x) ordertype = options["ordertype"] txfee = int(options["txfee"]) txfee_factor = float(options["txfee_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 / 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, cjfee_a, cjfee_r, ordertype, minsize, txfee_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 receive_snicker_main(): usage = """ Use this script to receive proposals for SNICKER coinjoins, parse them and then broadcast coinjoins that fit your criteria. See the SNICKER section of joinmarket.cfg to set your criteria. The only argument to this script is the (JM) wallet file against which to check. Once all proposals have been parsed, the script will quit. Usage: %prog [options] wallet file [proposal] """ parser = OptionParser(usage=usage) add_base_options(parser) parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=6, help='gap limit for wallet, default=6') parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=0, help="mixdepth to source coins from") parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) parser.add_option( '-n', '--no-upload', action='store_true', dest='no_upload', default=False, help="if set, we read the proposal from the command line" ) (options, args) = parser.parse_args() if len(args) < 1: parser.error('Needs a wallet file as argument') sys.exit(EXIT_ARGERROR) wallet_name = args[0] snicker_plugin = JMPluginService("SNICKER") load_program_config(config_path=options.datadir, plugin_services=[snicker_plugin]) check_and_start_tor() check_regtest() wallet_path = get_wallet_path(wallet_name, None) max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) snicker_plugin.start_plugin_logging(wallet_service) while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) wallet_service.startService() nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False snicker_r = SNICKERReceiver(wallet_service) if options.no_upload: proposal = args[1] snicker_r.process_proposals([proposal]) return servers = jm_single().config.get("SNICKER", "servers").split(",") snicker_pf = SNICKERClientProtocolFactory(snicker_r, servers, oneshot=True) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), None, snickerfactory=snicker_pf, daemon=daemon)
def test_snicker_e2e(setup_snicker, nw, wallet_structures, mean_amt, sdev_amt, amt, net_transfer): """ Test strategy: 1. create two wallets. 2. with wallet 1 (Receiver), create a single transaction tx1, from mixdepth 0 to 1. 3. with wallet 2 (Proposer), take pubkey of all inputs from tx1, and use them to create snicker proposals to the non-change out of tx1, in base64 and place in proposals.txt. 4. Receiver polls for proposals in the file manually (instead of twisted LoopingCall) and processes them. 5. Check for valid final transaction with broadcast. """ # TODO: Make this test work with native segwit wallets wallets = make_wallets(nw, wallet_structures, mean_amt, sdev_amt, wallet_cls=SegwitLegacyWallet) for w in wallets.values(): 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, btc.coins_to_satoshi(0.3), 0, wallet_r.script_to_addr(our_destn_script), accept_callback=dummy_accept_callback, info_callback=dummy_info_callback, return_transaction=True) 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) # 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()]) # Now create a proposal for every input index in tx1 # (version 1 proposals mean we source keys from the/an # ancestor transaction) propose_keys = [] for i in range(len(tx.vin)): # todo check access to pubkey sig, pub = [a for a in iter(tx.wit.vtxinwit[i].scriptWitness)] 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_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'])) prop_input_amt = prop_utxo['value'] # construct the arguments for the snicker proposal: our_input = list(prop_m_utxos)[0] # should be (txid, index) their_input = (txid1, txid1_index) our_input_utxo = btc.CMutableTxOut(prop_utxo['value'], prop_utxo['script']) fee_est = estimate_tx_fee(len(tx.vin), 2) change_spk = wallet_p.get_new_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL) encrypted_proposals = [] for p in propose_keys: # TODO: this can be a loop over all outputs, # not just one guessed output, if desired. encrypted_proposals.append( wallet_p.create_snicker_proposal(our_input, their_input, our_input_utxo, tx.vout[txid1_index], net_transfer, fee_est, priv, p, prop_utxo['script'], change_spk, version_byte=1) + b"," + bintohex(p).encode('utf-8')) with open(TEST_PROPOSALS_FILE, "wb") as f: f.write(b"\n".join(encrypted_proposals)) sR = SNICKERReceiver(wallet_r) sR.proposals_source = TEST_PROPOSALS_FILE # avoid clashing with mainnet sR.poll_for_proposals() assert len(sR.successful_txs) == 1 wallet_r.process_new_tx(sR.successful_txs[0]) end_utxos = 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