Exemple #1
0
    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)
Exemple #2
0
def get_address_generator(script_pre, script_post, p2sh=False):
    counter = 0
    while True:
        script = script_pre + struct.pack(b'=LQQ', 0, 0, counter) + script_post
        if p2sh:
            addr = btc.CCoinAddress.from_scriptPubKey(
                btc.CScript(script).to_p2sh_scriptPubKey())
        else:
            addr = btc.CCoinAddress.from_scriptPubKey(btc.CScript(script))
        yield str(addr), binascii.hexlify(script).decode('ascii')
        counter += 1
def detect_script_type(script_str):
    """ Given a scriptPubKey, decide which engine
    to use, one of: p2pkh, p2sh-p2wpkh, p2wpkh.
    Note that for the p2sh case, we are assuming the nature
    of the redeem script (p2wpkh wrapped) because that is what
    we support; but we can't know for sure, from the sPK only.
    Raises EngineError if the type cannot be detected, so
    callers MUST handle this exception to avoid crashes.
    """
    script = btc.CScript(script_str)
    if not script.is_valid():
        raise EngineError("Unknown script type for script '{}'"
                          .format(bintohex(script_str)))
    if script.is_p2pkh():
        return TYPE_P2PKH
    elif script.is_p2sh():
        # see note above.
        # note that is_witness_v0_nested_keyhash does not apply,
        # since that picks up scriptSigs not scriptPubKeys.
        return TYPE_P2SH_P2WPKH
    elif script.is_witness_v0_keyhash():
        return TYPE_P2WPKH
    elif script.is_witness_v0_scripthash():
        return TYPE_P2WSH
    raise EngineError("Unknown script type for script '{}'"
                      .format(bintohex(script_str)))
Exemple #4
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
Exemple #5
0
 def script_to_address(cls, script):
     """ a script passed in as binary converted to a
     Bitcoin address of the appropriate type.
     """
     s = btc.CScript(script)
     assert s.is_valid()
     return str(btc.CCoinAddress.from_scriptPubKey(s))
Exemple #6
0
def test_valid_bip341_scriptpubkeys_addresses():
    with ChainParams("bitcoin"):
        with open(os.path.join(testdir, "bip341_wallet_test_vectors.json"),
                  "r") as f:
            json_data = json.loads(f.read())
        for x in json_data["scriptPubKey"]:
            sPK = hextobin(x["expected"]["scriptPubKey"])
            addr = x["expected"]["bip350Address"]
            res, message = validate_address(addr)
            assert res, message
            print("address {} was valid bech32m".format(addr))
            # test this specific conversion because this is how
            # our human readable outputs work:
            assert str(CCoinAddress.from_scriptPubKey(
                btc.CScript(sPK))) == addr
            print("and it converts correctly from scriptPubKey: {}".format(
                btc.CScript(sPK)))
Exemple #7
0
def test_sign_standard_txs(addrtype):
    # liberally copied from python-bitcoinlib tests,
    # in particular see:
    # https://github.com/petertodd/python-bitcoinlib/pull/227

    # Create the (in)famous correct brainwallet secret key.
    priv = hashlib.sha256(b'correct horse battery staple').digest() + b"\x01"
    pub = btc.privkey_to_pubkey(priv)

    # Create an address from that private key.
    # (note that the input utxo is fake so we are really only creating
    # a destination here).
    scriptPubKey = btc.CScript([btc.OP_0, btc.Hash160(pub)])
    address = btc.P2WPKHCoinAddress.from_scriptPubKey(scriptPubKey)

    # Create a dummy outpoint; use same 32 bytes for convenience
    txid = priv[:32]
    vout = 2
    amount = btc.coins_to_satoshi(float('0.12345'))

    # Calculate an amount for the upcoming new UTXO. Set a high fee to bypass
    # bitcoind minfee setting.
    amount_less_fee = int(amount - btc.coins_to_satoshi(0.01))

    # Create a destination to send the coins.
    destination_address = address
    target_scriptPubKey = scriptPubKey

    # Create the unsigned transaction.
    txin = btc.CTxIn(btc.COutPoint(txid[::-1], vout))
    txout = btc.CTxOut(amount_less_fee, target_scriptPubKey)
    tx = btc.CMutableTransaction([txin], [txout])

    # Calculate the signature hash for the transaction. This is then signed by the
    # private key that controls the UTXO being spent here at this txin_index.
    if addrtype == "p2wpkh":
        sig, msg = btc.sign(tx, 0, priv, amount=amount, native="p2wpkh")
    elif addrtype == "p2sh-p2wpkh":
        sig, msg = btc.sign(tx, 0, priv, amount=amount, native=False)
    elif addrtype == "p2pkh":
        sig, msg = btc.sign(tx, 0, priv)
    else:
        assert False
    if not sig:
        print(msg)
        raise
    print("created signature: ", bintohex(sig))
    print("serialized transaction: {}".format(bintohex(tx.serialize())))
    print("deserialized transaction: {}\n".format(
        btc.human_readable_transaction(tx)))
