示例#1
0
    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")

        #Temporary list used to aggregate all ioauth data that must be removed
        rejected_counterparties = []

        #Need to authorize against the btc pubkey first.
        for nick, nickdata in ioauth_data.items():
            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: " +
                    nick)
                #This counterparty must be rejected
                rejected_counterparties.append(nick)

            if not validate_address(cj_addr)[0] or not validate_address(
                    change_addr)[0]:
                jlog.warn("Counterparty provided invalid address: {}".format(
                    (cj_addr, change_addr)))
                # Interpreted as malicious
                self.add_ignored_makers([nick])
                rejected_counterparties.append(nick)

        for rc in rejected_counterparties:
            del ioauth_data[rc]

        self.maker_utxo_data = {}

        for nick, nickdata in ioauth_data.items():
            utxo_list, auth_pub, cj_addr, change_addr, _, _ = nickdata
            utxo_data = jm_single().bc_interface.query_utxo_set(utxo_list)
            self.utxos[nick] = utxo_list
            if None in utxo_data:
                jlog.warn(('ERROR outputs unconfirmed or already spent. '
                           'utxo_data={}').format(pprint.pformat(utxo_data)))
                jlog.warn('Disregarding this counterparty.')
                del self.utxos[nick]
                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.
            for inp in utxo_data:
                try:
                    if self.wallet_service.pubkey_has_script(
                            auth_pub, inp['script']):
                        break
                except EngineError as e:
                    pass
            else:
                jlog.warn("ERROR maker's (" + nick + ")"
                          " authorising pubkey is not included "
                          "in the transaction!")
                #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.warn(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.info(
                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
            #We have succesfully processed the data from this nick:
            try:
                self.nonrespondants.remove(nick)
            except Exception as e:
                jlog.warn("Failure to remove counterparty from nonrespondants list: " + str(nick) + \
                          ", error message: " + repr(e))

        #Apply business logic of how many counterparties are enough; note that
        #this must occur after the above ioauth data processing, since we only now
        #know for sure that the data meets all business-logic requirements.
        if len(self.maker_utxo_data) < 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")

        #The list self.nonrespondants is now reset and
        #used to track return of signatures for phase 2
        self.nonrespondants = list(self.maker_utxo_data.keys())

        my_total_in = sum([va['value'] for u, va in self.input_utxos.items()])
        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,
                txtype=self.wallet_service.get_txtype())
            jlog.info("Based on initial guess: " +
                      btc.amount_to_str(self.total_txfee) +
                      ", we estimated a miner fee of: " +
                      btc.amount_to_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:
            if my_change_value < -1:
                raise ValueError(
                    "Calculated transaction fee of: " +
                    btc.amount_to_str(self.total_txfee) +
                    " is too large for our inputs; Please try again.")
            if my_change_value <= jm_single().BITCOIN_DUST_THRESHOLD:
                jlog.info("Dynamically calculated change lower than dust: " +
                          btc.amount_to_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.info(
                    ('WARNING CHANGE NOT BEING USED\nCHANGEVALUE = {}').format(
                        btc.amount_to_str(my_change_value)))
            # we need to check whether the *achieved* txfee-rate is outside
            # the range allowed by the user in config; if not, abort the tx.
            # this is done with using the same estimate fee function and comparing
            # the totals; this ratio will correspond to the ratio of the feerates.
            num_ins = len([u for u in sum(self.utxos.values(), [])])
            num_outs = len(self.outputs) + 2
            new_total_fee = estimate_tx_fee(
                num_ins, num_outs, txtype=self.wallet_service.get_txtype())
            feeratio = self.total_txfee / new_total_fee
            jlog.debug(
                "Ratio of actual to estimated sweep fee: {}".format(feeratio))
            sweep_delta = float(jm_single().config.get("POLICY",
                                                       "max_sweep_fee_change"))
            if feeratio < 1 - sweep_delta or feeratio > 1 + sweep_delta:
                jlog.warn(
                    "Transaction fee for sweep: {} too far from expected:"
                    " {}; check the setting 'max_sweep_fee_change'"
                    " in joinmarket.cfg. Aborting this attempt.".format(
                        self.total_txfee, new_total_fee))
                return (False, "Unacceptable feerate for sweep, giving up.")
        else:
            self.outputs.append({
                'address': self.my_change_addr,
                'value': my_change_value
            })
        self.utxo_tx = [u for u in sum(self.utxos.values(), [])]
        self.outputs.append({
            'address': self.coinjoin_address(),
            'value': self.cjamount
        })
        # pre-Nov-2020/v0.8.0: transactions used ver 1 and nlocktime 0
        # so only the new "pit" (using native segwit) will use the updated
        # version 2 and nlocktime ~ current block as per normal payments.
        # TODO makers do not check this; while there is no security risk,
        # it might be better for them to sanity check.
        if self.wallet_service.get_txtype() == "p2wpkh":
            n_version = 2
            locktime = compute_tx_locktime()
        else:
            n_version = 1
            locktime = 0
        self.latest_tx = btc.make_shuffled_tx(self.utxo_tx,
                                              self.outputs,
                                              version=n_version,
                                              locktime=locktime)
        jlog.info('obtained tx\n' +
                  btc.human_readable_transaction(self.latest_tx))

        self.taker_info_callback("INFO",
                                 "Built tx, sending to counterparties.")
        return (True, list(self.maker_utxo_data.keys()),
                bintohex(self.latest_tx.serialize()))
示例#2
0
    def prepare_my_bitcoin_data(self):
        """Get a coinjoin address and a change address; prepare inputs
        appropriate for this transaction"""
        if not self.my_cj_addr:
            #previously used for donations; TODO reimplement?
            raise NotImplementedError
        self.my_change_addr = None
        if self.cjamount != 0:
            try:
                self.my_change_addr = self.wallet_service.get_internal_addr(
                    self.mixdepth)
            except:
                self.taker_info_callback("ABORT",
                                         "Failed to get a change address")
                return False
            #adjust the required amount upwards to anticipate an increase in
            #transaction fees after re-estimation; this is sufficiently conservative
            #to make failures unlikely while keeping the occurence of failure to
            #find sufficient utxos extremely rare. Indeed, a doubling of 'normal'
            #txfee indicates undesirable behaviour on maker side anyway.
            self.total_txfee = estimate_tx_fee(
                3, 2, txtype=self.wallet_service.get_txtype(
                )) * self.n_counterparties
            total_amount = self.cjamount + self.total_cj_fee + self.total_txfee
            jlog.info('total estimated amount spent = ' +
                      btc.amount_to_str(total_amount))
            try:
                self.input_utxos = self.wallet_service.select_utxos(
                    self.mixdepth, total_amount, minconfs=1)
            except Exception as e:
                self.taker_info_callback(
                    "ABORT", "Unable to select sufficient coins: " + repr(e))
                return False
        else:
            #sweep
            self.input_utxos = self.wallet_service.get_utxos_by_mixdepth()[
                self.mixdepth]
            #do our best to estimate the fee based on the number of
            #our own utxos; this estimate may be significantly higher
            #than the default set in option.txfee * makercount, where
            #we have a large number of utxos to spend. If it is smaller,
            #we'll be conservative and retain the original estimate.
            est_ins = len(self.input_utxos) + 3 * self.n_counterparties
            jlog.debug("Estimated ins: " + str(est_ins))
            est_outs = 2 * self.n_counterparties + 1
            jlog.debug("Estimated outs: " + str(est_outs))
            self.total_txfee = estimate_tx_fee(
                est_ins, est_outs, txtype=self.wallet_service.get_txtype())
            jlog.debug("We have a fee estimate: " + str(self.total_txfee))
            total_value = sum(
                [va['value'] for va in self.input_utxos.values()])
            if self.wallet_service.get_txtype() == "p2pkh":
                allowed_types = ["reloffer", "absoffer"]
            elif self.wallet_service.get_txtype() == "p2sh-p2wpkh":
                allowed_types = ["swreloffer", "swabsoffer"]
            elif self.wallet_service.get_txtype() == "p2wpkh":
                allowed_types = ["sw0reloffer", "sw0absoffer"]
            else:
                jlog.error("Unrecognized wallet type, taker cannot continue.")
                return False
            self.orderbook, self.cjamount, self.total_cj_fee = choose_sweep_orders(
                self.orderbook,
                total_value,
                self.total_txfee,
                self.n_counterparties,
                self.order_chooser,
                self.ignored_makers,
                allowed_types=allowed_types,
                max_cj_fee=self.max_cj_fee)
            if not self.orderbook:
                self.taker_info_callback(
                    "ABORT", "Could not find orders to complete transaction")
                return False
            if self.filter_orders_callback:
                if not self.filter_orders_callback(
                    (self.orderbook, self.total_cj_fee), self.cjamount):
                    return False

        self.utxos = {None: list(self.input_utxos.keys())}
        return True
示例#3
0
    def on_tx_received(self, nick, txhex):
        """ Called when the sender-counterparty has sent a transaction proposal.
        1. First we check for the expected destination and amount (this is
           sufficient to identify our cp, as this info was presumably passed
           out of band, as for any normal payment).
        2. Then we verify the validity of the proposed non-coinjoin
           transaction; if not, reject, otherwise store this as a
           fallback transaction in case the protocol doesn't complete.
        3. Next, we select utxos from our wallet, to add into the
           payment transaction as input. Try to select so as to not
           trigger the UIH2 condition, but continue (and inform user)
           even if we can't (if we can't select any coins, broadcast the
           non-coinjoin payment, if the user agrees).
           Proceeding with payjoin:
        4. We update the output amount at the destination address.
        5. We modify the change amount in the original proposal (which
           will be the only other output other than the destination),
           reducing it to account for the increased transaction fee
           caused by our additional proposed input(s).
        6. Finally we sign our own input utxo(s) and re-serialize the
           tx, allowing it to be sent back to the counterparty.
        7. If the transaction is not fully signed and broadcast within
           the time unconfirm_timeout_sec as specified in the joinmarket.cfg,
           we broadcast the non-coinjoin fallback tx instead.
        """
        try:
            tx = btc.deserialize(txhex)
        except (IndexError, SerializationError,
                SerializationTruncationError) as e:
            return (False, 'malformed txhex. ' + repr(e))
        self.user_info('obtained proposed fallback (non-coinjoin) ' +\
                       'transaction from sender:\n' + pprint.pformat(tx))

        if len(tx["outs"]) != 2:
            return (False,
                    "Transaction has more than 2 outputs; not supported.")
        dest_found = False
        destination_index = -1
        change_index = -1
        proposed_change_value = 0
        for index, out in enumerate(tx["outs"]):
            if out["script"] == btc.address_to_script(self.destination_addr):
                # we found the expected destination; is the amount correct?
                if not out["value"] == self.receiving_amount:
                    return (False,
                            "Wrong payout value in proposal from sender.")
                dest_found = True
                destination_index = index
            else:
                change_found = True
                proposed_change_out = out["script"]
                proposed_change_value = out["value"]
                change_index = index

        if not dest_found:
            return (False, "Our expected destination address was not found.")

        # Verify valid input utxos provided and check their value.
        # batch retrieval of utxo data
        utxo = {}
        ctr = 0
        for index, ins in enumerate(tx['ins']):
            utxo_for_checking = ins['outpoint']['hash'] + ':' + str(
                ins['outpoint']['index'])
            utxo[ctr] = [index, utxo_for_checking]
            ctr += 1

        utxo_data = jm_single().bc_interface.query_utxo_set(
            [x[1] for x in utxo.values()])

        total_sender_input = 0
        for i, u in iteritems(utxo):
            if utxo_data[i] is None:
                return (False, "Proposed transaction contains invalid utxos")
            total_sender_input += utxo_data[i]["value"]

        # Check that the transaction *as proposed* balances; check that the
        # included fee is within 0.3-3x our own current estimates, if not user
        # must decide.
        btc_fee = total_sender_input - self.receiving_amount - proposed_change_value
        self.user_info("Network transaction fee of fallback tx is: " +
                       str(btc_fee) + " satoshis.")
        fee_est = estimate_tx_fee(len(tx['ins']),
                                  len(tx['outs']),
                                  txtype=self.wallet.get_txtype())
        fee_ok = False
        if btc_fee > 0.3 * fee_est and btc_fee < 3 * fee_est:
            fee_ok = True
        else:
            if self.user_check("Is this transaction fee acceptable? (y/n):"):
                fee_ok = True
        if not fee_ok:
            return (False,
                    "Proposed transaction fee not accepted due to tx fee: " +
                    str(btc_fee))

        # This direct rpc call currently assumes Core 0.17, so not using now.
        # It has the advantage of (a) being simpler and (b) allowing for any
        # non standard coins.
        #
        #res = jm_single().bc_interface.rpc('testmempoolaccept', [txhex])
        #print("Got this result from rpc call: ", res)
        #if not res["accepted"]:
        #    return (False, "Proposed transaction was rejected from mempool.")

        # Manual verification of the transaction signatures. Passing this
        # test does imply that the transaction is valid (unless there is
        # a double spend during the process), but is restricted to standard
        # types: p2pkh, p2wpkh, p2sh-p2wpkh only. Double spend is not counted
        # as a risk as this is a payment.
        for i, u in iteritems(utxo):
            if "txinwitness" in tx["ins"][u[0]]:
                ver_amt = utxo_data[i]["value"]
                try:
                    ver_sig, ver_pub = tx["ins"][u[0]]["txinwitness"]
                except Exception as e:
                    self.user_info("Segwit error: " + repr(e))
                    return (False, "Segwit input not of expected type, "
                            "either p2sh-p2wpkh or p2wpkh")
                # note that the scriptCode is the same whether nested or not
                # also note that the scriptCode has to be inferred if we are
                # only given a transaction serialization.
                scriptCode = "76a914" + btc.hash160(
                    unhexlify(ver_pub)) + "88ac"
            else:
                scriptCode = None
                ver_amt = None
                scriptSig = btc.deserialize_script(tx["ins"][u[0]]["script"])
                if len(scriptSig) != 2:
                    return (
                        False,
                        "Proposed transaction contains unsupported input type")
                ver_sig, ver_pub = scriptSig
            if not btc.verify_tx_input(txhex,
                                       u[0],
                                       utxo_data[i]['script'],
                                       ver_sig,
                                       ver_pub,
                                       scriptCode=scriptCode,
                                       amount=ver_amt):
                return (False, "Proposed transaction is not correctly signed.")

        # At this point we are satisfied with the proposal. Record the fallback
        # in case the sender disappears and the payjoin tx doesn't happen:
        self.user_info(
            "We'll use this serialized transaction to broadcast if your"
            " counterparty fails to broadcast the payjoin version:")
        self.user_info(txhex)
        # Keep a local copy for broadcast fallback:
        self.fallback_tx = txhex

        # Now we add our own inputs:
        # See the gist comment here:
        # https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2799709
        # which sets out the decision Bob must make.
        # In cases where Bob can add any amount, he selects one utxo
        # to keep it simple.
        # In cases where he must choose at least X, he selects one utxo
        # which provides X if possible, otherwise defaults to a normal
        # selection algorithm.
        # In those cases where he must choose X but X is unavailable,
        # he selects all coins, and proceeds anyway with payjoin, since
        # it has other advantages (CIOH and utxo defrag).
        my_utxos = {}
        largest_out = max(self.receiving_amount, proposed_change_value)
        max_sender_amt = max([u['value'] for u in utxo_data])
        not_uih2 = False
        if max_sender_amt < largest_out:
            # just select one coin.
            # have some reasonable lower limit but otherwise choose
            # randomly; note that this is actually a great way of
            # sweeping dust ...
            self.user_info("Choosing one coin at random")
            try:
                my_utxos = self.wallet.select_utxos(self.mixdepth,
                                                    jm_single().DUST_THRESHOLD,
                                                    select_fn=select_one_utxo)
            except:
                return self.no_coins_fallback()
            not_uih2 = True
        else:
            # get an approximate required amount assuming 4 inputs, which is
            # fairly conservative (but guess by necessity).
            fee_for_select = estimate_tx_fee(len(tx['ins']) + 4,
                                             2,
                                             txtype=self.wallet.get_txtype())
            approx_sum = max_sender_amt - self.receiving_amount + fee_for_select
            try:
                my_utxos = self.wallet.select_utxos(self.mixdepth, approx_sum)
                not_uih2 = True
            except Exception:
                # TODO probably not logical to always sweep here.
                self.user_info("Sweeping all coins in this mixdepth.")
                my_utxos = self.wallet.get_utxos_by_mixdepth()[self.mixdepth]
                if my_utxos == {}:
                    return self.no_coins_fallback()
        if not_uih2:
            self.user_info("The proposed tx does not trigger UIH2, which "
                           "means it is indistinguishable from a normal "
                           "payment. This is the ideal case. Continuing..")
        else:
            self.user_info("The proposed tx does trigger UIH2, which it makes "
                           "it somewhat distinguishable from a normal payment,"
                           " but proceeding with payjoin..")

        my_total_in = sum([va['value'] for va in my_utxos.values()])
        self.user_info("We selected inputs worth: " + str(my_total_in))
        # adjust the output amount at the destination based on our contribution
        new_destination_amount = self.receiving_amount + my_total_in
        # estimate the required fee for the new version of the transaction
        total_ins = len(tx["ins"]) + len(my_utxos.keys())
        est_fee = estimate_tx_fee(total_ins,
                                  2,
                                  txtype=self.wallet.get_txtype())
        self.user_info("We estimated a fee of: " + str(est_fee))
        new_change_amount = total_sender_input + my_total_in - \
            new_destination_amount - est_fee
        self.user_info("We calculated a new change amount of: " +
                       str(new_change_amount))
        self.user_info("We calculated a new destination amount of: " +
                       str(new_destination_amount))
        # now reconstruct the transaction with the new inputs and the
        # amount-changed outputs
        new_outs = [{
            "address": self.destination_addr,
            "value": new_destination_amount
        }]
        if new_change_amount >= jm_single().BITCOIN_DUST_THRESHOLD:
            new_outs.append({
                "script": proposed_change_out,
                "value": new_change_amount
            })
        new_ins = [x[1] for x in utxo.values()]
        new_ins.extend(my_utxos.keys())
        # set locktime for best anonset (Core, Electrum) - most recent block.
        # this call should never fail so no catch here.
        currentblock = jm_single().bc_interface.rpc("getblockchaininfo",
                                                    [])["blocks"]
        new_tx = make_shuffled_tx(new_ins, new_outs, False, 2, currentblock)
        new_tx_deser = btc.deserialize(new_tx)

        # sign our inputs before transfer
        our_inputs = {}
        for index, ins in enumerate(new_tx_deser['ins']):
            utxo = ins['outpoint']['hash'] + ':' + str(
                ins['outpoint']['index'])
            if utxo not in my_utxos:
                continue
            script = self.wallet.addr_to_script(my_utxos[utxo]['address'])
            amount = my_utxos[utxo]['value']
            our_inputs[index] = (script, amount)

        txs = self.wallet.sign_tx(btc.deserialize(new_tx), our_inputs)
        jm_single().bc_interface.add_tx_notify(
            txs,
            self.on_tx_unconfirmed,
            self.on_tx_confirmed,
            self.destination_addr,
            wallet_name=jm_single().bc_interface.get_wallet_name(self.wallet),
            txid_flag=False,
            vb=self.wallet._ENGINE.VBYTE)
        # The blockchain interface just abandons monitoring if the transaction
        # is not broadcast before the configured timeout; we want to take
        # action in this case, so we add an additional callback to the reactor:
        reactor.callLater(
            jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec"),
            self.broadcast_fallback)
        return (True, nick, btc.serialize(txs))
    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)
示例#5
0
    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")

        self.maker_utxo_data = {}

        verified_data = self._verify_ioauth_data(ioauth_data)
        for maker_inputs in verified_data:
            # We have succesfully processed the data from this nick
            self.utxos[maker_inputs.nick] = maker_inputs.utxo_list
            self.outputs.append({'address': maker_inputs.change_addr,
                                 'value': maker_inputs.change_amount})
            self.outputs.append({'address': maker_inputs.cj_addr,
                                 'value': self.cjamount})
            self.cjfee_total += maker_inputs.real_cjfee
            self.maker_txfee_contributions +=\
                self.orderbook[maker_inputs.nick]['txfee']
            self.maker_utxo_data[maker_inputs.nick] = maker_inputs.utxo_data
            jlog.info(
                f"fee breakdown for {maker_inputs.nick} "
                f"totalin={maker_inputs.total_input:d} "
                f"cjamount={self.cjamount:d} "
                f"txfee={self.orderbook[maker_inputs.nick]['txfee']:d} "
                f"realcjfee={maker_inputs.real_cjfee:d}")

            try:
                self.nonrespondants.remove(maker_inputs.nick)
            except Exception as e:
                jlog.warn(
                    "Failure to remove counterparty from nonrespondants list:"
                    f" {maker_inputs.nick}), error message: {repr(e)})")

        #Apply business logic of how many counterparties are enough; note that
        #this must occur after the above ioauth data processing, since we only now
        #know for sure that the data meets all business-logic requirements.
        if len(self.maker_utxo_data) < 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")

        #The list self.nonrespondants is now reset and
        #used to track return of signatures for phase 2
        self.nonrespondants = list(self.maker_utxo_data.keys())

        my_total_in = sum([va['value'] for u, va in self.input_utxos.items()])
        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,
                txtype=self.wallet_service.get_txtype())
            jlog.info("Based on initial guess: " +
                      btc.amount_to_str(self.total_txfee) +
                      ", we estimated a miner fee of: " +
                      btc.amount_to_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:
            if my_change_value < -1:
                raise ValueError("Calculated transaction fee of: " +
                    btc.amount_to_str(self.total_txfee) +
                    " is too large for our inputs; Please try again.")
            if my_change_value <= jm_single().BITCOIN_DUST_THRESHOLD:
                jlog.info("Dynamically calculated change lower than dust: " +
                    btc.amount_to_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.info(
                ('WARNING CHANGE NOT BEING USED\nCHANGEVALUE = {}').format(
                    btc.amount_to_str(my_change_value)))
            # we need to check whether the *achieved* txfee-rate is outside
            # the range allowed by the user in config; if not, abort the tx.
            # this is done with using the same estimate fee function and comparing
            # the totals; this ratio will correspond to the ratio of the feerates.
            num_ins = len([u for u in sum(self.utxos.values(), [])])
            num_outs = len(self.outputs) + 1
            new_total_fee = estimate_tx_fee(num_ins, num_outs,
                                    txtype=self.wallet_service.get_txtype())
            feeratio = new_total_fee/self.total_txfee
            jlog.debug("Ratio of actual to estimated sweep fee: {}".format(
                feeratio))
            sweep_delta = float(jm_single().config.get("POLICY",
                                                       "max_sweep_fee_change"))
            if feeratio < 1 - sweep_delta or feeratio > 1 + sweep_delta:
                jlog.warn("Transaction fee for sweep: {} too far from expected:"
                          " {}; check the setting 'max_sweep_fee_change'"
                          " in joinmarket.cfg. Aborting this attempt.".format(
                              new_total_fee, self.total_txfee))
                return (False, "Unacceptable feerate for sweep, giving up.")
        else:
            self.outputs.append({'address': self.my_change_addr,
                                 'value': my_change_value})
        self.utxo_tx = [u for u in sum(self.utxos.values(), [])]
        self.outputs.append({'address': self.coinjoin_address(),
                             'value': self.cjamount})
        # pre-Nov-2020/v0.8.0: transactions used ver 1 and nlocktime 0
        # so only the new "pit" (using native segwit) will use the updated
        # version 2 and nlocktime ~ current block as per normal payments.
        # TODO makers do not check this; while there is no security risk,
        # it might be better for them to sanity check.
        if self.wallet_service.get_txtype() == "p2wpkh":
            n_version = 2
            locktime = compute_tx_locktime()
        else:
            n_version = 1
            locktime = 0
        self.latest_tx = btc.make_shuffled_tx(self.utxo_tx, self.outputs,
                                              version=n_version, locktime=locktime)
        jlog.info('obtained tx\n' + btc.human_readable_transaction(
            self.latest_tx))

        self.taker_info_callback("INFO", "Built tx, sending to counterparties.")
        return (True, list(self.maker_utxo_data.keys()),
                bintohex(self.latest_tx.serialize()))