Browse Source

Fixes bugs in restart-with-subset including #169; see PR for more

master
AdamISZ 8 years ago
parent
commit
3bb6c785ff
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 16
      jmclient/jmclient/client_protocol.py
  2. 39
      jmclient/jmclient/taker.py
  3. 85
      jmclient/jmclient/taker_utils.py
  4. 3
      jmdaemon/jmdaemon/daemon_protocol.py
  5. 35
      scripts/sendpayment.py

16
jmclient/jmclient/client_protocol.py

@ -370,14 +370,20 @@ class JMTakerClientProtocol(JMClientProtocol):
def on_JM_FILL_RESPONSE(self, success, ioauth_data): def on_JM_FILL_RESPONSE(self, success, ioauth_data):
"""Receives the entire set of phase 1 data (principally utxos) """Receives the entire set of phase 1 data (principally utxos)
from the counterparties and passes through to the Taker for from the counterparties and passes through to the Taker for
tx construction, if successful. Then passes back the phase 2 tx construction. If there were sufficient makers, data is passed
initiating data to the daemon. over for exactly those makers that responded. If not, the list
of non-responsive makers is added to the permanent "ignored_makers"
list, but the Taker processing is bypassed and the transaction
is abandoned here (so will be picked up as stalled in multi-join
schedules).
In the first of the above two cases, after the Taker processes
the ioauth data and returns the proposed
transaction, passes the phase 2 initiating data to the daemon.
""" """
ioauth_data = json.loads(ioauth_data) ioauth_data = json.loads(ioauth_data)
if not success: if not success:
nonresponders = ioauth_data jlog.info("Makers who didnt respond: " + str(ioauth_data))
jlog.info("Makers didnt respond: " + str(nonresponders)) self.client.add_ignored_makers(ioauth_data)
self.client.add_ignored_makers(nonresponders)
return {'accepted': True} return {'accepted': True}
else: else:
jlog.info("Makers responded with: " + json.dumps(ioauth_data)) jlog.info("Makers responded with: " + json.dumps(ioauth_data))

39
jmclient/jmclient/taker.py

