def sign(utxo, priv, destaddrs, utxo_address_type): """Sign a tx sending the amount amt, from utxo utxo, equally to each of addresses in list destaddrs, after fees; the purpose is to create multiple utxos. utxo_address_type must be one of p2sh-p2wpkh/p2wpkh/p2pkh. """ results = validate_utxo_data([(utxo, priv)], retrieve=True, utxo_address_type=utxo_address_type) if not results: return False assert results[0][0] == utxo amt = results[0][1] ins = [utxo] estfee = estimate_tx_fee(1, len(destaddrs), txtype=utxo_address_type) outs = [] share = int((amt - estfee) / len(destaddrs)) fee = amt - share*len(destaddrs) assert fee >= estfee log.info("Using fee: " + str(fee)) for i, addr in enumerate(destaddrs): outs.append({'address': addr, 'value': share}) tx = btc.make_shuffled_tx(ins, outs, version=2, locktime=compute_tx_locktime()) amtforsign = amt if utxo_address_type != "p2pkh" else None rawpriv, _ = BTCEngine.wif_to_privkey(priv) if utxo_address_type == "p2wpkh": native = utxo_address_type else: native = False success, msg = btc.sign(tx, 0, rawpriv, amount=amtforsign, native=native) assert success, msg return tx
def test_mk_shuffled_tx(): # prepare two addresses for the outputs pub = btc.privkey_to_pubkey(btc.Hash(b"priv") + b"\x01") scriptPubKey = btc.CScript([btc.OP_0, btc.Hash160(pub)]) addr1 = btc.P2WPKHCoinAddress.from_scriptPubKey(scriptPubKey) scriptPubKey_p2sh = scriptPubKey.to_p2sh_scriptPubKey() addr2 = btc.CCoinAddress.from_scriptPubKey(scriptPubKey_p2sh) ins = [(btc.Hash(b"blah"), 7), (btc.Hash(b"foo"), 15)] # note the casts str() ; most calls to mktx will have addresses fed # as strings, so this is enforced for simplicity. outs = [{"address": str(addr1), "value": btc.coins_to_satoshi(float("0.1"))}, {"address": str(addr2), "value": btc.coins_to_satoshi(float("45981.23331234"))}] tx = btc.make_shuffled_tx(ins, outs, version=2, locktime=500000)
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]))
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 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 direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, accept_callback=None, info_callback=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 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 returns: True if accepted, False if not ==== The info_callback takes one parameter, the information message (when tx is pushed), and returns nothing. This function returns: The txid if transaction is pushed, False otherwise """ #Sanity checks assert validate_address(destination)[0] or is_burn_destination(destination) 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 from pprint import pformat txtype = wallet_service.get_txtype() if amount == 0: 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 iteritems(utxos)]) 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 = bin_hash160(pubkey) #size of burn output is slightly different from regular outputs burn_script = mk_burn_script(pubkeyhash) #in hex 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 send (non-burn) fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) outs = [{"address": destination, "value": total_inputs_val - fee_est}] else: #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 iteritems(utxos)]) changeval = total_inputs_val - fee_est - amount outs = [{"value": amount, "address": destination}] change_addr = wallet_service.get_internal_addr(mixdepth) 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( wallet_service.addr_to_script(utxo["address"])) 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) + ".") txsigned = sign_tx(wallet_service, make_shuffled_tx( list(utxos.keys()), outs, False, 2, tx_locktime), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) log.info("In serialized form (for copy-paste):") log.info(tx) actual_amount = amount if amount != 0 else total_inputs_val - fee_est log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination) 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(pformat(txsigned), destination, actual_amount, fee_est) if not accepted: return False jm_single().bc_interface.pushtx(tx) txid = txhash(tx) successmsg = "Transaction sent: " + txid cb = log.info if not info_callback else info_callback cb(successmsg) return txid
def test_is_snicker_tx(our_input_val, their_input_val, network_fee, script_type, net_transfer): our_input = (bytes([1]) * 32, 0) their_input = (bytes([2]) * 32, 1) assert our_input_val - their_input_val - network_fee > 0 total_input_amount = our_input_val + their_input_val total_output_amount = total_input_amount - network_fee receiver_output_amount = their_input_val + net_transfer proposer_output_amount = total_output_amount - receiver_output_amount # all keys are just made up; only the script type will be checked privs = [bytes([i]) * 32 + bytes([1]) for i in range(1, 4)] pubs = [btc.privkey_to_pubkey(x) for x in privs] if script_type == "p2wpkh": spks = [btc.pubkey_to_p2wpkh_script(x) for x in pubs] elif script_type == "p2sh-p2wpkh": spks = [btc.pubkey_to_p2sh_p2wpkh_script(x) for x in pubs] else: assert False tweaked_addr, our_addr, change_addr = [ str(btc.CCoinAddress.from_scriptPubKey(x)) for x in spks ] # now we must construct the three outputs with correct output amounts. outputs = [{"address": tweaked_addr, "value": receiver_output_amount}] outputs.append({"address": our_addr, "value": receiver_output_amount}) outputs.append({ "address": change_addr, "value": total_output_amount - 2 * receiver_output_amount }) assert all([x["value"] > 0 for x in outputs]) # make_shuffled_tx mutates ordering (yuck), work with copies only: outputs1 = copy.deepcopy(outputs) # version and locktime as currently specified in the BIP # for 0/1 version SNICKER. (Note the locktime is partly because # of expected delays). tx = btc.make_shuffled_tx([our_input, their_input], outputs1, version=2, locktime=0) assert btc.is_snicker_tx(tx) # construct variants which will be invalid. # mixed script types in outputs wrong_tweaked_spk = btc.pubkey_to_p2pkh_script(pubs[1]) wrong_tweaked_addr = str( btc.CCoinAddress.from_scriptPubKey(wrong_tweaked_spk)) outputs2 = copy.deepcopy(outputs) outputs2[0] = { "address": wrong_tweaked_addr, "value": receiver_output_amount } tx2 = btc.make_shuffled_tx([our_input, their_input], outputs2, version=2, locktime=0) assert not btc.is_snicker_tx(tx2) # nonequal output amounts outputs3 = copy.deepcopy(outputs) outputs3[1] = {"address": our_addr, "value": receiver_output_amount - 1} tx3 = btc.make_shuffled_tx([our_input, their_input], outputs3, version=2, locktime=0) assert not btc.is_snicker_tx(tx3) # too few outputs outputs4 = copy.deepcopy(outputs) outputs4 = outputs4[:2] tx4 = btc.make_shuffled_tx([our_input, their_input], outputs4, version=2, locktime=0) assert not btc.is_snicker_tx(tx4) # too many outputs outputs5 = copy.deepcopy(outputs) outputs5.append({"address": change_addr, "value": 200000}) tx5 = btc.make_shuffled_tx([our_input, their_input], outputs5, version=2, locktime=0) assert not btc.is_snicker_tx(tx5) # wrong nVersion tx6 = btc.make_shuffled_tx([our_input, their_input], outputs, version=1, locktime=0) assert not btc.is_snicker_tx(tx6) # wrong nLockTime tx7 = btc.make_shuffled_tx([our_input, their_input], outputs, version=2, locktime=1) assert not btc.is_snicker_tx(tx7)
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()))
def on_tx_received(self, nick, txhex): """ Called when the sender-counterparty has sent a transaction proposal. 1. First we check for the expected destination and amount (this is sufficient to identify our cp, as this info was presumably passed out of band, as for any normal payment). 2. Then we verify the validity of the proposed non-coinjoin transaction; if not, reject, otherwise store this as a fallback transaction in case the protocol doesn't complete. 3. Next, we select utxos from our wallet, to add into the payment transaction as input. Try to select so as to not trigger the UIH2 condition, but continue (and inform user) even if we can't (if we can't select any coins, broadcast the non-coinjoin payment, if the user agrees). Proceeding with payjoin: 4. We update the output amount at the destination address. 5. We modify the change amount in the original proposal (which will be the only other output other than the destination), reducing it to account for the increased transaction fee caused by our additional proposed input(s). 6. Finally we sign our own input utxo(s) and re-serialize the tx, allowing it to be sent back to the counterparty. 7. If the transaction is not fully signed and broadcast within the time unconfirm_timeout_sec as specified in the joinmarket.cfg, we broadcast the non-coinjoin fallback tx instead. """ try: tx = btc.deserialize(txhex) except (IndexError, SerializationError, SerializationTruncationError) as e: return (False, 'malformed txhex. ' + repr(e)) self.user_info('obtained proposed fallback (non-coinjoin) ' +\ 'transaction from sender:\n' + pprint.pformat(tx)) if len(tx["outs"]) != 2: return (False, "Transaction has more than 2 outputs; not supported.") dest_found = False destination_index = -1 change_index = -1 proposed_change_value = 0 for index, out in enumerate(tx["outs"]): if out["script"] == btc.address_to_script(self.destination_addr): # we found the expected destination; is the amount correct? if not out["value"] == self.receiving_amount: return (False, "Wrong payout value in proposal from sender.") dest_found = True destination_index = index else: change_found = True proposed_change_out = out["script"] proposed_change_value = out["value"] change_index = index if not dest_found: return (False, "Our expected destination address was not found.") # Verify valid input utxos provided and check their value. # batch retrieval of utxo data utxo = {} ctr = 0 for index, ins in enumerate(tx['ins']): utxo_for_checking = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) utxo[ctr] = [index, utxo_for_checking] ctr += 1 utxo_data = jm_single().bc_interface.query_utxo_set( [x[1] for x in utxo.values()]) total_sender_input = 0 for i, u in iteritems(utxo): if utxo_data[i] is None: return (False, "Proposed transaction contains invalid utxos") total_sender_input += utxo_data[i]["value"] # Check that the transaction *as proposed* balances; check that the # included fee is within 0.3-3x our own current estimates, if not user # must decide. btc_fee = total_sender_input - self.receiving_amount - proposed_change_value self.user_info("Network transaction fee of fallback tx is: " + str(btc_fee) + " satoshis.") fee_est = estimate_tx_fee(len(tx['ins']), len(tx['outs']), txtype=self.wallet_service.get_txtype()) fee_ok = False if btc_fee > 0.3 * fee_est and btc_fee < 3 * fee_est: fee_ok = True else: if self.user_check("Is this transaction fee acceptable? (y/n):"): fee_ok = True if not fee_ok: return (False, "Proposed transaction fee not accepted due to tx fee: " + str(btc_fee)) # This direct rpc call currently assumes Core 0.17, so not using now. # It has the advantage of (a) being simpler and (b) allowing for any # non standard coins. # #res = jm_single().bc_interface.rpc('testmempoolaccept', [txhex]) #print("Got this result from rpc call: ", res) #if not res["accepted"]: # return (False, "Proposed transaction was rejected from mempool.") # Manual verification of the transaction signatures. Passing this # test does imply that the transaction is valid (unless there is # a double spend during the process), but is restricted to standard # types: p2pkh, p2wpkh, p2sh-p2wpkh only. Double spend is not counted # as a risk as this is a payment. for i, u in iteritems(utxo): if "txinwitness" in tx["ins"][u[0]]: ver_amt = utxo_data[i]["value"] try: ver_sig, ver_pub = tx["ins"][u[0]]["txinwitness"] except Exception as e: self.user_info("Segwit error: " + repr(e)) return (False, "Segwit input not of expected type, " "either p2sh-p2wpkh or p2wpkh") # note that the scriptCode is the same whether nested or not # also note that the scriptCode has to be inferred if we are # only given a transaction serialization. scriptCode = "76a914" + btc.hash160( unhexlify(ver_pub)) + "88ac" else: scriptCode = None ver_amt = None scriptSig = btc.deserialize_script(tx["ins"][u[0]]["script"]) if len(scriptSig) != 2: return ( False, "Proposed transaction contains unsupported input type") ver_sig, ver_pub = scriptSig if not btc.verify_tx_input(txhex, u[0], utxo_data[i]['script'], ver_sig, ver_pub, scriptCode=scriptCode, amount=ver_amt): return (False, "Proposed transaction is not correctly signed.") # At this point we are satisfied with the proposal. Record the fallback # in case the sender disappears and the payjoin tx doesn't happen: self.user_info( "We'll use this serialized transaction to broadcast if your" " counterparty fails to broadcast the payjoin version:") self.user_info(txhex) # Keep a local copy for broadcast fallback: self.fallback_tx = txhex # Now we add our own inputs: # See the gist comment here: # https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2799709 # which sets out the decision Bob must make. # In cases where Bob can add any amount, he selects one utxo # to keep it simple. # In cases where he must choose at least X, he selects one utxo # which provides X if possible, otherwise defaults to a normal # selection algorithm. # In those cases where he must choose X but X is unavailable, # he selects all coins, and proceeds anyway with payjoin, since # it has other advantages (CIOH and utxo defrag). my_utxos = {} largest_out = max(self.receiving_amount, proposed_change_value) max_sender_amt = max([u['value'] for u in utxo_data]) not_uih2 = False if max_sender_amt < largest_out: # just select one coin. # have some reasonable lower limit but otherwise choose # randomly; note that this is actually a great way of # sweeping dust ... self.user_info("Choosing one coin at random") try: my_utxos = self.wallet_service.select_utxos( self.mixdepth, jm_single().DUST_THRESHOLD, select_fn=select_one_utxo) except: return self.no_coins_fallback() not_uih2 = True else: # get an approximate required amount assuming 4 inputs, which is # fairly conservative (but guess by necessity). fee_for_select = estimate_tx_fee( len(tx['ins']) + 4, 2, txtype=self.wallet_service.get_txtype()) approx_sum = max_sender_amt - self.receiving_amount + fee_for_select try: my_utxos = self.wallet_service.select_utxos( self.mixdepth, approx_sum) not_uih2 = True except Exception: # TODO probably not logical to always sweep here. self.user_info("Sweeping all coins in this mixdepth.") my_utxos = self.wallet_service.get_utxos_by_mixdepth()[ self.mixdepth] if my_utxos == {}: return self.no_coins_fallback() if not_uih2: self.user_info("The proposed tx does not trigger UIH2, which " "means it is indistinguishable from a normal " "payment. This is the ideal case. Continuing..") else: self.user_info("The proposed tx does trigger UIH2, which it makes " "it somewhat distinguishable from a normal payment," " but proceeding with payjoin..") my_total_in = sum([va['value'] for va in my_utxos.values()]) self.user_info("We selected inputs worth: " + str(my_total_in)) # adjust the output amount at the destination based on our contribution new_destination_amount = self.receiving_amount + my_total_in # estimate the required fee for the new version of the transaction total_ins = len(tx["ins"]) + len(my_utxos.keys()) est_fee = estimate_tx_fee(total_ins, 2, txtype=self.wallet_service.get_txtype()) self.user_info("We estimated a fee of: " + str(est_fee)) new_change_amount = total_sender_input + my_total_in - \ new_destination_amount - est_fee self.user_info("We calculated a new change amount of: " + str(new_change_amount)) self.user_info("We calculated a new destination amount of: " + str(new_destination_amount)) # now reconstruct the transaction with the new inputs and the # amount-changed outputs new_outs = [{ "address": self.destination_addr, "value": new_destination_amount }] if new_change_amount >= jm_single().BITCOIN_DUST_THRESHOLD: new_outs.append({ "script": proposed_change_out, "value": new_change_amount }) new_ins = [x[1] for x in utxo.values()] new_ins.extend(my_utxos.keys()) new_tx = btc.make_shuffled_tx(new_ins, new_outs, False, 2, compute_tx_locktime()) new_tx_deser = btc.deserialize(new_tx) # sign our inputs before transfer our_inputs = {} for index, ins in enumerate(new_tx_deser['ins']): utxo = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) if utxo not in my_utxos: continue script = self.wallet_service.addr_to_script( my_utxos[utxo]['address']) amount = my_utxos[utxo]['value'] our_inputs[index] = (script, amount) txs = self.wallet_service.sign_tx(btc.deserialize(new_tx), our_inputs) txinfo = tuple((x["script"], x["value"]) for x in txs["outs"]) self.wallet_service.register_callbacks([self.on_tx_unconfirmed], txinfo, "unconfirmed") self.wallet_service.register_callbacks([self.on_tx_confirmed], txinfo, "confirmed") # The blockchain interface just abandons monitoring if the transaction # is not broadcast before the configured timeout; we want to take # action in this case, so we add an additional callback to the reactor: reactor.callLater( jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec"), self.broadcast_fallback) return (True, nick, btc.serialize(txs))
def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, accept_callback=None, info_callback=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 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 returns: True if accepted, False if not ==== The info_callback takes one parameter, the information message (when tx is pushed), and returns nothing. This function returns: The txid if transaction is pushed, False otherwise """ #Sanity checks assert validate_address(destaddr)[0] assert isinstance(mixdepth, numbers.Integral) assert mixdepth >= 0 assert isinstance(amount, numbers.Integral) assert amount >= 0 assert isinstance(wallet_service.wallet, BaseWallet) from pprint import pformat txtype = wallet_service.get_txtype() if amount == 0: utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth] if utxos == {}: log.error("There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.") return total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) outs = [{"address": destaddr, "value": total_inputs_val - fee_est}] else: #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 iteritems(utxos)]) changeval = total_inputs_val - fee_est - amount outs = [{"value": amount, "address": destaddr}] change_addr = wallet_service.get_internal_addr(mixdepth) outs.append({"value": changeval, "address": change_addr}) #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) + ".") txsigned = sign_tx( wallet_service, make_shuffled_tx(list(utxos.keys()), outs, False, 2, compute_tx_locktime()), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) log.info("In serialized form (for copy-paste):") log.info(tx) actual_amount = amount if amount != 0 else total_inputs_val - fee_est log.info("Sends: " + amount_to_str(actual_amount) + " to address: " + destaddr) 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(pformat(txsigned), destaddr, actual_amount, fee_est) if not accepted: return False jm_single().bc_interface.pushtx(tx) txid = txhash(tx) successmsg = "Transaction sent: " + txid cb = log.info if not info_callback else info_callback cb(successmsg) return txid