Skip to content
This repository was archived by the owner on May 13, 2022. It is now read-only.

Commit 75eac86

Browse files
committed
configurable thresholds for utxo merge policies
still no tests
1 parent 1002bf2 commit 75eac86

File tree

6 files changed

+75
-48
lines changed

6 files changed

+75
-48
lines changed

bitcoin/secp256k1_transaction.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -433,20 +433,3 @@ def mktx(*args):
433433
txobj["outs"].append(outobj)
434434

435435
return serialize(txobj)
436-
437-
438-
def select(unspent, value):
439-
value = int(value)
440-
high = [u for u in unspent if u["value"] >= value]
441-
high.sort(key=lambda u: u["value"])
442-
low = [u for u in unspent if u["value"] < value]
443-
low.sort(key=lambda u: -u["value"])
444-
if len(high):
445-
return [high[0]]
446-
i, tv = 0, 0
447-
while tv < value and i < len(low):
448-
tv += low[i]["value"]
449-
i += 1
450-
if tv < value:
451-
raise Exception("Not enough funds")
452-
return low[:i]

joinmarket/configure.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,15 @@ def jm_single():
121121
confirm_timeout_hours = 6
122122
123123
[POLICY]
124-
# for dust sweeping, try merge_algorithm = gradual
125-
# for more rapid dust sweeping, try merge_algorithm = greedy
126-
# for most rapid dust sweeping, try merge_algorithm = greediest
127-
# but don't forget to bump your miner fees!
128-
merge_algorithm = default
129124
# For takers: the minimum number of makers you allow in a transaction
130125
# to complete, accounting for the fact that some makers might not be
131126
# responsive. Should be an integer >=2 for privacy, or set to 0 if you
132127
# want to disallow any reduction from your chosen number of makers.
133128
minimum_makers = 2
129+
# this knob is a list of ints, being each the amount of utxos in a mixdepth
130+
# sufficient for kicking in the next-mergiest utxo selection algorithm. the
131+
# default errs on the side of privacy; lower values make tracking easier.
132+
merge_algorithm = [42, 59, 72]
134133
# the fee estimate is based on a projection of how many satoshis
135134
# per kB are needed to get in one of the next N blocks, N set here
136135
# as the value of 'tx_fees'. This estimate is high if you set N=2,

joinmarket/maker.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -437,20 +437,8 @@ def create_my_orders(self):
437437
# Maker code so im adding utxo and mixdepth here
438438
return orderlist
439439

440-
# has to return a list of utxos and mixing depth the cj address will
441-
# be in the change address will be in mixing_depth-1
442-
440+
# has to return a list of utxos, and addresses for cjout and change
443441
def oid_to_order(self, cjorder, oid, amount):
444-
"""
445-
unspent = []
446-
for utxo, addrvalue in self.wallet.unspent.iteritems():
447-
unspent.append({'value': addrvalue['value'], 'utxo': utxo})
448-
inputs = btc.select(unspent, amount)
449-
#TODO this raises an exception if you dont have enough money, id rather it just returned None
450-
mixing_depth = 1
451-
return [i['utxo'] for i in inputs], mixing_depth
452-
"""
453-
454442
order = [o for o in self.orderlist if o['oid'] == oid][0]
455443
cj_addr = self.wallet.get_internal_addr(order['mixdepth'] + 1)
456444
change_addr = self.wallet.get_internal_addr(order['mixdepth'])

joinmarket/support.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,21 @@ def rand_weighted_choice(n, p_arr):
9898
def chunks(d, n):
9999
return [d[x:x + n] for x in xrange(0, len(d), n)]
100100

101+
def select_default(unspent, value):
102+
value = int(value)
103+
high = [u for u in unspent if u["value"] >= value]
104+
high.sort(key=lambda u: u["value"])
105+
low = [u for u in unspent if u["value"] < value]
106+
low.sort(key=lambda u: -u["value"])
107+
if len(high):
108+
return [high[0]]
109+
i, tv = 0, 0
110+
while tv < value and i < len(low):
111+
tv += low[i]["value"]
112+
i += 1
113+
if tv < value:
114+
raise Exception("Not enough funds")
115+
return low[:i]
101116

