Skip to content

Commit c89347a

Browse files
committed
initial draft allowing takers to optionally complete transactions with less than the initially requested number of makers; sweep function is unchanged
Insert message to console advising user if minmakers!=0 disallow sendpayment -N choice < minimum_makers, fail before sync
1 parent ab91528 commit c89347a

File tree

4 files changed

+121
-69
lines changed

4 files changed

+121
-69
lines changed

joinmarket/configure.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ def jm_single():
126126
# for most rapid dust sweeping, try merge_algorithm = greediest
127127
# but don't forget to bump your miner fees!
128128
merge_algorithm = default
129+
# For takers: the minimum number of makers you allow in a transaction
130+
# to complete, accounting for the fact that some makers might not be
131+
# responsive. Should be an integer >=2 for privacy, or set to 0 if you
132+
# want to disallow any reduction from your chosen number of makers.
133+
minimum_makers = 2
129134
# the fee estimate is based on a projection of how many satoshis
130135
# per kB are needed to get in one of the next N blocks, N set here
131136
# as the value of 'tx_fees'. This estimate is high if you set N=1,

joinmarket/taker.py

Lines changed: 92 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -147,58 +147,65 @@ def auth_counterparty(self, nick, btc_sig, auth_pub):
147147
return True
148148

149149
def recv_txio(self, nick, utxo_list, auth_pub, cj_addr, change_addr):
150-
if nick not in self.nonrespondants:
151-
log.debug(('recv_txio => nick={} not in '
152-
'nonrespondants {}').format(nick, self.nonrespondants))
153-
return
154-
self.utxos[nick] = utxo_list
155-
utxo_data = jm_single().bc_interface.query_utxo_set(self.utxos[nick])
156-
if None in utxo_data:
157-
log.error(('ERROR outputs unconfirmed or already spent. '
158-
'utxo_data={}').format(pprint.pformat(utxo_data)))
159-
# when internal reviewing of makers is created, add it here to
160-
# immediately quit; currently, the timeout thread suffices.
161-
return
162-
#Complete maker authorization:
163-
#Extract the address fields from the utxos
164-
#Construct the Bitcoin address for the auth_pub field
165-
#Ensure that at least one address from utxos corresponds.
166-
input_addresses = [d['address'] for d in utxo_data]
167-
auth_address = btc.pubkey_to_address(auth_pub, get_p2pk_vbyte())
168-
if not auth_address in input_addresses:
169-
log.error("ERROR maker's authorising pubkey is not included "
170-
"in the transaction: " + str(auth_address))
171-
return
150+
if nick:
151+
if nick not in self.nonrespondants:
152+
log.debug(('recv_txio => nick={} not in '
153+
'nonrespondants {}').format(nick, self.nonrespondants))
154+
return
155+
self.utxos[nick] = utxo_list
156+
utxo_data = jm_single().bc_interface.query_utxo_set(self.utxos[nick])
157+
if None in utxo_data:
158+
log.error(('ERROR outputs unconfirmed or already spent. '
159+
'utxo_data={}').format(pprint.pformat(utxo_data)))
160+
# when internal reviewing of makers is created, add it here to
161+
# immediately quit; currently, the timeout thread suffices.
162+
return
163+
#Complete maker authorization:
164+
#Extract the address fields from the utxos
165+
#Construct the Bitcoin address for the auth_pub field
166+
#Ensure that at least one address from utxos corresponds.
167+
input_addresses = [d['address'] for d in utxo_data]
168+
auth_address = btc.pubkey_to_address(auth_pub, get_p2pk_vbyte())
169+
if not auth_address in input_addresses:
170+
log.error("ERROR maker's authorising pubkey is not included "
171+
"in the transaction: " + str(auth_address))
172+
return
172173

