Beispiel #1
0
    def get_commitment(self, utxos, amount):
        """Create commitment to fulfil anti-DOS requirement of makers,
        storing the corresponding reveal/proof data for next step.
        """
        while True:
            self.commitment, self.reveal_commitment = self.commitment_creator(
                self.wallet, utxos, amount)
            if (self.commitment) or (jm_single().wait_for_commitments == 0):
                break
            log.info("Failed to source commitments, waiting 3 minutes")
            time.sleep(3 * 60)
        if not self.commitment:
            log.error("Cannot construct transaction, failed to generate "
                    "commitment, shutting down. Please read commitments_debug.txt "
                      "for some information on why this is, and what can be "
                      "done to remedy it.")
            #TODO: would like to raw_input here to show the user, but
            #interactivity is undesirable here.
            #Test only:
            if jm_single().config.get(
                "BLOCKCHAIN", "blockchain_source") == 'regtest':
                raise btc.PoDLEError("For testing raising podle exception")

            #The timeout/recovery code is designed to handle non-responsive
            #counterparties, but this condition means that the current bot
            #is not able to create transactions following its *own* rules,
            #so shutting down is appropriate no matter what style
            #of bot this is.
            import thread
            thread.interrupt_main()
            return False
        return True
    def sync_unspent(self, wallet):
        from joinmarket.wallet import BitcoinCoreWallet

        if isinstance(wallet, BitcoinCoreWallet):
            return
        st = time.time()
        wallet_name = self.get_wallet_name(wallet)
        wallet.unspent = {}

        listunspent_args = []
        if 'listunspent_args' in jm_single().config.options('POLICY'):
            listunspent_args = ast.literal_eval(
                jm_single().config.get('POLICY', 'listunspent_args'))

        unspent_list = self.rpc('listunspent', listunspent_args)
        for u in unspent_list:
            if 'account' not in u:
                continue
            if u['account'] != wallet_name:
                continue
            if u['address'] not in wallet.addr_cache:
                continue
            wallet.unspent[u['txid'] + ':' + str(u['vout'])] = {
                'address': u['address'],
                'value': int(Decimal(str(u['amount'])) * Decimal('1e8'))}
        et = time.time()
        log.debug('bitcoind sync_unspent took ' + str((et - st)) + 'sec')
Beispiel #3
0
    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.info('obtained tx\n' + pprint.pformat(self.tx))
        goodtx, errmsg = self.verify_unsigned_tx(self.tx)
        if not goodtx:
            log.info('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.info('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)
        self.maker.msgchan.send_sigs(nick, sigs)
        self.maker.active_orders[nick] = None
Beispiel #4
0
    def push(self):
        tx = btc.serialize(self.latest_tx)
        log.debug('\n' + tx)
        self.txid = btc.txhash(tx)
        log.debug('txid = ' + self.txid)
        
        tx_broadcast = jm_single().config.get('POLICY', 'tx_broadcast')
        if tx_broadcast == 'self':
            pushed = jm_single().bc_interface.pushtx(tx)
        elif tx_broadcast in ['random-peer', 'not-self']:
            n = len(self.active_orders)
            if tx_broadcast == 'random-peer':
                i = random.randrange(n + 1)
            else:
                i = random.randrange(n)
            if i == n:
                pushed = jm_single().bc_interface.pushtx(tx)
            else:
                self.msgchan.push_tx(self.active_orders.keys()[i], tx)
                pushed = True
        elif tx_broadcast == 'random-maker':
            crow = self.db.execute(
                'SELECT DISTINCT counterparty FROM orderbook ORDER BY ' +
                'RANDOM() LIMIT 1;'
            ).fetchone()
            counterparty = crow['counterparty']
            log.debug('pushing tx to ' + counterparty)
            self.msgchan.push_tx(counterparty, tx)
            pushed = True

        if not pushed:
            log.debug('unable to pushtx')
        return pushed
    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
            tx = self.btcinterface.rpc('getrawtransaction', [txid])
            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']])

            unconfirmfun, confirmfun = None, None
            for tx_out, ucfun, cfun in self.btcinterface.txnotify_fun:
                if tx_out == tx_output_set:
                    unconfirmfun = ucfun
                    confirmfun = cfun
                    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
                    log.debug('ran unconfirmfun')
                else:
                    confirmfun(txd, txid, txdata['confirmations'])
                    self.btcinterface.txnotify_fun.remove(
                            (tx_out, unconfirmfun, confirmfun))
                    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.')

        os.system('curl -sI --connect-timeout 1 http://localhost:' + str(
                self.base_server.server_address[1] + 1) + self.path)
        self.send_response(200)
        # self.send_header('Connection', 'close')
        self.end_headers()
Beispiel #6
0
    def on_order_seen(self, counterparty, oid, ordertype, minsize, maxsize,
                      txfee, cjfee):
        try:
            self.dblock.acquire(True)
            if int(oid) < 0 or int(oid) > sys.maxint:
                log.debug(
                    "Got invalid order ID: " + oid + " from " + counterparty)
                return
            # delete orders eagerly, so in case a buggy maker sends an
            # invalid offer, we won't accidentally !fill based on the ghost
            # of its previous message.
            self.db.execute(("DELETE FROM orderbook WHERE counterparty=? "
                             "AND oid=?;"), (counterparty, oid))
            # now validate the remaining fields
            if int(minsize) < 0 or int(minsize) > 21 * 10 ** 14:
                log.debug("Got invalid minsize: {} from {}".format(
                        minsize, counterparty))
                return
            if int(minsize) < jm_single().DUST_THRESHOLD:
                minsize = jm_single().DUST_THRESHOLD
                log.debug("{} has dusty minsize, capping at {}".format(
                        counterparty, minsize))
                # do not pass return, go not drop this otherwise fine offer
            if int(maxsize) < 0 or int(maxsize) > 21 * 10 ** 14:
                log.debug("Got invalid maxsize: " + maxsize + " from " +
                          counterparty)
                return
            if int(txfee) < 0:
                log.debug("Got invalid txfee: {} from {}".format(
                        txfee, counterparty))
                return
            if int(minsize) > int(maxsize):

                fmt = ("Got minsize bigger than maxsize: {} - {} "
                       "from {}").format
                log.debug(fmt(minsize, maxsize, counterparty))
                return
            if ordertype == 'absoffer' and not isinstance(cjfee, int):
                try:
                    cjfee = int(cjfee)
                except ValueError:
                    log.debug("Got non integer coinjoin fee: " + str(cjfee) +
                            " for an absoffer from " + counterparty)
                    return
            self.db.execute(
                    'INSERT INTO orderbook VALUES(?, ?, ?, ?, ?, ?, ?);',
                    (counterparty, oid, ordertype, minsize, maxsize, txfee,
                     str(Decimal(
                         cjfee))))  # any parseable Decimal is a valid cjfee
        except InvalidOperation:
            log.debug("Got invalid cjfee: " + cjfee + " from " + counterparty)
        except Exception as e:
            log.debug("Error parsing order " + oid + " from " + counterparty)
            log.debug("Exception was: " + repr(e))
        finally:
            self.dblock.release()
