Skip to content
This repository was archived by the owner on Apr 27, 2026. It is now read-only.

Commit 02ef5fa

Browse files
-Adding support for tx_max_expected_probability, a parameter that defines the maximum expected probability for a single maker to be included in a collaborative transaction. A value of 1 (100%) for this probability corresponds to the previous behavior. A value smaller than 1 allows to prevent a large maker from always being included in a transaction. For a given total amount of fidelity bonds, this mechanism allows to reduce the ability of an attacker to be systematically included in a transaction, and also to be the only entity included as makers in the transaction.
1 parent f4c2b1b commit 02ef5fa

5 files changed

Lines changed: 121 additions & 18 deletions

File tree

scripts/tumbler.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def main():
6565
log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} sat"
6666
.format(*maxcjfee))
6767

68+
tx_max_expected_probability = jm_single().config.getfloat("POLICY", "tx_max_expected_probability")
69+
70+
if tx_max_expected_probability <= 0:
71+
jmprint('Error: tx_max_expected_probability must be greater than 0', "error")
72+
sys.exit(EXIT_FAILURE)
73+
74+
elif tx_max_expected_probability > 1:
75+
tx_max_expected_probability = 1
76+
77+
log.info("Using maximum expected probability of selecting a maker of {:.2%}"
78+
.format(tx_max_expected_probability))
79+
6880
#Parse options and generate schedule
6981
#Output information to log files
7082
jm_single().mincjamount = options['mincjamount']
@@ -185,6 +197,7 @@ def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
185197
taker = Taker(wallet_service,
186198
schedule,
187199
maxcjfee,
200+
tx_max_expected_probability,
188201
order_chooser=options['order_choose_fn'],
189202
callbacks=(filter_orders_callback, None, taker_finished),
190203
tdestaddrs=destaddrs)

src/jmclient/configure.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ def jm_single() -> AttributeDict:
400400
# where x > 1. It is a real number (so written as a decimal).
401401
bond_value_exponent = 1.3
402402
403+
tx_max_expected_probability = 1.0
404+
403405
##############################
404406
# THE FOLLOWING SETTINGS ARE REQUIRED TO DEFEND AGAINST SNOOPERS.
405407
# DON'T ALTER THEM UNLESS YOU UNDERSTAND THE IMPLICATIONS.

src/jmclient/support.py

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def calc_cj_fee(ordertype, cjfee, cj_amount):
177177
return real_cjfee
178178

179179

