Exemple #1
0
 def handle_unbroadcast_transaction(self, txid, tx):
     """ The wallet service will handle dangling
     callbacks for transactions but we want to reattempt
     broadcast in case the cause of the problem is a
     counterparty who refused to broadcast it for us.
     """
     if not self.wallet_service.check_callback_called(
             self.txid, self.unconfirm_callback, "unconfirmed",
             "transaction with txid: " + str(self.txid) +
             " not broadcast."):
         # we now know the transaction was not pushed, so we reinstigate
         # the cancelledcallback with the same logic as explained
         # in Taker.push():
         self.wallet_service.register_callbacks([self.unconfirm_callback],
                                                txid, "unconfirmed")
         if jm_single().config.get('POLICY', 'tx_broadcast') == "not-self":
             warnmsg = ("You have chosen not to broadcast from your own "
                        "node. The transaction is NOT broadcast.")
             self.taker_info_callback("ABORT",
                                      warnmsg + "\nSee log for details.")
             # warning is arguably not correct but it will stand out more:
             jlog.warn(warnmsg)
             jlog.info(btc.human_readable_transaction(tx))
             return
         if not self.push_ourselves():
             jlog.error("Failed to broadcast transaction: ")
             jlog.info(btc.human_readable_transaction(tx))
Exemple #2
0
 def report(self, jsonified=False, verbose=False):
     """ Returns a dict (optionally jsonified) containing
     the following information (if they are
     available):
     * current status of Payjoin
     * payment transaction (original, non payjoin)
     * payjoin partial (PSBT) sent by receiver
     * final payjoin transaction
     * whether or not the payjoin transaction is
       broadcast and/or confirmed.
     If verbose is True, we include the full deserialization
     of transactions and PSBTs, which is too verbose for GUI
     display.
     """
     reportdict = {"name:", "PAYJOIN STATUS REPORT"}
     reportdict["status"] = self.pj_state  # TODO: string
     if self.payment_tx:
         txdata = btc.human_readable_transaction(self.payment_tx)
         if verbose:
             txdata = txdata["hex"]
         reportdict["payment-tx"] = txdata
     if self.payjoin_psbt:
         psbtdata = PSBTWalletMixin.human_readable_psbt(
             self.payjoin_psbt) if verbose else self.payjoin_psbt.to_base64(
             )
         reportdict["payjoin-proposed"] = psbtdata
     if self.final_psbt:
         finaldata = PSBTWalletMixin.human_readable_psbt(
             self.final_psbt) if verbose else self.final_psbt.to_base64()
         reportdict["payjoin-final"] = finaldata
     if jsonified:
         return json.dumps(reportdict, indent=4)
     else:
         return reportdict
 def directsend(self, request, walletname):
     """ Use the contents of the POST body to do a direct send from
     the active wallet at the chosen mixdepth.
     """
     self.check_cookie(request)
     assert isinstance(request.content, BytesIO)
     payment_info_json = self.get_POST_body(
         request, ["mixdepth", "amount_sats", "destination"])
     if not payment_info_json:
         raise InvalidRequestFormat()
     if not self.services["wallet"]:
         raise NoWalletFound()
     if not self.wallet_name == walletname:
         raise InvalidRequestFormat()
     try:
         tx = direct_send(self.services["wallet"],
                          int(payment_info_json["amount_sats"]),
                          int(payment_info_json["mixdepth"]),
                          destination=payment_info_json["destination"],
                          return_transaction=True,
                          answeryes=True)
     except AssertionError:
         raise InvalidRequestFormat()
     except NotEnoughFundsException as e:
         raise TransactionFailed(repr(e))
     if not tx:
         # this should not really happen; not a coinjoin
         # so tx should go through.
         raise TransactionFailed()
     return make_jmwalletd_response(request,
                                    txinfo=human_readable_transaction(
                                        tx, False))
Exemple #4
0
def fallback_nonpayjoin_broadcast(manager, err):
    """ Sends the non-coinjoin payment onto the network,
    assuming that the payjoin failed. The reason for failure is
    `err` and will usually be communicated by the server, and must
    be a bytestring.
    Note that the reactor is shutdown after sending the payment (one-shot
    processing).
    """
    assert isinstance(manager, JMPayjoinManager)

    def quit():
        for dc in reactor.getDelayedCalls():
            dc.cancel()
        reactor.stop()

    log.warn("Payjoin did not succeed, falling back to non-payjoin payment.")
    log.warn("Error message was: " + err.decode("utf-8"))
    original_tx = manager.initial_psbt.extract_transaction()
    if not jm_single().bc_interface.pushtx(original_tx.serialize()):
        log.error(
            "Unable to broadcast original payment. The payment is NOT made.")
        quit()
        return
    log.info("We paid without coinjoin. Transaction: ")
    log.info(btc.human_readable_transaction(original_tx))
    quit()
Exemple #5
0
def fallback_nonpayjoin_broadcast(err, manager):
    """ Sends the non-coinjoin payment onto the network,
    assuming that the payjoin failed. The reason for failure is
    `err` and will usually be communicated by the server, and must
    be a bytestring.
    Note that the reactor is shutdown after sending the payment (one-shot
    processing) if this is called on the command line.
    """
    assert isinstance(manager, JMPayjoinManager)

    def quit():
        if manager.mode == "command-line" and reactor.running:
            process_shutdown()

    log.warn("Payjoin did not succeed, falling back to non-payjoin payment.")
    log.warn("Error message was: " + err.decode("utf-8"))
    original_tx = manager.initial_psbt.extract_transaction()
    if not jm_single().bc_interface.pushtx(original_tx.serialize()):
        errormsg = ("Unable to broadcast original payment. Check your wallet\n"
                    "to see whether original payment was made.")
        log.error(errormsg)
        # ensure any GUI as well as command line sees the message:
        manager.user_info_callback(errormsg)
        quit()
        return
    log.info("Payment made without coinjoin. Transaction: ")
    log.info(btc.human_readable_transaction(original_tx))
    manager.set_broadcast(False)
    if manager.timeout_fallback_dc and manager.timeout_fallback_dc.active():
        manager.timeout_fallback_dc.cancel()
    quit()
 def sendTxNotification(self, txd, txid):
     """ Note that this is a WalletService callback;
     the return value is only important for conf/unconf
     callbacks, not for 'all' callbacks, so we return
     None
     """
     json_tx = json.loads(human_readable_transaction(txd))
     for client in self.clients:
         client.sendNotification({"txid": txid, "txdetails": json_tx})
def process_payjoin_proposal_from_server(response_body, manager):
    assert isinstance(manager, JMPayjoinManager)
    try:
        payjoin_proposal_psbt = \
            btc.PartiallySignedTransaction.from_base64(response_body)
    except Exception as e:
        log.error("Payjoin tx from server could not be parsed: " + repr(e))
        fallback_nonpayjoin_broadcast(manager, err=b"Server sent invalid psbt")
        return

    log.debug("Receiver sent us this PSBT: ")
    log.debug(
        manager.wallet_service.human_readable_psbt(payjoin_proposal_psbt))
    # we need to add back in our utxo information to the received psbt,
    # since the servers remove it (not sure why?)
    for i, inp in enumerate(payjoin_proposal_psbt.unsigned_tx.vin):
        for j, inp2 in enumerate(manager.initial_psbt.unsigned_tx.vin):
            if (inp.prevout.hash, inp.prevout.n) == (inp2.prevout.hash,
                                                     inp2.prevout.n):
                payjoin_proposal_psbt.set_utxo(
                    manager.initial_psbt.inputs[j].utxo, i)
    signresultandpsbt, err = manager.wallet_service.sign_psbt(
        payjoin_proposal_psbt.serialize(), with_sign_result=True)
    if err:
        log.error("Failed to sign PSBT from the receiver, error: " + err)
        fallback_nonpayjoin_broadcast(manager,
                                      err=b"Failed to sign receiver PSBT")
        return

    signresult, sender_signed_psbt = signresultandpsbt
    assert signresult.is_final
    success, msg = manager.set_payjoin_psbt(payjoin_proposal_psbt,
                                            sender_signed_psbt)
    if not success:
        log.error(msg)
        fallback_nonpayjoin_broadcast(manager,
                                      err=b"Receiver PSBT checks failed.")
        return
    # All checks have passed. We can use the already signed transaction in
    # sender_signed_psbt.
    log.info("Our final signed PSBT is:\n{}".format(
        manager.wallet_service.human_readable_psbt(sender_signed_psbt)))
    manager.set_final_payjoin_psbt(sender_signed_psbt)

    # broadcast the tx
    extracted_tx = sender_signed_psbt.extract_transaction()
    log.info("Here is the final payjoin transaction:")
    log.info(btc.human_readable_transaction(extracted_tx))
    if not jm_single().bc_interface.pushtx(extracted_tx.serialize()):
        log.info("The above transaction failed to broadcast.")
    else:
        log.info("Payjoin transaction broadcast successfully.")
        # if transaction is succesfully broadcast, remove the
        # timeout fallback to avoid confusing error messages:
        manager.timeout_fallback_dc.cancel()
    if manager.mode == "command-line" and reactor.running:
        reactor.stop()