Beispiel #7
0
    def auth_counterparty(self, nick, cr):
        #deserialize the commitment revelation
        cr_dict = btc.PoDLE.deserialize_revelation(cr)
        #check the validity of the proof of discrete log equivalence
        tries = jm_single().config.getint("POLICY", "taker_utxo_retries")
        def reject(msg):
            log.info("Counterparty commitment not accepted, reason: " + msg)
            return False
        if not btc.verify_podle(cr_dict['P'], cr_dict['P2'], cr_dict['sig'],
                                cr_dict['e'], self.maker.commit,
                                index_range=range(tries)):
            reason = "verify_podle failed"
            return reject(reason)
        #finally, check that the proffered utxo is real, old enough, large enough,
        #and corresponds to the pubkey
        res = jm_single().bc_interface.query_utxo_set([cr_dict['utxo']],
                                                      includeconf=True)
        if len(res) != 1 or not res[0]:
            reason = "authorizing utxo is not valid"
            return reject(reason)
        age = jm_single().config.getint("POLICY", "taker_utxo_age")
        if res[0]['confirms'] < age:
            reason = "commitment utxo not old enough: " + str(res[0]['confirms'])
            return reject(reason)
        reqd_amt = int(self.cj_amount * jm_single().config.getint(
            "POLICY", "taker_utxo_amtpercent") / 100.0)
        if res[0]['value'] < reqd_amt:
            reason = "commitment utxo too small: " + str(res[0]['value'])
            return reject(reason)
        if res[0]['address'] != btc.pubkey_to_address(cr_dict['P'],
                                                         get_p2pk_vbyte()):
            reason = "Invalid podle pubkey: " + str(cr_dict['P'])
            return reject(reason)

        # authorisation of taker passed

        # Send auth request to taker
        # Need to choose an input utxo pubkey to sign with
        # (no longer using the coinjoin pubkey from 0.2.0)
        # Just choose the first utxo in self.utxos and retrieve key from wallet.
        auth_address = self.utxos[self.utxos.keys()[0]]['address']
        auth_key = self.maker.wallet.get_key_from_addr(auth_address)
        auth_pub = btc.privtopub(auth_key)
        btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), auth_key)
        self.maker.msgchan.send_ioauth(nick, self.utxos.keys(), auth_pub,
                                       self.cj_addr, self.change_addr, btc_sig)
        #In case of *blacklisted (ie already used) commitments, we already
        #broadcasted them on receipt; in case of valid, and now used commitments,
        #we broadcast them here, and not early - to avoid accidentally
        #blacklisting commitments that are broadcast between makers in real time
        #for the same transaction.
        self.maker.transfer_commitment(self.maker.commit)
        #now persist the fact that the commitment is actually used.
        check_utxo_blacklist(self.maker.commit, persist=True)
        return True
Beispiel #8
0
def estimate_tx_fee(ins, outs, txtype='p2pkh'):
    '''Returns an estimate of the number of satoshis required
    for a transaction with the given number of inputs and outputs,
    based on information from the blockchain interface.
    '''
    tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype)
    log.debug("Estimated transaction size: "+str(tx_estimated_bytes))
    fee_per_kb = jm_single().bc_interface.estimate_fee_per_kb(
        jm_single().config.getint("POLICY","tx_fees"))
    log.debug("got estimated tx bytes: "+str(tx_estimated_bytes))
    return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0))
Beispiel #9
0
    def __init__(self, maker, nick, oid, amount, taker_pk):
        self.tx = None
        self.i_utxo_pubkey = None

        self.maker = maker
        self.oid = oid
        self.cj_amount = amount
        if self.cj_amount <= jm_single().DUST_THRESHOLD:
            self.maker.msgchan.send_error(nick, 'amount below dust threshold')
        # the btc pubkey of the utxo that the taker plans to use as input
        self.taker_pk = taker_pk
        # create DH keypair on the fly for this Order object
        self.kp = init_keypair()
        # the encryption channel crypto box for this Order object
        self.crypto_box = as_init_encryption(self.kp,
                                             init_pubkey(taker_pk))

        order_s = [o for o in maker.orderlist if o['oid'] == oid]
        if len(order_s) == 0:
            self.maker.msgchan.send_error(nick, 'oid not found')
        order = order_s[0]
        if amount < order['minsize'] or amount > order['maxsize']:
            self.maker.msgchan.send_error(nick, 'amount out of range')
        self.ordertype = order['ordertype']
        self.txfee = order['txfee']
        self.cjfee = order['cjfee']
        log.debug('new cjorder nick=%s oid=%d amount=%d' % (nick, oid, amount))
        self.utxos, self.cj_addr, self.change_addr = maker.oid_to_order(
                self, oid, amount)
        self.maker.wallet.update_cache_index()
        if not self.utxos:
            self.maker.msgchan.send_error(
                    nick, 'unable to fill order constrained by dust avoidance')
            # TODO make up orders offers in a way that this error cant appear
            #  check nothing has messed up with the wallet code, remove this
            # code after a while
        import pprint
        log.debug('maker utxos = ' + pprint.pformat(self.utxos))
        utxo_list = self.utxos.keys()
        utxo_data = jm_single().bc_interface.query_utxo_set(utxo_list)
        if None in utxo_data:
            log.debug('wrongly using an already spent utxo. utxo_data = ' +
                      pprint.pformat(utxo_data))
            sys.exit(0)
        for utxo, data in zip(utxo_list, utxo_data):
            if self.utxos[utxo]['value'] != data['value']:
                fmt = 'wrongly labeled utxo, expected value: {} got {}'.format
                log.debug(fmt(self.utxos[utxo]['value'], data['value']))
                sys.exit(0)

        # always a new address even if the order ends up never being
        # furfilled, you dont want someone pretending to fill all your
        # orders to find out which addresses you use
        self.maker.msgchan.send_pubkey(nick, self.kp.hex_pk())