def test_mk_shuffled_tx():
    # prepare two addresses for the outputs
    pub = btc.privkey_to_pubkey(btc.Hash(b"priv") + b"\x01")
    scriptPubKey = btc.CScript([btc.OP_0, btc.Hash160(pub)])
    addr1 = btc.P2WPKHCoinAddress.from_scriptPubKey(scriptPubKey)
    scriptPubKey_p2sh = scriptPubKey.to_p2sh_scriptPubKey()
    addr2 = btc.CCoinAddress.from_scriptPubKey(scriptPubKey_p2sh)

    ins = [(btc.Hash(b"blah"), 7), (btc.Hash(b"foo"), 15)]
    # note the casts str() ; most calls to mktx will have addresses fed
    # as strings, so this is enforced for simplicity.
    outs = [{"address": str(addr1), "value": btc.coins_to_satoshi(float("0.1"))},
            {"address": str(addr2), "value": btc.coins_to_satoshi(float("45981.23331234"))}]
    tx = btc.make_shuffled_tx(ins, outs, version=2, locktime=500000)
Exemple #9
0
 def self_sign(self):
     # now sign it ourselves
     our_inputs = {}
     for index, ins in enumerate(self.latest_tx.vin):
         if not self._is_our_input(ins):
             continue
         utxo = (ins.prevout.hash[::-1], ins.prevout.n)
         self.latest_tx.vin[index].scriptSig = btc.CScript(b'')
         script = self.input_utxos[utxo]['script']
         amount = self.input_utxos[utxo]['value']
         our_inputs[index] = (script, amount)
     success, msg = self.wallet_service.sign_tx(self.latest_tx, our_inputs)
     if not success:
         jlog.error("Failed to sign transaction: " + msg)
Exemple #10
0
def test_signing_simple(setup_wallet, wallet_cls, type_check):
    jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
    storage = VolatileStorage()
    wallet_cls.initialize(storage, get_network(), entropy=b"\xaa"*16)
    wallet = wallet_cls(storage)
    utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(0))
    # The dummy output is constructed as an unspendable p2sh:
    tx = btc.mktx([utxo],
            [{"address": str(btc.CCoinAddress.from_scriptPubKey(
                btc.CScript(b"\x00").to_p2sh_scriptPubKey())),
              "value": 10**8 - 9000}])    
    script = wallet.get_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
    success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
    assert success, msg
    type_check(tx)
    txout = jm_single().bc_interface.pushtx(tx.serialize())
    assert txout
Exemple #11
0
def test_signing_imported(setup_wallet, wif, type_check):
    jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
    storage = VolatileStorage()
    SegwitLegacyWallet.initialize(storage, get_network())
    wallet = SegwitLegacyWallet(storage)

    MIXDEPTH = 0
    path = wallet.import_private_key(MIXDEPTH, wif)
    utxo = fund_wallet_addr(wallet, wallet.get_address_from_path(path))
    # The dummy output is constructed as an unspendable p2sh:
    tx = btc.mktx([utxo],
                [{"address": str(btc.CCoinAddress.from_scriptPubKey(
                    btc.CScript(b"\x00").to_p2sh_scriptPubKey())),
                  "value": 10**8 - 9000}])    
    script = wallet.get_script_from_path(path)
    success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
    assert success, msg
    type_check(tx)
    txout = jm_single().bc_interface.pushtx(tx.serialize())
    assert txout