173-
total_input = sum([d['value'] for d in utxo_data])
174-
real_cjfee = calc_cj_fee(self.active_orders[nick]['ordertype'],
175-
self.active_orders[nick]['cjfee'], self.cj_amount)
176-
change_amount = (total_input - self.cj_amount -
177-
self.active_orders[nick]['txfee'] + real_cjfee)
178-
179-
# certain malicious and/or incompetent liquidity providers send
180-
# inputs totalling less than the coinjoin amount! this leads to
181-
# a change output of zero satoshis, so the invalid transaction
182-
# fails harmlessly; let's fail earlier, with a clear message.
183-
if change_amount < jm_single().DUST_THRESHOLD:
184-
fmt = ('ERROR counterparty requires sub-dust change. No '
185-
'action required. nick={}'
186-
'totalin={:d} cjamount={:d} change={:d}').format
187-
log.warn(fmt(nick, total_input, self.cj_amount, change_amount))
188-
return # timeout marks this maker as nonresponsive
189-
190-
self.outputs.append({'address': change_addr, 'value': change_amount})
191-
fmt = ('fee breakdown for {} totalin={:d} '
192-
'cjamount={:d} txfee={:d} realcjfee={:d}').format
193-
log.debug(fmt(nick, total_input, self.cj_amount,
194-
self.active_orders[nick]['txfee'], real_cjfee))
195-
self.outputs.append({'address': cj_addr, 'value': self.cj_amount})
196-
self.cjfee_total += real_cjfee
197-
self.maker_txfee_contributions += self.active_orders[nick]['txfee']
198-
self.nonrespondants.remove(nick)
199-
if len(self.nonrespondants) > 0:
200-
log.debug('nonrespondants = ' + str(self.nonrespondants))
201-
return
174+
total_input = sum([d['value'] for d in utxo_data])
175+
real_cjfee = calc_cj_fee(self.active_orders[nick]['ordertype'],
176+
self.active_orders[nick]['cjfee'], self.cj_amount)
177+
change_amount = (total_input - self.cj_amount -
178+
self.active_orders[nick]['txfee'] + real_cjfee)
179+
180+
# certain malicious and/or incompetent liquidity providers send
181+
# inputs totalling less than the coinjoin amount! this leads to
182+
# a change output of zero satoshis, so the invalid transaction
183+
# fails harmlessly; let's fail earlier, with a clear message.
184+
if change_amount < jm_single().DUST_THRESHOLD:
185+
fmt = ('ERROR counterparty requires sub-dust change. No '
186+
'action required. nick={}'
187+
'totalin={:d} cjamount={:d} change={:d}').format
188+
log.warn(fmt(nick, total_input, self.cj_amount, change_amount))
189+
return # timeout marks this maker as nonresponsive
190+
191+
self.outputs.append({'address': change_addr, 'value': change_amount})
192+
fmt = ('fee breakdown for {} totalin={:d} '
193+
'cjamount={:d} txfee={:d} realcjfee={:d}').format
194+
log.debug(fmt(nick, total_input, self.cj_amount,
195+
self.active_orders[nick]['txfee'], real_cjfee))
196+
self.outputs.append({'address': cj_addr, 'value': self.cj_amount})
197+
self.cjfee_total += real_cjfee
198+
self.maker_txfee_contributions += self.active_orders[nick]['txfee']
199+
self.nonrespondants.remove(nick)
200+
if len(self.nonrespondants) > 0:
201+
log.debug('nonrespondants = ' + str(self.nonrespondants))
202+
return
203+
#Note we fall through here immediately if nick is None;
204+
#this is the case for recovery where we are going to do a join with
205+
#less participants than originally intended. If minmakers is set to 0,
206+
#disallowing completion with subset, assert is still true.
207+
assert len(self.active_orders.keys()) >= jm_single().config.getint(
208+
"POLICY", "minimum_makers")
202209
log.info('got all parts, enough to build a tx')
203210
self.nonrespondants = list(self.active_orders.keys())
204211

@@ -405,43 +412,59 @@ def self_sign_and_push(self):
405412
return self.push()
406413

407414
def recover_from_nonrespondants(self):
415+
416+
def restart():
417+
self.end_timeout_thread = True
418+
if self.finishcallback is not None:
419+
self.finishcallback(self)
420+
# finishcallback will check if self.all_responded is True
421+
# and will know it came from here
422+
408423
log.info('nonresponding makers = ' + str(self.nonrespondants))
409424
# if there is no choose_orders_recover then end and call finishcallback
410425
# so the caller can handle it in their own way, notable for sweeping
411426
# where simply replacing the makers wont work
412427
if not self.choose_orders_recover:
413-
self.end_timeout_thread = True
414-
if self.finishcallback is not None:
415-
self.finishcallback(self)
428+
restart()
416429
return
417430