180-
def weighted_order_choose(orders, n):
180+
def weighted_order_choose(orders, n, nrem = None, large_makers_not_chosen_prob = None, tx_max_expected_probability = None):
181181
"""
182182
Algorithm for choosing the weighting function
183183
it is an exponential
@@ -208,37 +208,120 @@ def weighted_order_choose(orders, n):
208208
return orders[chosen_order_index]
209209

210210

211-
def random_under_max_order_choose(orders, n):
211+
def random_under_max_order_choose(orders, n, nrem = None, large_makers_not_chosen_prob = [1], tx_max_expected_probability = None):
212212
# orders are already pre-filtered for max_cj_fee
213+
if tx_max_expected_probability is not None and tx_max_expected_probability<1:
214+
log.debug('Remaining probability of not being selected for large makers: ' + str(large_makers_not_chosen_prob[0]) + ' -> ' + str(large_makers_not_chosen_prob[0]*(1-1./len(orders))))
215+
large_makers_not_chosen_prob[0] *= (1 - 1./len(orders))
213216
return random.choice(orders)
214217

215218

216-
def cheapest_order_choose(orders, n):
219+
def cheapest_order_choose(orders, n, nrem = None, large_makers_not_chosen_prob = None, tx_max_expected_probability = None):
217220
"""
218221
Return the cheapest order from the orders.
219222
"""
220223
return orders[0]
221224

222-
def fidelity_bond_weighted_order_choose(orders, n):
225+
def fidelity_bond_weighted_order_choose(orders, n, nrem = None, large_makers_not_chosen_prob = [1], tx_max_expected_probability = None):
223226
"""
224227
choose orders based on fidelity bond for improved sybil resistance
225228
226229
* with probability `bondless_makers_allowance`: will revert to previous default
227230
order choose (random_under_max_order_choose)
228231
* with probability `1 - bondless_makers_allowance`: if there are no bond offerings, revert
229-
to previous default as above. If there are, choose randomly from those, with weighting
230-
being the fidelity bond values.
232+
to previous default as above, or if tx_max_expected_probability is defined and the number
233+
of bond offerings is smaller than n/tx_max_expected_probability. If not, choose
234+
randomly from those, with weighting being the fidelity bond values.
231235
"""
232236

233237
if random.random() < get_bondless_makers_allowance():
234-
return random_under_max_order_choose(orders, n)
238+
log.debug('Bondless or bond maker randomly selected')
239+
return random_under_max_order_choose(orders, n, large_makers_not_chosen_prob=large_makers_not_chosen_prob, tx_max_expected_probability=tx_max_expected_probability)
235240
#remove orders without fidelity bonds
236-
filtered_orders = list(filter(lambda x: x[0]["fidelity_bond_value"] != 0, orders))
237-
if len(filtered_orders) == 0:
238-
return random_under_max_order_choose(orders, n)
241+
filtered_orders = sorted(list(filter(lambda x: x[0]["fidelity_bond_value"] != 0, orders)), key=lambda x: x[0]["fidelity_bond_value"])
242+
nforders = len(filtered_orders)
243+
244+
if nforders == 0:
245+
log.debug('Bondless maker selected because no alternative')
246+
return random_under_max_order_choose(orders, n, large_makers_not_chosen_prob=large_makers_not_chosen_prob, tx_max_expected_probability=tx_max_expected_probability)
247+
239248
weights = list(map(lambda x: x[0]["fidelity_bond_value"], filtered_orders))
249+
prob = 1 - pow(((1 - tx_max_expected_probability) / large_makers_not_chosen_prob[0]), 1./nrem) if tx_max_expected_probability is not None and tx_max_expected_probability<1. else None
250+
251+
if prob is not None:
252+
253+
#If maximum expected probability target for large makers cannot be achieved using a constant value for prob
254+
if prob<=0 or nforders-nrem+1 <= 1. / prob:
255+
max_exp_prob = large_makers_not_chosen_prob[0]
256+
257+
for i in range(nforders-nrem+1, nforders+1):
258+
max_exp_prob *= 1 - 1./i
259+
max_exp_prob = 1 - max_exp_prob
260+
261+
#If the probability target cannot be achieved at all
262+
if max_exp_prob > tx_max_expected_probability:
263+
log.warn('A large maker maximum expected probability target of ' + str(tx_max_expected_probability) + ' cannot be achieved. A probability of ' + str(max_exp_prob) + ' will be targeted instead')
264+
#Update prob using the maximum achievable target
265+
prob = 1 - pow(((1 - max_exp_prob) / large_makers_not_chosen_prob[0]), 1./nrem)
266+
267+
else:
268+
log.debug('Large maker maximum expected probability target of ' + str(tx_max_expected_probability) + ' achievable using an increasing draw probability due to the limited number of makers')
269+
rem_not_chosen_prob = (1 - max_exp_prob) / large_makers_not_chosen_prob[0]
270+
271+
for i in range(nforders-nrem+1, nforders):
272+
273+
if 1./i > prob:
274+
rem_not_chosen_prob /= 1 - 1./i
275+
log.debug('Large maker draw probability for draw ' + str(nforders + 1 - i) + ' set to ' + str(1./i))
276+
prob = 1 - pow(rem_not_chosen_prob, 1. / (nforders - i))
277+
278+
else:
279+
break
280+
log.debug('Large maker draw probability for first ' + str(nforders - i) + ' draws set to ' + str(prob) + ' per draw')
281+
282+
else:
283+
log.debug('Large maker draw probability set to ' + str(prob) + ' for each draw')
284+
285+
normal_bond_value_sum = 0
286+
nlargemakers = 0
287+
islargemaker = [False] * nforders
288+
289+
log.debug(str(nforders) + ' remaining makers for the draw')
290+
for i, o in enumerate(filtered_orders):
291+
#log.debug(o[0])
292+
normal_bond_value_sum += weights[i]
293+
log.debug('Total value of fidelity bonds: ' + str(normal_bond_value_sum))
294+
295+
for i, o in enumerate(filtered_orders[::-1]):
296+
i = nforders - i - 1
297+
298+
if prob * (nlargemakers + 1) >= 1:
299+
break
300+
bvmax = prob * (normal_bond_value_sum - weights[i]) / (1. - prob * (nlargemakers + 1))
301+
#log.debug('Maker ' + o[0]['counterparty'] + ' weight ' + str(weights[i]) + ' vs ' + str(bvmax) + ": " + ('normal' if weights[i] <= bvmax else 'large'))
302+
303+
if weights[i] <= bvmax:
304+
break
305+
islargemaker[i] = True
306+
normal_bond_value_sum -= weights[i]
307+
nlargemakers += 1
308+
309+
if normal_bond_value_sum <= 0:
310+
log.warn('Only large makers are left, selecting a bond maker randomly')
311+
return random_under_max_order_choose(filtered_orders, nforders, large_makers_not_chosen_prob=large_makers_not_chosen_prob, tx_max_expected_probability=tx_max_expected_probability)
312+
313+
log.debug('Remaining probability of not being selected for large makers: ' + str(large_makers_not_chosen_prob[0]) + ' -> ' + str(large_makers_not_chosen_prob[0]*(1-prob)))
314+
large_makers_not_chosen_prob[0] *= 1 - prob
315+
bvmax = prob * normal_bond_value_sum / (1. - prob * nlargemakers)
316+
317+
for i, o in enumerate(filtered_orders):
318+
319+
if islargemaker[i] == True:
320+
log.warn('Weight of counterparty ' + o[0]['counterparty'] + ' brought down to ' + str(bvmax) + ' from ' + str(weights[i]))
321+
weights[i]=bvmax
322+
240323
weights = [x / sum(weights) for x in weights]
241-
return filtered_orders[rand_weighted_choice(len(filtered_orders), weights)]
324+
return filtered_orders[rand_weighted_choice(nforders, weights)]
242325

243326
def _get_is_within_max_limits(max_fee_rel, max_fee_abs, cjvalue):
244327
def check_max_fee(fee):
@@ -249,7 +332,7 @@ def check_max_fee(fee):
249332

250333
def choose_orders(offers, cj_amount, n, chooseOrdersBy, ignored_makers=None,
251334
pick=False, allowed_types=["sw0reloffer", "sw0absoffer"],
252-
max_cj_fee=(1, float('inf'))):
335+
max_cj_fee=(1, float('inf')), tx_max_expected_probability=None):
253336
is_within_max_limits = _get_is_within_max_limits(
254337
max_cj_fee[0], max_cj_fee[1], cj_amount)
255338
if ignored_makers is None:
@@ -294,8 +377,10 @@ def choose_orders(offers, cj_amount, n, chooseOrdersBy, ignored_makers=None,
294377
]))
295378
total_cj_fee = 0
296379
chosen_orders = []
380+
large_makers_not_chosen_prob = [1]
297381
for i in range(n):
298-
chosen_order, chosen_fee = chooseOrdersBy(orders_fees, n)
382+
chosen_order, chosen_fee = chooseOrdersBy(orders_fees, n, n - i, large_makers_not_chosen_prob, tx_max_expected_probability)
383+
log.debug('Choice is ' + str(chosen_order))
299384
# remove all orders from that same counterparty
300385
# only needed if offers are manually picked
301386
orders_fees = [o
@@ -315,7 +400,7 @@ def choose_sweep_orders(offers,
315400
chooseOrdersBy,
316401
ignored_makers=None,
317402
allowed_types=['sw0reloffer', 'sw0absoffer'],
318-
max_cj_fee=(1, float('inf'))):
403+
max_cj_fee=(1, float('inf')), tx_max_expected_probability=None):
319404
"""
320405
choose an order given that we want to be left with no change
321406
i.e. sweep an entire group of utxos
@@ -376,13 +461,14 @@ def calc_zero_change_cj_amount(ordercombo):
376461
if is_within_max_limits(v[1])).values(),
377462
key=feekey)
378463
chosen_orders = []
464+
large_makers_not_chosen_prob = [1]
379465
while len(chosen_orders) < n:
380466
for i in range(n - len(chosen_orders)):
381467
if len(orders_fees) < n - len(chosen_orders):
382468
log.debug('ERROR not enough liquidity in the orderbook')
383469
# TODO handle not enough liquidity better, maybe an Exception
384470
return None, 0, 0
385-
chosen_order, chosen_fee = chooseOrdersBy(orders_fees, n)
471+
chosen_order, chosen_fee = chooseOrdersBy(orders_fees, n, n - len(chosen_orders), large_makers_not_chosen_prob, tx_max_expected_probability)
386472
log.debug('chosen = ' + str(chosen_order))
387473
# remove all orders from that same counterparty
388474
orders_fees = [

src/jmclient/taker.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __init__(self,
4848
wallet_service,
4949
schedule,
5050
max_cj_fee,
51+
tx_max_expected_probability,
5152
order_chooser=fidelity_bond_weighted_order_choose,
5253
callbacks=None,
5354
tdestaddrs=None,
@@ -104,6 +105,7 @@ def __init__(self,
104105
self.schedule = schedule
105106
self.order_chooser = order_chooser
106107
self.max_cj_fee = max_cj_fee
108+
self.tx_max_expected_probability = tx_max_expected_probability
107109
self.custom_change_address = custom_change_address
108110
self.change_label = change_label
109111

@@ -290,7 +292,7 @@ def filter_orderbook(self, orderbook, sweep=False):
290292
self.orderbook, self.total_cj_fee = choose_orders(
291293
orderbook, self.cjamount, self.n_counterparties, self.order_chooser,
292294
self.ignored_makers, allowed_types=allowed_types,
293-
max_cj_fee=self.max_cj_fee)
295+
max_cj_fee=self.max_cj_fee, tx_max_expected_probability=self.tx_max_expected_probability)
294296
if self.orderbook is None:
295297
#Failure to get an orderbook means order selection failed
296298
#for some reason; no action is taken, we let the stallMonitor
@@ -381,7 +383,7 @@ def prepare_my_bitcoin_data(self):
381383
self.orderbook, total_value, self.total_txfee,
382384
self.n_counterparties, self.order_chooser,
383385
self.ignored_makers, allowed_types=allowed_types,
384-
max_cj_fee=self.max_cj_fee)
386+
max_cj_fee=self.max_cj_fee, tx_max_expected_probability=self.tx_max_expected_probability)
385387
if not self.orderbook:
386388
self.taker_info_callback("ABORT",
387389
"Could not find orders to complete transaction")

test/jmclient/test_support.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_choose_orders():
7676
#test the fidelity bond one
7777
for i, o in enumerate(orderbook):
7878
o["fidelity_bond_value"] = i+1
79-
orders_fees = choose_orders(orderbook, 100000000, 3, fidelity_bond_weighted_order_choose)
79+
orders_fees = choose_orders(orderbook, 100000000, 3, fidelity_bond_weighted_order_choose, tx_max_expected_probability=0.75)
8080
assert len(orders_fees[0]) == 3
8181
#test sweep
8282
result, cjamount, total_fee = choose_sweep_orders(orderbook, 50000000,

0 commit comments

Comments
 (0)