@ -77,11 +77,25 @@ class Taker(object):
self.wallet = wallet self.wallet = wallet
self.schedule = schedule self.schedule = schedule
self.order_chooser = order_chooser self.order_chooser = order_chooser
#List (which persists between transactions) of makers
#who have not responded or behaved maliciously at any
#stage of the protocol.
self.ignored_makers = [] if not ignored_makers else ignored_makers self.ignored_makers = [] if not ignored_makers else ignored_makers
#Used in attempts to complete with subset after second round failure: #Used in attempts to complete with subset after second round failure:
self.honest_makers = [] self.honest_makers = []
#Toggle: if set, only honest makers will be used from orderbook #Toggle: if set, only honest makers will be used from orderbook
self.honest_only = False self.honest_only = False
#Temporary (per transaction) list of makers that keeps track of
#which have responded, both in Stage 1 and Stage 2. Before each
#stage, the list is set to the full set of expected responders,
#and entries are removed when honest responses are received;
#emptiness of the list can be used to trigger completion of
#processing.
self.nonrespondants = []
self.waiting_for_conf = False self.waiting_for_conf = False
self.txid = None self.txid = None
self.schedule_index = -1 self.schedule_index = -1
@ -104,6 +118,7 @@ class Taker(object):
for the duration of the Taker run (so, the whole schedule). for the duration of the Taker run (so, the whole schedule).
""" """
self.ignored_makers.extend(makers) self.ignored_makers.extend(makers)
self.ignored_makers = list(set(self.ignored_makers))
def add_honest_makers(self, makers): def add_honest_makers(self, makers):
"""A maker who has shown willigness to complete the protocol """A maker who has shown willigness to complete the protocol
@ -112,6 +127,7 @@ class Taker(object):
offers from thus-defined "honest" makers. offers from thus-defined "honest" makers.
""" """
self.honest_makers.extend(makers) self.honest_makers.extend(makers)
self.honest_makers = list(set(self.honest_makers))
def set_honest_only(self, truefalse): def set_honest_only(self, truefalse):
"""Toggle; if set, offers will only be accepted """Toggle; if set, offers will only be accepted
@ -173,6 +189,7 @@ class Taker(object):
self.cjfee_total = 0 self.cjfee_total = 0
self.maker_txfee_contributions = 0 self.maker_txfee_contributions = 0
self.txfee_default = 5000 self.txfee_default = 5000
self.latest_tx = None
self.txid = None self.txid = None
sweep = True if self.cjamount == 0 else False sweep = True if self.cjamount == 0 else False
@ -198,6 +215,11 @@ class Taker(object):
return (False,) return (False,)
else: else:
self.taker_info_callback("INFO", errmsg) self.taker_info_callback("INFO", errmsg)
#Initialization has been successful. We must set the nonrespondants
#now to keep track of what changed when we receive the utxo data
self.nonrespondants = self.orderbook.keys()
return (True, self.cjamount, commitment, revelation, self.orderbook) return (True, self.cjamount, commitment, revelation, self.orderbook)
def filter_orderbook(self, orderbook, sweep=False): def filter_orderbook(self, orderbook, sweep=False):
@ -311,8 +333,10 @@ class Taker(object):
""" """
if self.aborted: if self.aborted:
return (False, "User aborted") return (False, "User aborted")
#Temporary list used to aggregate all ioauth data that must be removed
rejected_counterparties = [] rejected_counterparties = []
#Enough data, but need to authorize against the btc pubkey first. #Need to authorize against the btc pubkey first.
for nick, nickdata in ioauth_data.iteritems(): for nick, nickdata in ioauth_data.iteritems():
utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata
if not self.auth_counterparty(btc_sig, auth_pub, maker_pk): if not self.auth_counterparty(btc_sig, auth_pub, maker_pk):
@ -378,8 +402,16 @@ class Taker(object):
self.cjfee_total += real_cjfee self.cjfee_total += real_cjfee
self.maker_txfee_contributions += self.orderbook[nick]['txfee'] self.maker_txfee_contributions += self.orderbook[nick]['txfee']
self.maker_utxo_data[nick] = utxo_data self.maker_utxo_data[nick] = utxo_data
#We have succesfully processed the data from this nick:
try:
self.nonrespondants.remove(nick)
except Exception as e:
jlog.warn("Failure to remove counterparty from nonrespondants list: " + str(nick) + \
", error message: " + repr(e))
#Apply business logic of how many counterparties are enough: #Apply business logic of how many counterparties are enough; note that
#this must occur after the above ioauth data processing, since we only now
#know for sure that the data meets all business-logic requirements.
if len(self.maker_utxo_data.keys()) < jm_single().config.getint( if len(self.maker_utxo_data.keys()) < jm_single().config.getint(
"POLICY", "minimum_makers"): "POLICY", "minimum_makers"):
self.taker_info_callback("INFO", "Not enough counterparties, aborting.") self.taker_info_callback("INFO", "Not enough counterparties, aborting.")
@ -387,6 +419,9 @@ class Taker(object):
"Not enough counterparties responded to fill, giving up") "Not enough counterparties responded to fill, giving up")
self.taker_info_callback("INFO", "Got all parts, enough to build a tx") self.taker_info_callback("INFO", "Got all parts, enough to build a tx")
#The list self.nonrespondants is now reset and
#used to track return of signatures for phase 2
self.nonrespondants = list(self.maker_utxo_data.keys()) self.nonrespondants = list(self.maker_utxo_data.keys())
my_total_in = sum([va['value'] for u, va in self.input_utxos.iteritems() my_total_in = sum([va['value'] for u, va in self.input_utxos.iteritems()

85
jmclient/jmclient/taker_utils.py

@ -238,52 +238,57 @@ def tumbler_taker_finished_update(taker, schedulefile, tumble_log, options,
taker.wallet.add_new_utxos(txd, txid) taker.wallet.add_new_utxos(txd, txid)
else: else:
#a transaction failed, either because insufficient makers #a transaction failed, either because insufficient makers
#(acording to minimum_makers) responded in Stage 1, or not all #(acording to minimum_makers) responded in Phase 1, or not all
#makers responded in Stage 2. We'll first try to repeat without the #makers responded in Phase 2. We'll first try to repeat without the
#troublemakers. #troublemakers.
#Note that Taker.nonrespondants is always set to the full maker
#list at the start of Taker.receive_utxos, so it is always updated
#to a new list in each tx run.
log.info("Schedule entry: " + str( log.info("Schedule entry: " + str(
taker.schedule[taker.schedule_index]) + \ taker.schedule[taker.schedule_index]) + \
" failed after timeout, trying again") " failed after timeout, trying again")
taker.add_ignored_makers(taker.nonrespondants) taker.add_ignored_makers(taker.nonrespondants)
#Now we have to set the specific group we want to use, and hopefully #Is the failure in Phase 2?
#they will respond again as they showed honesty last time. if not taker.latest_tx is None:
taker.add_honest_makers(list(set( #Now we have to set the specific group we want to use, and hopefully
taker.maker_utxo_data.keys()).symmetric_difference( #they will respond again as they showed honesty last time.
set(taker.nonrespondants)))) #Note that we must wipe the list first; other honest makers needn't
#If no makers were honest, we can only tweak the schedule. #have the right settings (e.g. max cjamount), so can't be carried
#If some were, we prefer the restart with them only: #over from earlier transactions.
if len(taker.honest_makers) != 0: taker.honest_makers = []
tumble_log.info("Transaction attempt failed, attempting to " taker.add_honest_makers(list(set(
"restart with subset.") taker.maker_utxo_data.keys()).symmetric_difference(
tumble_log.info("The paramaters of the failed attempt: ") set(taker.nonrespondants))))
tumble_log.info(str(taker.schedule[taker.schedule_index])) #If insufficient makers were honest, we can only tweak the schedule.
#we must reset the number of counterparties, as well as fix who they #If enough were, we prefer the restart with them only:
#are; this is because the number is used to e.g. calculate fees. log.info("Inside a Phase 2 failure; number of honest respondants was: " + str(len(taker.honest_makers)))
#cleanest way is to reset the number in the schedule before restart. log.info("They were: " + str(taker.honest_makers))
taker.schedule[taker.schedule_index][2] = len(taker.honest_makers) if len(taker.honest_makers) >= jm_single().config.getint(
retry_str = "Retrying with: " + str(taker.schedule[ "POLICY", "minimum_makers"):
taker.schedule_index][2]) + " counterparties." tumble_log.info("Transaction attempt failed, attempting to "
tumble_log.info(retry_str) "restart with subset.")
log.info(retry_str) tumble_log.info("The paramaters of the failed attempt: ")
taker.set_honest_only(True) tumble_log.info(str(taker.schedule[taker.schedule_index]))
taker.schedule_index -= 1 #we must reset the number of counterparties, as well as fix who they
#are; this is because the number is used to e.g. calculate fees.
#cleanest way is to reset the number in the schedule before restart.
taker.schedule[taker.schedule_index][2] = len(taker.honest_makers)
retry_str = "Retrying with: " + str(taker.schedule[
taker.schedule_index][2]) + " counterparties."
tumble_log.info(retry_str)
log.info(retry_str)
taker.set_honest_only(True)
taker.schedule_index -= 1
return
#a tumbler is aggressive in trying to complete; we tweak the schedule #There were not enough honest counterparties.
#from this point in the mixdepth, then try again. However we only #Tumbler is aggressive in trying to complete; we tweak the schedule
#try this strategy if the previous (select-honest-only) strategy #from this point in the mixdepth, then try again.
#failed. tumble_log.info("Transaction attempt failed, tweaking schedule"
else: " and trying again.")
tumble_log.info("Transaction attempt failed, tweaking schedule" tumble_log.info("The paramaters of the failed attempt: ")
" and trying again.") tumble_log.info(str(taker.schedule[taker.schedule_index]))
tumble_log.info("The paramaters of the failed attempt: ") taker.schedule_index -= 1
tumble_log.info(str(taker.schedule[taker.schedule_index])) taker.schedule = tweak_tumble_schedule(options, taker.schedule,
taker.schedule_index -= 1 taker.schedule_index,
taker.schedule = tweak_tumble_schedule(options, taker.schedule, taker.tdestaddrs)
taker.schedule_index,
taker.tdestaddrs)
tumble_log.info("We tweaked the schedule, the new schedule is:") tumble_log.info("We tweaked the schedule, the new schedule is:")
tumble_log.info(pprint.pformat(taker.schedule)) tumble_log.info(pprint.pformat(taker.schedule))
else: else:

3
jmdaemon/jmdaemon/daemon_protocol.py

@ -632,7 +632,8 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
""" """
if nick in self.crypto_boxes and self.crypto_boxes[nick] != None: if nick in self.crypto_boxes and self.crypto_boxes[nick] != None:
return self.crypto_boxes[nick][1] return self.crypto_boxes[nick][1]
elif nick in self.active_orders and self.active_orders[nick] != None: elif nick in self.active_orders and self.active_orders[nick] != None \
and "crypto_box" in self.active_orders[nick]:
return self.active_orders[nick]["crypto_box"] return self.active_orders[nick]["crypto_box"]
else: else:
log.msg('something wrong, no crypto object, nick=' + nick + log.msg('something wrong, no crypto object, nick=' + nick +

35
scripts/sendpayment.py

@ -189,24 +189,35 @@ def main():
clientfactory.getClient().clientStart) clientfactory.getClient().clientStart)
else: else:
#a transaction failed; we'll try to repeat without the #a transaction failed; we'll try to repeat without the
#troublemakers #troublemakers.
#Note that Taker.nonrespondants is always set to the full maker #If this error condition is reached from Phase 1 processing,
#list at the start of Taker.receive_utxos, so it is always updated #and there are less than minimum_makers honest responses, we
#to a new list in each run. #just give up (note that in tumbler we tweak and retry, but
print("We failed to complete the transaction. The following " #for sendpayment the user is "online" and so can manually
"makers didn't respond: ", taker.nonrespondants, #try again).
", so we will retry without them.") #However if the error is in Phase 2 and we have minimum_makers
taker.add_ignored_makers(taker.nonrespondants) #or more responses, we do try to restart with the honest set, here.
#Now we have to set the specific group we want to use, and hopefully if taker.latest_tx is None:
#they will respond again as they showed honesty last time. #can only happen with < minimum_makers; see above.
log.info("A transaction failed but there are insufficient "
"honest respondants to continue; giving up.")
reactor.stop()
return
#This is Phase 2; do we have enough to try again?
taker.add_honest_makers(list(set( taker.add_honest_makers(list(set(
taker.maker_utxo_data.keys()).symmetric_difference( taker.maker_utxo_data.keys()).symmetric_difference(
set(taker.nonrespondants)))) set(taker.nonrespondants))))
if len(taker.honest_makers) == 0: if len(taker.honest_makers) < jm_single().config.getint(
log.info("None of the makers responded honestly; " "POLICY", "minimum_makers"):
log.info("Too few makers responded honestly; "
"giving up this attempt.") "giving up this attempt.")
reactor.stop() reactor.stop()
return return
print("We failed to complete the transaction. The following "
"makers responded honestly: ", taker.honest_makers,
", so we will retry with them.")
#Now we have to set the specific group we want to use, and hopefully
#they will respond again as they showed honesty last time.
#we must reset the number of counterparties, as well as fix who they #we must reset the number of counterparties, as well as fix who they
#are; this is because the number is used to e.g. calculate fees. #are; this is because the number is used to e.g. calculate fees.
#cleanest way is to reset the number in the schedule before restart. #cleanest way is to reset the number in the schedule before restart.

Loading…
Cancel
Save