def get_receiving_address(self): # the receiving address is sourced from the 'next' mixdepth # to avoid clustering of input and output: next_mixdepth = (self.mixdepth + 1) % (self.wallet_service.wallet.mixdepth + 1) self.receiving_address = btc.CCoinAddress( self.wallet_service.get_internal_addr(next_mixdepth))
def parse_payjoin_setup(bip21_uri, wallet_service, mixdepth): """ Takes the payment request data from the uri and returns a JMPayjoinManager object initialised for that payment. """ assert btc.is_bip21_uri(bip21_uri), "invalid bip21 uri: " + bip21_uri decoded = btc.decode_bip21_uri(bip21_uri) assert "amount" in decoded assert "address" in decoded assert "pj" in decoded amount = decoded["amount"] destaddr = decoded["address"] # this will throw for any invalid address: destaddr = btc.CCoinAddress(destaddr) server = decoded["pj"] disable_output_substitution = False if "pjos" in decoded and decoded["pjos"] == "0": disable_output_substitution = True return JMPayjoinManager( wallet_service, mixdepth, destaddr, amount, server, disable_output_substitution=disable_output_substitution)
def validate_address(addr): try: # automatically respects the network # as set in btc.select_chain_params(...) dummyaddr = btc.CCoinAddress(addr) except Exception as e: return False, repr(e) # additional check necessary because python-bitcointx # does not check hash length on p2sh construction. try: dummyaddr.to_scriptPubKey() except Exception as e: return False, repr(e) return True, "address validated"
def push(self): jlog.debug('\n' + bintohex(self.latest_tx.serialize())) self.txid = bintohex(self.latest_tx.GetTxid()[::-1]) jlog.info('txid = ' + self.txid) #If we are sending to a bech32 address, in case of sweep, will #need to use that bech32 for address import, which requires #converting to script (Core does not allow import of bech32) if self.my_cj_addr.lower()[:2] in ['bc', 'tb']: notify_addr = btc.CCoinAddress(self.my_cj_addr).to_scriptPubKey() else: notify_addr = self.my_cj_addr #add the callbacks *before* pushing to ensure triggering; #this does leave a dangling notify callback if the push fails, but #that doesn't cause problems. self.wallet_service.register_callbacks([self.unconfirm_callback], self.txid, "unconfirmed") self.wallet_service.register_callbacks([self.confirm_callback], self.txid, "confirmed") task.deferLater( reactor, float(jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec")), self.handle_unbroadcast_transaction, self.txid, self.latest_tx) tx_broadcast = jm_single().config.get('POLICY', 'tx_broadcast') nick_to_use = None if tx_broadcast == 'self': pushed = self.push_ourselves() elif tx_broadcast in ['random-peer', 'not-self']: n = len(self.maker_utxo_data) if tx_broadcast == 'random-peer': i = random.randrange(n + 1) else: i = random.randrange(n) if i == n: pushed = self.push_ourselves() else: nick_to_use = list(self.maker_utxo_data.keys())[i] pushed = True else: jlog.info("Only self, random-peer and not-self broadcast " "methods supported. Reverting to self-broadcast.") pushed = self.push_ourselves() if not pushed: self.on_finished_callback(False, fromtx=True) else: if nick_to_use: return (nick_to_use, bintohex(self.latest_tx.serialize()))
def address_to_script(addr): return btc.CCoinAddress(addr).to_scriptPubKey()
def verify_unsigned_tx(self, tx, offerinfo): """This code is security-critical. Before signing the transaction the Maker must ensure that all details are as expected, and most importantly that it receives the exact number of coins to expected in total. The data is taken from the offerinfo dict and compared with the serialized txhex. """ tx_utxo_set = set((x.prevout.hash[::-1], x.prevout.n) for x in tx.vin) utxos = offerinfo["utxos"] cjaddr = offerinfo["cjaddr"] cjaddr_script = btc.CCoinAddress(cjaddr).to_scriptPubKey() changeaddr = offerinfo["changeaddr"] changeaddr_script = btc.CCoinAddress(changeaddr).to_scriptPubKey() #Note: this value is under the control of the Taker, #see comment below. amount = offerinfo["amount"] cjfee = offerinfo["offer"]["cjfee"] txfee = offerinfo["offer"]["txfee"] ordertype = offerinfo["offer"]["ordertype"] my_utxo_set = set(utxos.keys()) if not tx_utxo_set.issuperset(my_utxo_set): return (False, 'my utxos are not contained') #The three lines below ensure that the Maker receives #back what he puts in, minus his bitcointxfee contribution, #plus his expected fee. These values are fully under #Maker control so no combination of messages from the Taker #can change them. #(mathematically: amount + expected_change_value is independent #of amount); there is not a (known) way for an attacker to #alter the amount (note: !fill resubmissions *overwrite* #the active_orders[dict] entry in daemon), but this is an #extra layer of safety. my_total_in = sum([va['value'] for va in utxos.values()]) real_cjfee = calc_cj_fee(ordertype, cjfee, amount) expected_change_value = (my_total_in - amount - txfee + real_cjfee) jlog.info('potentially earned = {}'.format(real_cjfee - txfee)) jlog.info('mycjaddr, mychange = {}, {}'.format(cjaddr, changeaddr)) #The remaining checks are needed to ensure #that the coinjoin and change addresses occur #exactly once with the required amts, in the output. times_seen_cj_addr = 0 times_seen_change_addr = 0 for outs in tx.vout: if outs.scriptPubKey == cjaddr_script: times_seen_cj_addr += 1 if outs.nValue != amount: return (False, 'Wrong cj_amount. I expect ' + str(amount)) if outs.scriptPubKey == changeaddr_script: times_seen_change_addr += 1 if outs.nValue != expected_change_value: return (False, 'wrong change, i expect ' + str(expected_change_value)) if times_seen_cj_addr != 1 or times_seen_change_addr != 1: fmt = ('cj or change addr not in tx ' 'outputs once, #cjaddr={}, #chaddr={}').format return (False, (fmt(times_seen_cj_addr, times_seen_change_addr))) return (True, None)
def on_tx_received(self, nick, txser): """ 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.CMutableTransaction.deserialize(txser) except Exception as e: return (False, 'malformed txhex. ' + repr(e)) self.user_info('obtained proposed fallback (non-coinjoin) ' +\ 'transaction from sender:\n' + str(tx)) if len(tx.vout) != 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.vout): if out.scriptPubKey == btc.CCoinAddress( self.destination_addr).to_scriptPubKey(): # we found the expected destination; is the amount correct? if not out.nValue == 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.scriptPubKey proposed_change_value = out.nValue 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.vin): utxo_for_checking = (ins.prevout.hash[::-1], ins.prevout.n) 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 utxo.items(): 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.vin), len(tx.vout), 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', [txser]) #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. # TODO handle native segwit properly for i, u in utxo.items(): if not btc.verify_tx_input( tx, i, tx.vin[i].scriptSig, btc.CScript(utxo_data[i]["script"]), amount=utxo_data[i]["value"], witness=tx.wit.vtxinwit[i].scriptWitness): 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(bintohex(txser)) # Keep a local copy for broadcast fallback: self.fallback_tx = tx # 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.vin) + 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.vin) + 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({ "address": str(btc.CCoinAddress.from_scriptPubKey(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, 2, compute_tx_locktime()) # sign our inputs before transfer our_inputs = {} for index, ins in enumerate(new_tx.vin): utxo = (ins.prevout.hash[::-1], ins.prevout.n) if utxo not in my_utxos: continue script = my_utxos[utxo]["script"] amount = my_utxos[utxo]["value"] our_inputs[index] = (script, amount) success, msg = self.wallet_service.sign_tx(new_tx, our_inputs) if not success: return (False, "Failed to sign new transaction, error: " + msg) txinfo = tuple((x.scriptPubKey, x.nValue) for x in new_tx.vout) 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, bintohex(new_tx.serialize()))