def unconf_update(taker, schedulefile, tumble_log, addtolog=False): """Provide a Taker object, a schedulefile path for the current schedule, a logging instance for TUMBLE.log, and a parameter for whether to update TUMBLE.log. Makes the necessary state updates explained below, including to the wallet. Note that this is re-used for confirmation with addtolog=False, to avoid a repeated entry in the log. """ #on taker side, cache index update is only required after tx #push, to avoid potential of address reuse in case of a crash, #because addresses are not public until broadcast (whereas for makers, #they are public *during* negotiation). So updating the cache here #is sufficient taker.wallet.update_cache_index() #If honest-only was set, and we are going to continue (e.g. Tumbler), #we switch off the honest-only filter. We also wipe the honest maker #list, because the intention is to isolate the source of liquidity #to exactly those that participated, in 1 transaction (i.e. it's a 1 #transaction feature). This code is here because it *must* be called #before any continuation, even if confirm_callback happens before #unconfirm_callback taker.set_honest_only(False) taker.honest_makers = [] #We persist the fact that the transaction is complete to the #schedule file. Note that if a tweak to the schedule occurred, #it only affects future (non-complete) transactions, so the final #full record should always be accurate; but TUMBLE.log should be #used for checking what actually happened. completion_flag = 1 if not addtolog else taker.txid taker.schedule[taker.schedule_index][5] = completion_flag with open(schedulefile, "wb") as f: f.write(schedule_to_text(taker.schedule)) if addtolog: tumble_log.info("Completed successfully this entry:") #the log output depends on if it's to INTERNAL hrdestn = None if taker.schedule[taker.schedule_index][3] in ["INTERNAL", "addrask"]: hrdestn = taker.my_cj_addr #Whether sweep or not, the amt is not in satoshis; use taker data hramt = taker.cjamount tumble_log.info(human_readable_schedule_entry( taker.schedule[taker.schedule_index], hramt, hrdestn)) tumble_log.info("Txid was: " + taker.txid)
def main(): tumble_log = get_tumble_log(logsdir) (options, args) = get_tumbler_parser().parse_args() options_org = options options = vars(options) if len(args) < 1: print('Error: Needs a wallet file') sys.exit(0) load_program_config() #Load the wallet wallet_name = args[0] max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount'] wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe(wallet_path, wallet_name, max_mix_depth) if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": jm_single().bc_interface.synctype = "with-script" while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options['fastsync']) maxcjfee = get_max_cj_fee_values(jm_single().config, options_org) log.info( "Using maximum coinjoin fee limits per maker of {:.4%}, {} sat".format( *maxcjfee)) #Parse options and generate schedule #Output information to log files jm_single().mincjamount = options['mincjamount'] destaddrs = args[1:] print(destaddrs) #If the --restart flag is set we read the schedule #from the file, and filter out entries that are #already complete if options['restart']: res, schedule = get_schedule( os.path.join(logsdir, options['schedulefile'])) if not res: print("Failed to load schedule, name: " + str(options['schedulefile'])) print("Error was: " + str(schedule)) sys.exit(0) #This removes all entries that are marked as done schedule = [s for s in schedule if s[5] != 1] if isinstance(schedule[0][5], str) and len(schedule[0][5]) == 64: #ensure last transaction is confirmed before restart tumble_log.info("WAITING TO RESTART...") txid = schedule[0][5] restart_waiter(txid + ":0") #add 0 index because all have it #remove the already-done entry (this connects to the other TODO, #probably better *not* to truncate the done-already txs from file, #but simplest for now. schedule = schedule[1:] elif schedule[0][5] != 0: print("Error: first schedule entry is invalid.") sys.exit(0) with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) tumble_log.info("TUMBLE RESTARTING") else: #Create a new schedule from scratch schedule = get_tumble_schedule(options, destaddrs) tumble_log.info("TUMBLE STARTING") with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) print("Schedule written to logs/" + options['schedulefile']) tumble_log.info("With this schedule: ") tumble_log.info(pprint.pformat(schedule)) print("Progress logging to logs/TUMBLE.log") def filter_orders_callback(orders_fees, cjamount): """Decide whether to accept fees """ return tumbler_filter_orders_callback(orders_fees, cjamount, taker, options) def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): """on_finished_callback for tumbler; processing is almost entirely deferred to generic taker_finished in tumbler_support module, except here reactor signalling. """ sfile = os.path.join(logsdir, options['schedulefile']) tumbler_taker_finished_update(taker, sfile, tumble_log, options, res, fromtx, waittime, txdetails) if not fromtx: reactor.stop() elif fromtx != "unconfirmed": reactor.callLater(waittime * 60, clientfactory.getClient().clientStart) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.tick_forward_chain_interval = 10 jm_single().bc_interface.simulating = True jm_single().maker_timeout_sec = 15 #instantiate Taker with given schedule and run taker = Taker(wallet, schedule, order_chooser=options['order_choose_fn'], max_cj_fee=maxcjfee, callbacks=(filter_orders_callback, None, taker_finished), tdestaddrs=destaddrs) clientfactory = JMClientProtocolFactory(taker) 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"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def main(): tumble_log = get_tumble_log(logsdir) (options, args) = get_tumbler_parser().parse_args() options_org = options options = vars(options) if len(args) < 1: jmprint('Error: Needs a wallet file', "error") sys.exit(EXIT_ARGERROR) load_program_config(config_path=options['datadir']) if jm_single().bc_interface is None: jmprint('Error: Needs a blockchain source', "error") sys.exit(EXIT_FAILURE) check_regtest() #Load the wallet wallet_name = args[0] max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount'] if options['amtmixdepths'] > max_mix_depth: max_mix_depth = options['amtmixdepths'] wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options_org.wallet_password_stdin) wallet_service = WalletService(wallet) if wallet_service.rpc_error: sys.exit(EXIT_FAILURE) # in this script, we need the wallet synced before # logic processing for some paths, so do it now: while not wallet_service.synced: wallet_service.sync_wallet(fast=not options['recoversync']) # the sync call here will now be a no-op: wallet_service.startService() maxcjfee = get_max_cj_fee_values(jm_single().config, options_org) log.info( "Using maximum coinjoin fee limits per maker of {:.4%}, {} sat".format( *maxcjfee)) #Parse options and generate schedule #Output information to log files jm_single().mincjamount = options['mincjamount'] destaddrs = args[1:] for daddr in destaddrs: success, errmsg = validate_address(daddr) if not success: jmprint("Invalid destination address: " + daddr, "error") sys.exit(EXIT_ARGERROR) jmprint("Destination addresses: " + str(destaddrs), "important") #If the --restart flag is set we read the schedule #from the file, and filter out entries that are #already complete if options['restart']: res, schedule = get_schedule( os.path.join(logsdir, options['schedulefile'])) if not res: jmprint( "Failed to load schedule, name: " + str(options['schedulefile']), "error") jmprint("Error was: " + str(schedule), "error") sys.exit(EXIT_FAILURE) #This removes all entries that are marked as done schedule = [s for s in schedule if s[-1] != 1] # remaining destination addresses must be stored in Taker.tdestaddrs # in case of tweaks; note we can't change, so any passed on command # line must be ignored: if len(destaddrs) > 0: jmprint( "For restarts, destinations are taken from schedule file," " so passed destinations on the command line were ignored.", "important") if input("OK? (y/n)") != "y": sys.exit(EXIT_SUCCESS) destaddrs = [ s[3] for s in schedule if s[3] not in ["INTERNAL", "addrask"] ] jmprint( "Remaining destination addresses in restart: " + ",".join(destaddrs), "important") if isinstance(schedule[0][-1], str) and len(schedule[0][-1]) == 64: #ensure last transaction is confirmed before restart tumble_log.info("WAITING TO RESTART...") txid = schedule[0][-1] restart_waiter(txid) #remove the already-done entry (this connects to the other TODO, #probably better *not* to truncate the done-already txs from file, #but simplest for now. schedule = schedule[1:] elif schedule[0][-1] != 0: print("Error: first schedule entry is invalid.") sys.exit(EXIT_FAILURE) with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) tumble_log.info("TUMBLE RESTARTING") else: #Create a new schedule from scratch schedule = get_tumble_schedule(options, destaddrs, wallet.get_balance_by_mixdepth()) tumble_log.info("TUMBLE STARTING") with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) print("Schedule written to logs/" + options['schedulefile']) tumble_log.info("With this schedule: ") tumble_log.info(pprint.pformat(schedule)) # If tx_fees are set manually by CLI argument, override joinmarket.cfg: if int(options['txfee']) > 0: jm_single().config.set("POLICY", "tx_fees", str(options['txfee'])) # Dynamically estimate an expected tx fee for the whole tumbling run. # This is very rough: we guess with 2 inputs and 2 outputs each. fee_per_cp_guess = estimate_tx_fee(2, 2, txtype=wallet_service.get_txtype()) log.debug("Estimated miner/tx fee for each cj participant: " + str(fee_per_cp_guess)) # From the estimated tx fees, check if the expected amount is a # significant value compared the the cj amount involved_parties = len(schedule) # own participation in each CJ for item in schedule: involved_parties += item[2] # number of total tumble counterparties total_tumble_amount = int(0) max_mix_to_tumble = min(options['mixdepthsrc']+options['mixdepthcount'], \ max_mix_depth) for i in range(options['mixdepthsrc'], max_mix_to_tumble): total_tumble_amount += wallet_service.get_balance_by_mixdepth()[i] if total_tumble_amount == 0: raise ValueError( "No confirmed coins in the selected mixdepth(s). Quitting") exp_tx_fees_ratio = (involved_parties * fee_per_cp_guess) \ / total_tumble_amount if exp_tx_fees_ratio > 0.05: jmprint( 'WARNING: Expected bitcoin network miner fees for the whole ' 'tumbling run are roughly {:.1%}'.format(exp_tx_fees_ratio), "warning") if not options['restart'] and input( 'You might want to modify your tx_fee' ' settings in joinmarket.cfg. Still continue? (y/n):' )[0] != 'y': sys.exit('Aborted by user.') else: log.info("Estimated miner/tx fees for this coinjoin amount for the " "whole tumbling run: {:.1%}".format(exp_tx_fees_ratio)) print("Progress logging to logs/TUMBLE.log") def filter_orders_callback(orders_fees, cjamount): """Decide whether to accept fees """ return tumbler_filter_orders_callback(orders_fees, cjamount, taker) def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): """on_finished_callback for tumbler; processing is almost entirely deferred to generic taker_finished in tumbler_support module, except here reactor signalling. """ sfile = os.path.join(logsdir, options['schedulefile']) tumbler_taker_finished_update(taker, sfile, tumble_log, options, res, fromtx, waittime, txdetails) if not fromtx: reactor.stop() elif fromtx != "unconfirmed": reactor.callLater(waittime * 60, clientfactory.getClient().clientStart) #instantiate Taker with given schedule and run taker = Taker(wallet_service, schedule, maxcjfee, order_chooser=options['order_choose_fn'], callbacks=(filter_orders_callback, None, taker_finished), tdestaddrs=destaddrs) clientfactory = JMClientProtocolFactory(taker) 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"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def tumbler_taker_finished_update(taker, schedulefile, tumble_log, options, res, fromtx=False, waittime=0.0, txdetails=None): """on_finished_callback processing for tumbler. Note that this is *not* the full callback, but provides common processing across command line and other GUI versions. """ if fromtx == "unconfirmed": #unconfirmed event means transaction has been propagated, #we update state to prevent accidentally re-creating it in #any crash/restart condition unconf_update(taker, schedulefile, tumble_log, True) return if fromtx: if res: #this has no effect except in the rare case that confirmation #is immediate; also it does not repeat the log entry. unconf_update(taker, schedulefile, tumble_log, False) #note that Qt does not yet support 'addrask', so this is only #for command line script TODO if taker.schedule[taker.schedule_index + 1][3] == 'addrask': jm_single().debug_silence[0] = True print('\n'.join(['=' * 60] * 3)) print( 'Tumbler requires more addresses to stop amount correlation' ) print( 'Obtain a new destination address from your bitcoin recipient' ) print( ' for example click the button that gives a new deposit address' ) print('\n'.join(['=' * 60] * 1)) while True: destaddr = raw_input('insert new address: ') addr_valid, errormsg = validate_address(destaddr) if addr_valid: break print('Address ' + destaddr + ' invalid. ' + errormsg + ' try again') jm_single().debug_silence[0] = False taker.schedule[taker.schedule_index + 1][3] = destaddr taker.tdestaddrs.append(destaddr) waiting_message = "Waiting for: " + str(waittime) + " minutes." tumble_log.info(waiting_message) log.info(waiting_message) txd, txid = txdetails taker.wallet.remove_old_utxos(txd) taker.wallet.add_new_utxos(txd, txid) else: #a transaction failed; tumbler is aggressive in trying to #complete; we tweak the schedule from this point in the mixdepth, #then try again: tumble_log.info("Transaction attempt failed, tweaking schedule" " and trying again.") tumble_log.info("The paramaters of the failed attempt: ") tumble_log.info(str(taker.schedule[taker.schedule_index])) log.info("Schedule entry: " + str( taker.schedule[taker.schedule_index]) + \ " failed after timeout, trying again") taker.schedule_index -= 1 taker.schedule = tweak_tumble_schedule(options, taker.schedule, taker.schedule_index, taker.tdestaddrs) tumble_log.info("We tweaked the schedule, the new schedule is:") tumble_log.info(pprint.pformat(taker.schedule)) else: if not res: failure_msg = "Did not complete successfully, shutting down" tumble_log.info(failure_msg) log.info(failure_msg) else: log.info("All transactions completed correctly") tumble_log.info("Completed successfully the last entry:") #Whether sweep or not, the amt is not in satoshis; use taker data hramt = taker.cjamount tumble_log.info( human_readable_schedule_entry( taker.schedule[taker.schedule_index], hramt)) #copy of above, TODO refactor out taker.schedule[taker.schedule_index][5] = 1 with open(schedulefile, "wb") as f: f.write(schedule_to_text(taker.schedule))
def main(): tumble_log = get_tumble_log(logsdir) (options, args) = get_tumbler_parser().parse_args() options = vars(options) if len(args) < 1: parser.error('Needs a wallet file') sys.exit(0) load_program_config() #Load the wallet wallet_name = args[0] max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount'] if not os.path.exists(os.path.join('wallets', wallet_name)): wallet = Wallet(wallet_name, None, max_mix_depth) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = Wallet(wallet_name, pwd, max_mix_depth) except WalletError: print("Wrong password, try again.") continue except Exception as e: print("Failed to load wallet, error message: " + repr(e)) sys.exit(0) break sync_wallet(wallet, fast=options['fastsync']) #Parse options and generate schedule #Output information to log files jm_single().mincjamount = options['mincjamount'] destaddrs = args[1:] print(destaddrs) #If the --restart flag is set we read the schedule #from the file, and filter out entries that are #already complete if options['restart']: res, schedule = get_schedule(os.path.join(logsdir, options['schedulefile'])) if not res: print("Failed to load schedule, name: " + str( options['schedulefile'])) print("Error was: " + str(schedule)) sys.exit(0) #This removes all entries that are marked as done schedule = [s for s in schedule if s[5] != 1] if isinstance(schedule[0][5], str) and len(schedule[0][5]) == 64: #ensure last transaction is confirmed before restart tumble_log.info("WAITING TO RESTART...") txid = schedule[0][5] restart_waiter(txid + ":0") #add 0 index because all have it #remove the already-done entry (this connects to the other TODO, #probably better *not* to truncate the done-already txs from file, #but simplest for now. schedule = schedule[1:] elif schedule[0][5] != 0: print("Error: first schedule entry is invalid.") sys.exit(0) with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) tumble_log.info("TUMBLE RESTARTING") else: #Create a new schedule from scratch schedule = get_tumble_schedule(options, destaddrs) tumble_log.info("TUMBLE STARTING") with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) print("Schedule written to logs/" + options['schedulefile']) tumble_log.info("With this schedule: ") tumble_log.info(pprint.pformat(schedule)) print("Progress logging to logs/TUMBLE.log") def filter_orders_callback(orders_fees, cjamount): """Decide whether to accept fees """ return tumbler_filter_orders_callback(orders_fees, cjamount, taker, options) def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): """on_finished_callback for tumbler; processing is almost entirely deferred to generic taker_finished in tumbler_support module, except here reactor signalling. """ sfile = os.path.join(logsdir, options['schedulefile']) tumbler_taker_finished_update(taker, sfile, tumble_log, options, res, fromtx, waittime, txdetails) if not fromtx: reactor.stop() elif fromtx != "unconfirmed": reactor.callLater(waittime*60, clientfactory.getClient().clientStart) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.tick_forward_chain_interval = 10 jm_single().maker_timeout_sec = 5 #instantiate Taker with given schedule and run taker = Taker(wallet, schedule, order_chooser=weighted_order_choose, callbacks=(filter_orders_callback, None, taker_finished), tdestaddrs=destaddrs) clientfactory = JMTakerClientProtocolFactory(taker) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def tumbler_taker_finished_update(taker, schedulefile, tumble_log, options, res, fromtx=False, waittime=0.0, txdetails=None): """on_finished_callback processing for tumbler. Note that this is *not* the full callback, but provides common processing across command line and other GUI versions. """ if fromtx == "unconfirmed": #unconfirmed event means transaction has been propagated, #we update state to prevent accidentally re-creating it in #any crash/restart condition unconf_update(taker, schedulefile, tumble_log, True) return if fromtx: if res: #this has no effect except in the rare case that confirmation #is immediate; also it does not repeat the log entry. unconf_update(taker, schedulefile, tumble_log, False) #note that Qt does not yet support 'addrask', so this is only #for command line script TODO if taker.schedule[taker.schedule_index+1][3] == 'addrask': jm_single().debug_silence[0] = True print('\n'.join(['=' * 60] * 3)) print('Tumbler requires more addresses to stop amount correlation') print('Obtain a new destination address from your bitcoin recipient') print(' for example click the button that gives a new deposit address') print('\n'.join(['=' * 60] * 1)) while True: destaddr = raw_input('insert new address: ') addr_valid, errormsg = validate_address(destaddr) if addr_valid: break print( 'Address ' + destaddr + ' invalid. ' + errormsg + ' try again') jm_single().debug_silence[0] = False taker.schedule[taker.schedule_index+1][3] = destaddr taker.tdestaddrs.append(destaddr) waiting_message = "Waiting for: " + str(waittime) + " minutes." tumble_log.info(waiting_message) log.info(waiting_message) txd, txid = txdetails taker.wallet.remove_old_utxos(txd) taker.wallet.add_new_utxos(txd, txid) else: #a transaction failed, either because insufficient makers #(acording to minimum_makers) responded in Phase 1, or not all #makers responded in Phase 2. We'll first try to repeat without the #troublemakers. log.info("Schedule entry: " + str( taker.schedule[taker.schedule_index]) + \ " failed after timeout, trying again") taker.add_ignored_makers(taker.nonrespondants) #Is the failure in Phase 2? if not taker.latest_tx is None: #Now we have to set the specific group we want to use, and hopefully #they will respond again as they showed honesty last time. #Note that we must wipe the list first; other honest makers needn't #have the right settings (e.g. max cjamount), so can't be carried #over from earlier transactions. taker.honest_makers = [] taker.add_honest_makers(list(set( taker.maker_utxo_data.keys()).symmetric_difference( set(taker.nonrespondants)))) #If insufficient makers were honest, we can only tweak the schedule. #If enough were, we prefer the restart with them only: log.info("Inside a Phase 2 failure; number of honest respondants was: " + str(len(taker.honest_makers))) log.info("They were: " + str(taker.honest_makers)) if len(taker.honest_makers) >= jm_single().config.getint( "POLICY", "minimum_makers"): tumble_log.info("Transaction attempt failed, attempting to " "restart with subset.") tumble_log.info("The paramaters of the failed attempt: ") tumble_log.info(str(taker.schedule[taker.schedule_index])) #we must reset the number of counterparties, as well as fix who they #are; this is because the number is used to e.g. calculate fees. #cleanest way is to reset the number in the schedule before restart. taker.schedule[taker.schedule_index][2] = len(taker.honest_makers) retry_str = "Retrying with: " + str(taker.schedule[ taker.schedule_index][2]) + " counterparties." tumble_log.info(retry_str) log.info(retry_str) taker.set_honest_only(True) taker.schedule_index -= 1 return #There were not enough honest counterparties. #Tumbler is aggressive in trying to complete; we tweak the schedule #from this point in the mixdepth, then try again. tumble_log.info("Transaction attempt failed, tweaking schedule" " and trying again.") tumble_log.info("The paramaters of the failed attempt: ") tumble_log.info(str(taker.schedule[taker.schedule_index])) taker.schedule_index -= 1 taker.schedule = tweak_tumble_schedule(options, taker.schedule, taker.schedule_index, taker.tdestaddrs) tumble_log.info("We tweaked the schedule, the new schedule is:") tumble_log.info(pprint.pformat(taker.schedule)) else: if not res: failure_msg = "Did not complete successfully, shutting down" tumble_log.info(failure_msg) log.info(failure_msg) else: log.info("All transactions completed correctly") tumble_log.info("Completed successfully the last entry:") #Whether sweep or not, the amt is not in satoshis; use taker data hramt = taker.cjamount tumble_log.info(human_readable_schedule_entry( taker.schedule[taker.schedule_index], hramt)) #copy of above, TODO refactor out taker.schedule[taker.schedule_index][5] = 1 with open(schedulefile, "wb") as f: f.write(schedule_to_text(taker.schedule))