Exemple #8
0
    def on_tx_received(self, nick, tx, offerinfo):
        """Called when the counterparty has sent an unsigned
        transaction. Sigs are created and returned if and only
        if the transaction passes verification checks (see
        verify_unsigned_tx()).
        """
        # special case due to cjfee passed as string: it can accidentally parse
        # as hex:
        if not isinstance(offerinfo["offer"]["cjfee"], str):
            offerinfo["offer"]["cjfee"] = bintohex(offerinfo["offer"]["cjfee"])
        try:
            tx = btc.CMutableTransaction.deserialize(tx)
        except Exception as e:
            return (False, 'malformed tx. ' + repr(e))
        # if the above deserialization was successful, the human readable
        # parsing will be also:
        jlog.info('obtained tx\n' + btc.human_readable_transaction(tx))
        goodtx, errmsg = self.verify_unsigned_tx(tx, offerinfo)
        if not goodtx:
            jlog.info('not a good tx, reason=' + errmsg)
            return (False, errmsg)
        jlog.info('goodtx')
        sigs = []
        utxos = offerinfo["utxos"]

        our_inputs = {}
        for index, ins in enumerate(tx.vin):
            utxo = (ins.prevout.hash[::-1], ins.prevout.n)
            if utxo not in utxos:
                continue
            script = self.wallet_service.addr_to_script(utxos[utxo]['address'])
            amount = utxos[utxo]['value']
            our_inputs[index] = (script, amount)

        success, msg = self.wallet_service.sign_tx(tx, our_inputs)
        assert success, msg
        for index in our_inputs:
            # The second case here is kept for backwards compatibility.
            if self.wallet_service.get_txtype() == 'p2pkh':
                sigmsg = tx.vin[index].scriptSig
            elif self.wallet_service.get_txtype() == 'p2sh-p2wpkh':
                sig, pub = [
                    a for a in iter(tx.wit.vtxinwit[index].scriptWitness)
                ]
                scriptCode = btc.pubkey_to_p2wpkh_script(pub)
                sigmsg = btc.CScript([sig]) + btc.CScript(pub) + scriptCode
            elif self.wallet_service.get_txtype() == 'p2wpkh':
                sig, pub = [
                    a for a in iter(tx.wit.vtxinwit[index].scriptWitness)
                ]
                sigmsg = btc.CScript([sig]) + btc.CScript(pub)
            else:
                jlog.error("Taker has unknown wallet type")
                sys.exit(EXIT_FAILURE)
            sigs.append(base64.b64encode(sigmsg).decode('ascii'))
        return (True, sigs)
def main():
    parser = OptionParser(
        usage='usage: %prog [options] utxo destaddr1 destaddr2 ..',
        description=description,
        formatter=IndentedHelpFormatterWithNL())
    parser.add_option(
        '-t',
        '--utxo-address-type',
        action='store',
        dest='utxo_address_type',
        help=
        ('type of address of coin being spent - one of "p2pkh", "p2wpkh", "p2sh-p2wpkh". '
         'No other scriptpubkey types (e.g. multisig) are supported. If not set, we default '
         'to what is in joinmarket.cfg.'),
        default="")
    add_base_options(parser)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    if len(args) < 2:
        quit(parser, 'Invalid syntax')
    u = args[0]
    priv = input('input private key for ' + u +
                 ', in WIF compressed format : ')
    u, priv = get_utxo_info(','.join([u, priv]))
    if not u:
        quit(parser, "Failed to parse utxo info: " + u)
    destaddrs = args[1:]
    for d in destaddrs:
        if not validate_address(d):
            quit(parser, "Address was not valid; wrong network?: " + d)
    success, utxo = utxostr_to_utxo(u)
    if not success:
        quit(parser, "Failed to load utxo from string: " + utxo)
    if options.utxo_address_type == "":
        if jm_single().config.get("POLICY", "segwit") == "false":
            utxo_address_type = "p2pkh"
        elif jm_single().config.get("POLICY", "native") == "false":
            utxo_address_type = "p2sh-p2wpkh"
        else:
            utxo_address_type = "p2wpkh"
    else:
        utxo_address_type = options.utxo_address_type
    txsigned = sign(utxo, priv, destaddrs, utxo_address_type)
    if not txsigned:
        log.info(
            "Transaction signing operation failed, see debug messages for details."
        )
        return
    log.info("Got signed transaction:\n" + bintohex(txsigned.serialize()))
    log.info(btc.human_readable_transaction(txsigned))
    if input('Would you like to push to the network? (y/n):')[0] != 'y':
        log.info("You chose not to broadcast the transaction, quitting.")
        return
    jm_single().bc_interface.pushtx(txsigned.serialize())
Exemple #10
0
def write_candidate_to_file(ttype, candidate, blocknum, unspents, filename):
    """ Appends the details for the candidate
    transaction to the chosen textfile.
    """
    with open(filename, "a") as f:
        f.write(found_str(ttype, candidate, blocknum) + "\n")
        f.write(btc.human_readable_transaction(candidate) + "\n")
        f.write("Full transaction hex for creating a proposal is "
                "found in the above.\n")
        f.write("The unspent indices are: " +
                " ".join((str(u) for u in unspents)) + "\n")
Exemple #11
0
def test_sign_standard_txs(addrtype):
    # liberally copied from python-bitcoinlib tests,
    # in particular see:
    # https://github.com/petertodd/python-bitcoinlib/pull/227

    # Create the (in)famous correct brainwallet secret key.
    priv = hashlib.sha256(b'correct horse battery staple').digest() + b"\x01"
    pub = btc.privkey_to_pubkey(priv)

    # Create an address from that private key.
    # (note that the input utxo is fake so we are really only creating
    # a destination here).
    scriptPubKey = btc.CScript([btc.OP_0, btc.Hash160(pub)])
    address = btc.P2WPKHCoinAddress.from_scriptPubKey(scriptPubKey)

    # Create a dummy outpoint; use same 32 bytes for convenience
    txid = priv[:32]
    vout = 2
    amount = btc.coins_to_satoshi(float('0.12345'))

    # Calculate an amount for the upcoming new UTXO. Set a high fee to bypass
    # bitcoind minfee setting.
    amount_less_fee = int(amount - btc.coins_to_satoshi(0.01))

    # Create a destination to send the coins.
    destination_address = address
    target_scriptPubKey = scriptPubKey

    # Create the unsigned transaction.
    txin = btc.CTxIn(btc.COutPoint(txid[::-1], vout))
    txout = btc.CTxOut(amount_less_fee, target_scriptPubKey)
    tx = btc.CMutableTransaction([txin], [txout])

    # Calculate the signature hash for the transaction. This is then signed by the
    # private key that controls the UTXO being spent here at this txin_index.
    if addrtype == "p2wpkh":
        sig, msg = btc.sign(tx, 0, priv, amount=amount, native="p2wpkh")
    elif addrtype == "p2sh-p2wpkh":
        sig, msg = btc.sign(tx, 0, priv, amount=amount, native=False)
    elif addrtype == "p2pkh":
        sig, msg = btc.sign(tx, 0, priv)
    else:
        assert False
    if not sig:
        print(msg)
        raise
    print("created signature: ", bintohex(sig))
    print("serialized transaction: {}".format(bintohex(tx.serialize())))
    print("deserialized transaction: {}\n".format(
        btc.human_readable_transaction(tx)))
Exemple #12
0
 def handle_unbroadcast_transaction(self, txid, tx):
     """ The wallet service will handle dangling
     callbacks for transactions but we want to reattempt
     broadcast in case the cause of the problem is a
     counterparty who refused to broadcast it for us.
     """
     if not self.wallet_service.check_callback_called(
             self.txid, self.unconfirm_callback, "unconfirmed",
             "transaction with txid: " + str(self.txid) +
             " not broadcast."):
         # we now know the transaction was not pushed, so we reinstigate
         # the cancelledcallback with the same logic as explained
         # in Taker.push():
         self.wallet_service.register_callbacks([self.unconfirm_callback],
                                                txid, "unconfirmed")
         if not self.push_ourselves():
             jlog.error("Failed to broadcast transaction: ")
             jlog.info(btc.human_readable_transaction(tx))