Beispiel #10
0
 def confirm_callback(self, txd, txid, confirmations):
     self.maker.wallet_unspent_lock.acquire()
     try:
         jm_single().bc_interface.sync_unspent(self.maker.wallet)
     finally:
         self.maker.wallet_unspent_lock.release()
     log.info('tx in a block')
     log.info('earned = ' + str(self.real_cjfee - self.txfee))
     to_cancel, to_announce = self.maker.on_tx_confirmed(self, confirmations,
                                                         txid)
     self.maker.modify_orders(to_cancel, to_announce)
Beispiel #11
0
def estimate_tx_fee(ins, outs, txtype='p2pkh'):
    '''Returns an estimate of the number of satoshis required
    for a transaction with the given number of inputs and outputs,
    based on information from the blockchain interface.
    '''
    tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype)
    log.debug("Estimated transaction size: "+str(tx_estimated_bytes))
    fee_per_kb = jm_single().bc_interface.estimate_fee_per_kb(
        jm_single().config.getint("POLICY", "tx_fees"))
    absurd_fee = jm_single().config.getint("POLICY", "absurd_fee_per_kb")
    if fee_per_kb > absurd_fee:
        #This error is considered critical; for safety reasons, shut down.
        raise ValueError("Estimated fee per kB greater than absurd value: " + \
                         str(absurd_fee) + ", quitting.")
    log.debug("got estimated tx bytes: "+str(tx_estimated_bytes))
    return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0))
Beispiel #12
0
 def _pubmsg(self, message):
     line = "PRIVMSG " + self.channel + " :" + message
     assert len(line) <= MAX_PRIVMSG_LEN
     ob = False
     if any([x in line for x in jm_single().ordername_list]):
         ob = True
     self.send_raw(line, ob)
Beispiel #13
0
 def __init__(self,
              msgchan,
              wallet,
              db,
              cj_amount,
              orders,
              input_utxos,
              my_cj_addr,
              my_change_addr,
              total_txfee,
              finishcallback,
              choose_orders_recover,
              commitment_creator
              ):
     """
     if my_change is None then there wont be a change address
     thats used if you want to entirely coinjoin one utxo with no change left over
     orders is the orders you want to fill {'counterpartynick': order1, 'cp2': order2}
     each order object is a dict of properties {'oid': 0, 'maxsize': 2000000, 'minsize':
         5000, 'cjfee': 10000, 'txfee': 5000}
     """
     log.info(
         'starting cj to ' + str(my_cj_addr) + ' with change at ' + str(
                 my_change_addr))
     # parameters
     self.msgchan = msgchan
     self.wallet = wallet
     self.db = db
     self.cj_amount = cj_amount
     self.active_orders = dict(orders)
     self.input_utxos = input_utxos
     self.finishcallback = finishcallback
     self.total_txfee = total_txfee
     self.my_cj_addr = my_cj_addr
     self.my_change_addr = my_change_addr
     self.choose_orders_recover = choose_orders_recover
     self.commitment_creator = commitment_creator
     self.timeout_lock = threading.Condition()  # used to wait() and notify()
     # used to restrict access to certain variables across threads
     self.timeout_thread_lock = threading.Condition()
     self.end_timeout_thread = False
     self.maker_timeout_sec = jm_single().maker_timeout_sec
     CoinJoinTX.TimeoutThread(self).start()
     # state variables
     self.txid = None
     self.cjfee_total = 0
     self.maker_txfee_contributions = 0
     self.nonrespondants = list(self.active_orders.keys())
     self.all_responded = False
     self.latest_tx = None
     # None means they belong to me
     self.utxos = {None: self.input_utxos.keys()}
     self.outputs = []
     # create DH keypair on the fly for this Tx object
     self.kp = init_keypair()
     self.crypto_boxes = {}
     if not self.get_commitment(input_utxos, self.cj_amount):
         return
     self.msgchan.fill_orders(self.active_orders, self.cj_amount,
                              self.kp.hex_pk(), self.commitment)
 def estimate_fee_per_kb(self, N):
     if not self.absurd_fees:
         return super(RegtestBitcoinCoreInterface,
                      self).estimate_fee_per_kb(N)
     else:
         return jm_single().config.getint("POLICY",
                                          "absurd_fee_per_kb") + 100
    def add_tx_notify(self,
                      txd,
                      unconfirmfun,
                      confirmfun,
                      notifyaddr,
                      timeoutfun=None):
        if not self.notifythread:
            self.notifythread = BitcoinCoreNotifyThread(self)
            self.notifythread.start()
        one_addr_imported = False
        for outs in txd['outs']:
            addr = btc.script_to_address(outs['script'], get_p2pk_vbyte())
            if self.rpc('getaccount', [addr]) != '':
                one_addr_imported = True
                break
        if not one_addr_imported:
            self.rpc('importaddress', [notifyaddr, 'joinmarket-notify', False])
        tx_output_set = set([(sv['script'], sv['value']) for sv in txd['outs']])
        self.txnotify_fun.append((tx_output_set, unconfirmfun, confirmfun,
                                  timeoutfun, False))

        #create unconfirm timeout here, create confirm timeout in the other thread
        if timeoutfun:
            threading.Timer(jm_single().config.getint('TIMEOUT',
                                                      'unconfirm_timeout_sec'),
                            bitcoincore_timeout_callback,
                            args=(False, tx_output_set, self.txnotify_fun,
                                  timeoutfun)).start()
Beispiel #16
0
 def on_set_topic(newtopic):
     chunks = newtopic.split('|')
     for msg in chunks[1:]:
         try:
             msg = msg.strip()
             params = msg.split(' ')
             min_version = int(params[0])
             max_version = int(params[1])
             alert = msg[msg.index(params[1]) + len(params[1]):].strip()
         except ValueError, IndexError:
             continue
         if min_version < jm_single().JM_VERSION < max_version:
             print('=' * 60)
             print('JOINMARKET ALERT')
             print(alert)
             print('=' * 60)
             jm_single().joinmarket_alert[0] = alert
Beispiel #17
0
 def on_push_tx(self, nick, txhex):
     log.debug('received txhex from ' + nick + ' to push\n' + txhex)
     pushed = jm_single().bc_interface.pushtx(txhex)
     if pushed:
         log.debug('pushed tx ' + btc.txhash(txhex))
     else:
         log.debug('failed to push tx sent by taker')
         self.msgchan.send_error(nick, 'Unable to push tx')
