mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-13 12:33:40 +02:00
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
This commit is contained in:
parent
35f23eb6b7
commit
f40ef2c795
12 changed files with 145 additions and 77 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue