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_p2sh_p2wpkh_script(pub), amount=bitcoin.coins_to_satoshi(1), witness=bitcoin.CScript([sig, pub])) assert res
def attach_signatures(self): """Once all signatures are available, they can be attached to construct a "fully_signed_tx" form of the transaction ready for broadcast (as distinct from the "base_form" without any signatures attached). """ assert self.fully_signed() self.fully_signed_tx = copy.deepcopy(self.base_form) for idx in range(len(self.ins)): tp = self.template.ins[idx].spk_type assert tp in ["NN", "p2sh-p2wpkh"] if tp == "NN": self.fully_signed_tx = btc.apply_p2wsh_multisignatures( self.fully_signed_tx, idx, self.signing_redeem_scripts[idx], self.signatures[idx]) else: k = self.keys["ins"][idx][self.keys["ins"][idx].keys()[0]] dtx = btc.deserialize(self.fully_signed_tx) dtx["ins"][idx][ "script"] = "16" + btc.pubkey_to_p2sh_p2wpkh_script(k) dtx["ins"][idx]["txinwitness"] = [self.signatures[idx][0], k] self.fully_signed_tx = btc.serialize(dtx)
def apply_key(self, key, insouts, idx, cpr): """This is the only way (apart from instantiating the object with all keys in the constructor) to specify the public keys used in the inputs and outputs of the transaction, so must be called once for each. Note that when all the required keys have been provided for a particular input, that input's redeem script will be automatically generated, ready for signing. """ self.keys[insouts][idx][cpr] = key if insouts == "ins": #if all keys are available for this input, #we can set the signing redeem script tp = self.template.ins[idx].spk_type if tp == "p2sh-p2wpkh": #only one signer: apply immediately self.signing_redeem_scripts[ idx] = btc.pubkey_to_p2sh_p2wpkh_script(key) elif tp == "NN": #do we have N signers? if len(self.keys["ins"][idx].keys()) == self.n_counterparties: self.signing_redeem_scripts[idx] = NN_script_from_pubkeys( self.keys["ins"][idx].values())
def mktx(self): """First, construct input and output lists as for a normal transaction construction, using the OCCTemplateTx corresponding inputs and outputs as information. To do this completely requires txids for all inputs. Thus, this must be called for this OCCTx *after* it has been called for all parent txs. We ensure that the txid for this Tx is set here, and is attached to all the Outpoint objects for its outputs. """ self.build_ins_from_template() self.build_outs_from_template() assert all([self.ins, self.outs]) self.base_form = btc.mktx([x[0] for x in self.ins], self.outs) dtx = btc.deserialize(self.base_form) if self.locktime: dtx["ins"][0]["sequence"] = 0 dtx["locktime"] = self.locktime #To set the txid, it's required that we set the #scriptSig and scriptPubkey objects. We don't yet #need to flag it segwit (we're not yet attaching #signatures) since we want txid not wtxid and the #former doesn't use segwit formatting anyway. for i, inp in enumerate(dtx["ins"]): sti = self.template.ins[i] if sti.spk_type == "p2sh-p2wpkh": inp["script"] = "16" + btc.pubkey_to_p2sh_p2wpkh_script( self.keys["ins"][i][sti.counterparty]) elif sti.spk_type == "NN": inp["script"] = "" self.txid = btc.txhash(btc.serialize(dtx)) #by setting the txid of the outpoints, we allow child #transactions to know the outpoint references for their inputs. for to in self.template.outs: to.txid = self.txid
def pubkey_to_script(cls, pubkey): return btc.pubkey_to_p2sh_p2wpkh_script(pubkey)
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_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 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