Beispiel #18
0
 def __init__(self, fromaccount):
     super(BitcoinCoreWallet, self).__init__()
     if not isinstance(jm_single().bc_interface,
                       BitcoinCoreInterface):
         raise RuntimeError('Bitcoin Core wallet can only be used when '
                            'blockchain interface is BitcoinCoreInterface')
     self.fromaccount = fromaccount
     self.max_mix_depth = 1
Beispiel #19
0
 def ensure_wallet_unlocked():
     wallet_info = jm_single().bc_interface.rpc('getwalletinfo', [])
     if 'unlocked_until' in wallet_info and wallet_info[
         'unlocked_until'] <= 0:
         while True:
             password = getpass(
                     'Enter passphrase to unlock wallet: ')
             if password == '':
                 raise RuntimeError('Aborting wallet unlock')
             try:
                 # TODO cleanly unlock wallet after use, not with arbitrary timeout
                 jm_single().bc_interface.rpc(
                         'walletpassphrase', [password, 10])
                 break
             except jm_single().JsonRpcError as exc:
                 if exc.code != -14:
                     raise exc
Beispiel #20
0
 def on_push_tx(self, nick, txhex):
     log.debug('received txhex from ' + nick + ' to push\n' + txhex)
     pushed = jm_single().bc_interface.pushtx(txhex)
     if pushed[0]:
         log.debug('pushed tx ' + str(pushed[1]))
     else:
         log.debug('failed to push tx, reason: '+str(pushed[1]))
         self.msgchan.send_error(nick, 'Unable to push tx')
Beispiel #21
0
    def on_commitment_seen(self, nick, commitment):
        """Triggered when we see a commitment for blacklisting
	appear in the public pit channel. If the policy is set,
	we blacklist this commitment.
	"""
        if jm_single().config.has_option("POLICY", "accept_commitment_broadcasts"):
            blacklist_add = jm_single().config.getint("POLICY",
                                                    "accept_commitment_broadcasts")
        else:
            blacklist_add = 0
        if blacklist_add > 0:
            #just add if necessary, ignore return value.
            check_utxo_blacklist(commitment, persist=True)
            log.debug("Received commitment broadcast by other maker: " + str(
                commitment) + ", now blacklisted.")
        else:
            log.debug("Received commitment broadcast by other maker: " + str(
                commitment) + ", ignored.")
Beispiel #22
0
    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:
            restart()
            return

        if self.latest_tx is None:
            # 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]
            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 = '
                       '{}').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)
            else:
                log.info("Too few makers responded to complete, trying again.")
                restart()
        else:
            log.debug('nonresponse to !tx')
            # have to restart tx from the beginning
            restart()
 def add_watchonly_addresses(self, addr_list, wallet_name):
     log.debug('importing ' + str(len(addr_list)) +
               ' addresses into account ' + wallet_name)
     for addr in addr_list:
         self.rpc('importaddress', [addr, wallet_name, False])
     if jm_single().config.get(
             "BLOCKCHAIN", "blockchain_source") != 'regtest':
         print('restart Bitcoin Core with -rescan if you\'re '
               'recovering an existing wallet from backup seed')
         print(' otherwise just restart this joinmarket script')
         sys.exit(0)
Beispiel #24
0
    def push(self):
        tx = btc.serialize(self.latest_tx)
        log.debug('\n' + tx)
        self.txid = btc.txhash(tx)
        log.info('txid = ' + self.txid)

        tx_broadcast = jm_single().config.get('POLICY', 'tx_broadcast')
        if tx_broadcast == 'self':
            pushed = jm_single().bc_interface.pushtx(tx)
        elif tx_broadcast in ['random-peer', 'not-self']:
            n = len(self.active_orders)
            if tx_broadcast == 'random-peer':
                i = random.randrange(n + 1)
            else:
                i = random.randrange(n)
            if i == n:
                pushed = jm_single().bc_interface.pushtx(tx)
            else:
                self.msgchan.push_tx(self.active_orders.keys()[i], tx)
                pushed = True
        elif tx_broadcast == 'random-maker':
            crow = self.db.execute(
                'SELECT DISTINCT counterparty FROM orderbook ORDER BY ' +
                'RANDOM() LIMIT 1;'
            ).fetchone()
            counterparty = crow['counterparty']
            log.info('pushing tx to ' + counterparty)
            self.msgchan.push_tx(counterparty, tx)
            pushed = True
        elif tx_broadcast == 'tor':
            socks5_host = jm_single().config.get("MESSAGING",
                "socks5_host").split(",")[0]
            socks5_port = int(jm_single().config.get("MESSAGING",
                "socks5_port").split(",")[0])
            pushed = tor_broadcast_tx(tx, (socks5_host, socks5_port),
                testnet=(get_network() == "testnet"))

        if not pushed:
            log.error('unable to pushtx')
        return pushed
Beispiel #25
0
 def push(self, txd):
     tx = btc.serialize(txd)
     log.debug('\n' + tx)
     log.debug('txid = ' + btc.txhash(tx))
     # TODO send to a random maker or push myself
     # TODO need to check whether the other party sent it
     # self.msgchan.push_tx(self.active_orders.keys()[0], txhex)
     pushed = jm_single().bc_interface.pushtx(tx)
     if pushed[0]:
         self.txid = btc.txhash(tx)
     else:
         log.debug('unable to pushtx, reason: '+str(pushed[1]))
     return pushed[0]
Beispiel #26
0
 def __init__(self, mchannels):
     self.mchannels = mchannels
     #To keep track of chosen channels
     #for private messaging counterparties.
     self.active_channels = {}
     #To keep track of message channel status;
     #0: not started 1: started 2: failed/broken/inactive
     self.mc_status = dict([(x, 0) for x in self.mchannels])
     #To keep track of counterparties having at least once
     #made their presence known on a channel
     self.nicks_seen = {}
     for mc in self.mchannels:
         self.nicks_seen[mc] = set()
         #callback to mark nicks as seen when they privmsg
         mc.on_privmsg_trigger = self.on_privmsg
     #keep track of whether we want to deliberately
     #shut down the connections
     self.give_up = False
     #only allow on_welcome() to fire once.
     self.welcomed = False
     #control access
     self.mc_lock = threading.Lock()
     #Create an ephemeral keypair for the duration
     #of this run, same across all message channels,
     #and set the nickname for all message channels using it.
     self.nick_priv = hashlib.sha256(os.urandom(16)).hexdigest() + '01'
     self.nick_pubkey = btc.privtopub(self.nick_priv)
     self.nick_pkh_raw = hashlib.sha256(self.nick_pubkey).digest()[
         :NICK_HASH_LENGTH]
     self.nick_pkh = btc.changebase(self.nick_pkh_raw, 256, 58)
     #right pad to maximum possible; b58 is not fixed length.
     #Use 'O' as one of the 4 not included chars in base58.
     self.nick_pkh += 'O' * (NICK_MAX_ENCODED - len(self.nick_pkh))
     #The constructed length will be 1 + 1 + NICK_MAX_ENCODED
     self.nick = JOINMARKET_NICK_HEADER + str(
         jm_single().JM_VERSION) + self.nick_pkh
     jm_single().nickname = self.nick
     for mc in self.mchannels:
         mc.set_nick(self.nick, self.nick_priv, self.nick_pubkey)
