Browse Source

Occasionally round amounts in tumbler schedule

The schedule format gets an extra field added denoting the number of
significant figures to round the coinjoin amounts to, with 16 meaning
no rounding.

This is part of the 2/2019 Plan to improve the privacy of JoinMarket's
tumbler script:
https://gist.github.com/chris-belcher/7e92810f07328fdfdef2ce444aad0968
master
chris-belcher 6 years ago
parent
commit
f40ef2c795
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 5
      jmclient/jmclient/__init__.py
  2. 35
      jmclient/jmclient/schedule.py
  3. 15
      jmclient/jmclient/taker.py
  4. 2
      jmclient/jmclient/taker_utils.py
  5. 19
      jmclient/test/test_coinjoin.py
  6. 22
      jmclient/test/test_schedule.py
  7. 40
      jmclient/test/test_taker.py
  8. 18
      scripts/cli_options.py
  9. 13
      scripts/joinmarket-qt.py
  10. 41
      scripts/qtsupport.py
  11. 4
      scripts/sendpayment.py
  12. 8
      scripts/tumbler.py

5
jmclient/jmclient/__init__.py

@ -6,7 +6,8 @@ import logging
from .support import (calc_cj_fee, choose_sweep_orders, choose_orders, from .support import (calc_cj_fee, choose_sweep_orders, choose_orders,
cheapest_order_choose, weighted_order_choose, 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, select_gradual, select_greedy, select_greediest,
get_random_bytes, random_under_max_order_choose, get_random_bytes, random_under_max_order_choose,
select_one_utxo) select_one_utxo)
@ -37,7 +38,7 @@ from .output import generate_podle_error_string, fmt_utxos, fmt_utxo,\
fmt_tx_data fmt_tx_data
from .schedule import (get_schedule, get_tumble_schedule, schedule_to_text, from .schedule import (get_schedule, get_tumble_schedule, schedule_to_text,
tweak_tumble_schedule, human_readable_schedule_entry, 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 .commitment_utils import get_utxo_info, validate_utxo_data, quit
from .taker_utils import (tumbler_taker_finished_update, restart_waiter, from .taker_utils import (tumbler_taker_finished_update, restart_waiter,
restart_wait, get_tumble_log, direct_send, restart_wait, get_tumble_log, direct_send,

35
jmclient/jmclient/schedule.py

@ -7,7 +7,7 @@ import random
import sys import sys
from .configure import validate_address, jm_single 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. """Utility functions for dealing with Taker schedules.
- get_schedule(filename): - 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 the chance of success on re-trying
""" """
NO_ROUNDING = 16 #max btc significant figures not including LN
def get_schedule(filename): def get_schedule(filename):
with open(filename, "rb") as f: with open(filename, "rb") as f:
schedule = [] schedule = []
@ -29,8 +31,8 @@ def get_schedule(filename):
if sl.startswith("#"): if sl.startswith("#"):
continue continue
try: try:
mixdepth, amount, makercount, destaddr, waittime, completed = \ (mixdepth, amount, makercount, destaddr, waittime,
sl.split(',') rounding, completed) = sl.split(',')
except ValueError as e: except ValueError as e:
return (False, "Failed to parse schedule line: " + sl) return (False, "Failed to parse schedule line: " + sl)
try: try:
@ -44,6 +46,7 @@ def get_schedule(filename):
makercount = int(makercount) makercount = int(makercount)
destaddr = destaddr.strip() destaddr = destaddr.strip()
waittime = float(waittime) waittime = float(waittime)
rounding = int(rounding)
completed = completed.strip() completed = completed.strip()
if not len(completed) == 64: if not len(completed) == 64:
completed = int(completed) completed = int(completed)
@ -54,7 +57,7 @@ def get_schedule(filename):
if not success: if not success:
return (False, "Invalid address: " + destaddr + "," + errmsg) return (False, "Invalid address: " + destaddr + "," + errmsg)
schedule.append([mixdepth, amount, makercount, destaddr, schedule.append([mixdepth, amount, makercount, destaddr,
waittime, completed]) waittime, rounding, completed])
return (True, schedule) return (True, schedule)
def get_amount_fractions(power, count): def get_amount_fractions(power, count):
@ -119,7 +122,8 @@ def get_tumble_schedule(options, destaddrs, mixdepth_balance_dict):
'wait': round(wait, 2), 'wait': round(wait, 2),
'srcmixdepth': mixdepth, 'srcmixdepth': mixdepth,
'makercount': makercount, 'makercount': makercount,
'destination': 'INTERNAL' 'destination': 'INTERNAL',
'rounding': NO_ROUNDING
} }
tx_list.append(tx) tx_list.append(tx)
### stage 2 coinjoins, which create a number of random-amount coinjoins from each mixdepth ### 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], makercounts = rand_norm_array(options['makercountrange'][0],
options['makercountrange'][1], txcount) options['makercountrange'][1], txcount)
makercounts = lower_bounded_int(makercounts, options['minmakercount']) makercounts = lower_bounded_int(makercounts, options['minmakercount'])
do_rounds = [random.random() < options['rounding_chance'] for _ in range(txcount)]
for amount_fraction, wait, makercount in zip(amount_fractions, waits, for amount_fraction, wait, makercount, do_round in zip(amount_fractions, waits,
makercounts): 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, tx = {'amount_fraction': amount_fraction,
'wait': round(wait, 2), 'wait': round(wait, 2),
'srcmixdepth': lowest_initial_filled_mixdepth + m + options['mixdepthsrc'] + 1, 'srcmixdepth': lowest_initial_filled_mixdepth + m + options['mixdepthsrc'] + 1,
'makercount': makercount, 'makercount': makercount,
'destination': 'INTERNAL'} 'destination': 'INTERNAL',
'rounding': rounding
}
tx_list.append(tx) tx_list.append(tx)
#reset the final amt_frac to zero, as it's the last one for this mixdepth: #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]['amount_fraction'] = 0
tx_list[-1]['rounding'] = NO_ROUNDING
addrask = options['addrcount'] - len(destaddrs) addrask = options['addrcount'] - len(destaddrs)
external_dest_addrs = ['addrask'] * addrask + destaddrs[::-1] external_dest_addrs = ['addrask'] * addrask + destaddrs[::-1]
@ -172,7 +184,8 @@ def get_tumble_schedule(options, destaddrs, mixdepth_balance_dict):
schedule = [] schedule = []
for t in tx_list: for t in tx_list:
schedule.append([t['srcmixdepth'], t['amount_fraction'], 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 return schedule
def tweak_tumble_schedule(options, schedule, last_completed, destaddrs=[]): 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]) amt_info = str(amt) if amt else str(se[1])
hrs.append("sends amount: " + amt_info + " satoshis") hrs.append("sends amount: " + amt_info + " satoshis")
dest_info = destn if destn else str(se[3]) 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("to destination address: " + dest_info)
hrs.append("after coinjoin with " + str(se[2]) + " counterparties.") hrs.append("after coinjoin with " + str(se[2]) + " counterparties.")
return ", ".join(hrs) return ", ".join(hrs)

15
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 jmclient.wallet_service import WalletService
from .output import generate_podle_error_string from .output import generate_podle_error_string
from .cryptoengine import EngineError from .cryptoengine import EngineError
from .schedule import NO_ROUNDING
jlog = get_log() jlog = get_log()
@ -168,6 +169,7 @@ class Taker(object):
si = self.schedule[self.schedule_index] si = self.schedule[self.schedule_index]
self.mixdepth = si[0] self.mixdepth = si[0]
self.cjamount = si[1] self.cjamount = si[1]
rounding = si[5]
#non-integer coinjoin amounts are treated as fractions #non-integer coinjoin amounts are treated as fractions
#this is currently used by the tumbler algo #this is currently used by the tumbler algo
if isinstance(self.cjamount, float): if isinstance(self.cjamount, float):
@ -179,6 +181,9 @@ class Taker(object):
)[self.mixdepth] )[self.mixdepth]
#reset to satoshis #reset to satoshis
self.cjamount = int(self.cjamount * self.mixdepthbal) 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: if self.cjamount < jm_single().mincjamount:
jlog.info("Coinjoin amount too low, bringing up to: " + str( jlog.info("Coinjoin amount too low, bringing up to: " + str(
jm_single().mincjamount)) jm_single().mincjamount))
@ -1222,3 +1227,13 @@ class P2EPTaker(Taker):
self.self_sign_and_push() self.self_sign_and_push()
# returning False here is not an error condition, only stops processing. # returning False here is not an error condition, only stops processing.
return (False, "OK") 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()

2
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 #full record should always be accurate; but TUMBLE.log should be
#used for checking what actually happened. #used for checking what actually happened.
completion_flag = 1 if not addtolog else taker.txid 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: with open(schedulefile, "wb") as f:
f.write(schedule_to_text(taker.schedule)) f.write(schedule_to_text(taker.schedule))

19
jmclient/test/test_coinjoin.py

@ -13,7 +13,8 @@ from twisted.internet import reactor
from jmbase import get_log from jmbase import get_log
from jmclient import load_program_config, jm_single,\ 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 jmclient.podle import set_commitment_file
from commontest import make_wallets, binarize_tx from commontest import make_wallets, binarize_tx
from test_taker import dummy_filter_orderbook 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 assert len(orderbook) == MAKER_NUM
cj_amount = int(1.1 * 10**8) cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime # mixdepth, amount, counterparties, dest_addr, waittime, rounding
schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0)] schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
taker = create_taker(wallet_services[-1], schedule, monkeypatch) taker = create_taker(wallet_services[-1], schedule, monkeypatch)
active_orders, maker_data = init_coinjoin(taker, makers, 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 assert len(orderbook) == MAKER_NUM
cj_amount = int(1.1 * 10**8) cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime # mixdepth, amount, counterparties, dest_addr, waittime, rounding
schedule = [(4, cj_amount, MAKER_NUM, 'INTERNAL', 0)] schedule = [(4, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
taker = create_taker(wallet_services[-1], schedule, monkeypatch) taker = create_taker(wallet_services[-1], schedule, monkeypatch)
active_orders, maker_data = init_coinjoin(taker, makers, 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 assert len(orderbook) == MAKER_NUM
cj_amount = int(1.1 * 10**8) cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime # mixdepth, amount, counterparties, dest_addr, waittime, rounding
schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0)] schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
taker = create_taker(wallet_services[-1], schedule, monkeypatch) taker = create_taker(wallet_services[-1], schedule, monkeypatch)
active_orders, maker_data = init_coinjoin(taker, makers, 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) orderbook = create_orderbook(makers)
cj_amount = int(1.1 * 10**8) cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime # mixdepth, amount, counterparties, dest_addr, waittime, rounding
schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0)] schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0, NO_ROUNDING)]
taker = create_taker(wallet_services[-1], schedule, monkeypatch) taker = create_taker(wallet_services[-1], schedule, monkeypatch)
active_orders, maker_data = init_coinjoin(taker, makers, active_orders, maker_data = init_coinjoin(taker, makers,

22
jmclient/test/test_schedule.py

@ -10,30 +10,30 @@ from jmclient import (get_schedule, get_tumble_schedule,
import os import os
valids = """#sample for testing valids = """#sample for testing
1, 110000000, 3, INTERNAL, 0, 1 1, 110000000, 3, INTERNAL, 0, 16, 1
0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 9.88, 0 0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 9.88, 16, 0
""" """
invalids1 = """#sample for testing 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 #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 invalids2 = """#sample for testing
1, 110000000, notinteger, INTERNAL, 0, 0 1, 110000000, notinteger, INTERNAL, 0, 16, 0
0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 0 0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 16, 0
""" """
invalids3 = """#sample for testing invalids3 = """#sample for testing
1, 110000000, 3, INTERNAL, 0, 0 1, 110000000, 3, INTERNAL, 0, 16, 0
0, notinteger, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 0 0, notinteger, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0, 16, 0
""" """
#invalid address #invalid address
invalids4 = """#sample for testing invalids4 = """#sample for testing
1, 110000000, 3, INTERNAL, 0, 0 1, 110000000, 3, INTERNAL, 0, 16, 0
0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qq, 0, 0 0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qq, 0, 16, 0
""" """
@ -71,6 +71,8 @@ def get_options():
options.stage1_timelambda_increase = 3 options.stage1_timelambda_increase = 3
options.mincjamount = 1000000 options.mincjamount = 1000000
options.liquiditywait = 5 options.liquiditywait = 5
options.rounding_chance = 0.25
options.rounding_sigfig_weights = (55, 15, 25, 65, 40)
options = vars(options) options = vars(options)
return options return options

40
jmclient/test/test_taker.py

@ -15,7 +15,7 @@ import struct
from base64 import b64encode from base64 import b64encode
from jmclient import load_program_config, jm_single, set_commitment_file,\ from jmclient import load_program_config, jm_single, set_commitment_file,\
get_commitment_file, SegwitLegacyWallet, Taker, VolatileStorage,\ 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,\ from taker_test_data import t_utxos_by_mixdepth, t_orderbook,\
t_maker_response, t_chosen_orders, t_dummy_ext 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): filter_orders=None):
if not schedule: if not schedule:
#note, for taker.initalize() this will result in junk #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)) print("Using schedule: " + str(schedule))
on_finished_callback = on_finished if on_finished else taker_finished on_finished_callback = on_finished if on_finished else taker_finished
filter_orders_callback = filter_orders if filter_orders else dummy_filter_orderbook 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") print("calling filter orders rejection")
return False return False
taker = get_taker(filter_orders=filter_orders_reject) 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) res = taker.initialize(t_orderbook)
assert not res[0] assert not res[0]
taker = get_taker(filter_orders=filter_orders_reject) 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) res = taker.initialize(t_orderbook)
assert not res[0] 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") jm_single().config.set("POLICY", "taker_utxo_amtpercent", "20")
mixdepth = 0 mixdepth = 0
amount = 110000000 amount = 110000000
taker = get_taker([(mixdepth, amount, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw")]) taker = get_taker([(mixdepth, amount, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", NO_ROUNDING)])
taker.cjamount = amount taker.cjamount = amount
taker.input_utxos = t_utxos_by_mixdepth[0] taker.input_utxos = t_utxos_by_mixdepth[0]
if failquery: if failquery:
@ -180,7 +180,7 @@ def test_make_commitment(setup_taker, failquery, external):
clean_up() clean_up()
def test_not_found_maker_utxos(setup_taker): 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) orderbook = copy.deepcopy(t_orderbook)
res = taker.initialize(orderbook) res = taker.initialize(orderbook)
taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same 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) jm_single().bc_interface.setQUSFail(False)
def test_auth_pub_not_found(setup_taker): 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) orderbook = copy.deepcopy(t_orderbook)
res = taker.initialize(orderbook) res = taker.initialize(orderbook)
taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same 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( @pytest.mark.parametrize(
"schedule, highfee, toomuchcoins, minmakers, notauthed, ignored, nocommit", "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), 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 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 2, False, None, None), #tumble style non-int amounts
#edge case triggers that don't fail #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 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 2, False, None, None), #trigger sub dust change for taker
#edge case triggers that do fail #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 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 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 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 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 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 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 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 2, False, ["J659UPUSLLjHJpaB", "J65z23xdjxJjC7er", 0], None), #test inadequate for sweep
]) ])
def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers, def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,