102117
def select_gradual(unspent, value):
103118
"""
@@ -174,6 +189,20 @@ def select_greediest(unspent, value):
174189
end += 1
175190
return low[0:end]
176191

192+
# ordered from most dusty (arguably, most private) to most mergiest (cheaper!)
193+
selectors = [select_default, select_gradual, select_greedy, select_greediest]
194+
195+
def utxo_selector(configured_levels):
196+
def select(unspent, value):
197+
length = len(unspent) # NB - counted only within each mixdepth
198+
try:
199+
for i in xrange(len(configured_levels)):
200+
if length < configured_levels[i]:
201+
return selectors[i](unspent, value)
202+
except IndexError: # luser configured more levels than algos
203+
log.debug("I'm sorry, Dave, but I can't let you merge that!")
204+
return selectors[0](unspent, value) # default to improve privacy
205+
return select
177206

178207
def calc_cj_fee(ordertype, cjfee, cj_amount):
179208
if ordertype == 'absoffer':

joinmarket/wallet.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import pprint
55
import sys
6+
import ast
67
from decimal import Decimal
78

89
from ConfigParser import NoSectionError
@@ -13,8 +14,7 @@
1314
from joinmarket.blockchaininterface import BitcoinCoreInterface, RegtestBitcoinCoreInterface
1415
from joinmarket.configure import jm_single, get_network, get_p2pk_vbyte
1516

16-
from joinmarket.support import get_log, select_gradual, select_greedy, \
17-
select_greediest
17+
from joinmarket.support import get_log, utxo_selector
1818

1919
log = get_log()
2020

@@ -46,19 +46,27 @@ def __init__(self):
4646
#some consumer scripts don't use an unspent, this marks it
4747
#as specifically absent (rather than just empty).
4848
self.unspent = None
49-
self.utxo_selector = btc.select # default fallback: upstream
5049
try:
51-
config = jm_single().config
52-
if config.get("POLICY", "merge_algorithm") == "gradual":
53-
self.utxo_selector = select_gradual
54-
elif config.get("POLICY", "merge_algorithm") == "greedy":
55-
self.utxo_selector = select_greedy
56-
elif config.get("POLICY", "merge_algorithm") == "greediest":
57-
self.utxo_selector = select_greediest
58-
elif config.get("POLICY", "merge_algorithm") != "default":
59-
raise Exception("Unknown merge algorithm")
50+
policy = jm_single().config.get("POLICY", "merge_algorithm")
6051
except NoSectionError:
61-
pass
52+
policy = "default" # maintain backwards compatibility!
53+
if policy == "default":
54+
self.merge_policy = [42] # well, almost (python lacks infinites)
55+
elif policy == "gradual":
56+
self.merge_policy = [60] # never goes beyond gradual
57+
elif policy == "greedy":
58+
self.merge_policy = [70, 70] # skip gradual, go greedy
59+
elif policy == "greediest":
60+
self.merge_policy = [80, 80, 80] # straight to greediest
61+
else:
62+
try: # stop supporting word configs, someday...
63+
self.merge_policy = ast.literal_eval(policy)
64+
if ((type(self.merge_policy) is not list) or
65+
any(type(level) is not int for level in self.merge_policy)):
66+
raise Exception("Merge policy must be a list of ints")
67+
except ValueError:
68+
raise Exception("Unparseable merge policy: "+policy)
69+
self.utxo_selector = utxo_selector(self.merge_policy)
6270

6371
def get_key_from_addr(self, addr):
6472
return None

test/test_wallets.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,26 @@ def test_utxo_selection(setup_wallets, nw, wallet_structures, mean_amt,
350350
"failed to select sufficient coins, total: " + \
351351
str(total_selected) + ", should be: " + str(amount)
352352

353+
def test_merge_algo_switching(setup_wallets):
354+
wallet = make_wallets(1, [[25, 45, 60, 75, 0]], 1, 0.5)[0]['wallet']
355+
sync_wallet(wallet)
356+
all_utxos = wallet.get_utxos_by_mixdepth()
357+
# test merge-avoiding with small utxo sets
358+
utxos = sorted(map(lambda x:x['value'], all_utxos[0]))
359+
for i in range(4):
360+
amount = (utxos[i]+utxos[i+1])/2
361+
selected = wallet.select_utxos(0, amount)
362+
assert (1 == len(selected)), "Default selection misbehaved! " + \
363+
"Selected " + str(selected) + " to reach sum " + str(amount) + \
364+
" from utxos " + str(utxos) + " (should pick SINGLE utxo)"
365+
# test merging with larger utxo sets
366+
utxos = sorted(map(lambda x:x['value'], all_utxos[1]))
367+
for i in range(40):
368+
amount = (utxos[i+3]+utxos[i+4])/2
369+
assert (1 < len(wallet.select_utxos(1, amount))), "Failed to merge UTXOs! "+\
370+
"Selected " + str(selected) + " to reach sum " + str(amount) + \
371+
" from utxos " + str(utxos) + " (should pick MULTIPLE utxos)"
372+
# TODO: more specific tests of different merge algos
353373

354374
class TestWalletCreation(unittest.TestCase):
355375

0 commit comments

Comments
 (0)