@@ -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 = {} \n new 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 = {} \n new 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
0 commit comments