def test_all_same_priv(setup_tx_creation):
    #recipient
    priv = b"\xaa"*32 + b"\x01"
    pub = bitcoin.privkey_to_pubkey(priv)
    addr = str(bitcoin.CCoinAddress.from_scriptPubKey(
        bitcoin.CScript([bitcoin.OP_0, bitcoin.Hash160(pub)])))
    wallet_service = make_wallets(1, [[1,0,0,0,0]], 1)[0]['wallet']
    #make another utxo on the same address
    addrinwallet = wallet_service.get_addr(0,0,0)
    jm_single().bc_interface.grab_coins(addrinwallet, 1)
    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 = {}
    for i, j in enumerate(ins):
        scripts[i] = (insfull[j]["script"], insfull[j]["value"])
    success, msg = wallet_service.sign_tx(tx, scripts)
    assert success, msg
Exemple #13
0
    def on_tx_received(self, nick, txser):
        """ Called when the sender-counterparty has sent a transaction proposal.
        1. First we check for the expected destination and amount (this is
           sufficient to identify our cp, as this info was presumably passed
           out of band, as for any normal payment).
        2. Then we verify the validity of the proposed non-coinjoin
           transaction; if not, reject, otherwise store this as a
           fallback transaction in case the protocol doesn't complete.
        3. Next, we select utxos from our wallet, to add into the
           payment transaction as input. Try to select so as to not
           trigger the UIH2 condition, but continue (and inform user)
           even if we can't (if we can't select any coins, broadcast the
           non-coinjoin payment, if the user agrees).
           Proceeding with payjoin:
        4. We update the output amount at the destination address.
        5. We modify the change amount in the original proposal (which
           will be the only other output other than the destination),
           reducing it to account for the increased transaction fee
           caused by our additional proposed input(s).
        6. Finally we sign our own input utxo(s) and re-serialize the
           tx, allowing it to be sent back to the counterparty.
        7. If the transaction is not fully signed and broadcast within
           the time unconfirm_timeout_sec as specified in the joinmarket.cfg,
           we broadcast the non-coinjoin fallback tx instead.
        """
        try:
            tx = btc.CMutableTransaction.deserialize(txser)
        except Exception as e:
            return (False, 'malformed txhex. ' + repr(e))
        self.user_info('obtained proposed fallback (non-coinjoin) ' +\
                       'transaction from sender:\n' + str(tx))

        if len(tx.vout) != 2:
            return (False,
                    "Transaction has more than 2 outputs; not supported.")
        dest_found = False
        destination_index = -1
        change_index = -1
        proposed_change_value = 0
        for index, out in enumerate(tx.vout):
            if out.scriptPubKey == btc.CCoinAddress(
                    self.destination_addr).to_scriptPubKey():
                # we found the expected destination; is the amount correct?
                if not out.nValue == self.receiving_amount:
                    return (False,
                            "Wrong payout value in proposal from sender.")
                dest_found = True
                destination_index = index
            else:
                change_found = True
                proposed_change_out = out.scriptPubKey
                proposed_change_value = out.nValue
                change_index = index

        if not dest_found:
            return (False, "Our expected destination address was not found.")

        # Verify valid input utxos provided and check their value.
        # batch retrieval of utxo data
        utxo = {}
        ctr = 0
        for index, ins in enumerate(tx.vin):
            utxo_for_checking = (ins.prevout.hash[::-1], ins.prevout.n)
            utxo[ctr] = [index, utxo_for_checking]
            ctr += 1

        utxo_data = jm_single().bc_interface.query_utxo_set(
            [x[1] for x in utxo.values()])

        total_sender_input = 0
        for i, u in utxo.items():
            if utxo_data[i] is None:
                return (False, "Proposed transaction contains invalid utxos")
            total_sender_input += utxo_data[i]["value"]

        # Check that the transaction *as proposed* balances; check that the
        # included fee is within 0.3-3x our own current estimates, if not user
        # must decide.
        btc_fee = total_sender_input - self.receiving_amount - proposed_change_value
        self.user_info("Network transaction fee of fallback tx is: " +
                       str(btc_fee) + " satoshis.")
        fee_est = estimate_tx_fee(len(tx.vin),
                                  len(tx.vout),
                                  txtype=self.wallet_service.get_txtype())
        fee_ok = False
        if btc_fee > 0.3 * fee_est and btc_fee < 3 * fee_est:
            fee_ok = True
        else:
            if self.user_check("Is this transaction fee acceptable? (y/n):"):
                fee_ok = True
        if not fee_ok:
            return (False,
                    "Proposed transaction fee not accepted due to tx fee: " +
                    str(btc_fee))

        # This direct rpc call currently assumes Core 0.17, so not using now.
        # It has the advantage of (a) being simpler and (b) allowing for any
        # non standard coins.
        #
        #res = jm_single().bc_interface.rpc('testmempoolaccept', [txser])
        #print("Got this result from rpc call: ", res)
        #if not res["accepted"]:
        #    return (False, "Proposed transaction was rejected from mempool.")

        # Manual verification of the transaction signatures.
        # TODO handle native segwit properly
        for i, u in utxo.items():
            if not btc.verify_tx_input(
                    tx,
                    i,
                    tx.vin[i].scriptSig,
                    btc.CScript(utxo_data[i]["script"]),
                    amount=utxo_data[i]["value"],
                    witness=tx.wit.vtxinwit[i].scriptWitness):
                return (False, "Proposed transaction is not correctly signed.")

        # At this point we are satisfied with the proposal. Record the fallback
        # in case the sender disappears and the payjoin tx doesn't happen:
        self.user_info(
            "We'll use this serialized transaction to broadcast if your"
            " counterparty fails to broadcast the payjoin version:")
        self.user_info(bintohex(txser))
        # Keep a local copy for broadcast fallback:
        self.fallback_tx = tx

        # Now we add our own inputs:
        # See the gist comment here:
        # https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2799709
        # which sets out the decision Bob must make.
        # In cases where Bob can add any amount, he selects one utxo
        # to keep it simple.
        # In cases where he must choose at least X, he selects one utxo
        # which provides X if possible, otherwise defaults to a normal
        # selection algorithm.
        # In those cases where he must choose X but X is unavailable,
        # he selects all coins, and proceeds anyway with payjoin, since
        # it has other advantages (CIOH and utxo defrag).
        my_utxos = {}
        largest_out = max(self.receiving_amount, proposed_change_value)
        max_sender_amt = max([u['value'] for u in utxo_data])
        not_uih2 = False
        if max_sender_amt < largest_out:
            # just select one coin.
            # have some reasonable lower limit but otherwise choose
            # randomly; note that this is actually a great way of
            # sweeping dust ...
            self.user_info("Choosing one coin at random")
            try:
                my_utxos = self.wallet_service.select_utxos(
                    self.mixdepth,
                    jm_single().DUST_THRESHOLD,
                    select_fn=select_one_utxo)
            except:
                return self.no_coins_fallback()
            not_uih2 = True
        else:
            # get an approximate required amount assuming 4 inputs, which is
            # fairly conservative (but guess by necessity).
            fee_for_select = estimate_tx_fee(
                len(tx.vin) + 4, 2, txtype=self.wallet_service.get_txtype())
            approx_sum = max_sender_amt - self.receiving_amount + fee_for_select
            try:
                my_utxos = self.wallet_service.select_utxos(
                    self.mixdepth, approx_sum)
                not_uih2 = True
            except Exception:
                # TODO probably not logical to always sweep here.
                self.user_info("Sweeping all coins in this mixdepth.")
                my_utxos = self.wallet_service.get_utxos_by_mixdepth()[
                    self.mixdepth]
                if my_utxos == {}:
                    return self.no_coins_fallback()
        if not_uih2:
            self.user_info("The proposed tx does not trigger UIH2, which "
                           "means it is indistinguishable from a normal "
                           "payment. This is the ideal case. Continuing..")
        else:
            self.user_info("The proposed tx does trigger UIH2, which it makes "
                           "it somewhat distinguishable from a normal payment,"
                           " but proceeding with payjoin..")

        my_total_in = sum([va['value'] for va in my_utxos.values()])
        self.user_info("We selected inputs worth: " + str(my_total_in))
        # adjust the output amount at the destination based on our contribution
        new_destination_amount = self.receiving_amount + my_total_in
        # estimate the required fee for the new version of the transaction
        total_ins = len(tx.vin) + len(my_utxos.keys())
        est_fee = estimate_tx_fee(total_ins,
                                  2,
                                  txtype=self.wallet_service.get_txtype())
        self.user_info("We estimated a fee of: " + str(est_fee))
        new_change_amount = total_sender_input + my_total_in - \
            new_destination_amount - est_fee
        self.user_info("We calculated a new change amount of: " +
                       str(new_change_amount))
        self.user_info("We calculated a new destination amount of: " +
                       str(new_destination_amount))
        # now reconstruct the transaction with the new inputs and the
        # amount-changed outputs
        new_outs = [{
            "address": self.destination_addr,
            "value": new_destination_amount
        }]
        if new_change_amount >= jm_single().BITCOIN_DUST_THRESHOLD:
            new_outs.append({
                "address":
                str(btc.CCoinAddress.from_scriptPubKey(proposed_change_out)),
                "value":
                new_change_amount
            })
        new_ins = [x[1] for x in utxo.values()]
        new_ins.extend(my_utxos.keys())
        new_tx = btc.make_shuffled_tx(new_ins, new_outs, 2,
                                      compute_tx_locktime())

        # sign our inputs before transfer
        our_inputs = {}
        for index, ins in enumerate(new_tx.vin):
            utxo = (ins.prevout.hash[::-1], ins.prevout.n)
            if utxo not in my_utxos:
                continue
            script = my_utxos[utxo]["script"]
            amount = my_utxos[utxo]["value"]
            our_inputs[index] = (script, amount)

        success, msg = self.wallet_service.sign_tx(new_tx, our_inputs)
        if not success:
            return (False, "Failed to sign new transaction, error: " + msg)
        txinfo = tuple((x.scriptPubKey, x.nValue) for x in new_tx.vout)
        self.wallet_service.register_callbacks([self.on_tx_unconfirmed],
                                               txinfo, "unconfirmed")
        self.wallet_service.register_callbacks([self.on_tx_confirmed], txinfo,
                                               "confirmed")
        # The blockchain interface just abandons monitoring if the transaction
        # is not broadcast before the configured timeout; we want to take
        # action in this case, so we add an additional callback to the reactor:
        reactor.callLater(
            jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec"),
            self.broadcast_fallback)
        return (True, nick, bintohex(new_tx.serialize()))