18
scripts/cli_options.py

@ -385,6 +385,24 @@ def get_tumbler_parser():
'mixdepthsrc + number of mixdepths to tumble ' 'mixdepthsrc + number of mixdepths to tumble '
'have been used.', 'have been used.',
default=-1) 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) add_common_options(parser)
return parser return parser

13
scripts/joinmarket-qt.py

@ -74,7 +74,8 @@ from jmclient import load_program_config, get_network,\
get_blockchain_interface_instance, direct_send, WalletService,\ get_blockchain_interface_instance, direct_send, WalletService,\
RegtestBitcoinCoreInterface, tumbler_taker_finished_update,\ RegtestBitcoinCoreInterface, tumbler_taker_finished_update,\
get_tumble_log, restart_wait, tumbler_filter_orders_callback,\ 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,\ from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\ config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\ PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
@ -561,17 +562,17 @@ class SpendTab(QWidget):
#follow restart logic #follow restart logic
#1. filter out complete: #1. filter out complete:
self.spendstate.loaded_schedule = [ 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 #reload destination addresses
self.tumbler_destaddrs = [x[3] for x in self.spendstate.loaded_schedule self.tumbler_destaddrs = [x[3] for x in self.spendstate.loaded_schedule
if x not in ["INTERNAL", "addrask"]] if x not in ["INTERNAL", "addrask"]]
#2 Check for unconfirmed #2 Check for unconfirmed
if isinstance(self.spendstate.loaded_schedule[0][5], str) and len( if isinstance(self.spendstate.loaded_schedule[0][-1], str) and len(
self.spendstate.loaded_schedule[0][5]) == 64: self.spendstate.loaded_schedule[0][-1]) == 64:
#ensure last transaction is confirmed before restart #ensure last transaction is confirmed before restart
tumble_log.info("WAITING TO RESTART...") tumble_log.info("WAITING TO RESTART...")
mainWindow.statusBar().showMessage("Waiting for confirmation 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, #remove the already-done entry (this connects to the other TODO,
#probably better *not* to truncate the done-already txs from file, #probably better *not* to truncate the done-already txs from file,
#but simplest for now. #but simplest for now.
@ -647,7 +648,7 @@ class SpendTab(QWidget):
#note 'amount' is integer, so not interpreted as fraction #note 'amount' is integer, so not interpreted as fraction
#see notes in sample testnet schedule for format #see notes in sample testnet schedule for format
self.spendstate.loaded_schedule = [[mixdepth, amount, makercount, self.spendstate.loaded_schedule = [[mixdepth, amount, makercount,
destaddr, 0, 0]] destaddr, 0, NO_ROUNDING, 0]]
self.spendstate.updateType('single') self.spendstate.updateType('single')
self.spendstate.updateRun('running') self.spendstate.updateRun('running')
self.startJoin() self.startJoin()

41
scripts/qtsupport.py

@ -631,24 +631,34 @@ class SchFinishPage(QWizardPage):
'Minimum transaction count', 'Minimum transaction count',
'Min coinjoin amount', 'Min coinjoin amount',
'Response wait time', '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 #Tooltips
sH = ["Standard deviation of the number of makers to use in each " sH = ["Standard deviation of the number of makers to use in each "
"transaction.", "transaction.",
"Standard deviation of the number of transactions to use in each " "Standard deviation of the number of transactions to use in each "
"mixdepth", "mixdepth",
"A parameter to control the random coinjoin sizes.", "A parameter to control the random coinjoin sizes.",
"The lowest allowed number of maker counterparties.", "The lowest allowed number of maker counterparties.",
"The lowest allowed number of transactions in one mixdepth.", "The lowest allowed number of transactions in one mixdepth.",
"The lowest allowed size of any coinjoin, in satoshis.", "The lowest allowed size of any coinjoin, in satoshis.",
"The time in seconds to wait for response from counterparties.", "The time in seconds to wait for response from counterparties.",
"The factor increase in wait time for stage 1 sweep coinjoins"] "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 #types
sT = [float, float, float, int, int, int, float, float] sT = [float, float, float, int, int, int, float, float, float] + [int]*5
#constraints #constraints
sMM = [(0.0, 10.0, 2), (0.0, 10.0, 2), (1.0, 10000.0, 1), (2,20), 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)] (1, 10), (100000, 100000000), (10.0, 500.0, 2), (0, 100, 1),
sD = ['1.0', '1.0', '100.0', '2', '1', '1000000', '20', '3'] (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): for x in zip(sN, sH, sT, sD, sMM):
ql = QLabel(x[0]) ql = QLabel(x[0])
ql.setToolTip(x[1]) ql.setToolTip(x[1])
@ -673,6 +683,9 @@ class SchFinishPage(QWizardPage):
self.registerField("mincjamount", results[5][1]) self.registerField("mincjamount", results[5][1])
self.registerField("waittime", results[6][1]) self.registerField("waittime", results[6][1])
self.registerField("stage1_timelambda_increase", results[7][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): class SchIntroPage(QWizardPage):
def __init__(self, parent): def __init__(self, parent):
@ -749,6 +762,8 @@ class ScheduleWizard(QWizard):
absfeeval = int(self.field("maxabsfee")) absfeeval = int(self.field("maxabsfee"))
self.opts['maxcjfee'] = (relfeeval, absfeeval) self.opts['maxcjfee'] = (relfeeval, absfeeval)
#needed for Taker to check: #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'] jm_single().mincjamount = self.opts['mincjamount']
return get_tumble_schedule(self.opts, self.destaddrs, return get_tumble_schedule(self.opts, self.destaddrs,
wallet_balance_by_mixdepth) wallet_balance_by_mixdepth)

4
scripts/sendpayment.py

@ -17,7 +17,7 @@ import pprint
from jmclient import Taker, P2EPTaker, load_program_config, get_schedule,\ from jmclient import Taker, P2EPTaker, load_program_config, get_schedule,\
JMClientProtocolFactory, start_reactor, validate_address, jm_single,\ JMClientProtocolFactory, start_reactor, validate_address, jm_single,\
estimate_tx_fee, direct_send, WalletService,\ 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 twisted.python.log import startLogging
from jmbase.support import get_log, set_logging_level, jmprint from jmbase.support import get_log, set_logging_level, jmprint
from cli_options import get_sendpayment_parser, get_max_cj_fee_values, \ from cli_options import get_sendpayment_parser, get_max_cj_fee_values, \
@ -75,7 +75,7 @@ def main():
jmprint('ERROR: Address invalid. ' + errormsg, "error") jmprint('ERROR: Address invalid. ' + errormsg, "error")
return return
schedule = [[options.mixdepth, amount, options.makercount, schedule = [[options.mixdepth, amount, options.makercount,
destaddr, 0.0, 0]] destaddr, 0.0, NO_ROUNDING, 0]]
else: else:
if options.p2ep: if options.p2ep:
parser.error("Schedule files are not compatible with PayJoin") parser.error("Schedule files are not compatible with PayJoin")

8
scripts/tumbler.py

@ -76,7 +76,7 @@ def main():
jmprint("Error was: " + str(schedule), "error") jmprint("Error was: " + str(schedule), "error")
sys.exit(0) sys.exit(0)
#This removes all entries that are marked as done #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 # remaining destination addresses must be stored in Taker.tdestaddrs
# in case of tweaks; note we can't change, so any passed on command # in case of tweaks; note we can't change, so any passed on command
# line must be ignored: # line must be ignored:
@ -89,16 +89,16 @@ def main():
destaddrs = [s[3] for s in schedule if s[3] not in ["INTERNAL", "addrask"]] destaddrs = [s[3] for s in schedule if s[3] not in ["INTERNAL", "addrask"]]
jmprint("Remaining destination addresses in restart: " + ",".join(destaddrs), jmprint("Remaining destination addresses in restart: " + ",".join(destaddrs),
"important") "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 #ensure last transaction is confirmed before restart
tumble_log.info("WAITING TO RESTART...") tumble_log.info("WAITING TO RESTART...")
txid = schedule[0][5] txid = schedule[0][-1]
restart_waiter(txid) restart_waiter(txid)
#remove the already-done entry (this connects to the other TODO, #remove the already-done entry (this connects to the other TODO,
#probably better *not* to truncate the done-already txs from file, #probably better *not* to truncate the done-already txs from file,
#but simplest for now. #but simplest for now.
schedule = schedule[1:] schedule = schedule[1:]
elif schedule[0][5] != 0: elif schedule[0][-1] != 0:
print("Error: first schedule entry is invalid.") print("Error: first schedule entry is invalid.")
sys.exit(0) sys.exit(0)
with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:

Loading…
Cancel
Save