def test_podle_constructor(setup_podle): """Tests rules about construction of PoDLE object are conformed to. """ priv = "aa"*32 #pub and priv together not allowed with pytest.raises(PoDLEError) as e_info: p = PoDLE(priv=priv, P="dummypub") #no pub or priv is allowed, i forget if this is useful for something p = PoDLE() #create from priv p = PoDLE(priv=priv+"01", u="dummyutxo") pdict = p.generate_podle(2) assert all([k in pdict for k in ['used', 'utxo', 'P', 'P2', 'commit', 'sig', 'e']]) #using the valid data, serialize/deserialize test deser = p.deserialize_revelation(p.serialize_revelation()) assert all([deser[x] == pdict[x] for x in ['utxo', 'P', 'P2', 'sig', 'e']]) #deserialization must fail for wrong number of items with pytest.raises(PoDLEError) as e_info: p.deserialize_revelation(':'.join([str(x) for x in range(4)]), separator=':') #reveal() must work without pre-generated commitment p.commitment = None pdict2 = p.reveal() assert pdict2 == pdict #corrupt P2, cannot commit: p.P2 = "blah" with pytest.raises(PoDLEError) as e_info: p.get_commitment() #generation fails without a utxo p = PoDLE(priv=priv) with pytest.raises(PoDLEError) as e_info: p.generate_podle(0) #Test construction from pubkey pub = bitcoin.privkey_to_pubkey(priv+"01") p = PoDLE(P=pub) with pytest.raises(PoDLEError) as e_info: p.get_commitment() with pytest.raises(PoDLEError) as e_info: p.verify("dummycommitment", range(3))
def test_spend_p2sh_utxos(setup_tx_creation): #make a multisig address 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 ] script = bitcoin.mk_multisig_script(pubs, 2) msig_addr = bitcoin.p2sh_scriptaddr(script, magicbyte=196) #pay into it wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet'] wallet_service.sync_wallet(fast=True) amount = 350000000 ins_full = wallet_service.select_utxos(0, amount) txid = make_sign_and_push(ins_full, wallet_service, amount, output_addr=msig_addr) assert txid #wait for mining time.sleep(1) #spend out; the input can be constructed from the txid of previous msig_in = txid + ":0" ins = [msig_in] #random output address and change addr output_addr = wallet_service.get_internal_addr(1) amount2 = amount - 50000 outs = [{'value': amount2, 'address': output_addr}] tx = bitcoin.mktx(ins, outs) sigs = [] for priv in privs[:2]: sigs.append( bitcoin.multisign(tx, 0, script, binascii.hexlify(priv).decode('ascii'))) tx = bitcoin.apply_multisignatures(tx, 0, script, sigs) txid = jm_single().bc_interface.pushtx(tx) assert txid
def validate_utxo_data(utxo_datas, retrieve=False, segwit=False): """For each txid: N, privkey, first convert the privkey and convert to address, then use the blockchain instance to look up the utxo and check that its address field matches. If retrieve is True, return the set of utxos and their values. """ results = [] for u, priv in utxo_datas: jmprint('validating this utxo: ' + str(u), "info") hexpriv = btc.from_wif_privkey(priv, vbyte=get_p2pk_vbyte()) if segwit: addr = btc.pubkey_to_p2sh_p2wpkh_address( btc.privkey_to_pubkey(hexpriv), get_p2sh_vbyte()) else: addr = btc.privkey_to_address(hexpriv, magicbyte=get_p2pk_vbyte()) jmprint('claimed address: ' + addr, "info") res = jm_single().bc_interface.query_utxo_set([u]) if len(res) != 1 or None in res: jmprint("utxo not found on blockchain: " + str(u), "error") return False if res[0]['address'] != addr: jmprint( "privkey corresponds to the wrong address for utxo: " + str(u), "error") jmprint( "blockchain returned address: {}".format(res[0]['address']), "error") jmprint("your privkey gave this address: " + addr, "error") return False if retrieve: results.append((u, res[0]['value'])) jmprint('all utxos validated OK', "success") if retrieve: return results return True
def privkey_to_pubkey(privkey): return btc.privkey_to_pubkey(privkey)
def send_tx1id_tx2_sig_tx3_sig(self): our_tx2_sig = self.tx2.signatures[0][1] #**CONSTRUCT TX1** #This call can throw insufficient funds; handled by backout. #But, this should be avoided (see handshake). At least, any #throw here will not cause fees for client. print('wallet used coins is: ', self.wallet.used_coins) self.initial_utxo_inputs = self.wallet.select_utxos(0, self.coinswap_parameters.tx1_amount, utxo_filter=self.wallet.used_coins) #Lock these coins; only unlock if there is a pre-funding backout. self.wallet.used_coins.extend(self.initial_utxo_inputs.keys()) total_in = sum([x['value'] for x in self.initial_utxo_inputs.values()]) self.signing_privkeys = [] for i, v in enumerate(self.initial_utxo_inputs.values()): privkey = self.wallet.get_key_from_addr(v['address']) if not privkey: raise CoinSwapException("Failed to get key to sign TX1") self.signing_privkeys.append(privkey) signing_pubkeys = [[btc.privkey_to_pubkey(x)] for x in self.signing_privkeys] signing_redeemscripts = [btc.address_to_script( x['address']) for x in self.initial_utxo_inputs.values()] change_amount = total_in - self.coinswap_parameters.tx1_amount - \ self.coinswap_parameters.bitcoin_fee cslog.debug("got tx1 change amount: " + str(change_amount)) #get a change address in same mixdepth change_address = self.wallet.get_internal_addr(0) self.tx1 = CoinSwapTX01.from_params( self.coinswap_parameters.pubkeys["key_2_2_CB_0"], self.coinswap_parameters.pubkeys["key_2_2_CB_1"], utxo_ins=self.initial_utxo_inputs.keys(), signing_pubkeys=signing_pubkeys, signing_redeem_scripts=signing_redeemscripts, output_amount=self.coinswap_parameters.tx1_amount, change_address=change_address, change_amount=change_amount) #sign and hold signature, recover txid self.tx1.signall(self.signing_privkeys) self.tx1.attach_signatures() self.tx1.set_txid() cslog.info("Carol created and signed TX1:") cslog.info(self.tx1) #**CONSTRUCT TX3** utxo_in = self.tx1.txid + ":"+str(self.tx1.pay_out_index) self.tx3 = CoinSwapTX23.from_params( self.coinswap_parameters.pubkeys["key_2_2_CB_0"], self.coinswap_parameters.pubkeys["key_2_2_CB_1"], self.coinswap_parameters.pubkeys["key_TX3_secret"], utxo_in=utxo_in, recipient_amount=self.coinswap_parameters.tx3_amounts["script"], hashed_secret=self.hashed_secret, absolutelocktime=self.coinswap_parameters.timeouts["LOCK1"], refund_pubkey=self.coinswap_parameters.pubkeys["key_TX3_lock"], carol_only_address=self.coinswap_parameters.output_addresses["tx3_carol_address"], carol_only_amount=self.coinswap_parameters.tx3_amounts["carol"]) #create our signature on TX3 self.tx3.sign_at_index(self.keyset["key_2_2_CB_0"][0], 0) our_tx3_sig = self.tx3.signatures[0][0] cslog.info("Carol now has partially signed TX3:") cslog.info(self.tx3) return ([self.tx1.txid + ":" + str(self.tx1.pay_out_index), our_tx2_sig, our_tx3_sig], "OK")
def on_auth_received(self, nick, offer, commitment, cr, amount, kphex): """Receives data on proposed transaction offer from daemon, verifies commitment, returns necessary data to send ioauth message (utxos etc) """ # special case due to cjfee passed as string: it can accidentally parse # as hex: if not isinstance(offer["cjfee"], str): offer["cjfee"] = bintohex(offer["cjfee"]) #check the validity of the proof of discrete log equivalence tries = jm_single().config.getint("POLICY", "taker_utxo_retries") def reject(msg): jlog.info("Counterparty commitment not accepted, reason: " + msg) return (False, ) # deserialize the commitment revelation try: cr_dict = PoDLE.deserialize_revelation(cr) except PoDLEError as e: reason = repr(e) return reject(reason) if not verify_podle(cr_dict['P'], cr_dict['P2'], cr_dict['sig'], cr_dict['e'], commitment, index_range=range(tries)): reason = "verify_podle failed" return reject(reason) #finally, check that the proffered utxo is real, old enough, large enough, #and corresponds to the pubkey res = jm_single().bc_interface.query_utxo_set([cr_dict['utxo']], includeconf=True) if len(res) != 1 or not res[0]: reason = "authorizing utxo is not valid" return reject(reason) age = jm_single().config.getint("POLICY", "taker_utxo_age") if res[0]['confirms'] < age: reason = "commitment utxo not old enough: " + str( res[0]['confirms']) return reject(reason) reqd_amt = int( amount * jm_single().config.getint("POLICY", "taker_utxo_amtpercent") / 100.0) if res[0]['value'] < reqd_amt: reason = "commitment utxo too small: " + str(res[0]['value']) return reject(reason) try: if not self.wallet_service.pubkey_has_script( cr_dict['P'], res[0]['script']): raise EngineError() except EngineError: reason = "Invalid podle pubkey: " + str(cr_dict['P']) return reject(reason) # authorisation of taker passed # Find utxos for the transaction now: utxos, cj_addr, change_addr = self.oid_to_order(offer, amount) if not utxos: #could not find funds return (False, ) # for index update persistence: self.wallet_service.save_wallet() # Construct data for auth request back to taker. # Need to choose an input utxo pubkey to sign with # (no longer using the coinjoin pubkey from 0.2.0) # Just choose the first utxo in self.utxos and retrieve key from wallet. auth_address = utxos[list(utxos.keys())[0]]['address'] auth_key = self.wallet_service.get_key_from_addr(auth_address) auth_pub = btc.privkey_to_pubkey(auth_key) # kphex was auto-converted by @hexbin but we actually need to sign the # hex version to comply with pre-existing JM protocol: btc_sig = btc.ecdsa_sign(bintohex(kphex), auth_key) return (True, utxos, auth_pub, cj_addr, change_addr, btc_sig)
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 send_tx0id_hx_tx2sig(self): """Create coinswap secret, create TX0 paying into 2 of 2 AC, use the utxo/txid:n of it to create TX2, sign it, and send the hash, the tx2 sig and the utxo to Carol. """ self.secret, self.hashed_secret = get_coinswap_secret() #**CONSTRUCT TX0** #precompute the entirely signed transaction, so as to pass the txid self.initial_utxo_inputs = self.wallet.select_utxos( 0, self.coinswap_parameters.tx0_amount) total_in = sum([x['value'] for x in self.initial_utxo_inputs.values()]) self.signing_privkeys = [] for i, v in enumerate(self.initial_utxo_inputs.values()): privkey = self.wallet.get_key_from_addr(v['address']) if not privkey: raise CoinSwapException("Failed to get key to sign TX0") self.signing_privkeys.append(privkey) signing_pubkeys = [[btc.privkey_to_pubkey(x)] for x in self.signing_privkeys] signing_redeemscripts = [ btc.address_to_script(x['address']) for x in self.initial_utxo_inputs.values() ] change_amount = total_in - self.coinswap_parameters.tx0_amount - \ self.coinswap_parameters.bitcoin_fee cslog.debug("got tx0 change amount: " + str(change_amount)) #get a change address in same mixdepth change_address = self.wallet.get_internal_addr(0) self.tx0 = CoinSwapTX01.from_params( self.coinswap_parameters.pubkeys["key_2_2_AC_0"], self.coinswap_parameters.pubkeys["key_2_2_AC_1"], utxo_ins=self.initial_utxo_inputs, signing_pubkeys=signing_pubkeys, signing_redeem_scripts=signing_redeemscripts, output_amount=self.coinswap_parameters.tx0_amount, change_address=change_address, change_amount=change_amount, segwit=True) #sign and hold signature, recover txid self.tx0.signall(self.signing_privkeys) self.tx0.attach_signatures() self.tx0.set_txid() cslog.info("Alice created and signed TX0:") cslog.info(self.tx0) #**CONSTRUCT TX2** #Input is outpoint from TX0 utxo_in = self.tx0.txid + ":" + str(self.tx0.pay_out_index) self.tx2 = CoinSwapTX23.from_params( self.coinswap_parameters.pubkeys["key_2_2_AC_0"], self.coinswap_parameters.pubkeys["key_2_2_AC_1"], self.coinswap_parameters.pubkeys["key_TX2_secret"], utxo_in=utxo_in, recipient_amount=self.coinswap_parameters.tx2_amounts["script"], hashed_secret=self.hashed_secret, absolutelocktime=self.coinswap_parameters.timeouts["LOCK0"], refund_pubkey=self.coinswap_parameters.pubkeys["key_TX2_lock"], carol_only_address=self.coinswap_parameters. output_addresses["tx2_carol_address"], carol_only_amount=self.coinswap_parameters.tx2_amounts["carol"]) #Create our own signature for TX2 self.tx2.sign_at_index(self.keyset["key_2_2_AC_0"][0], 0) sigtx2 = self.tx2.signatures[0][0] self.send(self.tx0.txid + ":" + str(self.tx0.pay_out_index), self.hashed_secret, sigtx2) return (True, "TX0id, H(X), TX2 sig sent OK")
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 test_make_commitment(setup_taker, mixdepth, cjamt, failquery, external, expected_success, amtpercent, age, mixdepth_extras): def clean_up(): jm_single().config.set("POLICY", "taker_utxo_age", old_taker_utxo_age) jm_single().config.set("POLICY", "taker_utxo_amtpercent", old_taker_utxo_amtpercent) set_commitment_file(old_commitment_file) jm_single().bc_interface.setQUSFail(False) jm_single().bc_interface.reset_confs() os.remove('dummyext') old_commitment_file = get_commitment_file() with open('dummyext', 'wb') as f: f.write(json.dumps(t_dummy_ext, indent=4).encode('utf-8')) if external: set_commitment_file('dummyext') # define the appropriate podle acceptance parameters in the global config: old_taker_utxo_age = jm_single().config.get("POLICY", "taker_utxo_age") old_taker_utxo_amtpercent = jm_single().config.get( "POLICY", "taker_utxo_amtpercent") if expected_success: # set to defaults for mainnet newtua = "5" newtuap = "20" else: newtua = str(age) newtuap = str(amtpercent) jm_single().config.set("POLICY", "taker_utxo_age", newtua) jm_single().config.set("POLICY", "taker_utxo_amtpercent", newtuap) taker = get_taker([(mixdepth, cjamt, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", NO_ROUNDING)]) # modify or add any extra utxos for this run: for k, v in mixdepth_extras.items(): if k == "confchange": for k2, v2 in v.items(): # set the utxos in mixdepth k2 to have confs v2: cdict = taker.wallet_service.get_utxos_by_mixdepth()[k2] jm_single().bc_interface.set_confs( {utxo: v2 for utxo in cdict.keys()}) elif k == "custom-script": # note: this is inspired by fidelity bonds, and currently # uses scripts of that specific timelock type, but is really # only testing the general concept: that commitments must # not be made on any non-standard script type. for k2, v2 in v.items(): priv = os.urandom(32) + b"\x01" tl = random.randrange(1430454400, 1430494400) script_inner = bitcoin.mk_freeze_script( bitcoin.privkey_to_pubkey(priv), tl) script_outer = bitcoin.redeem_script_to_p2wsh_script( script_inner) taker.wallet_service.wallet._script_map[script_outer] = ( "nonstandard_path", ) taker.wallet_service.add_extra_utxo(os.urandom(32), 0, v2, k2, script=script_outer) else: for value in v: taker.wallet_service.add_extra_utxo(os.urandom(32), 0, value, k) taker.cjamount = cjamt taker.input_utxos = taker.wallet_service.get_utxos_by_mixdepth()[mixdepth] taker.mixdepth = mixdepth if failquery: jm_single().bc_interface.setQUSFail(True) comm, revelation, msg = taker.make_commitment() if expected_success and failquery: # for manual tests, show the error message: print("Failure case due to QUS fail: ") print("Erromsg: ", msg) assert not comm elif expected_success: assert comm, "podle was not generated but should have been." else: # in these cases we have set the podle acceptance # parameters such that our in-mixdepth utxos are not good # enough. # for manual tests, show the errormsg: print("Failure case, errormsg: ", msg) assert not comm, "podle was generated but should not have been." clean_up()
def cli_get_pubkey(wallet_name, address): print("Checking for address: ", address) wallet = cli_get_wallet(wallet_name) privkey = wallet.get_key_from_addr(address) pubkey = btc.privkey_to_pubkey(privkey) print("Pubkey: ", pubkey)
def scan_for_coinjoins(privkey, amount, filename): """Given a file which contains encrypted coinjoin proposals, and a private key for a pubkey with a known utxo existing which we can spend, scan the entries in the file, all assumed to be ECIES encrypted to a pubkey, for one which is encrypted to *this* pubkey, if found, output the retrieved partially signed transaction, and destination key, address to a list which is returned to the caller. Only if the retrieved coinjoin transaction passes basic checks on validity in terms of amount paid, is it returned. This is an elementary implementation that will obviously fail any performance test (i.e. moderately large lists). Note that the tweaked output address must be of type p2sh/p2wpkh. """ try: with open(filename, "rb") as f: msgs = f.readlines() except: print("Failed to read from file: ", filename) return valid_coinjoins = [] for msg in msgs: try: decrypted_msg = decrypt_message(msg, privkey) tweak, tx = deserialize_coinjoin_proposal(decrypted_msg) except: print("Could not decrypt message, skipping") continue if not tweak: print("Could not decrypt message, reason: " + str(tx)) continue #We analyse the content of the transaction to check if it follows #our requirements try: deserialized_tx = btc.deserialize(tx) except: print("Proposed transaction is not correctly formatted, skipping.") continue #construct our receiving address according to the tweak pubkey = btc.privkey_to_pubkey(privkey) tweak, destnpt, my_destn_addr = create_recipient_address(pubkey, tweak=tweak, segwit=True) #add_privkeys requires both inputs to be compressed (or un-) consistently. tweak_priv = tweak + "01" my_destn_privkey = btc.add_privkeys(tweak_priv, privkey, True) my_output_index = -1 for i, o in enumerate(deserialized_tx['outs']): addr = btc.script_to_address(o['script'], get_p2sh_vbyte()) if addr == my_destn_addr: print('found our output address: ', my_destn_addr) my_output_index = i break if my_output_index == -1: print("Proposal doesn't contain our output address, rejecting") continue my_output_amount = deserialized_tx['outs'][i]['value'] required_amount = amount - 2 * estimate_tx_fee(3, 3, 'p2sh-p2wpkh') if my_output_amount < required_amount: print("Proposal pays too little, difference is: ", required_amount - my_output_amount) continue #now we know output is acceptable to us, we should check that the #ctrprty input is signed and the other input is ours, but will do this #later; if it's not, it just won't work so NBD for now. valid_coinjoins.append((my_destn_addr, my_destn_privkey, tx)) return valid_coinjoins
def process_proposals(self, proposals): """ Each entry in `proposals` is of form: encrypted_proposal - base64 string key - hex encoded compressed pubkey, or '' if the key is not null, we attempt to decrypt and process according to that key, else cycles over all keys. If all SNICKER validations succeed, the decision to spend is entirely dependent on self.acceptance_callback. If the callback returns True, we co-sign and broadcast the transaction and also update the wallet with the new imported key (TODO: future versions will enable searching for these keys using history + HD tree; note the jmbitcoin snicker.py module DOES insist on ECDH being correctly used, so this will always be possible for transactions created here. Returned is a list of txids of any transactions which were broadcast, unless a critical error occurs, in which case False is returned (to minimize this function's trust in other parts of the code being executed, if something appears to be inconsistent, we trigger immediate halt with this return). """ for kp in proposals: try: p, k = kp.split(b',') except: jlog.error("Invalid proposal string, ignoring: " + kp) if k is not None: # note that this operation will succeed as long as # the key is in the wallet._script_map, which will # be true if the key is at an HD index lower than # the current wallet.index_cache k = hextobin(k.decode('utf-8')) addr = self.wallet_service.pubkey_to_addr(k) if not self.wallet_service.is_known_addr(addr): jlog.debug("Key not recognized as part of our " "wallet, ignoring.") continue # TODO: interface/API of SNICKERWalletMixin would better take # address as argument here, not privkey: priv = self.wallet_service.get_key_from_addr(addr) result = self.wallet_service.parse_proposal_to_signed_tx( priv, p, self.acceptance_callback) if result[0] is not None: tx, tweak, out_spk = result # We will: rederive the key as a sanity check, # and see if it matches the claimed spk. # Then, we import the key into the wallet # (even though it's re-derivable from history, this # is the easiest for a first implementation). # Finally, we co-sign, then push. # (Again, simplest function: checks already passed, # so do it automatically). # TODO: the more sophisticated actions. tweaked_key = btc.snicker_pubkey_tweak(k, tweak) tweaked_spk = btc.pubkey_to_p2sh_p2wpkh_script(tweaked_key) if not tweaked_spk == out_spk: jlog.error("The spk derived from the pubkey does " "not match the scriptPubkey returned from " "the snicker module - code error.") return False # before import, we should derive the tweaked *private* key # from the tweak, also: tweaked_privkey = btc.snicker_privkey_tweak(priv, tweak) if not btc.privkey_to_pubkey( tweaked_privkey) == tweaked_key: jlog.error("Was not able to recover tweaked pubkey " "from tweaked privkey - code error.") jlog.error("Expected: " + bintohex(tweaked_key)) jlog.error( "Got: " + bintohex(btc.privkey_to_pubkey(tweaked_privkey))) return False # the recreated private key matches, so we import to the wallet, # note that type = None here is because we use the same # scriptPubKey type as the wallet, this has been implicitly # checked above by deriving the scriptPubKey. self.wallet_service.import_private_key( self.import_branch, self.wallet_service._ENGINE.privkey_to_wif( tweaked_privkey)) # TODO condition on automatic brdcst or not if not jm_single().bc_interface.pushtx(tx.serialize()): jlog.error("Failed to broadcast SNICKER CJ.") return False self.successful_txs.append(tx) return True else: jlog.debug('Failed to parse proposal: ' + result[1]) continue else: # Some extra work to implement checking all possible # keys. raise NotImplementedError() # Completed processing all proposals without any logic # errors (whether the proposals were valid or accepted # or not). return True