def test_on_sig(setup_taker, dummyaddr, schedule):
    #plan: create a new transaction with known inputs and dummy outputs;
    #then, create a signature with various inputs, pass in in b64 to on_sig.
    #in order for it to verify, the DummyBlockchainInterface will have to 
    #return the right values in query_utxo_set
    utxos = [(struct.pack(b"B", x) * 32, 1) for x in range(5)]
    #create 2 privkey + utxos that are to be ours
    privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,6)]]
    scripts = [BTC_P2PKH.key_to_script(privs[x]) for x in range(5)]
    addrs = [BTC_P2PKH.privkey_to_address(privs[x]) for x in range(5)]
    fake_query_results = [{'value': 200000000, 'utxo': utxos[x], 'address': addrs[x],
                           'script': scripts[x], 'confirms': 20} for x in range(5)]

    dbci = DummyBlockchainInterface()
    dbci.insert_fake_query_results(fake_query_results)
    jm_single().bc_interface = dbci
    #make a transaction with all the fake results above, and some outputs
    outs = [{'value': 100000000, 'address': dummyaddr},
            {'value': 899990000, 'address': dummyaddr}]
    tx = bitcoin.mktx(utxos, outs)
    # since tx will be updated as it is signed, unlike in real life
    # (where maker signing operation doesn't happen here), we'll create
    # a second copy without the signatures:
    tx2 = bitcoin.mktx(utxos, outs)

    #prepare the Taker with the right intermediate data
    taker = get_taker(schedule=schedule)
    taker.nonrespondants=["cp1", "cp2", "cp3"]
    taker.latest_tx = tx
    #my inputs are the first 2 utxos
    taker.input_utxos = {utxos[0]:
                        {'address': addrs[0],
                         'script': scripts[0],
                         'value': 200000000},
                        utxos[1]:
                        {'address': addrs[1],
                         'script': scripts[1],
                         'value': 200000000}}    
    taker.utxos = {None: utxos[:2], "cp1": [utxos[2]], "cp2": [utxos[3]], "cp3":[utxos[4]]}
    for i in range(2):
        # placeholders required for my inputs
        taker.latest_tx.vin[i].scriptSig = bitcoin.CScript(hextobin('deadbeef'))
        tx2.vin[i].scriptSig = bitcoin.CScript(hextobin('deadbeef'))
    #to prepare for my signing, need to mark cjaddr:
    taker.my_cj_addr = dummyaddr
    #make signatures for the last 3 fake utxos, considered as "not ours":
    sig, msg = bitcoin.sign(tx2, 2, privs[2])
    assert sig, "Failed to sign: " + msg
    sig3 = b64encode(tx2.vin[2].scriptSig)
    taker.on_sig("cp1", sig3)
    #try sending the same sig again; should be ignored
    taker.on_sig("cp1", sig3)
    sig, msg = bitcoin.sign(tx2, 3, privs[3])
    assert sig, "Failed to sign: " + msg
    sig4 = b64encode(tx2.vin[3].scriptSig)
    #try sending junk instead of cp2's correct sig
    assert not taker.on_sig("cp2", str("junk")), "incorrectly accepted junk signature"
    taker.on_sig("cp2", sig4)
    sig, msg = bitcoin.sign(tx2, 4, privs[4])
    assert sig, "Failed to sign: " + msg
    #Before completing with the final signature, which will trigger our own
    #signing, try with an injected failure of query utxo set, which should
    #prevent this signature being accepted.
    dbci.setQUSFail(True)
    sig5 = b64encode(tx2.vin[4].scriptSig)
    assert not taker.on_sig("cp3", sig5), "incorrectly accepted sig5"
    #allow it to succeed, and try again
    dbci.setQUSFail(False)
    #this should succeed and trigger the we-sign code
    taker.on_sig("cp3", sig5)