Beispiel #27
0
 def populate_utxo_data():
     self.utxos, self.cj_addr, self.change_addr = maker.oid_to_order(
         self, oid, amount)
     self.maker.wallet.update_cache_index()
     if not self.utxos:
         self.maker.msgchan.send_error(
             nick, 'unable to fill order constrained by dust avoidance')
     # TODO make up orders offers in a way that this error cant appear
     #  check nothing has messed up with the wallet code, remove this
     # code after a while
     log.debug('maker utxos = ' + pprint.pformat(self.utxos))
     utxos = self.utxos.keys()
     return (utxos, jm_single().bc_interface.query_utxo_set(utxos))
Beispiel #28
0
 def get_utxos_by_mixdepth(self):
     unspent_list = jm_single().bc_interface.rpc('listunspent', [])
     result = {0: {}}
     for u in unspent_list:
         if not u['spendable']:
             continue
         if self.fromaccount and (
                     ('account' not in u) or u['account'] !=
                     self.fromaccount):
             continue
         result[0][u['txid'] + ':' + str(u['vout'])] = {
             'address': u['address'],
             'value': int(Decimal(str(u['amount'])) * Decimal('1e8'))}
     return result
Beispiel #29
0
 def __init__(self):
     self.max_mix_depth = 0
     self.utxo_selector = btc.select  # default fallback: upstream
     try:
         config = jm_single().config
         if config.get("POLICY", "merge_algorithm") == "gradual":
             self.utxo_selector = select_gradual
         elif config.get("POLICY", "merge_algorithm") == "greedy":
             self.utxo_selector = select_greedy
         elif config.get("POLICY", "merge_algorithm") == "greediest":
             self.utxo_selector = select_greediest
         elif config.get("POLICY", "merge_algorithm") != "default":
             raise Exception("Unknown merge algorithm")
     except NoSectionError:
         pass
Beispiel #30
0
        def run(self):
            log.debug(('started timeout thread for coinjoin of amount {} to '
                       'addr {}').format(self.cjtx.cj_amount,
                                         self.cjtx.my_cj_addr))

            # how the threading to check for nonresponding makers works like this
            # there is a Condition object
            # in a loop, call cond.wait(timeout)
            # after it returns, check a boolean
            # to see if if the messages have arrived
            while not self.cjtx.end_timeout_thread:
                log.debug('waiting for all replies.. timeout=' + str(
                        jm_single().maker_timeout_sec))
                with self.cjtx.timeout_lock:
                    self.cjtx.timeout_lock.wait(jm_single().maker_timeout_sec)
                if self.cjtx.all_responded:
                    log.debug(('timeout thread woken by notify(), '
                               'makers responded in time'))
                    self.cjtx.all_responded = False
                else:
                    log.debug('timeout thread woken by timeout, '
                              'makers didnt respond')
                    with self.cjtx.timeout_thread_lock:
                        self.cjtx.recover_from_nonrespondants()
Beispiel #31
0
    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()
Beispiel #32
0
    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.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
        #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.debug("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. 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))
        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'