Exemple #13
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()))
Exemple #14
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()))
 def render_POST(self, request):
     """ The sender will use POST to send the initial
     payment transaction.
     """
     jmprint("The server got this POST request: ")
     print(request)
     print(request.method)
     print(request.uri)
     print(request.args)
     print(request.path)
     print(request.content)
     proposed_tx = request.content
     assert isinstance(proposed_tx, BytesIO)
     payment_psbt_base64 = proposed_tx.read()
     payment_psbt = btc.PartiallySignedTransaction.from_base64(
         payment_psbt_base64)
     all_receiver_utxos = self.wallet_service.get_all_utxos()
     # TODO is there a less verbose way to get any 2 utxos from the dict?
     receiver_utxos_keys = list(all_receiver_utxos.keys())[:2]
     receiver_utxos = {k: v for k, v in all_receiver_utxos.items(
         ) if k in receiver_utxos_keys}
 
     # receiver will do other checks as discussed above, including payment
     # amount; as discussed above, this is out of the scope of this PSBT test.
 
     # construct unsigned tx for payjoin-psbt:
     payjoin_tx_inputs = [(x.prevout.hash[::-1],
                 x.prevout.n) for x in payment_psbt.unsigned_tx.vin]
     payjoin_tx_inputs.extend(receiver_utxos.keys())
     # find payment output and change output
     pay_out = None
     change_out = None
     for o in payment_psbt.unsigned_tx.vout:
         jm_out_fmt = {"value": o.nValue,
         "address": str(btc.CCoinAddress.from_scriptPubKey(
         o.scriptPubKey))}
         if o.nValue == payment_amt:
             assert pay_out is None
             pay_out = jm_out_fmt
         else:
             assert change_out is None
             change_out = jm_out_fmt
 
     # we now know there were two outputs and know which is payment.
     # bump payment output with our input:
     outs = [pay_out, change_out]
     our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()])
     pay_out["value"] += our_inputs_val
     print("we bumped the payment output value by: ", our_inputs_val)
     print("It is now: ", pay_out["value"])
     unsigned_payjoin_tx = btc.make_shuffled_tx(payjoin_tx_inputs, outs,
                                 version=payment_psbt.unsigned_tx.nVersion,
                                 locktime=payment_psbt.unsigned_tx.nLockTime)
     print("we created this unsigned tx: ")
     print(btc.human_readable_transaction(unsigned_payjoin_tx))
     # to create the PSBT we need the spent_outs for each input,
     # in the right order:
     spent_outs = []
     for i, inp in enumerate(unsigned_payjoin_tx.vin):
         input_found = False
         for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin):
             if inp.prevout == inp2.prevout:
                 spent_outs.append(payment_psbt.inputs[j].utxo)
                 input_found = True
                 break
         if input_found:
             continue
         # if we got here this input is ours, we must find
         # it from our original utxo choice list:
         for ru in receiver_utxos.keys():
             if (inp.prevout.hash[::-1], inp.prevout.n) == ru:
                 spent_outs.append(
                     self.wallet_service.witness_utxos_to_psbt_utxos(
                         {ru: receiver_utxos[ru]})[0])
                 input_found = True
                 break
         # there should be no other inputs:
         assert input_found
 
     r_payjoin_psbt = self.wallet_service.create_psbt_from_tx(unsigned_payjoin_tx,
                                                   spent_outs=spent_outs)
     print("Receiver created payjoin PSBT:\n{}".format(
         self.wallet_service.human_readable_psbt(r_payjoin_psbt)))
 
     signresultandpsbt, err = self.wallet_service.sign_psbt(r_payjoin_psbt.serialize(),
                                                 with_sign_result=True)
     assert not err, err
     signresult, receiver_signed_psbt = signresultandpsbt
     assert signresult.num_inputs_final == len(receiver_utxos)
     assert not signresult.is_final
 
     print("Receiver signing successful. Payjoin PSBT is now:\n{}".format(
         self.wallet_service.human_readable_psbt(receiver_signed_psbt)))
     content = receiver_signed_psbt.to_base64()
     request.setHeader(b"content-length", ("%d" % len(content)).encode("ascii"))
     return content.encode("ascii")
def direct_send(wallet_service,
                amount,
                mixdepth,
                destination,
                answeryes=False,
                accept_callback=None,
                info_callback=None,
                error_callback=None,
                return_transaction=False,
                with_final_psbt=False,
                optin_rbf=False,
                custom_change_addr=None):
    """Send coins directly from one mixdepth to one destination address;
    does not need IRC. Sweep as for normal sendpayment (set amount=0).
    If answeryes is True, callback/command line query is not performed.
    If optin_rbf is True, the nSequence values are changed as appropriate.
    If accept_callback is None, command line input for acceptance is assumed,
    else this callback is called:
    accept_callback:
    ====
    args:
    deserialized tx, destination address, amount in satoshis,
    fee in satoshis, custom change address

    returns:
    True if accepted, False if not
    ====
    info_callback and error_callback takes one parameter, the information
    message (when tx is pushed or error occured), and returns nothing.

    This function returns:
    1. False if there is any failure.
    2. The txid if transaction is pushed, and return_transaction is False,
       and with_final_psbt is False.
    3. The full CMutableTransaction if return_transaction is True and
       with_final_psbt is False.
    4. The PSBT object if with_final_psbt is True, and in
       this case the transaction is *NOT* broadcast.
    """
    #Sanity checks
    assert validate_address(destination)[0] or is_burn_destination(destination)
    assert custom_change_addr is None or validate_address(
        custom_change_addr)[0]
    assert amount > 0 or custom_change_addr is None
    assert isinstance(mixdepth, numbers.Integral)
    assert mixdepth >= 0
    assert isinstance(amount, numbers.Integral)
    assert amount >= 0
    assert isinstance(wallet_service.wallet, BaseWallet)

    if is_burn_destination(destination):
        #Additional checks
        if not isinstance(wallet_service.wallet, FidelityBondMixin):
            log.error("Only fidelity bond wallets can burn coins")
            return
        if answeryes:
            log.error(
                "Burning coins not allowed without asking for confirmation")
            return
        if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH:
            log.error("Burning coins only allowed from mixdepth " +
                      str(FidelityBondMixin.FIDELITY_BOND_MIXDEPTH))
            return
        if amount != 0:
            log.error(
                "Only sweeping allowed when burning coins, to keep the tx " +
                "small. Tip: use the coin control feature to freeze utxos")
            return

    txtype = wallet_service.get_txtype()
    if amount == 0:
        #doing a sweep
        utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth]
        if utxos == {}:
            log.error("There are no available utxos in mixdepth: " +
                      str(mixdepth) + ", quitting.")
            return
        total_inputs_val = sum([va['value'] for u, va in utxos.items()])

        if is_burn_destination(destination):
            if len(utxos) > 1:
                log.error(
                    "Only one input allowed when burning coins, to keep " +
                    "the tx small. Tip: use the coin control feature to freeze utxos"
                )
                return
            address_type = FidelityBondMixin.BIP32_BURN_ID
            index = wallet_service.wallet.get_next_unused_index(
                mixdepth, address_type)
            path = wallet_service.wallet.get_path(mixdepth, address_type,
                                                  index)
            privkey, engine = wallet_service.wallet._get_key_from_path(path)
            pubkey = engine.privkey_to_pubkey(privkey)
            pubkeyhash = Hash160(pubkey)

            #size of burn output is slightly different from regular outputs
            burn_script = mk_burn_script(pubkeyhash)
            fee_est = estimate_tx_fee(len(utxos),
                                      0,
                                      txtype=txtype,
                                      extra_bytes=len(burn_script) / 2)

            outs = [{
                "script": burn_script,
                "value": total_inputs_val - fee_est
            }]
            destination = "BURNER OUTPUT embedding pubkey at " \
                + wallet_service.wallet.get_path_repr(path) \
                + "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n"
        else:
            #regular sweep (non-burn)
            fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
            outs = [{
                "address": destination,
                "value": total_inputs_val - fee_est
            }]
    else:
        #not doing a sweep; we will have change
        #8 inputs to be conservative
        initial_fee_est = estimate_tx_fee(8, 2, txtype=txtype)
        utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est)
        if len(utxos) < 8:
            fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype)
        else:
            fee_est = initial_fee_est
        total_inputs_val = sum([va['value'] for u, va in utxos.items()])
        changeval = total_inputs_val - fee_est - amount
        outs = [{"value": amount, "address": destination}]
        change_addr = wallet_service.get_internal_addr(mixdepth) if custom_change_addr is None \
                      else custom_change_addr
        outs.append({"value": changeval, "address": change_addr})

    #compute transaction locktime, has special case for spending timelocked coins
    tx_locktime = compute_tx_locktime()
    if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \
            isinstance(wallet_service.wallet, FidelityBondMixin):
        for outpoint, utxo in utxos.items():
            path = wallet_service.script_to_path(utxo["script"])
            if not FidelityBondMixin.is_timelocked_path(path):
                continue
            path_locktime = path[-1]
            tx_locktime = max(tx_locktime, path_locktime + 1)
            #compute_tx_locktime() gives a locktime in terms of block height
            #timelocked addresses use unix time instead
            #OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we
            #must use unix time as the transaction locktime

    #Now ready to construct transaction
    log.info("Using a fee of: " + amount_to_str(fee_est) + ".")
    if amount != 0:
        log.info("Using a change value of: " + amount_to_str(changeval) + ".")
    tx = make_shuffled_tx(list(utxos.keys()), outs, 2, tx_locktime)

    if optin_rbf:
        for inp in tx.vin:
            inp.nSequence = 0xffffffff - 2

    inscripts = {}
    spent_outs = []
    for i, txinp in enumerate(tx.vin):
        u = (txinp.prevout.hash[::-1], txinp.prevout.n)
        inscripts[i] = (utxos[u]["script"], utxos[u]["value"])
        spent_outs.append(CMutableTxOut(utxos[u]["value"], utxos[u]["script"]))
    if with_final_psbt:
        # here we have the PSBTWalletMixin do the signing stage
        # for us:
        new_psbt = wallet_service.create_psbt_from_tx(tx,
                                                      spent_outs=spent_outs)
        serialized_psbt, err = wallet_service.sign_psbt(new_psbt.serialize())
        if err:
            log.error("Failed to sign PSBT, quitting. Error message: " + err)
            return False
        new_psbt_signed = PartiallySignedTransaction.deserialize(
            serialized_psbt)
        print("Completed PSBT created: ")
        print(wallet_service.human_readable_psbt(new_psbt_signed))
        return new_psbt_signed
    else:
        success, msg = wallet_service.sign_tx(tx, inscripts)
        if not success:
            log.error("Failed to sign transaction, quitting. Error msg: " +
                      msg)
            return
        log.info("Got signed transaction:\n")
        log.info(human_readable_transaction(tx))
        actual_amount = amount if amount != 0 else total_inputs_val - fee_est
        sending_info = "Sends: " + amount_to_str(actual_amount) + \
            " to destination: " + destination
        if custom_change_addr:
            sending_info += ", custom change to: " + custom_change_addr
        log.info(sending_info)
        if not answeryes:
            if not accept_callback:
                if input('Would you like to push to the network? (y/n):'
                         )[0] != 'y':
                    log.info(
                        "You chose not to broadcast the transaction, quitting."
                    )
                    return False
            else:
                accepted = accept_callback(human_readable_transaction(tx),
                                           destination, actual_amount, fee_est,
                                           custom_change_addr)
                if not accepted:
                    return False
        if jm_single().bc_interface.pushtx(tx.serialize()):
            txid = bintohex(tx.GetTxid()[::-1])
            successmsg = "Transaction sent: " + txid
            cb = log.info if not info_callback else info_callback
            cb(successmsg)
            txinfo = txid if not return_transaction else tx
            return txinfo
        else:
            errormsg = "Transaction broadcast failed!"
            cb = log.error if not error_callback else error_callback
            cb(errormsg)
            return False
    def on_tx_received(self, nick, tx_from_taker, offerinfo):
        """Called when the counterparty has sent an unsigned
        transaction. Sigs are created and returned if and only
        if the transaction passes verification checks (see
        verify_unsigned_tx()).
        """
        # special case due to cjfee passed as string: it can accidentally parse
        # as hex:
        if not isinstance(offerinfo["offer"]["cjfee"], str):
            offerinfo["offer"]["cjfee"] = bintohex(offerinfo["offer"]["cjfee"])
        try:
            tx = btc.CMutableTransaction.deserialize(tx_from_taker)
        except Exception as e:
            return (False, 'malformed txhex. ' + repr(e))
        # if the above deserialization was successful, the human readable
        # parsing will be also:
        jlog.info('obtained tx\n' + btc.human_readable_transaction(tx))
        goodtx, errmsg = self.verify_unsigned_tx(tx, offerinfo)
        if not goodtx:
            jlog.info('not a good tx, reason=' + errmsg)
            return (False, errmsg)
        jlog.info('goodtx')
        sigs = []
        utxos = offerinfo["utxos"]

        our_inputs = {}
        for index, ins in enumerate(tx.vin):
            utxo = (ins.prevout.hash[::-1], ins.prevout.n)
            if utxo not in utxos:
                continue
            script = self.wallet_service.addr_to_script(utxos[utxo]['address'])
            amount = utxos[utxo]['value']
            our_inputs[index] = (script, amount)

        success, msg = self.wallet_service.sign_tx(tx, our_inputs)
        assert success, msg
        for index in our_inputs:
            sigmsg = tx.vin[index].scriptSig
            if tx.has_witness():
                # Note that this flag only implies that the transaction
                # *as a whole* is using segwit serialization; it doesn't
                # imply that this specific input is segwit type (to be
                # fully general, we allow that even our own wallet's
                # inputs might be of mixed type). So, we catch the EngineError
                # which is thrown by non-segwit types. This way the sigmsg
                # will only contain the scriptCode field if the wallet object
                # decides it's necessary/appropriate for this specific input
                # If it is segwit, we prepend the witness data since we want
                # (sig, pub, witnessprogram=scriptSig - note we could, better,
                # pass scriptCode here, but that is not backwards compatible,
                # as the taker uses this third field and inserts it into the
                # transaction scriptSig), else (non-sw) the !sig message remains
                # unchanged as (sig, pub).
                try:
                    sig, pub = [
                        a for a in iter(tx.wit.vtxinwit[index].scriptWitness)
                    ]
                    scriptCode = btc.pubkey_to_p2wpkh_script(pub)
                    sigmsg = btc.CScript([sig]) + btc.CScript(pub) + scriptCode
                except Exception as e:
                    #the sigmsg was already set before the segwit check
                    pass
            sigs.append(base64.b64encode(sigmsg).decode('ascii'))
        return (True, sigs)
Exemple #18
0
    def request_to_psbt(self, payment_psbt_base64, sender_parameters):
        """ Takes a payment psbt from a sender and their url parameters,
        and returns a new payment PSBT proposal, assuming all conditions
        are met.
        Returns:
        (False, errormsg, errortype) in case of failure.
        or:
        (True, base64_payjoin_psbt) in case of success.
        """
        # we only support version 1; reject others:
        if not self.pj_version == int(sender_parameters[b'v'][0]):
            return (False, "This version of payjoin is not supported. ",
                    "version-unsupported")
        try:
            payment_psbt = btc.PartiallySignedTransaction.from_base64(
                payment_psbt_base64)
        except:
            return (False, "invalid psbt format", "original-psbt-rejected")

        try:
            self.manager.set_payment_tx_and_psbt(payment_psbt)
        except Exception:
            # note that Assert errors, Value errors and CheckTransaction errors
            # are all possible, so we catch all exceptions to avoid a crash.
            return (False,
                    "Proposed initial PSBT does not pass sanity checks.",
                    "original-psbt-rejected")

        # if the sender set the additionalfeeoutputindex and maxadditionalfeecontribution
        # settings, pass them to the PayJoin manager:
        try:
            if b"additionalfeeoutputindex" in sender_parameters:
                afoi = int(sender_parameters[b"additionalfeeoutputindex"][0])
            else:
                afoi = None
            if b"maxadditionalfeecontribution" in sender_parameters:
                mafc = int(
                    sender_parameters[b"maxadditionalfeecontribution"][0])
            else:
                mafc = None
            if b"minfeerate" in sender_parameters:
                minfeerate = float(sender_parameters[b"minfeerate"][0])
            else:
                minfeerate = None
        except Exception as e:
            return (False, "Invalid request parameters.",
                    "original-psbt-rejected")

        # if sender chose a fee output it must be the change output,
        # and the mafc will be applied to that. Any more complex transaction
        # structure is not supported.
        # If they did not choose a fee output index, we must rely on the feerate
        # reduction being not too much, which is checked against minfeerate; if
        # it is too big a reduction, again we fail payjoin.
        if (afoi is not None and mafc is None) or (mafc is not None
                                                   and afoi is None):
            return (False, "Invalid request parameters.",
                    "original-psbt-rejected")

        if afoi and not (self.manager.change_out_index == afoi):
            return (False, "additionalfeeoutputindex is "
                    "not the change output. Joinmarket does "
                    "not currently support this.", "original-psbt-rejected")

        # while we do not need to defend against probing attacks,
        # it is still safer to at least verify the validity of the signatures
        # at this stage, to ensure no misbehaviour with using inputs
        # that are not signed correctly:
        res = jm_single().bc_interface.testmempoolaccept(
            bintohex(self.manager.payment_tx.serialize()))
        if not res[0]["allowed"]:
            return (False, "Proposed transaction was "
                    "rejected from mempool.", "original-psbt-rejected")

        # Now that the PSBT is accepted, we schedule fallback in case anything
        # fails later on in negotiation (as specified in BIP78):
        self.manager.timeout_fallback_dc = reactor.callLater(
            60, fallback_nonpayjoin_broadcast, b"timeout", self.manager)

        receiver_utxos = self.manager.select_receiver_utxos()
        if not receiver_utxos:
            return (False, "Could not select coins for payjoin", "unavailable")

        # construct unsigned tx for payjoin-psbt:
        payjoin_tx_inputs = [(x.prevout.hash[::-1], x.prevout.n)
                             for x in payment_psbt.unsigned_tx.vin]
        # See https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#Protocol
        random_insert(payjoin_tx_inputs, receiver_utxos.keys())

        pay_out = {
            "value":
            self.manager.pay_out.nValue,
            "address":
            str(
                btc.CCoinAddress.from_scriptPubKey(
                    self.manager.pay_out.scriptPubKey))
        }
        if self.manager.change_out:
            change_out = {
                "value":
                self.manager.change_out.nValue,
                "address":
                str(
                    btc.CCoinAddress.from_scriptPubKey(
                        self.manager.change_out.scriptPubKey))
            }

        # we now know there were one/two outputs and know which is payment.
        # set the ordering of the outputs correctly.
        if change_out:
            # indices of original payment were set in JMPayjoinManager
            # sanity check:
            if self.manager.change_out_index == 0 and \
               self.manager.pay_out_index == 1:
                outs = [change_out, pay_out]
            elif self.manager.change_out_index == 1 and \
                 self.manager.pay_out_index == 0:
                outs = [pay_out, change_out]
            else:
                assert False, "More than 2 outputs is not supported."
        else:
            outs = [pay_out]
        # bump payment output with our input:
        our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()])
        pay_out["value"] += our_inputs_val
        log.debug("We bumped the payment output value by: " +
                  str(our_inputs_val) + " sats.")
        log.debug("It is now: " + str(pay_out["value"]) + " sats.")

        # if the sender allowed a fee bump, we can apply it to the change output
        # now (we already checked it's the right index).
        # A note about checking `minfeerate`: it is impossible for the receiver
        # to be 100% certain on the size of the final transaction, since he does
        # not see in advance the (slightly) variable sizes of the sender's final
        # signatures; hence we do not attempt more than an estimate of the final
        # signed transaction's size and hence feerate. Very small inaccuracies
        # (< 1% typically) are possible, therefore.
        #
        # First, let's check that the user's requested minfeerate is not higher
        # than the feerate they already chose:
        if minfeerate and minfeerate > self.manager.get_payment_psbt_feerate():
            return (False, "Bad request: minfeerate "
                    "bigger than original psbt feerate.",
                    "original-psbt-rejected")
        # set the intended virtual size of our input:
        vsize = self.manager.get_vsize_for_input()
        our_fee_bump = 0
        if afoi:
            # We plan to reduce the change_out by a fee contribution.
            # Calculate the additional fee we think we need for our input,
            # to keep the same feerate as the original transaction (this also
            # accounts for rounding as per the BIP).
            # If it is more than mafc, then bump by mafc, else bump by the
            # calculated amount.
            # This should not meaningfully change the feerate.
            our_fee_bump = int(self.manager.get_payment_psbt_feerate() * vsize)
            if our_fee_bump > mafc:
                our_fee_bump = mafc

        elif minfeerate:
            # In this case the change_out will remain unchanged.
            # the user has not allowed a fee bump; calculate the new fee
            # rate; if it is lower than the limit, give up.
            expected_new_tx_size = self.manager.initial_psbt.extract_transaction(
            ).get_virtual_size() + vsize
            expected_new_fee_rate = self.manager.initial_psbt.get_fee() / (
                expected_new_tx_size + vsize)
            if expected_new_fee_rate < minfeerate:
                return (False, "Bad request: we cannot "
                        "achieve minfeerate requested.",
                        "original-psbt-rejected")

        # Having checked the sender's conditions, we can apply the fee bump
        # intended:
        outs[self.manager.change_out_index]["value"] -= our_fee_bump

        unsigned_payjoin_tx = btc.mktx(
            payjoin_tx_inputs,
            outs,
            version=payment_psbt.unsigned_tx.nVersion,
            locktime=payment_psbt.unsigned_tx.nLockTime)

        # to create the PSBT we need the spent_outs for each input,
        # in the right order:
        spent_outs = []
        for i, inp in enumerate(unsigned_payjoin_tx.vin):
            input_found = False
            for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin):
                if inp.prevout == inp2.prevout:
                    # this belongs to sender.
                    # respect sender's sequence number choice, even
                    # if they were not uniform:
                    inp.nSequence = inp2.nSequence
                    spent_outs.append(payment_psbt.inputs[j].utxo)
                    input_found = True
                    sender_index = i
                    break
            if input_found:
                continue
            # if we got here this input is ours, we must find
            # it from our original utxo choice list:
            for ru in receiver_utxos.keys():
                if (inp.prevout.hash[::-1], inp.prevout.n) == ru:
                    spent_outs.append(
                        self.wallet_service.witness_utxos_to_psbt_utxos(
                            {ru: receiver_utxos[ru]})[0])
                    input_found = True
                    break
            # there should be no other inputs:
            assert input_found

        # respect the sender's fixed sequence number, if it was used (we checked
        # in the initial sanity check)
        if self.manager.fixed_sequence_number:
            for inp in unsigned_payjoin_tx.vin:
                inp.nSequence = self.manager.fixed_sequence_number

        log.debug("We created this unsigned tx: ")
        log.debug(btc.human_readable_transaction(unsigned_payjoin_tx))

        r_payjoin_psbt = self.wallet_service.create_psbt_from_tx(
            unsigned_payjoin_tx, spent_outs=spent_outs)
        log.debug("Receiver created payjoin PSBT:\n{}".format(
            self.wallet_service.human_readable_psbt(r_payjoin_psbt)))

        signresultandpsbt, err = self.wallet_service.sign_psbt(
            r_payjoin_psbt.serialize(), with_sign_result=True)
        assert not err, err
        signresult, receiver_signed_psbt = signresultandpsbt
        assert signresult.num_inputs_final == len(receiver_utxos)
        assert not signresult.is_final

        # with signing succcessful, remove the utxo field from the
        # counterparty's input (this is required by BIP78). Note we don't
        # do this on PSBT creation as the psbt signing code throws ValueError
        # unless utxos are present.
        receiver_signed_psbt.inputs[sender_index] = btc.PSBT_Input(
            index=sender_index)
        log.debug(
            "Receiver signing successful. Payjoin PSBT is now:\n{}".format(
                self.wallet_service.human_readable_psbt(receiver_signed_psbt)))
        # construct txoutset for the wallet service callback; we cannot use
        # txid as we don't have all signatures (TODO: ? but segwit only? even so,
        # works anyway).
        txinfo = tuple((x.scriptPubKey, x.nValue)
                       for x in receiver_signed_psbt.unsigned_tx.vout)
        self.wallet_service.register_callbacks([self.end_receipt],
                                               txinfo=txinfo,
                                               cb_type="unconfirmed")
        return (True, receiver_signed_psbt.to_base64(), None)
 def log_successful_tx(self, tx):
     """ TODO: add dedicated SNICKER log file.
     """
     self.successful_txs.append(tx)
     jlog.info(btc.human_readable_transaction(tx))
Exemple #20
0
def main():
    parser = OptionParser(
        usage=
        'usage: %prog [options] utxo destaddr1 destaddr2 ..',
        description="For creating multiple utxos from one (for commitments in JM)."
                    "Provide a utxo in form txid:N that has some unspent coins;"
                    "Specify a list of destination addresses and the coins will"
                    "be split equally between them (after bitcoin fees)."

                    "You'll be prompted to enter the private key for the utxo"
                    "during the run; it must be in WIF compressed format."
                    "After the transaction is completed, the utxo strings for"

                    "the new outputs will be shown."
                    "Note that these utxos will not be ready for use as external"

                    "commitments in Joinmarket until 5 confirmations have passed."
                    " BE CAREFUL about handling private keys!"
                    " Don't do this in insecure environments."
                    " Works only with p2pkh ('1') or p2sh-p2wpkh (segwit '3')"
                    " utxos - set segwit=False in the POLICY section of"
                    " joinmarket.cfg for the former."
    )
    parser.add_option(
        '-t',
        '--utxo-address-type',
        action='store',
        dest='utxo_address_type',
        help=('type of address of coin being spent - one of "p2pkh", "p2wpkh", "p2sh-p2wpkh". '
        'No other scriptpubkey types (e.g. multisig) are supported. If not set, we default '
        'to what is in joinmarket.cfg.'),
        default=""
    )
    add_base_options(parser)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    if len(args) < 2:
        quit(parser, 'Invalid syntax')
    u = args[0]
    priv = input(
        'input private key for ' + u + ', in WIF compressed format : ')
    u, priv = get_utxo_info(','.join([u, priv]))
    if not u:
        quit(parser, "Failed to parse utxo info: " + u)
    destaddrs = args[1:]
    for d in destaddrs:
        if not validate_address(d):
            quit(parser, "Address was not valid; wrong network?: " + d)
    success, utxo = utxostr_to_utxo(u)
    if not success:
        quit(parser, "Failed to load utxo from string: " + utxo)
    if options.utxo_address_type == "":
        if jm_single().config.get("POLICY", "segwit") == "false":
            utxo_address_type = "p2pkh"
        elif jm_single().config.get("POLICY", "native") == "false":
            utxo_address_type = "p2sh-p2wpkh"
        else:
            utxo_address_type = "p2wpkh"
    else:
        utxo_address_type = options.utxo_address_type
    txsigned = sign(utxo, priv, destaddrs, utxo_address_type)
    if not txsigned:
        log.info("Transaction signing operation failed, see debug messages for details.")
        return
    log.info("Got signed transaction:\n" + bintohex(txsigned.serialize()))
    log.info(btc.human_readable_transaction(txsigned))
    if input('Would you like to push to the network? (y/n):')[0] != 'y':
        log.info("You chose not to broadcast the transaction, quitting.")
        return
    jm_single().bc_interface.pushtx(txsigned.serialize())
Exemple #21
0
def test_snicker_e2e(setup_snicker, nw, wallet_structures, mean_amt, sdev_amt,
                     amt, net_transfer):
    """ Test strategy:
    1. create two wallets.
    2. with wallet 1 (Receiver), create a single transaction
    tx1, from mixdepth 0 to 1.
    3. with wallet 2 (Proposer), take pubkey of all inputs from tx1, and use
    them to create snicker proposals to the non-change out of tx1,
    in base64 and place in proposals.txt.
    4. Receiver polls for proposals in the file manually (instead of twisted
    LoopingCall) and processes them.
    5. Check for valid final transaction with broadcast.
    """

    # TODO: Make this test work with native segwit wallets
    wallets = make_wallets(nw,
                           wallet_structures,
                           mean_amt,
                           sdev_amt,
                           wallet_cls=SegwitLegacyWallet)
    for w in wallets.values():
        w['wallet'].sync_wallet(fast=True)
    print(wallets)
    wallet_r = wallets[0]['wallet']
    wallet_p = wallets[1]['wallet']
    # next, create a tx from the receiver wallet
    our_destn_script = wallet_r.get_new_script(
        1, BaseWallet.ADDRESS_TYPE_INTERNAL)
    tx = direct_send(wallet_r,
                     btc.coins_to_satoshi(0.3),
                     0,
                     wallet_r.script_to_addr(our_destn_script),
                     accept_callback=dummy_accept_callback,
                     info_callback=dummy_info_callback,
                     return_transaction=True)

    assert tx, "Failed to spend from receiver wallet"
    print("Parent transaction OK. It was: ")
    print(btc.human_readable_transaction(tx))
    wallet_r.process_new_tx(tx)
    # we must identify the receiver's output we're going to use;
    # it can be destination or change, that's up to the proposer
    # to guess successfully; here we'll just choose index 0.
    txid1 = tx.GetTxid()[::-1]
    txid1_index = 0

    receiver_start_bal = sum(
        [x['value'] for x in wallet_r.get_all_utxos().values()])

    # Now create a proposal for every input index in tx1
    # (version 1 proposals mean we source keys from the/an
    # ancestor transaction)
    propose_keys = []
    for i in range(len(tx.vin)):
        # todo check access to pubkey
        sig, pub = [a for a in iter(tx.wit.vtxinwit[i].scriptWitness)]
        propose_keys.append(pub)
    # the proposer wallet needs to choose a single
    # utxo that is bigger than the output amount of tx1
    prop_m_utxos = wallet_p.get_utxos_by_mixdepth()[0]
    prop_utxo = prop_m_utxos[list(prop_m_utxos)[0]]
    # get the private key for that utxo
    priv = wallet_p.get_key_from_addr(
        wallet_p.script_to_addr(prop_utxo['script']))
    prop_input_amt = prop_utxo['value']
    # construct the arguments for the snicker proposal:
    our_input = list(prop_m_utxos)[0]  # should be (txid, index)
    their_input = (txid1, txid1_index)
    our_input_utxo = btc.CMutableTxOut(prop_utxo['value'], prop_utxo['script'])
    fee_est = estimate_tx_fee(len(tx.vin), 2)
    change_spk = wallet_p.get_new_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL)

    encrypted_proposals = []

    for p in propose_keys:
        # TODO: this can be a loop over all outputs,
        # not just one guessed output, if desired.
        encrypted_proposals.append(
            wallet_p.create_snicker_proposal(our_input,
                                             their_input,
                                             our_input_utxo,
                                             tx.vout[txid1_index],
                                             net_transfer,
                                             fee_est,
                                             priv,
                                             p,
                                             prop_utxo['script'],
                                             change_spk,
                                             version_byte=1) + b"," +
            bintohex(p).encode('utf-8'))
    with open(TEST_PROPOSALS_FILE, "wb") as f:
        f.write(b"\n".join(encrypted_proposals))
    sR = SNICKERReceiver(wallet_r)
    sR.proposals_source = TEST_PROPOSALS_FILE  # avoid clashing with mainnet
    sR.poll_for_proposals()
    assert len(sR.successful_txs) == 1
    wallet_r.process_new_tx(sR.successful_txs[0])
    end_utxos = wallet_r.get_all_utxos()
    print("At end the receiver has these utxos: ", end_utxos)
    receiver_end_bal = sum([x['value'] for x in end_utxos.values()])
    assert receiver_end_bal == receiver_start_bal + net_transfer
    def process_proposals(self, proposals):
        """ This is the "meat" of the SNICKERReceiver service.
        It parses proposals and creates and broadcasts transactions
        with the wallet, assuming all conditions are met.
        Note that this is ONLY called from the proposals poll loop.

        Each entry in `proposals` is of form:
        encrypted_proposal - base64 string
        key - hex encoded compressed pubkey, or ''
        if the key is not null, we attempt to decrypt and
        process according to that key, else cycles over all keys.

        If all SNICKER validations succeed, the decision to spend is
        entirely dependent on self.acceptance_callback.
        If the callback returns True, we co-sign and broadcast the
        transaction and also update the wallet with the new
        imported key (TODO: future versions will enable searching
        for these keys using history + HD tree; note the jmbitcoin
        snicker.py module DOES insist on ECDH being correctly used,
        so this will always be possible for transactions created here.

        Returned is a list of txids of any transactions which
        were broadcast, unless a critical error occurs, in which case
        False is returned (to minimize this function's trust in other
        parts of the code being executed, if something appears to be
        inconsistent, we trigger immediate halt with this return).
        """

        for kp in proposals:
            # handle empty list entries:
            if not kp:
                continue
            try:
                p, k = kp.split(',')
            except:
                # could argue for info or warning debug level,
                # but potential for a lot of unwanted output.
                jlog.debug("Invalid proposal string, ignoring: " + kp)
                continue
            if k is not None:
                # note that this operation will succeed as long as
                # the key is in the wallet._script_map, which will
                # be true if the key is at an HD index lower than
                # the current wallet.index_cache
                k = hextobin(k)
                addr = self.wallet_service.pubkey_to_addr(k)
                if not self.wallet_service.is_known_addr(addr):
                    jlog.debug("Key not recognized as part of our "
                               "wallet, ignoring.")
                    continue
                result = self.wallet_service.parse_proposal_to_signed_tx(
                    addr, p, self.acceptance_callback)
                if result[0] is not None:
                    tx, tweak, out_spk = result
                    # We will: rederive the key as a sanity check,
                    # and see if it matches the claimed spk.
                    # Then, we import the key into the wallet
                    # (even though it's re-derivable from history, this
                    # is the easiest for a first implementation).
                    # Finally, we co-sign, then push.
                    # (Again, simplest function: checks already passed,
                    # so do it automatically).
                    tweaked_key = btc.snicker_pubkey_tweak(k, tweak)
                    tweaked_spk = self.wallet_service.pubkey_to_script(
                        tweaked_key)
                    # Derive original path to make sure we change
                    # mixdepth:
                    source_path = self.wallet_service.script_to_path(
                        self.wallet_service.pubkey_to_script(k))
                    # NB This will give the correct source mixdepth independent
                    # of whether the key is imported or not:
                    source_mixdepth = self.wallet_service.get_details(
                        source_path)[0]
                    if not tweaked_spk == out_spk:
                        jlog.error("The spk derived from the pubkey does "
                                   "not match the scriptPubkey returned from "
                                   "the snicker module - code error.")
                        return False
                    # before import, we should derive the tweaked *private* key
                    # from the tweak, also; failure of this critical sanity check
                    # is a code error. If the recreated private key matches, we
                    # import to the wallet. Note that this happens *before* pushing
                    # the coinjoin transaction to the network, which is advisably
                    # conservative (never possible to have broadcast a tx without
                    # having already stored the output's key).
                    success, msg = self.wallet_service.check_tweak_matches_and_import(
                        addr, tweak, tweaked_key, source_mixdepth)
                    if not success:
                        jlog.error(msg)
                        return False

                    # TODO condition on automatic brdcst or not
                    if not jm_single().bc_interface.pushtx(tx.serialize()):
                        # this represents an error about state (or conceivably,
                        # an ultra-short window in which the spent utxo was
                        # consumed in another transaction), but not really
                        # an internal logic error, so we do NOT return False
                        jlog.error("Failed to broadcast SNICKER coinjoin: " +\
                                   bintohex(tx.GetTxid()[::-1]))
                        jlog.info(btc.human_readable_transaction(tx))
                    jlog.info("Successfully broadcast SNICKER coinjoin: " +\
                                  bintohex(tx.GetTxid()[::-1]))
                    self.log_successful_tx(tx)
                else:
                    jlog.debug('Failed to parse proposal: ' + result[1])
            else:
                # Some extra work to implement checking all possible
                # keys.
                jlog.info("Proposal without pubkey was not processed.")

        # Completed processing all proposals without any logic
        # errors (whether the proposals were valid or accepted
        # or not).
        return True