Exemple #15
0
    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)
Exemple #17
0
def main():
    parser = OptionParser(usage='usage: %prog [options] walletname',
                          description=description)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option('-g',
                      '--gap-limit',
                      type="int",
                      action='store',
                      dest='gaplimit',
                      help='gap limit for wallet, default=6',
                      default=6)
    add_base_options(parser)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    check_regtest()
    if len(args) != 1:
        log.error("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)

    # step 1: do a full recovery style sync. this will pick up
    # all addresses that we expect to match transactions against,
    # from a blank slate Core wallet that originally had no imports.
    if not options.recoversync:
        jmprint("Recovery sync was not set, but using it anyway.")
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=False)
    # Note that the user may be interrupted above by the rescan
    # request; this is as for normal scripts; after the rescan is done
    # (usually, only once, but, this *IS* needed here, unlike a normal
    # wallet generation event), we just try again.

    # Now all address from HD are imported, we need to grab
    # all the transactions for those addresses; this includes txs
    # that *spend* as well as receive our coins, so will include
    # "first-out" SNICKER txs as well as ordinary spends and JM coinjoins.
    seed_transactions = wallet_service.get_all_transactions()

    # Search for SNICKER txs and add them if they match.
    # We proceed recursively; we find all one-out matches, then
    # all 2-out matches, until we find no new ones and stop.

    if len(seed_transactions) == 0:
        jmprint("No transactions were found for this wallet. Did you rescan?")
        return False

    new_txs = []
    current_block_heights = set()
    for tx in seed_transactions:
        if btc.is_snicker_tx(tx):
            jmprint("Found a snicker tx: {}".format(
                bintohex(tx.GetTxid()[::-1])))
            equal_outs = btc.get_equal_outs(tx)
            if not equal_outs:
                continue
            if all([
                    wallet_service.is_known_script(x.scriptPubKey) == False
                    for x in [a[1] for a in equal_outs]
            ]):
                # it is now *very* likely that one of the two equal
                # outputs is our SNICKER custom output
                # script; notice that in this case, the transaction *must*
                # have spent our inputs, since it didn't recognize ownership
                # of either coinjoin output (and if it did recognize the change,
                # it would have recognized the cj output also).
                # We try to regenerate one of the outputs, but warn if
                # we can't.
                my_indices = get_pubs_and_indices_of_inputs(tx,
                                                            wallet_service,
                                                            ours=True)
                for mypub, mi in my_indices:
                    for eo in equal_outs:
                        for (other_pub, i) in get_pubs_and_indices_of_inputs(
                                tx, wallet_service, ours=False):
                            for (our_pub,
                                 j) in get_pubs_and_indices_of_ancestor_inputs(
                                     tx.vin[mi], wallet_service, ours=True):
                                our_spk = wallet_service.pubkey_to_script(
                                    our_pub)
                                our_priv = wallet_service.get_key_from_addr(
                                    wallet_service.script_to_addr(our_spk))
                                tweak_bytes = btc.ecdh(our_priv[:-1],
                                                       other_pub)
                                tweaked_pub = btc.snicker_pubkey_tweak(
                                    our_pub, tweak_bytes)
                                tweaked_spk = wallet_service.pubkey_to_script(
                                    tweaked_pub)
                                if tweaked_spk == eo[1].scriptPubKey:
                                    # TODO wallet.script_to_addr has a dubious assertion, that's why
                                    # we use btc method directly:
                                    address_found = str(
                                        btc.CCoinAddress.from_scriptPubKey(
                                            btc.CScript(tweaked_spk)))
                                    #address_found = wallet_service.script_to_addr(tweaked_spk)
                                    jmprint(
                                        "Found a new SNICKER output belonging to us."
                                    )
                                    jmprint(
                                        "Output address {} in the following transaction:"
                                        .format(address_found))
                                    jmprint(btc.human_readable_transaction(tx))
                                    jmprint(
                                        "Importing the address into the joinmarket wallet..."
                                    )
                                    # NB for a recovery we accept putting any imported keys all into
                                    # the same mixdepth (0); TODO investigate correcting this, it will
                                    # be a little complicated.
                                    success, msg = wallet_service.check_tweak_matches_and_import(
                                        wallet_service.script_to_addr(our_spk),
                                        tweak_bytes, tweaked_pub,
                                        wallet_service.mixdepth)
                                    if not success:
                                        jmprint(
                                            "Failed to import SNICKER key: {}".
                                            format(msg), "error")
                                        return False
                                    else:
                                        jmprint("... success.")
                                    # we want the blockheight to track where the next-round rescan
                                    # must start from
                                    current_block_heights.add(
                                        wallet_service.
                                        get_transaction_block_height(tx))
                                    # add this transaction to the next round.
                                    new_txs.append(tx)
    if len(new_txs) == 0:
        return True
    seed_transactions.extend(new_txs)
    earliest_new_blockheight = min(current_block_heights)
    jmprint("New SNICKER addresses were imported to the Core wallet; "
            "do rescanblockchain again, starting from block {}, before "
            "restarting this script.".format(earliest_new_blockheight))
    return False