Beispiel #33
0
    def make_commitment(self, wallet, input_utxos, cjamount):
        """The Taker default commitment function, which uses PoDLE.
        Alternative commitment types should use a different commit type byte.
        This will allow future upgrades to provide different style commitments
        by subclassing Taker and changing the commit_type_byte; existing makers
        will simply not accept this new type of commitment.
        In case of success, return the commitment and its opening.
        In case of failure returns (None, None) and constructs a detailed
        log for the user to read and discern the reason.
        """
        def filter_by_coin_age_amt(utxos, age, amt):
            results = jm_single().bc_interface.query_utxo_set(utxos,
                                                              includeconf=True)
            newresults = []
            too_old = []
            too_small = []
            for i, r in enumerate(results):
                #results return "None" if txo is spent; drop this
                if not r:
                    continue
                valid_age = r['confirms'] >= age
                valid_amt = r['value'] >= amt
                if not valid_age:
                    too_old.append(utxos[i])
                if not valid_amt:
                    too_small.append(utxos[i])
                if valid_age and valid_amt:
                    newresults.append(utxos[i])

            return newresults, too_old, too_small

        def priv_utxo_pairs_from_utxos(utxos, age, amt):
            #returns pairs list of (priv, utxo) for each valid utxo;
            #also returns lists "too_old" and "too_small" for any
            #utxos that did not satisfy the criteria for debugging.
            priv_utxo_pairs = []
            new_utxos, too_old, too_small = filter_by_coin_age_amt(
                utxos.keys(), age, amt)
            new_utxos_dict = {k: v for k, v in utxos.items() if k in new_utxos}
            for k, v in new_utxos_dict.iteritems():
                addr = v['address']
                priv = wallet.get_key_from_addr(addr)
                if priv:  #can be null from create-unsigned
                    priv_utxo_pairs.append((priv, k))
            return priv_utxo_pairs, too_old, too_small

        commit_type_byte = "P"
        podle_data = None
        tries = jm_single().config.getint("POLICY", "taker_utxo_retries")
        age = jm_single().config.getint("POLICY", "taker_utxo_age")
        #Minor rounding errors don't matter here
        amt = int(
            cjamount *
            jm_single().config.getint("POLICY", "taker_utxo_amtpercent") /
            100.0)
        priv_utxo_pairs, to, ts = priv_utxo_pairs_from_utxos(
            input_utxos, age, amt)
        #Note that we ignore the "too old" and "too small" lists in the first
        #pass through, because the same utxos appear in the whole-wallet check.

        #For podle data format see: btc.podle.PoDLE.reveal()
        #In first round try, don't use external commitments
        podle_data = btc.generate_podle(priv_utxo_pairs, tries)
        if not podle_data:
            #We defer to a second round to try *all* utxos in wallet;
            #this is because it's much cleaner to use the utxos involved
            #in the transaction, about to be consumed, rather than use
            #random utxos that will persist after. At this step we also
            #allow use of external utxos in the json file.
            if wallet.unspent:
                priv_utxo_pairs, to, ts = priv_utxo_pairs_from_utxos(
                    wallet.unspent, age, amt)
            #Pre-filter the set of external commitments that work for this
            #transaction according to its size and age.
            dummy, extdict = btc.get_podle_commitments()
            if len(extdict.keys()) > 0:
                ext_valid, ext_to, ext_ts = filter_by_coin_age_amt(
                    extdict.keys(), age, amt)
            else:
                ext_valid = None
            podle_data = btc.generate_podle(priv_utxo_pairs, tries, ext_valid)
        if podle_data:
            log.debug("Generated PoDLE: " + pprint.pformat(podle_data))
            revelation = btc.PoDLE(u=podle_data['utxo'],
                                   P=podle_data['P'],
                                   P2=podle_data['P2'],
                                   s=podle_data['sig'],
                                   e=podle_data['e']).serialize_revelation()
            return (commit_type_byte + podle_data["commit"], revelation)
        else:
            #we know that priv_utxo_pairs all passed age and size tests, so
            #they must have failed the retries test. Summarize this info
            #and publish to commitments_debug.txt
            with open("commitments_debug.txt", "wb") as f:
                f.write("THIS IS A TEMPORARY FILE FOR DEBUGGING; "
                        "IT CAN BE SAFELY DELETED ANY TIME.\n")
                f.write("***\n")
                f.write("1: Utxos that passed age and size limits, but have "
                        "been used too many times (see taker_utxo_retries "
                        "in the config):\n")
                if len(priv_utxo_pairs) == 0:
                    f.write("None\n")
                else:
                    for p, u in priv_utxo_pairs:
                        f.write(str(u) + "\n")
                f.write("2: Utxos that have less than " +
                        jm_single().config.get("POLICY", "taker_utxo_age") +
                        " confirmations:\n")
                if len(to) == 0:
                    f.write("None\n")
                else:
                    for t in to:
                        f.write(str(t) + "\n")
                f.write("3: Utxos that were not at least " + \
                        jm_single().config.get(
                            "POLICY", "taker_utxo_amtpercent") + "% of the "
                        "size of the coinjoin amount " + str(
                            self.proposed_cj_amount) + "\n")
                if len(ts) == 0:
                    f.write("None\n")
                else:
                    for t in ts:
                        f.write(str(t) + "\n")
                f.write('***\n')
                f.write(
                    "Utxos that appeared in item 1 cannot be used again.\n")
                f.write(
                    "Utxos only in item 2 can be used by waiting for more "
                    "confirmations, (set by the value of taker_utxo_age).\n")
                f.write("Utxos only in item 3 are not big enough for this "
                        "coinjoin transaction, set by the value "
                        "of taker_utxo_amtpercent.\n")
                f.write(
                    "If you cannot source a utxo from your wallet according "
                    "to these rules, use the tool add-utxo.py to source a "
                    "utxo external to your joinmarket wallet. Read the help "
                    "with 'python add-utxo.py --help'\n\n")
                f.write("You can also reset the rules in the joinmarket.cfg "
                        "file, but this is generally inadvisable.\n")
                f.write(
                    "***\nFor reference, here are the utxos in your wallet:\n")
                f.write("\n" + str(self.proposed_wallet.unspent))

            return (None, None)
Beispiel #34
0
 def get_key_from_addr(self, addr):
     self.ensure_wallet_unlocked()
     wifkey = jm_single().bc_interface.rpc('dumpprivkey', [addr])
     return btc.from_wif_privkey(wifkey, vbyte=get_p2pk_vbyte())
Beispiel #35
0
    def __init__(self, maker, nick, oid, amount, taker_pk):
        self.tx = None
        self.i_utxo_pubkey = None

        self.maker = maker
        self.oid = oid
        self.cj_amount = amount
        if self.cj_amount <= jm_single().DUST_THRESHOLD:
            self.maker.msgchan.send_error(nick, 'amount below dust threshold')
        # the btc pubkey of the utxo that the taker plans to use as input
        self.taker_pk = taker_pk
        # create DH keypair on the fly for this Order object
        self.kp = init_keypair()
        # the encryption channel crypto box for this Order object.
        # Invalid pubkeys must be handled by giving up gracefully (otherwise DOS)
        try:
            self.crypto_box = as_init_encryption(self.kp,
                                                 init_pubkey(taker_pk))
        except NaclError as e:
            log.debug("Unable to setup crypto box with counterparty: " +
                      repr(e))
            self.maker.msgchan.send_error(nick,
                                          "invalid nacl pubkey: " + taker_pk)
            return

        order_s = [o for o in maker.orderlist if o['oid'] == oid]
        if len(order_s) == 0:
            self.maker.msgchan.send_error(nick, 'oid not found')
        order = order_s[0]
        if amount < order['minsize'] or amount > order['maxsize']:
            self.maker.msgchan.send_error(nick, 'amount out of range')
        self.ordertype = order['ordertype']
        self.txfee = order['txfee']
        self.cjfee = order['cjfee']
        log.debug('new cjorder nick=%s oid=%d amount=%d' % (nick, oid, amount))

        def populate_utxo_data():
            self.utxos, self.cj_addr, self.change_addr = maker.oid_to_order(
                self, oid, amount)
            self.maker.wallet.update_cache_index()
            if not self.utxos:
                self.maker.msgchan.send_error(
                    nick, 'unable to fill order constrained by dust avoidance')
            # TODO make up orders offers in a way that this error cant appear
            #  check nothing has messed up with the wallet code, remove this
            # code after a while
            log.debug('maker utxos = ' + pprint.pformat(self.utxos))
            utxos = self.utxos.keys()
            return (utxos, jm_single().bc_interface.query_utxo_set(utxos))

        utxo_list, utxo_data = populate_utxo_data()
        while None in utxo_data:
            log.debug('wrongly selected stale utxos! utxo_data = ' +
                      pprint.pformat(utxo_data))
            self.maker.wallet_unspent_lock.acquire()
            try:
                jm_single().bc_interface.sync_unspent(self.maker.wallet)
            finally:
                self.maker.wallet_unspent_lock.release()
            utxo_list, utxo_data = populate_utxo_data()

        for utxo, data in zip(utxo_list, utxo_data):
            if self.utxos[utxo]['value'] != data['value']:
                fmt = 'wrongly labeled utxo, expected value: {} got {}'.format
                log.debug(fmt(self.utxos[utxo]['value'], data['value']))
                sys.exit(0)

        # always a new address even if the order ends up never being
        # furfilled, you dont want someone pretending to fill all your
        # orders to find out which addresses you use
        self.maker.msgchan.send_pubkey(nick, self.kp.hex_pk())
