diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index ac53155..38cb040 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -6,7 +6,8 @@ import logging from .support import (calc_cj_fee, choose_sweep_orders, choose_orders, cheapest_order_choose, weighted_order_choose, - rand_norm_array, rand_pow_array, rand_exp_array, select, + rand_norm_array, rand_pow_array, rand_exp_array, + rand_weighted_choice, select, select_gradual, select_greedy, select_greediest, get_random_bytes, random_under_max_order_choose, select_one_utxo) @@ -37,7 +38,7 @@ from .output import generate_podle_error_string, fmt_utxos, fmt_utxo,\ fmt_tx_data from .schedule import (get_schedule, get_tumble_schedule, schedule_to_text, tweak_tumble_schedule, human_readable_schedule_entry, - schedule_to_text) + schedule_to_text, NO_ROUNDING) from .commitment_utils import get_utxo_info, validate_utxo_data, quit from .taker_utils import (tumbler_taker_finished_update, restart_waiter, restart_wait, get_tumble_log, direct_send, diff --git a/jmclient/jmclient/schedule.py b/jmclient/jmclient/schedule.py index 336f182..a598ecd 100644 --- a/jmclient/jmclient/schedule.py +++ b/jmclient/jmclient/schedule.py @@ -7,7 +7,7 @@ import random import sys from .configure import validate_address, jm_single -from .support import rand_exp_array, rand_norm_array, rand_weighted_choice +from .support import rand_exp_array, rand_norm_array, rand_pow_array, rand_weighted_choice """Utility functions for dealing with Taker schedules. - get_schedule(filename): @@ -20,6 +20,8 @@ from .support import rand_exp_array, rand_norm_array, rand_weighted_choice the chance of success on re-trying """ +NO_ROUNDING = 16 #max btc significant figures not including LN + def get_schedule(filename): with open(filename, "rb") as f: schedule = [] @@ -29,8 +31,8 @@ def get_schedule(filename): if sl.startswith("#"): continue try: - mixdepth, amount, makercount, destaddr, waittime, completed = \ - sl.split(',') + (mixdepth, amount, makercount, destaddr, waittime, + rounding, completed) = sl.split(',') except ValueError as e: return (False, "Failed to parse schedule line: " + sl) try: @@ -44,6 +46,7 @@ def get_schedule(filename): makercount = int(makercount) destaddr = destaddr.strip() waittime = float(waittime) + rounding = int(rounding) completed = completed.strip() if not len(completed) == 64: completed = int(completed) @@ -54,7 +57,7 @@ def get_schedule(filename): if not success: return (False, "Invalid address: " + destaddr + "," + errmsg) schedule.append([mixdepth, amount, makercount, destaddr, - waittime, completed]) + waittime, rounding, completed]) return (True, schedule) def get_amount_fractions(power, count): @@ -119,7 +122,8 @@ def get_tumble_schedule(options, destaddrs, mixdepth_balance_dict): 'wait': round(wait, 2), 'srcmixdepth': mixdepth, 'makercount': makercount, - 'destination': 'INTERNAL' + 'destination': 'INTERNAL', + 'rounding': NO_ROUNDING } tx_list.append(tx) ### stage 2 coinjoins, which create a number of random-amount coinjoins from each mixdepth @@ -138,17 +142,25 @@ def get_tumble_schedule(options, destaddrs, mixdepth_balance_dict): makercounts = rand_norm_array(options['makercountrange'][0], options['makercountrange'][1], txcount) makercounts = lower_bounded_int(makercounts, options['minmakercount']) - - for amount_fraction, wait, makercount in zip(amount_fractions, waits, - makercounts): + do_rounds = [random.random() < options['rounding_chance'] for _ in range(txcount)] + for amount_fraction, wait, makercount, do_round in zip(amount_fractions, waits, + makercounts, do_rounds): + rounding = NO_ROUNDING + if do_round: + weight_sum = 1.0*sum(options['rounding_sigfig_weights']) + weight_prob = [a/weight_sum for a in options['rounding_sigfig_weights']] + rounding = rand_weighted_choice(len(weight_prob), weight_prob) + 1 tx = {'amount_fraction': amount_fraction, 'wait': round(wait, 2), 'srcmixdepth': lowest_initial_filled_mixdepth + m + options['mixdepthsrc'] + 1, 'makercount': makercount, - 'destination': 'INTERNAL'} + 'destination': 'INTERNAL', + 'rounding': rounding + } tx_list.append(tx) #reset the final amt_frac to zero, as it's the last one for this mixdepth: tx_list[-1]['amount_fraction'] = 0 + tx_list[-1]['rounding'] = NO_ROUNDING addrask = options['addrcount'] - len(destaddrs) external_dest_addrs = ['addrask'] * addrask + destaddrs[::-1] @@ -172,7 +184,8 @@ def get_tumble_schedule(options, destaddrs, mixdepth_balance_dict): schedule = [] for t in tx_list: schedule.append([t['srcmixdepth'], t['amount_fraction'], - t['makercount'], t['destination'], t['wait'], 0]) + t['makercount'], t['destination'], t['wait'], + t['rounding'], 0]) return schedule def tweak_tumble_schedule(options, schedule, last_completed, destaddrs=[]): @@ -237,6 +250,8 @@ def human_readable_schedule_entry(se, amt=None, destn=None): amt_info = str(amt) if amt else str(se[1]) hrs.append("sends amount: " + amt_info + " satoshis") dest_info = destn if destn else str(se[3]) + hrs.append(("rounded to " + str(se[5]) + " significant figures" + if se[5] != NO_ROUNDING else "without rounding")) hrs.append("to destination address: " + dest_info) hrs.append("after coinjoin with " + str(se[2]) + " counterparties.") return ", ".join(hrs) diff --git a/jmclient/jmclient/taker.py b/jmclient/jmclient/taker.py index 62374d3..89886ad 100644 --- a/jmclient/jmclient/taker.py +++ b/jmclient/jmclient/taker.py @@ -21,6 +21,7 @@ from jmclient.podle import generate_podle, get_podle_commitments, PoDLE from jmclient.wallet_service import WalletService from .output import generate_podle_error_string from .cryptoengine import EngineError +from .schedule import NO_ROUNDING jlog = get_log() @@ -168,6 +169,7 @@ class Taker(object): si = self.schedule[self.schedule_index] self.mixdepth = si[0] self.cjamount = si[1] + rounding = si[5] #non-integer coinjoin amounts are treated as fractions #this is currently used by the tumbler algo if isinstance(self.cjamount, float): @@ -179,6 +181,9 @@ class Taker(object): )[self.mixdepth] #reset to satoshis self.cjamount = int(self.cjamount * self.mixdepthbal) + if rounding != NO_ROUNDING: + self.cjamount = round_to_significant_figures(self.cjamount, + rounding) if self.cjamount < jm_single().mincjamount: jlog.info("Coinjoin amount too low, bringing up to: " + str( jm_single().mincjamount)) @@ -1222,3 +1227,13 @@ class P2EPTaker(Taker): self.self_sign_and_push() # returning False here is not an error condition, only stops processing. return (False, "OK") + +def round_to_significant_figures(d, sf): + '''Rounding number d to sf significant figures in base 10''' + for p in range(-10, 15): + power10 = 10**p + if power10 > d: + sf_power10 = 10**sf + sigfiged = int(round(d/power10*sf_power10)*power10/sf_power10) + return sigfiged + raise RuntimeError() diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index dae40c4..b40943e 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -195,7 +195,7 @@ def unconf_update(taker, schedulefile, tumble_log, addtolog=False): #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 + taker.schedule[taker.schedule_index][-1] = completion_flag with open(schedulefile, "wb") as f: f.write(schedule_to_text(taker.schedule)) diff --git a/jmclient/test/test_coinjoin.py b/jmclient/test/test_coinjoin.py index 1031d35..ed8bd0e 100644 --- a/jmclient/test/test_coinjoin.py +++ b/jmclient/test/test_coinjoin.py @@ -13,7 +13,8 @@ from twisted.internet import reactor from jmbase import get_log from jmclient import load_program_config, jm_single,\ - YieldGeneratorBasic, Taker, LegacyWallet, SegwitLegacyWallet + YieldGeneratorBasic, Taker, LegacyWallet, SegwitLegacyWallet,\ + NO_ROUNDING from jmclient.podle import set_commitment_file from commontest import make_wallets, binarize_tx from test_taker import dummy_filter_orderbook @@ -139,8 +140,8 @@ def test_simple_coinjoin(monkeypatch, tmpdir, setup_cj, wallet_cls): assert len(orderbook) == MAKER_NUM cj_amount = int(1.1 * 10**8) - # mixdepth, amount, counterparties, dest_addr, waittime - schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0)] + # mixdepth, amount, counterparties, dest_addr, waittime, rounding + 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, @@ -183,8 +184,8 @@ def test_coinjoin_mixdepth_wrap_taker(monkeypatch, tmpdir, setup_cj): assert len(orderbook) == MAKER_NUM cj_amount = int(1.1 * 10**8) - # mixdepth, amount, counterparties, dest_addr, waittime - schedule = [(4, cj_amount, MAKER_NUM, 'INTERNAL', 0)] + # mixdepth, amount, counterparties, dest_addr, waittime, rounding + 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, @@ -239,8 +240,8 @@ def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj): assert len(orderbook) == MAKER_NUM cj_amount = int(1.1 * 10**8) - # mixdepth, amount, counterparties, dest_addr, waittime - schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0)] + # mixdepth, amount, counterparties, dest_addr, waittime, rounding + 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, @@ -301,8 +302,8 @@ def test_coinjoin_mixed_maker_addresses(monkeypatch, tmpdir, setup_cj, orderbook = create_orderbook(makers) cj_amount = int(1.1 * 10**8) - # mixdepth, amount, counterparties, dest_addr, waittime - schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0)] + # mixdepth, amount, counterparties, dest_addr, waittime, rounding + 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, diff --git a/jmclient/test/test_schedule.py b/jmclient/test/test_schedule.py index 1ea6d72..1c135e6 100644 --- a/jmclient/test/test_schedule.py +++ b/jmclient/test/test_schedule.py @@ -10,30 +10,30 @@ from jmclient import (get_schedule, get_tumble_schedule, import os valids = """#sample for testing -1, 110000000, 3, INTERNAL, 0, 1 -0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 9.88, 0 +1, 110000000, 3, INTERNAL, 0, 16, 1 +0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 9.88, 16, 0 """ invalids1 = """#sample for testing -1, 110000000, 3, 5, INTERNAL, 0 +1, 110000000, 3, 5, INTERNAL, 16, 0 #pointless comment here; following line has trailing spaces -0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw ,0, 0, +0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw ,0, 16, 0, """ invalids2 = """#sample for testing -1, 110000000, notinteger, INTERNAL, 0, 0 -0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 0 +1, 110000000, notinteger, INTERNAL, 0, 16, 0 +0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 16, 0 """ invalids3 = """#sample for testing -1, 110000000, 3, INTERNAL, 0, 0 -0, notinteger, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 0 +1, 110000000, 3, INTERNAL, 0, 16, 0 +0, notinteger, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 16, 0 """ #invalid address invalids4 = """#sample for testing -1, 110000000, 3, INTERNAL, 0, 0 -0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qq, 0, 0 +1, 110000000, 3, INTERNAL, 0, 16, 0 +0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qq, 0, 16, 0 """ @@ -71,6 +71,8 @@ def get_options(): options.stage1_timelambda_increase = 3 options.mincjamount = 1000000 options.liquiditywait = 5 + options.rounding_chance = 0.25 + options.rounding_sigfig_weights = (55, 15, 25, 65, 40) options = vars(options) return options diff --git a/jmclient/test/test_taker.py b/jmclient/test/test_taker.py index a9707cb..1100678 100644 --- a/jmclient/test/test_taker.py +++ b/jmclient/test/test_taker.py @@ -15,7 +15,7 @@ import struct from base64 import b64encode from jmclient import load_program_config, jm_single, set_commitment_file,\ get_commitment_file, SegwitLegacyWallet, Taker, VolatileStorage,\ - get_network, WalletService + get_network, WalletService, NO_ROUNDING from taker_test_data import t_utxos_by_mixdepth, t_orderbook,\ t_maker_response, t_chosen_orders, t_dummy_ext @@ -124,7 +124,7 @@ def get_taker(schedule=None, schedule_len=0, on_finished=None, filter_orders=None): if not schedule: #note, for taker.initalize() this will result in junk - schedule = [['a', 'b', 'c', 'd', 'e']]*schedule_len + schedule = [['a', 'b', 'c', 'd', 'e', 'f']]*schedule_len 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 @@ -138,11 +138,11 @@ def test_filter_rejection(setup_taker): print("calling filter orders rejection") return False taker = get_taker(filter_orders=filter_orders_reject) - taker.schedule = [[0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0]] + taker.schedule = [[0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]] res = taker.initialize(t_orderbook) assert not res[0] taker = get_taker(filter_orders=filter_orders_reject) - taker.schedule = [[0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0]] + taker.schedule = [[0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]] res = taker.initialize(t_orderbook) assert not res[0] @@ -171,7 +171,7 @@ def test_make_commitment(setup_taker, failquery, external): jm_single().config.set("POLICY", "taker_utxo_amtpercent", "20") mixdepth = 0 amount = 110000000 - taker = get_taker([(mixdepth, amount, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw")]) + taker = get_taker([(mixdepth, amount, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", NO_ROUNDING)]) taker.cjamount = amount taker.input_utxos = t_utxos_by_mixdepth[0] if failquery: @@ -180,7 +180,7 @@ def test_make_commitment(setup_taker, failquery, external): clean_up() def test_not_found_maker_utxos(setup_taker): - taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)]) + taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)]) orderbook = copy.deepcopy(t_orderbook) res = taker.initialize(orderbook) taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same @@ -192,7 +192,7 @@ def test_not_found_maker_utxos(setup_taker): jm_single().bc_interface.setQUSFail(False) def test_auth_pub_not_found(setup_taker): - taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)]) + taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)]) orderbook = copy.deepcopy(t_orderbook) res = taker.initialize(orderbook) taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same @@ -214,33 +214,33 @@ def test_auth_pub_not_found(setup_taker): @pytest.mark.parametrize( "schedule, highfee, toomuchcoins, minmakers, notauthed, ignored, nocommit", [ - ([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw")], False, False, + ([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False, 2, False, None, None), - ([(0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)], False, False, + ([(0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False, 2, False, None, None), #sweep - ([(0, 0.2, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)], False, False, + ([(0, 0.2, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False, 2, False, None, None), #tumble style non-int amounts #edge case triggers that don't fail - ([(0, 0, 4, "mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf", 0)], False, False, + ([(0, 0, 4, "mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf", 0, NO_ROUNDING)], False, False, 2, False, None, None), #sweep rounding error case - ([(0, 199850001, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)], False, False, + ([(0, 199850001, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False, 2, False, None, None), #trigger sub dust change for taker #edge case triggers that do fail - ([(0, 199850000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)], False, False, + ([(0, 199850000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False, 2, False, None, None), #trigger negative change - ([(0, 199599800, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)], False, False, + ([(0, 199599800, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False, 2, False, None, None), #trigger sub dust change for maker - ([(0, 20000000, 3, "INTERNAL", 0)], True, False, + ([(0, 20000000, 3, "INTERNAL", 0, NO_ROUNDING)], True, False, 2, False, None, None), #test high fee - ([(0, 20000000, 3, "INTERNAL", 0)], False, False, + ([(0, 20000000, 3, "INTERNAL", 0, NO_ROUNDING)], False, False, 7, False, None, None), #test not enough cp - ([(0, 80000000, 3, "INTERNAL", 0)], False, False, + ([(0, 80000000, 3, "INTERNAL", 0, NO_ROUNDING)], False, False, 2, False, None, "30000"), #test failed commit - ([(0, 20000000, 3, "INTERNAL", 0)], False, False, + ([(0, 20000000, 3, "INTERNAL", 0, NO_ROUNDING)], False, False, 2, True, None, None), #test unauthed response - ([(0, 5000000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)], False, True, + ([(0, 5000000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, True, 2, False, None, None), #test too much coins - ([(0, 0, 5, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)], False, False, + ([(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, diff --git a/scripts/cli_options.py b/scripts/cli_options.py index 298e9bb..d83b4bc 100644 --- a/scripts/cli_options.py +++ b/scripts/cli_options.py @@ -385,6 +385,24 @@ def get_tumbler_parser(): 'mixdepthsrc + number of mixdepths to tumble ' 'have been used.', default=-1) + parser.add_option( + '--rounding-chance', + action='store', + type='float', + dest='rounding_chance', + help='probability of non-sweep coinjoin amount being rounded, default=0.25 (25%)', + default=0.25) + parser.add_option( + '--rounding-sigfig-weights', + type='float', + nargs=5, + dest='rounding_sigfig_weights', + default=(55, 15, 25, 65, 40), + help= + "If rounding happens (determined by --rounding-chance) then the weights of how many" + " significant figures to round to. The five values refer to the probability of" + " rounding to one, two, three, four and five significant figures respectively." + " default=(55, 15, 25, 65, 40)") add_common_options(parser) return parser diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index 40026aa..87a7038 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -74,7 +74,8 @@ from jmclient import load_program_config, get_network,\ get_blockchain_interface_instance, direct_send, WalletService,\ RegtestBitcoinCoreInterface, tumbler_taker_finished_update,\ get_tumble_log, restart_wait, tumbler_filter_orders_callback,\ - wallet_generate_recover_bip39, wallet_display, get_utxos_enabled_disabled + wallet_generate_recover_bip39, wallet_display, get_utxos_enabled_disabled,\ + NO_ROUNDING from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\ config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\ PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\ @@ -561,17 +562,17 @@ class SpendTab(QWidget): #follow restart logic #1. filter out complete: self.spendstate.loaded_schedule = [ - s for s in self.spendstate.loaded_schedule if s[5] != 1] + s for s in self.spendstate.loaded_schedule if s[-1] != 1] #reload destination addresses self.tumbler_destaddrs = [x[3] for x in self.spendstate.loaded_schedule if x not in ["INTERNAL", "addrask"]] #2 Check for unconfirmed - if isinstance(self.spendstate.loaded_schedule[0][5], str) and len( - self.spendstate.loaded_schedule[0][5]) == 64: + if isinstance(self.spendstate.loaded_schedule[0][-1], str) and len( + self.spendstate.loaded_schedule[0][-1]) == 64: #ensure last transaction is confirmed before restart tumble_log.info("WAITING TO RESTART...") mainWindow.statusBar().showMessage("Waiting for confirmation to restart..") - txid = self.spendstate.loaded_schedule[0][5] + txid = self.spendstate.loaded_schedule[0][-1] #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. @@ -647,7 +648,7 @@ class SpendTab(QWidget): #note 'amount' is integer, so not interpreted as fraction #see notes in sample testnet schedule for format self.spendstate.loaded_schedule = [[mixdepth, amount, makercount, - destaddr, 0, 0]] + destaddr, 0, NO_ROUNDING, 0]] self.spendstate.updateType('single') self.spendstate.updateRun('running') self.startJoin() diff --git a/scripts/qtsupport.py b/scripts/qtsupport.py index 9127b70..36808d3 100644 --- a/scripts/qtsupport.py +++ b/scripts/qtsupport.py @@ -631,24 +631,34 @@ class SchFinishPage(QWizardPage): 'Minimum transaction count', 'Min coinjoin amount', 'Response wait time', - 'Stage 1 transaction wait time increase'] + 'Stage 1 transaction wait time increase', + 'Rounding Chance'] + for w in ["One", "Two", "Three", "Four", "Five"]: + sN += [w + " significant figures rounding weight"] #Tooltips sH = ["Standard deviation of the number of makers to use in each " - "transaction.", - "Standard deviation of the number of transactions to use in each " - "mixdepth", - "A parameter to control the random coinjoin sizes.", - "The lowest allowed number of maker counterparties.", - "The lowest allowed number of transactions in one mixdepth.", - "The lowest allowed size of any coinjoin, in satoshis.", - "The time in seconds to wait for response from counterparties.", - "The factor increase in wait time for stage 1 sweep coinjoins"] + "transaction.", + "Standard deviation of the number of transactions to use in each " + "mixdepth", + "A parameter to control the random coinjoin sizes.", + "The lowest allowed number of maker counterparties.", + "The lowest allowed number of transactions in one mixdepth.", + "The lowest allowed size of any coinjoin, in satoshis.", + "The time in seconds to wait for response from counterparties.", + "The factor increase in wait time for stage 1 sweep coinjoins", + "The probability of non-sweep coinjoin amounts being rounded"] + for w in ["one", "two", "three", "four", "five"]: + sH += ["If rounding happens (determined by Rounding Chance) then this " + "is the relative probability of rounding to " + w + + " significant figures"] #types - sT = [float, float, float, int, int, int, float, float] + sT = [float, float, float, int, int, int, float, float, float] + [int]*5 #constraints sMM = [(0.0, 10.0, 2), (0.0, 10.0, 2), (1.0, 10000.0, 1), (2,20), - (1, 10), (100000, 100000000), (10.0, 500.0, 2), (0, 100, 1)] - sD = ['1.0', '1.0', '100.0', '2', '1', '1000000', '20', '3'] + (1, 10), (100000, 100000000), (10.0, 500.0, 2), (0, 100, 1), + (0.0, 1.0, 3)] + [(0, 10000)]*5 + sD = ['1.0', '1.0', '100.0', '2', '1', '1000000', '20', '3', '0.25'] +\ + ['55', '15', '25', '65', '40'] for x in zip(sN, sH, sT, sD, sMM): ql = QLabel(x[0]) ql.setToolTip(x[1]) @@ -673,6 +683,9 @@ class SchFinishPage(QWizardPage): self.registerField("mincjamount", results[5][1]) self.registerField("waittime", results[6][1]) self.registerField("stage1_timelambda_increase", results[7][1]) + self.registerField("rounding_chance", results[8][1]) + for i in range(5): + self.registerField("rounding_sigfig_weight_" + str(i+1), results[9+i][1]) class SchIntroPage(QWizardPage): def __init__(self, parent): @@ -749,6 +762,8 @@ class ScheduleWizard(QWizard): absfeeval = int(self.field("maxabsfee")) self.opts['maxcjfee'] = (relfeeval, absfeeval) #needed for Taker to check: + self.opts['rounding_chance'] = float(self.field("rounding_chance")) + self.opts['rounding_sigfig_weights'] = tuple([int(self.field("rounding_sigfig_weight_" + str(i+1))) for i in range(5)]) jm_single().mincjamount = self.opts['mincjamount'] return get_tumble_schedule(self.opts, self.destaddrs, wallet_balance_by_mixdepth) diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index 12e6808..687d92c 100644 --- a/scripts/sendpayment.py +++ b/scripts/sendpayment.py @@ -17,7 +17,7 @@ import pprint from jmclient import Taker, P2EPTaker, load_program_config, get_schedule,\ JMClientProtocolFactory, start_reactor, validate_address, jm_single,\ estimate_tx_fee, direct_send, WalletService,\ - open_test_wallet_maybe, get_wallet_path + open_test_wallet_maybe, get_wallet_path, NO_ROUNDING from twisted.python.log import startLogging from jmbase.support import get_log, set_logging_level, jmprint from cli_options import get_sendpayment_parser, get_max_cj_fee_values, \ @@ -75,7 +75,7 @@ def main(): jmprint('ERROR: Address invalid. ' + errormsg, "error") return schedule = [[options.mixdepth, amount, options.makercount, - destaddr, 0.0, 0]] + destaddr, 0.0, NO_ROUNDING, 0]] else: if options.p2ep: parser.error("Schedule files are not compatible with PayJoin") diff --git a/scripts/tumbler.py b/scripts/tumbler.py index 59cf276..f46365f 100644 --- a/scripts/tumbler.py +++ b/scripts/tumbler.py @@ -76,7 +76,7 @@ def main(): jmprint("Error was: " + str(schedule), "error") sys.exit(0) #This removes all entries that are marked as done - schedule = [s for s in schedule if s[5] != 1] + 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: @@ -89,16 +89,16 @@ def main(): 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][5], str) and len(schedule[0][5]) == 64: + 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][5] + 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][5] != 0: + elif schedule[0][-1] != 0: print("Error: first schedule entry is invalid.") sys.exit(0) with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: