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

Commit

Permalink
Merge #647: initial draft allowing takers to optionally complete tran…
Browse files Browse the repository at this point in the history
…sactions wit…

c89347a 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 (Adam Gibson)
  • Loading branch information
AdamISZ committed Oct 27, 2016
2 parents 40b3d54 + c89347a commit 3276e3c
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 69 deletions.
5 changes: 5 additions & 0 deletions joinmarket/configure.py
Expand Up @@ -126,6 +126,11 @@ def jm_single():
# for most rapid dust sweeping, try merge_algorithm = greediest
# but don't forget to bump your miner fees!
merge_algorithm = default
# For takers: the minimum number of makers you allow in a transaction
# to complete, accounting for the fact that some makers might not be
# responsive. Should be an integer >=2 for privacy, or set to 0 if you
# want to disallow any reduction from your chosen number of makers.
minimum_makers = 2
# the fee estimate is based on a projection of how many satoshis
# per kB are needed to get in one of the next N blocks, N set here
# as the value of 'tx_fees'. This estimate is high if you set N=1,
Expand Down
161 changes: 92 additions & 69 deletions joinmarket/taker.py
Expand Up @@ -147,58 +147,65 @@ def auth_counterparty(self, nick, btc_sig, auth_pub):
return True

def recv_txio(self, nick, utxo_list, auth_pub, cj_addr, change_addr):
if nick not in self.nonrespondants:
log.debug(('recv_txio => nick={} not in '
'nonrespondants {}').format(nick, self.nonrespondants))
return
self.utxos[nick] = utxo_list
utxo_data = jm_single().bc_interface.query_utxo_set(self.utxos[nick])
if None in utxo_data:
log.error(('ERROR outputs unconfirmed or already spent. '
'utxo_data={}').format(pprint.pformat(utxo_data)))
# when internal reviewing of makers is created, add it here to
# immediately quit; currently, the timeout thread suffices.
return
#Complete maker authorization:
#Extract the address fields from the utxos
#Construct the Bitcoin address for the auth_pub field
#Ensure that at least one address from utxos corresponds.
input_addresses = [d['address'] for d in utxo_data]
auth_address = btc.pubkey_to_address(auth_pub, get_p2pk_vbyte())
if not auth_address in input_addresses:
log.error("ERROR maker's authorising pubkey is not included "
"in the transaction: " + str(auth_address))
return
if nick:
if nick not in self.nonrespondants:
log.debug(('recv_txio => nick={} not in '
'nonrespondants {}').format(nick, self.nonrespondants))
return
self.utxos[nick] = utxo_list
utxo_data = jm_single().bc_interface.query_utxo_set(self.utxos[nick])
if None in utxo_data:
log.error(('ERROR outputs unconfirmed or already spent. '
'utxo_data={}').format(pprint.pformat(utxo_data)))
# when internal reviewing of makers is created, add it here to
# immediately quit; currently, the timeout thread suffices.
return
#Complete maker authorization:
#Extract the address fields from the utxos
#Construct the Bitcoin address for the auth_pub field
#Ensure that at least one address from utxos corresponds.
input_addresses = [d['address'] for d in utxo_data]
auth_address = btc.pubkey_to_address(auth_pub, get_p2pk_vbyte())
if not auth_address in input_addresses:
log.error("ERROR maker's authorising pubkey is not included "
"in the transaction: " + str(auth_address))
return

total_input = sum([d['value'] for d in utxo_data])
real_cjfee = calc_cj_fee(self.active_orders[nick]['ordertype'],
self.active_orders[nick]['cjfee'], self.cj_amount)
change_amount = (total_input - self.cj_amount -
self.active_orders[nick]['txfee'] + real_cjfee)

# certain malicious and/or incompetent liquidity providers send
# inputs totalling less than the coinjoin amount! this leads to
# a change output of zero satoshis, so the invalid transaction
# fails harmlessly; let's fail earlier, with a clear message.
if change_amount < jm_single().DUST_THRESHOLD:
fmt = ('ERROR counterparty requires sub-dust change. No '
'action required. nick={}'
'totalin={:d} cjamount={:d} change={:d}').format
log.warn(fmt(nick, total_input, self.cj_amount, change_amount))
return # timeout marks this maker as nonresponsive

self.outputs.append({'address': change_addr, 'value': change_amount})
fmt = ('fee breakdown for {} totalin={:d} '
'cjamount={:d} txfee={:d} realcjfee={:d}').format
log.debug(fmt(nick, total_input, self.cj_amount,
self.active_orders[nick]['txfee'], real_cjfee))
self.outputs.append({'address': cj_addr, 'value': self.cj_amount})
self.cjfee_total += real_cjfee
self.maker_txfee_contributions += self.active_orders[nick]['txfee']
self.nonrespondants.remove(nick)
if len(self.nonrespondants) > 0:
log.debug('nonrespondants = ' + str(self.nonrespondants))
return
total_input = sum([d['value'] for d in utxo_data])
real_cjfee = calc_cj_fee(self.active_orders[nick]['ordertype'],
self.active_orders[nick]['cjfee'], self.cj_amount)
change_amount = (total_input - self.cj_amount -
self.active_orders[nick]['txfee'] + real_cjfee)

# certain malicious and/or incompetent liquidity providers send
# inputs totalling less than the coinjoin amount! this leads to
# a change output of zero satoshis, so the invalid transaction
# fails harmlessly; let's fail earlier, with a clear message.
if change_amount < jm_single().DUST_THRESHOLD:
fmt = ('ERROR counterparty requires sub-dust change. No '
'action required. nick={}'
'totalin={:d} cjamount={:d} change={:d}').format
log.warn(fmt(nick, total_input, self.cj_amount, change_amount))
return # timeout marks this maker as nonresponsive

self.outputs.append({'address': change_addr, 'value': change_amount})
fmt = ('fee breakdown for {} totalin={:d} '
'cjamount={:d} txfee={:d} realcjfee={:d}').format
log.debug(fmt(nick, total_input, self.cj_amount,
self.active_orders[nick]['txfee'], real_cjfee))
self.outputs.append({'address': cj_addr, 'value': self.cj_amount})
self.cjfee_total += real_cjfee
self.maker_txfee_contributions += self.active_orders[nick]['txfee']
self.nonrespondants.remove(nick)
if len(self.nonrespondants) > 0:
log.debug('nonrespondants = ' + str(self.nonrespondants))
return
#Note we fall through here immediately if nick is None;
#this is the case for recovery where we are going to do a join with
#less participants than originally intended. If minmakers is set to 0,
#disallowing completion with subset, assert is still true.
assert len(self.active_orders.keys()) >= jm_single().config.getint(
"POLICY", "minimum_makers")
log.info('got all parts, enough to build a tx')
self.nonrespondants = list(self.active_orders.keys())

Expand Down Expand Up @@ -405,43 +412,59 @@ def self_sign_and_push(self):
return self.push()

def recover_from_nonrespondants(self):

def restart():
self.end_timeout_thread = True
if self.finishcallback is not None:
self.finishcallback(self)
# finishcallback will check if self.all_responded is True
# and will know it came from here

log.info('nonresponding makers = ' + str(self.nonrespondants))
# if there is no choose_orders_recover then end and call finishcallback
# so the caller can handle it in their own way, notable for sweeping
# where simply replacing the makers wont work
if not self.choose_orders_recover:
self.end_timeout_thread = True
if self.finishcallback is not None:
self.finishcallback(self)
restart()
return