def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
                          wallet_cls_receiver):
    """ Workflow step 1:
    Create a payment from a wallet, and create a finalized PSBT.
    This step is fairly trivial as the functionality is built-in to
    PSBTWalletMixin.
    Note that only Segwit* wallets are supported for PayJoin.

        Workflow step 2:
    Receiver creates a new partially signed PSBT with the same amount
    and at least one more utxo.

        Workflow step 3:
    Given a partially signed PSBT created by a receiver, here the sender
    completes (co-signs) the PSBT they are given. Note this code is a PSBT
    functionality check, and does NOT include the detailed checks that
    the sender should perform before agreeing to sign (see:
    https://github.com/btcpayserver/btcpayserver-doc/blob/eaac676866a4d871eda5fd7752b91b88fdf849ff/Payjoin-spec.md#receiver-side
    ).
    """

    wallet_r = make_wallets(1, [[3, 0, 0, 0, 0]],
                            1,
                            wallet_cls=wallet_cls_receiver)[0]["wallet"]
    wallet_s = make_wallets(1, [[3, 0, 0, 0, 0]],
                            1,
                            wallet_cls=wallet_cls_sender)[0]["wallet"]
    for w in [wallet_r, wallet_s]:
        w.sync_wallet(fast=True)

    # destination address for payment:
    destaddr = str(
        bitcoin.CCoinAddress.from_scriptPubKey(
            bitcoin.pubkey_to_p2wpkh_script(
                bitcoin.privkey_to_pubkey(b"\x01" * 33))))

    payment_amt = bitcoin.coins_to_satoshi(payment_amt)

    # *** STEP 1 ***
    # **************

    # create a normal tx from the sender wallet:
    payment_psbt = direct_send(wallet_s,
                               payment_amt,
                               0,
                               destaddr,
                               accept_callback=dummy_accept_callback,
                               info_callback=dummy_info_callback,
                               with_final_psbt=True)

    print("Initial payment PSBT created:\n{}".format(
        wallet_s.human_readable_psbt(payment_psbt)))
    # ensure that the payemnt amount is what was intended:
    out_amts = [x.nValue for x in payment_psbt.unsigned_tx.vout]
    # NOTE this would have to change for more than 2 outputs:
    assert any([out_amts[i] == payment_amt for i in [0, 1]])

    # ensure that we can actually broadcast the created tx:
    # (note that 'extract_transaction' represents an implicit
    # PSBT finality check).
    extracted_tx = payment_psbt.extract_transaction().serialize()
    # don't want to push the tx right now, because of test structure
    # (in production code this isn't really needed, we will not
    # produce invalid payment transactions).
    res = jm_single().bc_interface.testmempoolaccept(bintohex(extracted_tx))
    assert res[0]["allowed"], "Payment transaction was rejected from mempool."

    # *** STEP 2 ***
    # **************

    # Simple receiver utxo choice heuristic.
    # For more generality we test with two receiver-utxos, not one.
    all_receiver_utxos = wallet_r.get_all_utxos()
    # TODO is there a less verbose way to get any 2 utxos from the dict?
    receiver_utxos_keys = list(all_receiver_utxos.keys())[:2]
    receiver_utxos = {
        k: v
        for k, v in all_receiver_utxos.items() if k in receiver_utxos_keys
    }

    # receiver will do other checks as discussed above, including payment
    # amount; as discussed above, this is out of the scope of this PSBT test.

    # construct unsigned tx for payjoin-psbt:
    payjoin_tx_inputs = [(x.prevout.hash[::-1], x.prevout.n)
                         for x in payment_psbt.unsigned_tx.vin]
    payjoin_tx_inputs.extend(receiver_utxos.keys())
    # find payment output and change output
    pay_out = None
    change_out = None
    for o in payment_psbt.unsigned_tx.vout:
        jm_out_fmt = {
            "value": o.nValue,
            "address":
            str(bitcoin.CCoinAddress.from_scriptPubKey(o.scriptPubKey))
        }
        if o.nValue == payment_amt:
            assert pay_out is None
            pay_out = jm_out_fmt
        else:
            assert change_out is None
            change_out = jm_out_fmt

    # we now know there were two outputs and know which is payment.
    # bump payment output with our input:
    outs = [pay_out, change_out]
    our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()])
    pay_out["value"] += our_inputs_val
    print("we bumped the payment output value by: ", our_inputs_val)
    print("It is now: ", pay_out["value"])
    unsigned_payjoin_tx = bitcoin.make_shuffled_tx(
        payjoin_tx_inputs,
        outs,
        version=payment_psbt.unsigned_tx.nVersion,
        locktime=payment_psbt.unsigned_tx.nLockTime)
    print("we created this unsigned tx: ")
    print(bitcoin.human_readable_transaction(unsigned_payjoin_tx))
    # to create the PSBT we need the spent_outs for each input,
    # in the right order:
    spent_outs = []
    for i, inp in enumerate(unsigned_payjoin_tx.vin):
        input_found = False
        for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin):
            if inp.prevout == inp2.prevout:
                spent_outs.append(payment_psbt.inputs[j].utxo)
                input_found = True
                break
        if input_found:
            continue
        # if we got here this input is ours, we must find
        # it from our original utxo choice list:
        for ru in receiver_utxos.keys():
            if (inp.prevout.hash[::-1], inp.prevout.n) == ru:
                spent_outs.append(
                    wallet_r.witness_utxos_to_psbt_utxos(
                        {ru: receiver_utxos[ru]})[0])
                input_found = True
                break
        # there should be no other inputs:
        assert input_found

    r_payjoin_psbt = wallet_r.create_psbt_from_tx(unsigned_payjoin_tx,
                                                  spent_outs=spent_outs)
    print("Receiver created payjoin PSBT:\n{}".format(
        wallet_r.human_readable_psbt(r_payjoin_psbt)))

    signresultandpsbt, err = wallet_r.sign_psbt(r_payjoin_psbt.serialize(),
                                                with_sign_result=True)
    assert not err, err
    signresult, receiver_signed_psbt = signresultandpsbt
    assert signresult.num_inputs_final == len(receiver_utxos)
    assert not signresult.is_final

    print("Receiver signing successful. Payjoin PSBT is now:\n{}".format(
        wallet_r.human_readable_psbt(receiver_signed_psbt)))

    # *** STEP 3 ***
    # **************

    # take the half-signed PSBT, validate and co-sign:

    signresultandpsbt, err = wallet_s.sign_psbt(
        receiver_signed_psbt.serialize(), with_sign_result=True)
    assert not err, err
    signresult, sender_signed_psbt = signresultandpsbt
    print("Sender's final signed PSBT is:\n{}".format(
        wallet_s.human_readable_psbt(sender_signed_psbt)))
    assert signresult.is_final

    # broadcast the tx
    extracted_tx = sender_signed_psbt.extract_transaction().serialize()
    assert jm_single().bc_interface.pushtx(extracted_tx)
