Example #1
0
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
Example #2
0
 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)
Example #3
0
 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())
Example #4
0
 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
Example #5
0
 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()
Example #7
0
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