if self.latest_tx is None:
# nonresponding to !fill, recover by finding another maker
# nonresponding to !fill-!auth, proceed with transaction anyway as long
# as number of makers is at least POLICY.minimum_makers (and not zero,
# i.e. disallow this kind of continuation).
log.debug('nonresponse to !fill')
for nr in self.nonrespondants:
del self.active_orders[nr]
new_orders, new_makers_fee = self.choose_orders_recover(
minmakers = jm_single().config.getint("POLICY", "minimum_makers")
if len(self.active_orders.keys()) >= minmakers and minmakers != 0:
log.info("Completing the transaction with: " + str(
len(self.active_orders.keys())) + " makers.")
self.recv_txio(None, None, None, None, None)
elif minmakers == 0:
#Revert to the old algorithm: re-source number of orders
#still needed, but ignoring non-respondants and currently active
new_orders, new_makers_fee = self.choose_orders_recover(
self.cj_amount, len(self.nonrespondants),
self.nonrespondants,
self.active_orders.keys())
for nick, order in new_orders.iteritems():
self.active_orders[nick] = order
self.nonrespondants = list(new_orders.keys())
log.debug(('new active_orders = {} \nnew nonrespondants = '
for nick, order in new_orders.iteritems():
self.active_orders[nick] = order
self.nonrespondants = list(new_orders.keys())
log.debug(('new active_orders = {} \nnew nonrespondants = '
'{}').format(
pprint.pformat(self.active_orders),
pprint.pformat(self.nonrespondants)))
#Re-source commitment; previous attempt will have been blacklisted
self.get_commitment(self.input_utxos, self.cj_amount)
self.msgchan.fill_orders(new_orders, self.cj_amount,
self.kp.hex_pk(), self.commitment)
#Re-source commitment; previous attempt will have been blacklisted
self.get_commitment(self.input_utxos, self.cj_amount)
self.msgchan.fill_orders(new_orders, self.cj_amount,
self.kp.hex_pk(), self.commitment)
else:
log.info("Too few makers responded to complete, trying again.")
restart()
else:
log.debug('nonresponse to !tx')
# nonresponding to !tx, have to restart tx from the beginning
self.end_timeout_thread = True
if self.finishcallback is not None:
self.finishcallback(self)
# finishcallback will check if self.all_responded is True and will know it came from here
# have to restart tx from the beginning
restart()

class TimeoutThread(threading.Thread):

Expand Down
21 changes: 21 additions & 0 deletions sendpayment.py
Expand Up @@ -218,6 +218,13 @@ def sendpayment_choose_orders(self,
log.info(noun + ' coinjoin fee = ' + str(float('%.3g' % (
100.0 * total_fee_pc))) + '%')
check_high_fee(total_fee_pc)
if jm_single().config.getint("POLICY", "minimum_makers") != 0:
log.info("If some makers don't respond, we will still "
"create a coinjoin with at least " + str(
jm_single().config.getint(
"POLICY", "minimum_makers")) + ". "
"If you don't want this feature, set minimum_makers="
"0 in joinmarket.cfg")
if raw_input('send with these orders? (y/n):')[0] != 'y':
log.info('ending')
self.taker.msgchan.shutdown()
Expand Down Expand Up @@ -377,6 +384,20 @@ def main():

log.info('starting sendpayment')

#If we are not direct sending, then minimum_maker setting should
#not be larger than the requested number of counterparties
if options.makercount !=0 and options.makercount < jm_single().config.getint(
"POLICY", "minimum_makers"):
log.error("You selected a number of counterparties (" + \
str(options.makercount) + \
") less than the "
"minimum requirement (" + \
str(jm_single().config.getint("POLICY","minimum_makers")) + \
"); you can edit the value 'minimum_makers'"
" in the POLICY section in joinmarket.cfg to correct this. "
"Quitting.")
exit(0)

if not options.userpcwallet:
wallet = Wallet(wallet_name, options.amtmixdepths, options.gaplimit)
else:
Expand Down
3 changes: 3 additions & 0 deletions test/regtest_joinmarket.cfg
Expand Up @@ -18,6 +18,8 @@ usessl = false, false
socks5 = false, false
socks5_host = localhost, localhost
socks5_port = 9150, 9150
[LOGGING]
console_log_level = DEBUG
[POLICY]
# for dust sweeping, try merge_algorithm = gradual
# for more rapid dust sweeping, try merge_algorithm = greedy
Expand All @@ -31,6 +33,7 @@ merge_algorithm = default
# as our default. Note that for clients not using a local blockchain
# instance, we retrieve an estimate from the API at cointape.com, currently.
tx_fees = 3
minimum_makers = 2
taker_utxo_retries = 3
taker_utxo_age = 1
taker_utxo_amtpercent = 20
Expand Down

0 comments on commit 3276e3c

Please sign in to comment.