def on_tx_received(self, nick, txhex, offerinfo): try: tx = btc.deserialize(txhex) except IndexError as e: return (False, 'malformed txhex. ' + repr(e)) jlog.info('obtained tx\n' + pprint.pformat(tx)) goodtx, errmsg = self.verify_unsigned_tx(tx, offerinfo) if not goodtx: jlog.info('not a good tx, reason=' + errmsg) return (False, errmsg) jlog.info('goodtx') sigs = [] utxos = offerinfo["utxos"] for index, ins in enumerate(tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in utxos.keys(): continue addr = utxos[utxo]['address'] amount = utxos[utxo]["value"] txs = self.wallet.sign(txhex, index, self.wallet.get_key_from_addr(addr), amount=amount) sigmsg = btc.deserialize(txs)["ins"][index]["script"].decode("hex") if "txinwitness" in btc.deserialize(txs)["ins"][index].keys(): #We prepend the witness data since we want (sig, pub, scriptCode); #also, the items in witness are not serialize_script-ed. sigmsg = "".join([ btc.serialize_script_unit(x.decode("hex")) for x in btc.deserialize(txs)["ins"][index]["txinwitness"] ]) + sigmsg sigs.append(base64.b64encode(sigmsg)) return (True, sigs)
def recv_tx(self, nick, txhex): try: self.tx = btc.deserialize(txhex) except IndexError as e: self.maker.msgchan.send_error(nick, 'malformed txhex. ' + repr(e)) log.debug('obtained tx\n' + pprint.pformat(self.tx)) goodtx, errmsg = self.verify_unsigned_tx(self.tx) if not goodtx: log.debug('not a good tx, reason=' + errmsg) self.maker.msgchan.send_error(nick, errmsg) # TODO: the above 3 errors should be encrypted, but it's a bit messy. log.debug('goodtx') sigs = [] for index, ins in enumerate(self.tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) if utxo not in self.utxos: continue addr = self.utxos[utxo]['address'] txs = btc.sign(txhex, index, self.maker.wallet.get_key_from_addr(addr)) sigs.append(base64.b64encode(btc.deserialize(txs)['ins'][index][ 'script'].decode('hex'))) # len(sigs) > 0 guarenteed since i did verify_unsigned_tx() jm_single().bc_interface.add_tx_notify( self.tx, self.unconfirm_callback, self.confirm_callback, self.cj_addr) log.debug('sending sigs ' + str(sigs)) self.maker.msgchan.send_sigs(nick, sigs) self.maker.active_orders[nick] = None
def recv_tx(self, nick, txhex): try: self.tx = btc.deserialize(txhex) except IndexError as e: self.maker.msgchan.send_error(nick, 'malformed txhex. ' + repr(e)) log.debug('obtained tx\n' + pprint.pformat(self.tx)) goodtx, errmsg = self.verify_unsigned_tx(self.tx) if not goodtx: log.debug('not a good tx, reason=' + errmsg) self.maker.msgchan.send_error(nick, errmsg) # TODO: the above 3 errors should be encrypted, but it's a bit messy. log.debug('goodtx') sigs = [] for index, ins in enumerate(self.tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.utxos: continue addr = self.utxos[utxo]['address'] txs = btc.sign(txhex, index, self.maker.wallet.get_key_from_addr(addr)) sigs.append( base64.b64encode( btc.deserialize(txs)['ins'][index]['script'].decode( 'hex'))) # len(sigs) > 0 guarenteed since i did verify_unsigned_tx() jm_single().bc_interface.add_tx_notify(self.tx, self.unconfirm_callback, self.confirm_callback, self.cj_addr) log.debug('sending sigs ' + str(sigs)) self.maker.msgchan.send_sigs(nick, sigs) self.maker.active_orders[nick] = None
def sign_donation_tx(tx, i, priv): from bitcoin.main import fast_multiply, decode_privkey, G, inv, N from bitcoin.transaction import der_encode_sig k = sign_k hashcode = btc.SIGHASH_ALL i = int(i) if len(priv) <= 33: priv = btc.safe_hexlify(priv) pub = btc.privkey_to_pubkey(priv) address = btc.pubkey_to_address(pub) signing_tx = btc.signature_form( tx, i, btc.mk_pubkey_script(address), hashcode) msghash = btc.bin_txhash(signing_tx, hashcode) z = btc.hash_to_int(msghash) # k = deterministic_generate_k(msghash, priv) r, y = fast_multiply(G, k) s = inv(k, N) * (z + r * decode_privkey(priv)) % N rawsig = 27 + (y % 2), r, s sig = der_encode_sig(*rawsig) + btc.encode(hashcode, 16, 2) # sig = ecdsa_tx_sign(signing_tx, priv, hashcode) txobj = btc.deserialize(tx) txobj["ins"][i]["script"] = btc.serialize_script([sig, pub]) return btc.serialize(txobj)
def self_sign(self): # now sign it ourselves our_inputs = {} for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue script = self.wallet.addr_to_script(self.input_utxos[utxo]['address']) amount = self.input_utxos[utxo]['value'] our_inputs[index] = (script, amount) # FIXME: ugly hack tx_bin = btc.deserialize(unhexlify(btc.serialize(self.latest_tx))) self.wallet.sign_tx(tx_bin, our_inputs) self.latest_tx = btc.deserialize(hexlify(btc.serialize(tx_bin)))
def self_sign(self): # now sign it ourselves tx = btc.serialize(self.latest_tx) if self.sign_method == "wallet": #Currently passes addresses of to-be-signed inputs #to backend wallet; this is correct for Electrum, may need #different info for other backends. addrs = {} for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue addrs[index] = self.input_utxos[utxo]['address'] tx = self.wallet.sign_tx(tx, addrs) else: for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue addr = self.input_utxos[utxo]['address'] tx = self.sign_tx(tx, index, self.wallet.get_key_from_addr(addr)) self.latest_tx = btc.deserialize(tx)
def sign_donation_tx(tx, i, priv): from bitcoin.main import fast_multiply, decode_privkey, G, inv, N from bitcoin.transaction import der_encode_sig k = sign_k hashcode = btc.SIGHASH_ALL i = int(i) if len(priv) <= 33: priv = btc.safe_hexlify(priv) pub = btc.privkey_to_pubkey(priv) address = btc.pubkey_to_address(pub) signing_tx = btc.signature_form(tx, i, btc.mk_pubkey_script(address), hashcode) msghash = btc.bin_txhash(signing_tx, hashcode) z = btc.hash_to_int(msghash) # k = deterministic_generate_k(msghash, priv) r, y = fast_multiply(G, k) s = inv(k, N) * (z + r * decode_privkey(priv)) % N rawsig = 27 + (y % 2), r, s sig = der_encode_sig(*rawsig) + btc.encode(hashcode, 16, 2) # sig = ecdsa_tx_sign(signing_tx, priv, hashcode) txobj = btc.deserialize(tx) txobj["ins"][i]["script"] = btc.serialize_script([sig, pub]) return btc.serialize(txobj)
def get_deser_from_gettransaction(self, rpcretval): """Get full transaction deserialization from a call to `gettransaction` """ if not "hex" in rpcretval: log.info("Malformed gettransaction output") return None #str cast for unicode hexval = str(rpcretval["hex"]) return btc.deserialize(hexval)
def on_tx_received(self, nick, txhex, offerinfo): """Called when the counterparty has sent an unsigned transaction. Sigs are created and returned if and only if the transaction passes verification checks (see verify_unsigned_tx()). """ try: tx = btc.deserialize(txhex) except (IndexError, SerializationError, SerializationTruncationError) as e: return (False, 'malformed txhex. ' + repr(e)) jlog.info('obtained tx\n' + pprint.pformat(tx)) goodtx, errmsg = self.verify_unsigned_tx(tx, offerinfo) if not goodtx: jlog.info('not a good tx, reason=' + errmsg) return (False, errmsg) jlog.info('goodtx') sigs = [] utxos = offerinfo["utxos"] our_inputs = {} for index, ins in enumerate(tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in utxos: continue script = self.wallet.addr_to_script(utxos[utxo]['address']) amount = utxos[utxo]['value'] our_inputs[index] = (script, amount) txs = self.wallet.sign_tx(btc.deserialize(unhexlify(txhex)), our_inputs) for index in our_inputs: sigmsg = txs['ins'][index]['script'] if 'txinwitness' in txs['ins'][index]: #We prepend the witness data since we want (sig, pub, scriptCode); #also, the items in witness are not serialize_script-ed. sigmsg = b''.join( btc.serialize_script_unit(x) for x in txs['ins'][index]['txinwitness']) + sigmsg sigs.append(base64.b64encode(sigmsg)) return (True, sigs)
def self_sign(self): # now sign it ourselves tx = btc.serialize(self.latest_tx) if isinstance(jm_single().bc_interface, ElectrumWalletInterface): #can defer signing to our wallet; it will handle #all relevant inputs tx = self.wallet.sign_tx(tx) log.debug("Back in self_sign in cjtx, tx is: " + str(tx)) else: for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue addr = self.input_utxos[utxo]['address'] tx = self.sign_tx(tx, index, self.wallet.get_key_from_addr(addr)) self.latest_tx = btc.deserialize(tx)
def on_JM_TX_RECEIVED(self, nick, txhex, offer): offer = _byteify(json.loads(offer)) retval = self.client.on_tx_received(nick, txhex, offer) if not retval[0]: jlog.info("Maker refuses to continue on receipt of tx") else: sigs = retval[1] self.finalized_offers[nick] = offer tx = btc.deserialize(txhex) self.finalized_offers[nick]["txd"] = tx jm_single().bc_interface.add_tx_notify(tx, self.unconfirm_callback, self.confirm_callback, offer["cjaddr"], wallet_name=jm_single().bc_interface.get_wallet_name( self.client.wallet), txid_flag=False, vb=get_p2sh_vbyte()) d = self.callRemote(commands.JMTXSigs, nick=nick, sigs=json.dumps(sigs)) self.defaultCallbacks(d) return {"accepted": True}
def outputs_watcher(self, wallet_name, notifyaddr, tx_output_set, unconfirmfun, confirmfun, timeoutfun): """Given a key for the watcher loop (notifyaddr), a wallet name (account), a set of outputs, and unconfirm, confirm and timeout callbacks, check to see if a transaction matching that output set has appeared in the wallet. Call the callbacks and update the watcher loop state. End the loop when the confirmation has been seen (no spent monitoring here). """ wl = self.tx_watcher_loops[notifyaddr] print('txoutset=' + pprint.pformat(tx_output_set)) unconftx = self.get_from_electrum('blockchain.address.get_mempool', notifyaddr, blocking=True).get('result') unconftxs = set([str(t['tx_hash']) for t in unconftx]) if len(unconftxs): txdatas = [] for txid in unconftxs: txdatas.append({ 'id': txid, 'hex': str( self.get_from_electrum('blockchain.transaction.get', txid, blocking=True).get('result')) }) unconfirmed_txid = None for txdata in txdatas: txhex = txdata['hex'] outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) print('unconfirm query outs = ' + str(outs)) if outs == tx_output_set: unconfirmed_txid = txdata['id'] unconfirmed_txhex = txhex break #call unconf callback if it was found in the mempool if unconfirmed_txid and not wl[1]: print("Tx: " + str(unconfirmed_txid) + " seen on network.") unconfirmfun(btc.deserialize(unconfirmed_txhex), unconfirmed_txid) wl[1] = True return conftx = self.get_from_electrum('blockchain.address.listunspent', notifyaddr, blocking=True).get('result') conftxs = set([str(t['tx_hash']) for t in conftx]) if len(conftxs): txdatas = [] for txid in conftxs: txdata = str( self.get_from_electrum('blockchain.transaction.get', txid, blocking=True).get('result')) txdatas.append({'hex': txdata, 'id': txid}) confirmed_txid = None for txdata in txdatas: txhex = txdata['hex'] outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) print('confirm query outs = ' + str(outs)) if outs == tx_output_set: confirmed_txid = txdata['id'] confirmed_txhex = txhex break if confirmed_txid and not wl[2]: confirmfun(btc.deserialize(confirmed_txhex), confirmed_txid, 1) wl[2] = True wl[0].stop() return
def do_HEAD(self): pages = ('/walletnotify?', '/alertnotify?') if self.path.startswith('/walletnotify?'): txid = self.path[len(pages[0]):] if not re.match('^[0-9a-fA-F]*$', txid): log.debug('not a txid') return try: tx = self.btcinterface.rpc('getrawtransaction', [txid]) except (JsonRpcError, JsonRpcConnectionError) as e: log.debug('transaction not found, probably a conflict') return if not re.match('^[0-9a-fA-F]*$', tx): log.debug('not a txhex') return txd = btc.deserialize(tx) tx_output_set = set([(sv['script'], sv['value']) for sv in txd[ 'outs']]) txnotify_tuple = None unconfirmfun, confirmfun, timeoutfun, uc_called = (None, None, None, None) for tnf in self.btcinterface.txnotify_fun: tx_out = tnf[0] if tx_out == tx_output_set: txnotify_tuple = tnf tx_out, unconfirmfun, confirmfun, timeoutfun, uc_called = tnf break if unconfirmfun is None: log.debug('txid=' + txid + ' not being listened for') else: # on rare occasions people spend their output without waiting # for a confirm txdata = None for n in range(len(txd['outs'])): txdata = self.btcinterface.rpc('gettxout', [txid, n, True]) if txdata is not None: break assert txdata is not None if txdata['confirmations'] == 0: unconfirmfun(txd, txid) # TODO pass the total transfered amount value here somehow # wallet_name = self.get_wallet_name() # amount = # bitcoin-cli move wallet_name "" amount self.btcinterface.txnotify_fun.remove(txnotify_tuple) self.btcinterface.txnotify_fun.append(txnotify_tuple[:-1] + (True,)) log.debug('ran unconfirmfun') if timeoutfun: threading.Timer(jm_single().config.getfloat('TIMEOUT', 'confirm_timeout_hours')*60*60, bitcoincore_timeout_callback, args=(True, tx_output_set, self.btcinterface.txnotify_fun, timeoutfun)).start() else: if not uc_called: unconfirmfun(txd, txid) log.debug('saw confirmed tx before unconfirmed, ' + 'running unconfirmfun first') confirmfun(txd, txid, txdata['confirmations']) self.btcinterface.txnotify_fun.remove(txnotify_tuple) log.debug('ran confirmfun') elif self.path.startswith('/alertnotify?'): jm_single().core_alert[0] = urllib.unquote(self.path[len(pages[1]):]) log.debug('Got an alert!\nMessage=' + jm_single().core_alert[0]) else: log.debug('ERROR: This is not a handled URL path. You may want to check your notify URL for typos.') request = urllib2.Request('http://localhost:' + str(self.base_server.server_address[1] + 1) + self.path) request.get_method = lambda : 'HEAD' try: urllib2.urlopen(request) except urllib2.URLError: pass self.send_response(200) # self.send_header('Connection', 'close') self.end_headers()
def receive_utxos(self, ioauth_data): """Triggered when the daemon returns utxo data from makers who responded; this is the completion of phase 1 of the protocol """ if self.aborted: return (False, "User aborted") rejected_counterparties = [] #Enough data, but need to authorize against the btc pubkey first. for nick, nickdata in ioauth_data.iteritems(): utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata if not self.auth_counterparty(btc_sig, auth_pub, maker_pk): jlog.debug( "Counterparty encryption verification failed, aborting") #This counterparty must be rejected rejected_counterparties.append(nick) for rc in rejected_counterparties: del ioauth_data[rc] self.maker_utxo_data = {} for nick, nickdata in ioauth_data.iteritems(): utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata self.utxos[nick] = utxo_list utxo_data = jm_single().bc_interface.query_utxo_set( self.utxos[nick]) if None in utxo_data: jlog.debug(('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. continue #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: jlog.warn("ERROR maker's (" + nick + ")" " authorising pubkey is not included " "in the transaction: " + str(auth_address)) #this will not be added to the transaction, so we will have #to recheck if we have enough continue total_input = sum([d['value'] for d in utxo_data]) real_cjfee = calc_cj_fee(self.orderbook[nick]['ordertype'], self.orderbook[nick]['cjfee'], self.cjamount) change_amount = (total_input - self.cjamount - self.orderbook[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; this counterparty must be removed. if change_amount < jm_single().DUST_THRESHOLD: fmt = ('ERROR counterparty requires sub-dust change. nick={}' 'totalin={:d} cjamount={:d} change={:d}').format jlog.debug(fmt(nick, total_input, self.cjamount, change_amount)) jlog.warn("Invalid change, too small, nick= " + nick) continue self.outputs.append({ 'address': change_addr, 'value': change_amount }) fmt = ('fee breakdown for {} totalin={:d} ' 'cjamount={:d} txfee={:d} realcjfee={:d}').format jlog.debug( fmt(nick, total_input, self.cjamount, self.orderbook[nick]['txfee'], real_cjfee)) self.outputs.append({'address': cj_addr, 'value': self.cjamount}) self.cjfee_total += real_cjfee self.maker_txfee_contributions += self.orderbook[nick]['txfee'] self.maker_utxo_data[nick] = utxo_data #Apply business logic of how many counterparties are enough: if len(self.maker_utxo_data.keys()) < jm_single().config.getint( "POLICY", "minimum_makers"): self.taker_info_callback("INFO", "Not enough counterparties, aborting.") return (False, "Not enough counterparties responded to fill, giving up") self.taker_info_callback("INFO", "Got all parts, enough to build a tx") self.nonrespondants = list(self.maker_utxo_data.keys()) my_total_in = sum( [va['value'] for u, va in self.input_utxos.iteritems()]) if self.my_change_addr: #Estimate fee per choice of next/3/6 blocks targetting. estimated_fee = estimate_tx_fee(len(sum(self.utxos.values(), [])), len(self.outputs) + 2) jlog.info("Based on initial guess: " + str(self.total_txfee) + ", we estimated a miner fee of: " + str(estimated_fee)) #reset total self.total_txfee = estimated_fee my_txfee = max(self.total_txfee - self.maker_txfee_contributions, 0) my_change_value = (my_total_in - self.cjamount - self.cjfee_total - my_txfee) #Since we could not predict the maker's inputs, we may end up needing #too much such that the change value is negative or small. Note that #we have tried to avoid this based on over-estimating the needed amount #in SendPayment.create_tx(), but it is still a possibility if one maker #uses a *lot* of inputs. if self.my_change_addr and my_change_value <= 0: raise ValueError("Calculated transaction fee of: " + str(self.total_txfee) + " is too large for our inputs;Please try again.") elif self.my_change_addr and my_change_value <= jm_single( ).BITCOIN_DUST_THRESHOLD: jlog.info("Dynamically calculated change lower than dust: " + str(my_change_value) + "; dropping.") self.my_change_addr = None my_change_value = 0 jlog.info( 'fee breakdown for me totalin=%d my_txfee=%d makers_txfee=%d cjfee_total=%d => changevalue=%d' % (my_total_in, my_txfee, self.maker_txfee_contributions, self.cjfee_total, my_change_value)) if self.my_change_addr is None: if my_change_value != 0 and abs(my_change_value) != 1: # seems you wont always get exactly zero because of integer # rounding so 1 satoshi extra or fewer being spent as miner # fees is acceptable jlog.debug(('WARNING CHANGE NOT BEING ' 'USED\nCHANGEVALUE = {}').format(my_change_value)) else: self.outputs.append({ 'address': self.my_change_addr, 'value': my_change_value }) self.utxo_tx = [ dict([('output', u)]) for u in sum(self.utxos.values(), []) ] self.outputs.append({ 'address': self.coinjoin_address(), 'value': self.cjamount }) random.shuffle(self.utxo_tx) random.shuffle(self.outputs) tx = btc.mktx(self.utxo_tx, self.outputs) jlog.debug('obtained tx\n' + pprint.pformat(btc.deserialize(tx))) self.latest_tx = btc.deserialize(tx) for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue # placeholders required ins['script'] = 'deadbeef' self.taker_info_callback("INFO", "Built tx, sending to counterparties.") return (True, self.maker_utxo_data.keys(), tx)
def run(self): st = int(time.time()) unconfirmed_txid = None unconfirmed_txhex = None while not unconfirmed_txid: time.sleep(unconfirm_poll_period) if int(time.time()) - st > unconfirm_timeout: log.debug('checking for unconfirmed tx timed out') return shared_txid = None for a in self.output_addresses: unconftx = self.blockchaininterface.get_from_electrum('blockchain.address.get_mempool', a).get('result') unconftxs = set([str(t['tx_hash']) for t in unconftx]) if not shared_txid: shared_txid = unconftxs else: shared_txid = shared_txid.intersection(unconftxs) log.debug('sharedtxid = ' + str(shared_txid)) if len(shared_txid) == 0: continue data = [] for txid in shared_txid: txdata = str(self.blockchaininterface.get_from_electrum('blockchain.transaction.get', txid).get('result')) data.append({'hex':txdata,'id':txid}) for txdata in data: txhex = txdata['hex'] outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) log.debug('unconfirm query outs = ' + str(outs)) if outs == self.tx_output_set: unconfirmed_txid = txdata['id'] unconfirmed_txhex = txhex break self.unconfirmfun(btc.deserialize(unconfirmed_txhex), unconfirmed_txid) st = int(time.time()) confirmed_txid = None confirmed_txhex = None while not confirmed_txid: time.sleep(confirm_poll_period) if int(time.time()) - st > confirm_timeout: log.debug('checking for confirmed tx timed out') return shared_txid = None for a in self.output_addresses: conftx = self.blockchaininterface.get_from_electrum('blockchain.address.listunspent', a).get('result') conftxs = set([str(t['tx_hash']) for t in conftx]) if not shared_txid: shared_txid = conftxs else: shared_txid = shared_txid.intersection(conftxs) log.debug('sharedtxid = ' + str(shared_txid)) if len(shared_txid) == 0: continue data = [] for txid in shared_txid: txdata = str(self.blockchaininterface.get_from_electrum('blockchain.transaction.get', txid).get('result')) data.append({'hex':txdata,'id':txid}) for txdata in data: txhex = txdata['hex'] outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) log.debug('confirm query outs = ' + str(outs)) if outs == self.tx_output_set: confirmed_txid = txdata['id'] confirmed_txhex = txhex break self.confirmfun(btc.deserialize(confirmed_txhex), confirmed_txid, 1)
def recv_txio(self, nick, utxo_list, cj_pub, 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 log.debug("Trying to retrieve utxos for: " + nick) utxo_data = jm_single().bc_interface.query_utxo_set(self.utxos[nick]) log.debug("Got this utxo data: " + pprint.pformat(utxo_data)) if None in utxo_data: log.debug(('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 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. nick={}' 'totalin={:d} cjamount={:d} change={:d}').format log.debug(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)) cj_addr = btc.pubtoaddr(cj_pub, get_p2pk_vbyte()) 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 log.debug('got all parts, enough to build a tx') self.nonrespondants = list(self.active_orders.keys()) my_total_in = sum( [va['value'] for u, va in self.input_utxos.iteritems()]) if self.my_change_addr: #Estimate fee per choice of next/3/6 blocks targetting. estimated_fee = estimate_tx_fee(len(sum(self.utxos.values(), [])), len(self.outputs) + 2) log.debug("Based on initial guess: " + str(self.total_txfee) + ", we estimated a fee of: " + str(estimated_fee)) #reset total self.total_txfee = estimated_fee my_txfee = max(self.total_txfee - self.maker_txfee_contributions, 0) my_change_value = (my_total_in - self.cj_amount - self.cjfee_total - my_txfee) #Since we could not predict the maker's inputs, we may end up needing #too much such that the change value is negative or small. Note that #we have tried to avoid this based on over-estimating the needed amount #in SendPayment.create_tx(), but it is still a possibility if one maker #uses a *lot* of inputs. if self.my_change_addr and my_change_value <= 0: raise ValueError("Calculated transaction fee of: " + str(self.total_txfee) + " is too large for our inputs;Please try again.") elif self.my_change_addr and my_change_value <= jm_single( ).DUST_THRESHOLD: log.debug("Dynamically calculated change lower than dust: " + str(my_change_value) + "; dropping.") self.my_change_addr = None my_change_value = 0 log.debug( 'fee breakdown for me totalin=%d my_txfee=%d makers_txfee=%d cjfee_total=%d => changevalue=%d' % (my_total_in, my_txfee, self.maker_txfee_contributions, self.cjfee_total, my_change_value)) if self.my_change_addr is None: if my_change_value != 0 and abs(my_change_value) != 1: # seems you wont always get exactly zero because of integer # rounding so 1 satoshi extra or fewer being spent as miner # fees is acceptable log.debug(('WARNING CHANGE NOT BEING ' 'USED\nCHANGEVALUE = {}').format(my_change_value)) else: self.outputs.append({ 'address': self.my_change_addr, 'value': my_change_value }) self.utxo_tx = [ dict([('output', u)]) for u in sum(self.utxos.values(), []) ] self.outputs.append({ 'address': self.coinjoin_address(), 'value': self.cj_amount }) random.shuffle(self.utxo_tx) random.shuffle(self.outputs) tx = btc.mktx(self.utxo_tx, self.outputs) log.debug('obtained tx\n' + pprint.pformat(btc.deserialize(tx))) #Re-calculate a sensible timeout wait based on the throttling #settings and the tx size. #Calculation: Let tx size be S; tx undergoes two b64 expansions, 1.8*S #So we're sending N*1.8*S over the wire, and the #maximum bytes/sec = B, means we need (1.8*N*S/B) seconds, #and need to add some leeway for network delays, we just add the #contents of jm_single().maker_timeout_sec (the user configured value) self.maker_timeout_sec = (len(tx) * 1.8 * len(self.active_orders.keys( ))) / (B_PER_SEC) + jm_single().maker_timeout_sec log.debug("Based on transaction size: " + str(len(tx)) + ", calculated time to wait for replies: " + str(self.maker_timeout_sec)) self.all_responded = True with self.timeout_lock: self.timeout_lock.notify() self.msgchan.send_tx(self.active_orders.keys(), tx) self.latest_tx = btc.deserialize(tx) for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue # placeholders required ins['script'] = 'deadbeef'
def run(self): st = int(time.time()) unconfirmed_txid = None unconfirmed_txhex = None while not unconfirmed_txid: time.sleep(unconfirm_poll_period) if int(time.time()) - st > unconfirm_timeout: log.debug('checking for unconfirmed tx timed out') if self.timeoutfun: self.timeoutfun(False) return blockr_url = 'https://' + self.blockr_domain blockr_url += '.blockr.io/api/v1/address/unspent/' random.shuffle(self.output_addresses ) # seriously weird bug with blockr.io data = json.loads( btc.make_request(blockr_url + ','.join( self.output_addresses ) + '?unconfirmed=1'))['data'] shared_txid = None for unspent_list in data: txs = set([str(txdata['tx']) for txdata in unspent_list['unspent']]) if not shared_txid: shared_txid = txs else: shared_txid = shared_txid.intersection(txs) log.debug('sharedtxid = ' + str(shared_txid)) if len(shared_txid) == 0: continue time.sleep( 2 ) # here for some race condition bullshit with blockr.io blockr_url = 'https://' + self.blockr_domain blockr_url += '.blockr.io/api/v1/tx/raw/' data = json.loads(btc.make_request(blockr_url + ','.join( shared_txid)))['data'] if not isinstance(data, list): data = [data] for txinfo in data: txhex = str(txinfo['tx']['hex']) outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) log.debug('unconfirm query outs = ' + str(outs)) if outs == self.tx_output_set: unconfirmed_txid = txinfo['tx']['txid'] unconfirmed_txhex = str(txinfo['tx']['hex']) break self.unconfirmfun( btc.deserialize(unconfirmed_txhex), unconfirmed_txid) st = int(time.time()) confirmed_txid = None confirmed_txhex = None while not confirmed_txid: time.sleep(confirm_poll_period) if int(time.time()) - st > confirm_timeout: log.debug('checking for confirmed tx timed out') if self.timeoutfun: self.timeoutfun(True) return blockr_url = 'https://' + self.blockr_domain blockr_url += '.blockr.io/api/v1/address/txs/' data = json.loads(btc.make_request(blockr_url + ','.join( self.output_addresses)))['data'] shared_txid = None for addrtxs in data: txs = set(str(txdata['tx']) for txdata in addrtxs['txs']) if not shared_txid: shared_txid = txs else: shared_txid = shared_txid.intersection(txs) log.debug('sharedtxid = ' + str(shared_txid)) if len(shared_txid) == 0: continue blockr_url = 'https://' + self.blockr_domain blockr_url += '.blockr.io/api/v1/tx/raw/' data = json.loads( btc.make_request( blockr_url + ','.join(shared_txid)))['data'] if not isinstance(data, list): data = [data] for txinfo in data: txhex = str(txinfo['tx']['hex']) outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) log.debug('confirm query outs = ' + str(outs)) if outs == self.tx_output_set: confirmed_txid = txinfo['tx']['txid'] confirmed_txhex = str(txinfo['tx']['hex']) break self.confirmfun( btc.deserialize(confirmed_txhex), confirmed_txid, 1)
def recv_txio(self, nick, utxo_list, cj_pub, 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 log.debug("Trying to retrieve utxos for: " + nick) utxo_data = jm_single().bc_interface.query_utxo_set(self.utxos[nick]) log.debug("Got this utxo data: " + pprint.pformat(utxo_data)) if None in utxo_data: log.debug(('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 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. nick={}' 'totalin={:d} cjamount={:d} change={:d}').format log.debug(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)) cj_addr = btc.pubtoaddr(cj_pub, get_p2pk_vbyte()) 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 log.debug('got all parts, enough to build a tx') self.nonrespondants = list(self.active_orders.keys()) my_total_in = sum([va['value'] for u, va in self.input_utxos.iteritems()]) if self.my_change_addr: #Estimate fee per choice of next/3/6 blocks targetting. estimated_fee = estimate_tx_fee(len(sum( self.utxos.values(),[])), len(self.outputs)+2) log.debug("Based on initial guess: "+str( self.total_txfee)+", we estimated a fee of: "+str(estimated_fee)) #reset total self.total_txfee = estimated_fee my_txfee = max(self.total_txfee - self.maker_txfee_contributions, 0) my_change_value = ( my_total_in - self.cj_amount - self.cjfee_total - my_txfee) #Since we could not predict the maker's inputs, we may end up needing #too much such that the change value is negative or small. Note that #we have tried to avoid this based on over-estimating the needed amount #in SendPayment.create_tx(), but it is still a possibility if one maker #uses a *lot* of inputs. if self.my_change_addr and my_change_value <= 0: raise ValueError("Calculated transaction fee of: "+str( self.total_txfee)+" is too large for our inputs;Please try again.") elif self.my_change_addr and my_change_value <= jm_single().DUST_THRESHOLD: log.debug("Dynamically calculated change lower than dust: "+str( my_change_value)+"; dropping.") self.my_change_addr = None my_change_value = 0 log.debug('fee breakdown for me totalin=%d my_txfee=%d makers_txfee=%d cjfee_total=%d => changevalue=%d' % (my_total_in, my_txfee, self.maker_txfee_contributions, self.cjfee_total, my_change_value)) if self.my_change_addr is None: if my_change_value != 0 and abs(my_change_value) != 1: # seems you wont always get exactly zero because of integer # rounding so 1 satoshi extra or fewer being spent as miner # fees is acceptable log.debug(('WARNING CHANGE NOT BEING ' 'USED\nCHANGEVALUE = {}').format(my_change_value)) else: self.outputs.append({'address': self.my_change_addr, 'value': my_change_value}) self.utxo_tx = [dict([('output', u)]) for u in sum(self.utxos.values(), [])] self.outputs.append({'address': self.coinjoin_address(), 'value': self.cj_amount}) random.shuffle(self.utxo_tx) random.shuffle(self.outputs) tx = btc.mktx(self.utxo_tx, self.outputs) log.debug('obtained tx\n' + pprint.pformat(btc.deserialize(tx))) #Re-calculate a sensible timeout wait based on the throttling #settings and the tx size. #Calculation: Let tx size be S; tx undergoes two b64 expansions, 1.8*S #So we're sending N*1.8*S over the wire, and the #maximum bytes/sec = B, means we need (1.8*N*S/B) seconds, #and need to add some leeway for network delays, we just add the #contents of jm_single().maker_timeout_sec (the user configured value) self.maker_timeout_sec = (len(tx) * 1.8 * len( self.active_orders.keys()))/(B_PER_SEC) + jm_single().maker_timeout_sec log.debug("Based on transaction size: " + str( len(tx)) + ", calculated time to wait for replies: " + str( self.maker_timeout_sec)) self.all_responded = True with self.timeout_lock: self.timeout_lock.notify() self.msgchan.send_tx(self.active_orders.keys(), tx) self.latest_tx = btc.deserialize(tx) for index, ins in enumerate(self.latest_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in self.input_utxos.keys(): continue # placeholders required ins['script'] = 'deadbeef'