def main():
    parser = OptionParser(usage='usage: %prog [options] walletname',
                          description=description)
    add_base_options(parser)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      help='mixdepth/account, default 0',
                      default=0)
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type='int',
                      dest='gaplimit',
                      default=6,
                      help='gap limit for Joinmarket wallet, default 6.')
    parser.add_option(
        '-f',
        '--txfee',
        action='store',
        type='int',
        dest='txfee',
        default=-1,
        help='Bitcoin miner tx_fee to use for transaction(s). A number higher '
        'than 1000 is used as "satoshi per KB" tx fee. A number lower than that '
        'uses the dynamic fee estimation of your blockchain provider as '
        'confirmation target. This temporarily overrides the "tx_fees" setting '
        'in your joinmarket.cfg. Works the same way as described in it. Check '
        'it for examples.')
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option(
        '-N',
        '--net-transfer',
        action='store',
        type='int',
        dest='net_transfer',
        help='how many sats are sent to the "receiver", default randomised.',
        default=-1000001)
    (options, args) = parser.parse_args()
    snicker_plugin = JMPluginService("SNICKER")
    load_program_config(config_path=options.datadir,
                        plugin_services=[snicker_plugin])
    if len(args) != 1:
        log.error("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    check_regtest()
    # If tx_fees are set manually by CLI argument, override joinmarket.cfg:
    if int(options.txfee) > 0:
        jm_single().config.set("POLICY", "tx_fees", str(options.txfee))
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)
    if wallet_service.rpc_error:
        sys.exit(EXIT_FAILURE)
    snicker_plugin.start_plugin_logging(wallet_service)
    # in this script, we need the wallet synced before
    # logic processing for some paths, so do it now:
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    # the sync call here will now be a no-op:
    wallet_service.startService()
    fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype())

    # first, order the utxos in the mixepth by size. Then (this is the
    # simplest algorithm; we could be more sophisticated), choose the
    # *second* largest utxo as the receiver utxo; this ensures that we
    # have enough for the proposer to cover. We consume utxos greedily,
    # meaning we'll at least some of the time, be consolidating.
    utxo_dict = wallet_service.get_utxos_by_mixdepth()[options.mixdepth]
    if not len(utxo_dict) >= 2:
        log.error(
            "Cannot create fake SNICKER tx without at least two utxos, quitting"
        )
        sys.exit(EXIT_ARGERROR)
    # sort utxos by size
    sorted_utxos = sorted(list(utxo_dict.keys()),
                          key=lambda k: utxo_dict[k]['value'],
                          reverse=True)
    # receiver is the second largest:
    receiver_utxo = sorted_utxos[1]
    receiver_utxo_val = utxo_dict[receiver_utxo]
    # gather the other utxos into a list to select from:
    nonreceiver_utxos = [sorted_utxos[0]] + sorted_utxos[2:]
    # get the net transfer in our fake coinjoin:
    if options.net_transfer < -1000001:
        log.error("Net transfer must be greater than negative 1M sats")
        sys.exit(EXIT_ARGERROR)
    if options.net_transfer == -1000001:
        # default; low-ish is more realistic and avoids problems
        # with dusty utxos
        options.net_transfer = random.randint(-1000, 1000)

    # select enough to cover: receiver value + fee + transfer + breathing room
    # we select relatively greedily to support consolidation, since
    # this transaction does not pretend to isolate the coins.
    try:
        available = [{
            'utxo': utxo,
            'value': utxo_dict[utxo]["value"]
        } for utxo in nonreceiver_utxos]
        # selection algos return [{"utxo":..,"value":..}]:
        prop_utxos = {
            x["utxo"]
            for x in select_greedy(
                available, receiver_utxo_val["value"] + fee_est +
                options.net_transfer + 1000)
        }
        prop_utxos = list(prop_utxos)
        prop_utxo_vals = [utxo_dict[prop_utxo] for prop_utxo in prop_utxos]
    except NotEnoughFundsException as e:
        log.error(repr(e))
        sys.exit(EXIT_FAILURE)

    # Due to the fake nature of this transaction, and its distinguishability
    # (not only in trivial output pattern, but also in subset-sum), there
    # is little advantage in making it use different output mixdepths, so
    # here to prevent fragmentation, everything is kept in the same mixdepth.
    receiver_addr, proposer_addr, change_addr = (wallet_service.script_to_addr(
        wallet_service.get_new_script(options.mixdepth, 1)) for _ in range(3))
    # persist index update:
    wallet_service.save_wallet()
    outputs = btc.construct_snicker_outputs(
        sum([x["value"] for x in prop_utxo_vals]), receiver_utxo_val["value"],
        receiver_addr, proposer_addr, change_addr, fee_est,
        options.net_transfer)
    tx = btc.make_shuffled_tx(prop_utxos + [receiver_utxo],
                              outputs,
                              version=2,
                              locktime=0)
    # before signing, check we satisfied the criteria, otherwise
    # this is pointless!
    if not btc.is_snicker_tx(tx):
        log.error("Code error, created non-SNICKER tx, not signing.")
        sys.exit(EXIT_FAILURE)

    # sign all inputs
    # scripts: {input_index: (output_script, amount)}
    our_inputs = {}
    for index, ins in enumerate(tx.vin):
        utxo = (ins.prevout.hash[::-1], ins.prevout.n)
        script = utxo_dict[utxo]['script']
        amount = utxo_dict[utxo]['value']
        our_inputs[index] = (script, amount)
    success, msg = wallet_service.sign_tx(tx, our_inputs)
    if not success:
        log.error("Failed to sign transaction: " + msg)
        sys.exit(EXIT_FAILURE)
    # TODO condition on automatic brdcst or not
    if not jm_single().bc_interface.pushtx(tx.serialize()):
        # this represents an error about state (or conceivably,
        # an ultra-short window in which the spent utxo was
        # consumed in another transaction), but not really
        # an internal logic error, so we do NOT return False
        log.error("Failed to broadcast fake SNICKER coinjoin: " +\
                   bintohex(tx.GetTxid()[::-1]))
        log.info(btc.human_readable_transaction(tx))
        sys.exit(EXIT_FAILURE)
    log.info("Successfully broadcast fake SNICKER coinjoin: " +\
              bintohex(tx.GetTxid()[::-1]))
Exemple #25
0
def main():
    parser = OptionParser(usage='usage: %prog [options] walletname',
                          description=description)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option('-g',
                      '--gap-limit',
                      type="int",
                      action='store',
                      dest='gaplimit',
                      help='gap limit for wallet, default=6',
                      default=6)
    add_base_options(parser)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    check_regtest()
    if len(args) != 1:
        log.error("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)

    # step 1: do a full recovery style sync. this will pick up
    # all addresses that we expect to match transactions against,
    # from a blank slate Core wallet that originally had no imports.
    if not options.recoversync:
        jmprint("Recovery sync was not set, but using it anyway.")
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=False)
    # Note that the user may be interrupted above by the rescan
    # request; this is as for normal scripts; after the rescan is done
    # (usually, only once, but, this *IS* needed here, unlike a normal
    # wallet generation event), we just try again.

    # Now all address from HD are imported, we need to grab
    # all the transactions for those addresses; this includes txs
    # that *spend* as well as receive our coins, so will include
    # "first-out" SNICKER txs as well as ordinary spends and JM coinjoins.
    seed_transactions = wallet_service.get_all_transactions()

    # Search for SNICKER txs and add them if they match.
    # We proceed recursively; we find all one-out matches, then
    # all 2-out matches, until we find no new ones and stop.

    if len(seed_transactions) == 0:
        jmprint("No transactions were found for this wallet. Did you rescan?")
        return False

    new_txs = []
    current_block_heights = set()
    for tx in seed_transactions:
        if btc.is_snicker_tx(tx):
            jmprint("Found a snicker tx: {}".format(
                bintohex(tx.GetTxid()[::-1])))
            equal_outs = btc.get_equal_outs(tx)
            if not equal_outs:
                continue
            if all([
                    wallet_service.is_known_script(x.scriptPubKey) == False
                    for x in [a[1] for a in equal_outs]
            ]):
                # it is now *very* likely that one of the two equal
                # outputs is our SNICKER custom output
                # script; notice that in this case, the transaction *must*
                # have spent our inputs, since it didn't recognize ownership
                # of either coinjoin output (and if it did recognize the change,
                # it would have recognized the cj output also).
                # We try to regenerate one of the outputs, but warn if
                # we can't.
                my_indices = get_pubs_and_indices_of_inputs(tx,
                                                            wallet_service,
                                                            ours=True)
                for mypub, mi in my_indices:
                    for eo in equal_outs:
                        for (other_pub, i) in get_pubs_and_indices_of_inputs(
                                tx, wallet_service, ours=False):
                            for (our_pub,
                                 j) in get_pubs_and_indices_of_ancestor_inputs(
                                     tx.vin[mi], wallet_service, ours=True):
                                our_spk = wallet_service.pubkey_to_script(
                                    our_pub)
                                our_priv = wallet_service.get_key_from_addr(
                                    wallet_service.script_to_addr(our_spk))
                                tweak_bytes = btc.ecdh(our_priv[:-1],
                                                       other_pub)
                                tweaked_pub = btc.snicker_pubkey_tweak(
                                    our_pub, tweak_bytes)
                                tweaked_spk = wallet_service.pubkey_to_script(
                                    tweaked_pub)
                                if tweaked_spk == eo[1].scriptPubKey:
                                    # TODO wallet.script_to_addr has a dubious assertion, that's why
                                    # we use btc method directly:
                                    address_found = str(
                                        btc.CCoinAddress.from_scriptPubKey(
                                            btc.CScript(tweaked_spk)))
                                    #address_found = wallet_service.script_to_addr(tweaked_spk)
                                    jmprint(
                                        "Found a new SNICKER output belonging to us."
                                    )
                                    jmprint(
                                        "Output address {} in the following transaction:"
                                        .format(address_found))
                                    jmprint(btc.human_readable_transaction(tx))
                                    jmprint(
                                        "Importing the address into the joinmarket wallet..."
                                    )
                                    # NB for a recovery we accept putting any imported keys all into
                                    # the same mixdepth (0); TODO investigate correcting this, it will
                                    # be a little complicated.
                                    success, msg = wallet_service.check_tweak_matches_and_import(
                                        wallet_service.script_to_addr(our_spk),
                                        tweak_bytes, tweaked_pub,
                                        wallet_service.mixdepth)
                                    if not success:
                                        jmprint(
                                            "Failed to import SNICKER key: {}".
                                            format(msg), "error")
                                        return False
                                    else:
                                        jmprint("... success.")
                                    # we want the blockheight to track where the next-round rescan
                                    # must start from
                                    current_block_heights.add(
                                        wallet_service.
                                        get_transaction_block_height(tx))
                                    # add this transaction to the next round.
                                    new_txs.append(tx)
    if len(new_txs) == 0:
        return True
    seed_transactions.extend(new_txs)
    earliest_new_blockheight = min(current_block_heights)
    jmprint("New SNICKER addresses were imported to the Core wallet; "
            "do rescanblockchain again, starting from block {}, before "
            "restarting this script.".format(earliest_new_blockheight))
    return False