418431
if self.latest_tx is None:
419-
# nonresponding to !fill, recover by finding another maker
432+
# nonresponding to !fill-!auth, proceed with transaction anyway as long
433+
# as number of makers is at least POLICY.minimum_makers (and not zero,
434+
# i.e. disallow this kind of continuation).
420435
log.debug('nonresponse to !fill')
421436
for nr in self.nonrespondants:
422437
del self.active_orders[nr]
423-
new_orders, new_makers_fee = self.choose_orders_recover(
438+
minmakers = jm_single().config.getint("POLICY", "minimum_makers")
439+
if len(self.active_orders.keys()) >= minmakers and minmakers != 0:
440+
log.info("Completing the transaction with: " + str(
441+
len(self.active_orders.keys())) + " makers.")
442+
self.recv_txio(None, None, None, None, None)
443+
elif minmakers == 0:
444+
#Revert to the old algorithm: re-source number of orders
445+
#still needed, but ignoring non-respondants and currently active
446+
new_orders, new_makers_fee = self.choose_orders_recover(
424447
self.cj_amount, len(self.nonrespondants),
425448
self.nonrespondants,
426449
self.active_orders.keys())
427-
for nick, order in new_orders.iteritems():
428-
self.active_orders[nick] = order
429-
self.nonrespondants = list(new_orders.keys())
430-
log.debug(('new active_orders = {} \nnew nonrespondants = '
450+
for nick, order in new_orders.iteritems():
451+
self.active_orders[nick] = order
452+
self.nonrespondants = list(new_orders.keys())
453+
log.debug(('new active_orders = {} \nnew nonrespondants = '
431454
'{}').format(
432455
pprint.pformat(self.active_orders),
433456
pprint.pformat(self.nonrespondants)))
434-
#Re-source commitment; previous attempt will have been blacklisted
435-
self.get_commitment(self.input_utxos, self.cj_amount)
436-
self.msgchan.fill_orders(new_orders, self.cj_amount,
437-
self.kp.hex_pk(), self.commitment)
457+
#Re-source commitment; previous attempt will have been blacklisted
458+
self.get_commitment(self.input_utxos, self.cj_amount)
459+
self.msgchan.fill_orders(new_orders, self.cj_amount,
460+
self.kp.hex_pk(), self.commitment)
461+
else:
462+
log.info("Too few makers responded to complete, trying again.")
463+
restart()
438464
else:
439465
log.debug('nonresponse to !tx')
440-
# nonresponding to !tx, have to restart tx from the beginning
441-
self.end_timeout_thread = True
442-
if self.finishcallback is not None:
443-
self.finishcallback(self)
444-
# finishcallback will check if self.all_responded is True and will know it came from here
466+
# have to restart tx from the beginning
467+
restart()
445468

446469
class TimeoutThread(threading.Thread):
447470

sendpayment.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ def sendpayment_choose_orders(self,
218218
log.info(noun + ' coinjoin fee = ' + str(float('%.3g' % (
219219
100.0 * total_fee_pc))) + '%')
220220
check_high_fee(total_fee_pc)
221+
if jm_single().config.getint("POLICY", "minimum_makers") != 0:
222+
log.info("If some makers don't respond, we will still "
223+
"create a coinjoin with at least " + str(
224+
jm_single().config.getint(
225+
"POLICY", "minimum_makers")) + ". "
226+
"If you don't want this feature, set minimum_makers="
227+
"0 in joinmarket.cfg")
221228
if raw_input('send with these orders? (y/n):')[0] != 'y':
222229
log.info('ending')
223230
self.taker.msgchan.shutdown()
@@ -377,6 +384,20 @@ def main():
377384

378385
log.info('starting sendpayment')
379386

387+
#If we are not direct sending, then minimum_maker setting should
388+
#not be larger than the requested number of counterparties
389+
if options.makercount !=0 and options.makercount < jm_single().config.getint(
390+
"POLICY", "minimum_makers"):
391+
log.error("You selected a number of counterparties (" + \
392+
str(options.makercount) + \
393+
") less than the "
394+
"minimum requirement (" + \
395+
str(jm_single().config.getint("POLICY","minimum_makers")) + \
396+
"); you can edit the value 'minimum_makers'"
397+
" in the POLICY section in joinmarket.cfg to correct this. "
398+
"Quitting.")
399+
exit(0)
400+
380401
if not options.userpcwallet:
381402
wallet = Wallet(wallet_name, options.amtmixdepths, options.gaplimit)
382403
else:

test/regtest_joinmarket.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ usessl = false, false
1818
socks5 = false, false
1919
socks5_host = localhost, localhost
2020
socks5_port = 9150, 9150
21+
[LOGGING]
22+
console_log_level = DEBUG
2123
[POLICY]
2224
# for dust sweeping, try merge_algorithm = gradual
2325
# for more rapid dust sweeping, try merge_algorithm = greedy
@@ -31,6 +33,7 @@ merge_algorithm = default
3133
# as our default. Note that for clients not using a local blockchain
3234
# instance, we retrieve an estimate from the API at cointape.com, currently.
3335
tx_fees = 3
36+
minimum_makers = 2
3437
taker_utxo_retries = 3
3538
taker_utxo_age = 1
3639
taker_utxo_amtpercent = 20

0 commit comments

Comments
 (0)