Beispiel #36
0
    def add_tx_notify(self,
                      txd,
                      unconfirmfun,
                      confirmfun,
                      notifyaddr,
                      timeoutfun=None):
        unconfirm_timeout = jm_single().config.getint('TIMEOUT',
                                                      'unconfirm_timeout_sec')
        unconfirm_poll_period = 5
        confirm_timeout = jm_single().config.getfloat(
            'TIMEOUT', 'confirm_timeout_hours') * 60 * 60
        confirm_poll_period = 5 * 60

        class NotifyThread(threading.Thread):
            def __init__(self, blockr_domain, txd, unconfirmfun, confirmfun,
                         timeoutfun):
                threading.Thread.__init__(self, name='BlockrNotifyThread')
                self.daemon = True
                self.blockr_domain = blockr_domain
                self.unconfirmfun = unconfirmfun
                self.confirmfun = confirmfun
                self.timeoutfun = timeoutfun
                self.tx_output_set = set([(sv['script'], sv['value'])
                                          for sv in txd['outs']])
                self.output_addresses = [
                    btc.script_to_address(scrval[0], get_p2pk_vbyte())
                    for scrval in self.tx_output_set
                ]
                log.debug('txoutset=' + pprint.pformat(self.tx_output_set))
                log.debug('outaddrs=' + ','.join(self.output_addresses))

            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)

        NotifyThread(self.blockr_domain, txd, unconfirmfun, confirmfun,
                     timeoutfun).start()
Beispiel #37
0
    def add_signature(self, nick, sigb64):
        if nick not in self.nonrespondants:
            log.debug(
                ('add_signature => nick={} '
                 'not in nonrespondants {}').format(nick, self.nonrespondants))
            return
        sig = base64.b64decode(sigb64).encode('hex')
        inserted_sig = False
        txhex = btc.serialize(self.latest_tx)

        # batch retrieval of utxo data
        utxo = {}
        ctr = 0
        for index, ins in enumerate(self.latest_tx['ins']):
            utxo_for_checking = ins['outpoint']['hash'] + ':' + str(
                ins['outpoint']['index'])
            if (ins['script'] != ''
                    or utxo_for_checking in self.input_utxos.keys()):
                continue
            utxo[ctr] = [index, utxo_for_checking]
            ctr += 1
        utxo_data = jm_single().bc_interface.query_utxo_set(
            [x[1] for x in utxo.values()])

        # insert signatures
        for i, u in utxo.iteritems():
            if utxo_data[i] is None:
                continue
            sig_good = btc.verify_tx_input(txhex, u[0], utxo_data[i]['script'],
                                           *btc.deserialize_script(sig))
            if sig_good:
                log.debug('found good sig at index=%d' % (u[0]))
                self.latest_tx['ins'][u[0]]['script'] = sig
                inserted_sig = True
                # check if maker has sent everything possible
                self.utxos[nick].remove(u[1])
                if len(self.utxos[nick]) == 0:
                    log.debug(('nick = {} sent all sigs, removing from '
                               'nonrespondant list').format(nick))
                    self.nonrespondants.remove(nick)
                break
        if not inserted_sig:
            log.debug('signature did not match anything in the tx')
            # TODO what if the signature doesnt match anything
            # nothing really to do except drop it, carry on and wonder why the
            # other guy sent a failed signature

        tx_signed = True
        for ins in self.latest_tx['ins']:
            if ins['script'] == '':
                tx_signed = False
        if not tx_signed:
            return
        self.end_timeout_thread = True
        self.all_responded = True
        with self.timeout_lock:
            self.timeout_lock.notify()
        log.debug('all makers have sent their signatures')
        for index, ins in enumerate(self.latest_tx['ins']):
            # remove placeholders
            if ins['script'] == 'deadbeef':
                ins['script'] = ''
        if self.finishcallback is not None:
            self.finishcallback(self)
Beispiel #38
0
    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
        order = self.db.execute(
            'SELECT ordertype, txfee, cjfee FROM '
            'orderbook WHERE oid=? AND counterparty=?',
            (self.active_orders[nick], nick)).fetchone()
        utxo_data = jm_single().bc_interface.query_utxo_set(self.utxos[nick])
        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
            return

        # ignore this message, eventually the timeout thread will recover
        total_input = sum([d['value'] for d in utxo_data])
        real_cjfee = calc_cj_fee(order['ordertype'], order['cjfee'],
                                 self.cj_amount)
        self.outputs.append({
            'address':
            change_addr,
            'value':
            total_input - self.cj_amount - order['txfee'] + real_cjfee
        })
        fmt = ('fee breakdown for {} totalin={:d} '
               'cjamount={:d} txfee={:d} realcjfee={:d}').format
        log.debug(
            fmt(nick, total_input, self.cj_amount, order['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 += order['txfee']
        self.nonrespondants.remove(nick)
        if len(self.nonrespondants) > 0:
            log.debug('nonrespondants = ' + str(self.nonrespondants))
            return
        self.all_responded = True
        with self.timeout_lock:
            self.timeout_lock.notify()
        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)))
        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'
