def test_verify_tx_input(setup_tx_creation): priv = b"\xaa" * 32 + b"\x01" pub = bitcoin.privkey_to_pubkey(priv) script = bitcoin.pubkey_to_p2sh_p2wpkh_script(pub) addr = str(bitcoin.CCoinAddress.from_scriptPubKey(script)) wallet_service = make_wallets(1, [[2, 0, 0, 0, 0]], 1)[0]['wallet'] wallet_service.sync_wallet(fast=True) insfull = wallet_service.select_utxos(0, 110000000) outs = [{"address": addr, "value": 1000000}] ins = list(insfull.keys()) tx = bitcoin.mktx(ins, outs) scripts = {0: (insfull[ins[0]]["script"], bitcoin.coins_to_satoshi(1))} success, msg = wallet_service.sign_tx(tx, scripts) assert success, msg # testing Joinmarket's ability to verify transaction inputs # of others: pretend we don't have a wallet owning the transaction, # and instead verify an input using the (sig, pub, scriptCode) data # that is sent by counterparties: cScrWit = tx.wit.vtxinwit[0].scriptWitness sig = cScrWit.stack[0] pub = cScrWit.stack[1] scriptSig = tx.vin[0].scriptSig tx2 = bitcoin.mktx(ins, outs) res = bitcoin.verify_tx_input(tx2, 0, scriptSig, bitcoin.pubkey_to_p2wpkh_script(pub), amount=bitcoin.coins_to_satoshi(1), witness=bitcoin.CScriptWitness([sig, pub])) assert res
def test_spend_p2wpkh(setup_tx_creation): #make 3 p2wpkh outputs from 3 privs privs = [struct.pack(b'B', x) * 32 + b'\x01' for x in range(1, 4)] pubs = [bitcoin.privkey_to_pubkey(priv) for priv in privs] scriptPubKeys = [bitcoin.pubkey_to_p2wpkh_script(pub) for pub in pubs] addresses = [str(bitcoin.CCoinAddress.from_scriptPubKey( spk)) for spk in scriptPubKeys] #pay into it wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet'] wallet_service.sync_wallet(fast=True) amount = 35000000 p2wpkh_ins = [] for i, addr in enumerate(addresses): ins_full = wallet_service.select_utxos(0, amount) txid = make_sign_and_push(ins_full, wallet_service, amount, output_addr=addr) assert txid p2wpkh_ins.append((txid, 0)) txhex = jm_single().bc_interface.get_transaction(txid) #wait for mining jm_single().bc_interface.tick_forward_chain(1) #random output address output_addr = wallet_service.get_internal_addr(1) amount2 = amount*3 - 50000 outs = [{'value': amount2, 'address': output_addr}] tx = bitcoin.mktx(p2wpkh_ins, outs) for i, priv in enumerate(privs): # sign each of 3 inputs; note that bitcoin.sign # automatically validates each signature it creates. sig, msg = bitcoin.sign(tx, i, priv, amount=amount, native="p2wpkh") if not sig: assert False, msg txid = jm_single().bc_interface.pushtx(tx.serialize()) assert txid
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 on_tx_received(self, nick, txhex, 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()). """ try: tx = btc.deserialize(txhex) except (IndexError, SerializationError, SerializationTruncationError) as e: return (False, 'malformed txhex. ' + repr(e)) jlog.info('obtained tx\n' + pprint.pformat(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['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) 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) txs = self.wallet_service.sign_tx(btc.deserialize(unhexlify(txhex)), our_inputs) for index in our_inputs: sigmsg = unhexlify(txs['ins'][index]['script']) if 'txinwitness' in txs['ins'][index]: # 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 scriptSig 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: scriptSig = btc.pubkey_to_p2wpkh_script(txs['ins'][index]['txinwitness'][1]) sigmsg = b''.join(btc.serialize_script_unit( x) for x in txs['ins'][index]['txinwitness'] + [scriptSig]) except IndexError: #the sigmsg was already set before the segwit check pass sigs.append(base64.b64encode(sigmsg).decode('ascii')) return (True, sigs)
def test_spend_p2wpkh(setup_tx_creation): #make 3 p2wpkh outputs from 3 privs privs = [struct.pack(b'B', x) * 32 + b'\x01' for x in range(1, 4)] pubs = [ bitcoin.privkey_to_pubkey(binascii.hexlify(priv).decode('ascii')) for priv in privs ] scriptPubKeys = [bitcoin.pubkey_to_p2wpkh_script(pub) for pub in pubs] addresses = [bitcoin.pubkey_to_p2wpkh_address(pub) for pub in pubs] #pay into it wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet'] wallet_service.sync_wallet(fast=True) amount = 35000000 p2wpkh_ins = [] for addr in addresses: ins_full = wallet_service.select_utxos(0, amount) txid = make_sign_and_push(ins_full, wallet_service, amount, output_addr=addr) assert txid p2wpkh_ins.append(txid + ":0") #wait for mining time.sleep(1) #random output address output_addr = wallet_service.get_internal_addr(1) amount2 = amount * 3 - 50000 outs = [{'value': amount2, 'address': output_addr}] tx = bitcoin.mktx(p2wpkh_ins, outs) sigs = [] for i, priv in enumerate(privs): # sign each of 3 inputs tx = bitcoin.p2wpkh_sign(tx, i, binascii.hexlify(priv), amount, native=True) # check that verify_tx_input correctly validates; # to do this, we need to extract the signature and get the scriptCode # of this pubkey scriptCode = bitcoin.pubkey_to_p2pkh_script(pubs[i]) witness = bitcoin.deserialize(tx)['ins'][i]['txinwitness'] assert len(witness) == 2 assert witness[1] == pubs[i] sig = witness[0] assert bitcoin.verify_tx_input(tx, i, scriptPubKeys[i], sig, pubs[i], scriptCode=scriptCode, amount=amount) txid = jm_single().bc_interface.pushtx(tx) assert txid
def pubkey_to_script(cls, pubkey): return btc.pubkey_to_p2wpkh_script(pubkey)
def on_sig(self, nick, sigb64): """Processes transaction signatures from counterparties. If all signatures received correctly, returns the result of self.self_sign_and_push() (i.e. we complete the signing and broadcast); else returns False (thus returns False for all but last signature). """ if self.aborted: return False if nick not in self.nonrespondants: jlog.debug( ('add_signature => nick={} ' 'not in nonrespondants {}').format(nick, self.nonrespondants)) return False sig = base64.b64decode(sigb64) inserted_sig = False # batch retrieval of utxo data utxo = {} ctr = 0 for index, ins in enumerate(self.latest_tx.vin): if self._is_our_input(ins) or ins.scriptSig != b"": continue 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()]) # insert signatures for i, u in utxo.items(): if utxo_data[i] is None: continue # Check if the sender included the scriptCode in the sig message; # if so, also pick up the amount from the utxo data retrieved # from the blockchain to verify the segwit-style signature. # Note that this allows a mixed SW/non-SW transaction as each utxo # is interpreted separately. try: sig_deserialized = [a for a in iter(btc.CScript(sig))] except Exception as e: jlog.debug("Failed to parse junk sig message, ignoring.") break # abort in case we were given a junk sig (note this previously had # to check to avoid crashes in verify_tx_input, no longer (Feb 2020)): if not all([x for x in sig_deserialized]): jlog.debug("Junk signature: " + str(sig_deserialized) + \ ", not attempting to verify") break # The second case here is kept for backwards compatibility. if len(sig_deserialized) == 2: ver_sig, ver_pub = sig_deserialized elif len(sig_deserialized) == 3: ver_sig, ver_pub, _ = sig_deserialized else: jlog.debug("Invalid signature message - not 2 or 3 items") break scriptPubKey = btc.CScript(utxo_data[i]['script']) is_witness_input = scriptPubKey.is_p2sh( ) or scriptPubKey.is_witness_v0_keyhash() ver_amt = utxo_data[i]['value'] if is_witness_input else None witness = btc.CScriptWitness([ver_sig, ver_pub ]) if is_witness_input else None # don't attempt to parse `pub` as pubkey unless it's valid. if scriptPubKey.is_p2sh(): try: s = btc.pubkey_to_p2wpkh_script(ver_pub) except: jlog.debug( "Junk signature message, invalid pubkey, ignoring.") break if scriptPubKey.is_witness_v0_keyhash(): scriptSig = btc.CScript(b'') elif scriptPubKey.is_p2sh(): scriptSig = btc.CScript([s]) else: scriptSig = btc.CScript([ver_sig, ver_pub]) sig_good = btc.verify_tx_input(self.latest_tx, u[0], scriptSig, scriptPubKey, amount=ver_amt, witness=witness) if sig_good: jlog.debug('found good sig at index=%d' % (u[0])) # Note that, due to the complexity of handling multisig or other # arbitrary script (considering sending multiple signatures OTW), # there is an assumption of p2sh-p2wpkh or p2wpkh, for the segwit # case. self.latest_tx.vin[u[0]].scriptSig = scriptSig if is_witness_input: self.latest_tx.wit.vtxinwit[u[0]] = btc.CTxInWitness( btc.CScriptWitness(witness)) inserted_sig = True # check if maker has sent everything possible try: self.utxos[nick].remove(u[1]) except ValueError: pass if len(self.utxos[nick]) == 0: jlog.debug(('nick = {} sent all sigs, removing from ' 'nonrespondant list').format(nick)) try: self.nonrespondants.remove(nick) except ValueError: pass break if not inserted_sig: jlog.debug('signature did not match anything in the tx') # TODO what if the signature doesnt match anything # nothing really to do except drop it, carry on and wonder why the # other guy sent a failed signature tx_signed = True for ins, witness in zip(self.latest_tx.vin, self.latest_tx.wit.vtxinwit): if ins.scriptSig == b"" \ and not self._is_our_input(ins) \ and witness == btc.CTxInWitness(btc.CScriptWitness([])): tx_signed = False if not tx_signed: return False assert not len(self.nonrespondants) jlog.info('all makers have sent their signatures') self.taker_info_callback("INFO", "Transaction is valid, signing..") jlog.debug("schedule item was: " + str(self.schedule[self.schedule_index])) return self.self_sign_and_push()
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)
def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls): """ Plan of test: 1. Create a wallet and source 3 destination addresses. 2. Make, and confirm, transactions that fund the 3 addrs. 3. Create a new tx spending 2 of those 3 utxos and spending another utxo we don't own (extra is optional per `unowned_utxo`). 4. Create a psbt using the above transaction and corresponding `spent_outs` field to fill in the redeem script. 5. Compare resulting PSBT with expected structure. 6. Use the wallet's sign_psbt method to sign the whole psbt, which means signing each input we own. 7. Check that each input is finalized as per expected. Check that the whole PSBT is or is not finalized as per whether there is an unowned utxo. 8. In case where whole psbt is finalized, attempt to broadcast the tx. """ # steps 1 and 2: wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 1, wallet_cls=wallet_cls)[0]['wallet'] wallet_service.sync_wallet(fast=True) utxos = wallet_service.select_utxos(0, bitcoin.coins_to_satoshi(1.5)) # for legacy wallets, psbt creation requires querying for the spending # transaction: if wallet_cls == LegacyWallet: fulltxs = [] for utxo, v in utxos.items(): fulltxs.append( jm_single().bc_interface.get_deser_from_gettransaction( jm_single().bc_interface.get_transaction(utxo[0]))) assert len(utxos) == 2 u_utxos = {} if unowned_utxo: # note: tx creation uses the key only; psbt creation uses the value, # which can be fake here; we do not intend to attempt to fully # finalize a psbt with an unowned input. See # https://github.com/Simplexum/python-bitcointx/issues/30 # the redeem script creation (which is artificial) will be # avoided in future. priv = b"\xaa" * 32 + b"\x01" pub = bitcoin.privkey_to_pubkey(priv) script = bitcoin.pubkey_to_p2sh_p2wpkh_script(pub) redeem_script = bitcoin.pubkey_to_p2wpkh_script(pub) u_utxos[(b"\xaa" * 32, 12)] = {"value": 1000, "script": script} utxos.update(u_utxos) # outputs aren't interesting for this test (we selected 1.5 but will get 2): outs = [{ "value": bitcoin.coins_to_satoshi(1.999), "address": wallet_service.get_addr(0, 0, 0) }] tx = bitcoin.mktx(list(utxos.keys()), outs) if wallet_cls != LegacyWallet: spent_outs = wallet_service.witness_utxos_to_psbt_utxos(utxos) force_witness_utxo = True else: spent_outs = fulltxs # the extra input is segwit: if unowned_utxo: spent_outs.extend( wallet_service.witness_utxos_to_psbt_utxos(u_utxos)) force_witness_utxo = False newpsbt = wallet_service.create_psbt_from_tx( tx, spent_outs, force_witness_utxo=force_witness_utxo) # see note above if unowned_utxo: newpsbt.inputs[-1].redeem_script = redeem_script print(bintohex(newpsbt.serialize())) print("human readable: ") print(wallet_service.human_readable_psbt(newpsbt)) # we cannot compare with a fixed expected result due to wallet randomization, but we can # check psbt structure: expected_inputs_length = 3 if unowned_utxo else 2 assert len(newpsbt.inputs) == expected_inputs_length assert len(newpsbt.outputs) == 1 # note: redeem_script field is a CScript which is a bytes instance, # so checking length is best way to check for existence (comparison # with None does not work): if wallet_cls == SegwitLegacyWallet: assert len(newpsbt.inputs[0].redeem_script) != 0 assert len(newpsbt.inputs[1].redeem_script) != 0 if unowned_utxo: assert newpsbt.inputs[2].redeem_script == redeem_script signed_psbt_and_signresult, err = wallet_service.sign_psbt( newpsbt.serialize(), with_sign_result=True) assert err is None signresult, signed_psbt = signed_psbt_and_signresult expected_signed_inputs = len(utxos) if not unowned_utxo else len(utxos) - 1 assert signresult.num_inputs_signed == expected_signed_inputs assert signresult.num_inputs_final == expected_signed_inputs if not unowned_utxo: assert signresult.is_final # only in case all signed do we try to broadcast: extracted_tx = signed_psbt.extract_transaction().serialize() assert jm_single().bc_interface.pushtx(extracted_tx) else: # transaction extraction must fail for not-fully-signed psbts: with pytest.raises(ValueError) as e: extracted_tx = signed_psbt.extract_transaction()
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 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)