Beispiel #39
0
    def auth_counterparty(self, nick, cr):
        #deserialize the commitment revelation
        cr_dict = btc.PoDLE.deserialize_revelation(cr)
        #check the validity of the proof of discrete log equivalence
        tries = jm_single().config.getint("POLICY", "taker_utxo_retries")

        def reject(msg):
            log.debug("Counterparty commitment not accepted, reason: " + msg)
            return False

        if not btc.verify_podle(cr_dict['P'],
                                cr_dict['P2'],
                                cr_dict['sig'],
                                cr_dict['e'],
                                self.maker.commit,
                                index_range=range(tries)):
            reason = "verify_podle failed"
            return reject(reason)
        #finally, check that the proffered utxo is real, old enough, large enough,
        #and corresponds to the pubkey
        res = jm_single().bc_interface.query_utxo_set([cr_dict['utxo']],
                                                      includeconf=True)
        if len(res) != 1 or not res[0]:
            reason = "authorizing utxo is not valid"
            return reject(reason)
        age = jm_single().config.getint("POLICY", "taker_utxo_age")
        if res[0]['confirms'] < age:
            reason = "commitment utxo not old enough: " + str(
                res[0]['confirms'])
            return reject(reason)
        reqd_amt = int(
            self.cj_amount *
            jm_single().config.getint("POLICY", "taker_utxo_amtpercent") /
            100.0)
        if res[0]['value'] < reqd_amt:
            reason = "commitment utxo too small: " + str(res[0]['value'])
            return reject(reason)
        if res[0]['address'] != btc.pubkey_to_address(cr_dict['P'],
                                                      get_p2pk_vbyte()):
            reason = "Invalid podle pubkey: " + str(cr_dict['P'])
            return reject(reason)

        # authorisation of taker passed

        # Send auth request to taker
        # Need to choose an input utxo pubkey to sign with
        # (no longer using the coinjoin pubkey from 0.2.0)
        # Just choose the first utxo in self.utxos and retrieve key from wallet.
        auth_address = self.utxos[self.utxos.keys()[0]]['address']
        auth_key = self.maker.wallet.get_key_from_addr(auth_address)
        auth_pub = btc.privtopub(auth_key)
        btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), auth_key)
        self.maker.msgchan.send_ioauth(nick, self.utxos.keys(), auth_pub,
                                       self.cj_addr, self.change_addr, btc_sig)
        #In case of *blacklisted (ie already used) commitments, we already
        #broadcasted them on receipt; in case of valid, and now used commitments,
        #we broadcast them here, and not early - to avoid accidentally
        #blacklisting commitments that are broadcast between makers in real time
        #for the same transaction.
        self.maker.transfer_commitment(self.maker.commit)
        #now persist the fact that the commitment is actually used.
        check_utxo_blacklist(self.maker.commit, persist=True)
        return True
Beispiel #40
0
 def get_internal_addr(self, mixing_depth):
     return jm_single().bc_interface.rpc('getrawchangeaddress', [])
Beispiel #41
0
 def get_key_from_addr(self, addr):
     self.ensure_wallet_unlocked()
     return jm_single().bc_interface.rpc('dumpprivkey', [addr])
Beispiel #42
0
import base64, abc, threading, time, hashlib, os, binascii
from joinmarket.enc_wrapper import encrypt_encode, decode_decrypt
from joinmarket.support import get_log, chunks
from joinmarket.configure import jm_single
import bitcoin as btc

from functools import wraps
COMMAND_PREFIX = '!'
JOINMARKET_NICK_HEADER = 'J'
NICK_HASH_LENGTH = 10
NICK_MAX_ENCODED = 14 #comes from base58 expansion; recalculate if above changes

encrypted_commands = ["auth", "ioauth", "tx", "sig"]
plaintext_commands = ["fill", "error", "pubkey", "orderbook", "push"]
plaintext_commands += jm_single().ordername_list
plaintext_commands += jm_single().commitment_broadcast_list

log = get_log()

class CJPeerError(StandardError):
    pass


class MChannelThread(threading.Thread):
    def __init__(self, mc):
        threading.Thread.__init__(self, name='MCThread')
        self.daemon = True
        self.mc = mc

    def run(self):
        self.mc.run()
Beispiel #43
0
    def run(self):
        self.waiting = {}
        self.built_privmsg = {}
        self.give_up = False
        self.ping_reply = True
        self.lockcond = threading.Condition()
        self.lockthrottle = threading.Condition()
        PingThread(self).start()
        ThrottleThread(self).start()

        while not self.give_up:
            try:
                config = jm_single().config
                log.debug('connecting')
                if self.newnyms:
                    log.debug("Grabbing new Tor identity")
                    self.newnym()
                    self.nick = random_nick()
                if config.get("MESSAGING", "socks5").lower() == 'true':
                    log.debug("Using socks5 proxy %s:%d" %
                              (self.socks5_host, self.socks5_port))
                    setdefaultproxy(PROXY_TYPE_SOCKS5, self.socks5_host,
                                    self.socks5_port, True)
                    self.sock = socksocket()
                else:
                    self.sock = socket.socket(socket.AF_INET,
                                              socket.SOCK_STREAM)
                self.sock.connect(self.serverport)
                if config.get("MESSAGING", "usessl").lower() == 'true':
                    self.sock = ssl.wrap_socket(self.sock)
                self.fd = self.sock.makefile()
                self.password = None
                if self.given_password:
                    self.password = self.given_password
                    self.send_raw('CAP REQ :sasl')
                self.send_raw('USER %s b c :%s' % self.userrealname)
                self.nick = self.given_nick
                self.send_raw('NICK ' + self.nick)
                while 1:
                    try:
                        line = self.fd.readline()
                    except AttributeError as e:
                        raise IOError(repr(e))
                    if line is None:
                        log.debug('line returned null')
                        break
                    if len(line) == 0:
                        log.debug('line was zero length')
                        break
                    self.__handle_line(line)
            except IOError as e:
                log.debug(repr(e))
            finally:
                try:
                    self.fd.close()
                    self.sock.close()
                except Exception as e:
                    print(repr(e))
            if self.on_disconnect:
                self.on_disconnect()
            log.debug('disconnected irc')
            if not self.give_up:
                time.sleep(self.reconnect_delay)
                if self.newnyms:
                    time.sleep(random.randint(0, self.newnym_delay))
        log.debug('ending irc')
        self.give_up = True
    def estimate_fee_per_kb(self, N):
	if not self.absurd_fees:
	    return super(RegtestBitcoinCoreInterface, self).estimate_fee_per_kb(N)
	else:
	    return jm_single().config.getint("POLICY", "absurd_fee_per_kb") + 100
Beispiel #45
0
    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
            tx = self.btcinterface.rpc('getrawtransaction', [txid])
            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']])

            unconfirmfun, confirmfun = None, None
            for tx_out, ucfun, cfun in self.btcinterface.txnotify_fun:
                if tx_out == tx_output_set:
                    unconfirmfun = ucfun
                    confirmfun = cfun
                    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
                    log.debug('ran unconfirmfun')
                else:
                    confirmfun(txd, txid, txdata['confirmations'])
                    self.btcinterface.txnotify_fun.remove(
                        (tx_out, unconfirmfun, confirmfun))
                    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.'
            )

        os.system('curl -sI --connect-timeout 1 http://localhost:' +
                  str(self.base_server.server_address[1] + 1) + self.path)
        self.send_response(200)
        # self.send_header('Connection', 'close')
        self.end_headers()