Esempio n. 1
0
    def _verify_ioauth_data(self, ioauth_data):
        verified_data = []
        # Need to authorize against the btc pubkey first.
        for nick, nickdata in ioauth_data.items():
            utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata
            if not self.auth_counterparty(btc_sig, auth_pub, maker_pk):
                jlog.debug(
                    "Counterparty encryption verification failed, aborting: " + nick)
                # This counterparty must be rejected
                continue

            if not validate_address(cj_addr)[0]\
                    or not validate_address(change_addr)[0]:
                jlog.warn("Counterparty provided invalid address: {}".format(
                    (cj_addr, change_addr)))
                # Interpreted as malicious
                self.add_ignored_makers([nick])
                continue

            try:
                maker_inputs_data = self._verify_ioauth_inputs(
                    nick, utxo_list, auth_pub)
            except IoauthInputVerificationError as e:
                for msg in e.messages:
                    jlog.warning(msg)
                continue

            verified_data.append(maker_inputs_data._replace(
                utxo_list=utxo_list, cj_addr=cj_addr, change_addr=change_addr))
        return verified_data
Esempio n. 2
0
def test_b58_invalid_addresses(setup_addresses):
    #none of these are valid as any kind of key or address
    with open(os.path.join(testdir,"base58_keys_invalid.json"), "r") as f:
        json_data = f.read()
    invalid_key_list = json.loads(json_data)
    for k in invalid_key_list:
        bad_key = k[0]
        res, message = validate_address(bad_key)
        assert res == False, "Incorrectly validated address: " + bad_key + " with message: " + message
Esempio n. 3
0
def test_invalid_bech32_addresses():
    invalids = [
    "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty",
    "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5",
    "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2",
    "bc1rw5uspcuh",
    "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90",
    "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
    "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7",
    "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du",
    "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv",
    "bc1gmk9yu"]
    for iva in invalids:
        print("Testing this address: ", iva)
        res, message = validate_address(iva)
        assert res == False, "Incorrectly validated address: " + iva
Esempio n. 4
0
def test_valid_bech32_addresses():
    valids = ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
    "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
    "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx",
    "BC1SW50QA3JX3S",
    "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj",
    "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"]
    for va in valids:
        print("Testing this address: ", va)
        if va.lower()[:2] == "bc":
            jm_single().config.set("BLOCKCHAIN", "network", "mainnet")
        else:
            jm_single().config.set("BLOCKCHAIN", "network", "testnet")
        res, message = validate_address(va)
        assert res == True, "Incorrect failed to validate address: " + va + " with message: " + message
    jm_single().config.set("BLOCKCHAIN", "network", "testnet")
Esempio n. 5
0
def test_b58_valid_addresses():
    with open(os.path.join(testdir,"base58_keys_valid.json"), "r") as f:
        json_data = f.read()
    valid_keys_list = json.loads(json_data)
    for a in valid_keys_list:
        addr, pubkey, prop_dict = a
        if not prop_dict["isPrivkey"]:
            if prop_dict["isTestnet"]:
                jm_single().config.set("BLOCKCHAIN", "network", "testnet")
            else:
                jm_single().config.set("BLOCKCHAIN", "network", "mainnet")
            #if using pytest -s ; sanity check to see what's actually being tested
            print('testing this address: ', addr)
            res, message = validate_address(addr)
            assert res == True, "Incorrectly failed to validate address: " + addr + " with message: " + message
    jm_single().config.set("BLOCKCHAIN", "network", "testnet")
Esempio n. 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)))
def test_valid_bech32_addresses():
    valids = ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
    "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
    # TODO these are valid bech32 addresses but rejected by bitcointx
    # because they are not witness version 0; add others.
    #"bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx",
    #"BC1SW50QA3JX3S",
    #"bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj",
    "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"]
    for va in valids:
        if va.lower()[:2] == "bc":
            jm_single().config.set("BLOCKCHAIN", "network", "mainnet")
            btc.select_chain_params("bitcoin")
        else:
            jm_single().config.set("BLOCKCHAIN", "network", "testnet")
            btc.select_chain_params("bitcoin/testnet")
        res, message = validate_address(va)
        assert res == True, "Incorrect failed to validate address: " + va + " with message: " + message
    jm_single().config.set("BLOCKCHAIN", "network", "testnet")
    btc.select_chain_params("bitcoin/regtest")
Esempio n. 8
0
    def receive_utxos(self, ioauth_data):
        """Triggered when the daemon returns utxo data from
        makers who responded; this is the completion of phase 1
        of the protocol
        """
        if self.aborted:
            return (False, "User aborted")

        #Temporary list used to aggregate all ioauth data that must be removed
        rejected_counterparties = []

        #Need to authorize against the btc pubkey first.
        for nick, nickdata in ioauth_data.items():
            utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata
            if not self.auth_counterparty(btc_sig, auth_pub, maker_pk):
                jlog.debug(
                    "Counterparty encryption verification failed, aborting: " +
                    nick)
                #This counterparty must be rejected
                rejected_counterparties.append(nick)

            if not validate_address(cj_addr)[0] or not validate_address(
                    change_addr)[0]:
                jlog.warn("Counterparty provided invalid address: {}".format(
                    (cj_addr, change_addr)))
                # Interpreted as malicious
                self.add_ignored_makers([nick])
                rejected_counterparties.append(nick)

        for rc in rejected_counterparties:
            del ioauth_data[rc]

        self.maker_utxo_data = {}

        for nick, nickdata in ioauth_data.items():
            utxo_list, auth_pub, cj_addr, change_addr, _, _ = nickdata
            utxo_data = jm_single().bc_interface.query_utxo_set(utxo_list)
            self.utxos[nick] = utxo_list
            if None in utxo_data:
                jlog.warn(('ERROR outputs unconfirmed or already spent. '
                           'utxo_data={}').format(pprint.pformat(utxo_data)))
                jlog.warn('Disregarding this counterparty.')
                del self.utxos[nick]
                continue

            #Complete maker authorization:
            #Extract the address fields from the utxos
            #Construct the Bitcoin address for the auth_pub field
            #Ensure that at least one address from utxos corresponds.
            for inp in utxo_data:
                try:
                    if self.wallet_service.pubkey_has_script(
                            auth_pub, inp['script']):
                        break
                except EngineError as e:
                    pass
            else:
                jlog.warn("ERROR maker's (" + nick + ")"
                          " authorising pubkey is not included "
                          "in the transaction!")
                #this will not be added to the transaction, so we will have
                #to recheck if we have enough
                continue
            total_input = sum([d['value'] for d in utxo_data])
            real_cjfee = calc_cj_fee(self.orderbook[nick]['ordertype'],
                                     self.orderbook[nick]['cjfee'],
                                     self.cjamount)
            change_amount = (total_input - self.cjamount -
                             self.orderbook[nick]['txfee'] + real_cjfee)

            # certain malicious and/or incompetent liquidity providers send
            # inputs totalling less than the coinjoin amount! this leads to
            # a change output of zero satoshis; this counterparty must be removed.
            if change_amount < jm_single().DUST_THRESHOLD:
                fmt = ('ERROR counterparty requires sub-dust change. nick={}'
                       'totalin={:d} cjamount={:d} change={:d}').format
                jlog.warn(fmt(nick, total_input, self.cjamount, change_amount))
                jlog.warn("Invalid change, too small, nick= " + nick)
                continue

            self.outputs.append({
                'address': change_addr,
                'value': change_amount
            })
            fmt = ('fee breakdown for {} totalin={:d} '
                   'cjamount={:d} txfee={:d} realcjfee={:d}').format
            jlog.info(
                fmt(nick, total_input, self.cjamount,
                    self.orderbook[nick]['txfee'], real_cjfee))
            self.outputs.append({'address': cj_addr, 'value': self.cjamount})
            self.cjfee_total += real_cjfee
            self.maker_txfee_contributions += self.orderbook[nick]['txfee']
            self.maker_utxo_data[nick] = utxo_data
            #We have succesfully processed the data from this nick:
            try:
                self.nonrespondants.remove(nick)
            except Exception as e:
                jlog.warn("Failure to remove counterparty from nonrespondants list: " + str(nick) + \
                          ", error message: " + repr(e))

        #Apply business logic of how many counterparties are enough; note that
        #this must occur after the above ioauth data processing, since we only now
        #know for sure that the data meets all business-logic requirements.
        if len(self.maker_utxo_data) < jm_single().config.getint(
                "POLICY", "minimum_makers"):
            self.taker_info_callback("INFO",
                                     "Not enough counterparties, aborting.")
            return (False,
                    "Not enough counterparties responded to fill, giving up")

        self.taker_info_callback("INFO", "Got all parts, enough to build a tx")

        #The list self.nonrespondants is now reset and
        #used to track return of signatures for phase 2
        self.nonrespondants = list(self.maker_utxo_data.keys())

        my_total_in = sum([va['value'] for u, va in self.input_utxos.items()])
        if self.my_change_addr:
            #Estimate fee per choice of next/3/6 blocks targetting.
            estimated_fee = estimate_tx_fee(
                len(sum(self.utxos.values(), [])),
                len(self.outputs) + 2,
                txtype=self.wallet_service.get_txtype())
            jlog.info("Based on initial guess: " +
                      btc.amount_to_str(self.total_txfee) +
                      ", we estimated a miner fee of: " +
                      btc.amount_to_str(estimated_fee))
            #reset total
            self.total_txfee = estimated_fee
        my_txfee = max(self.total_txfee - self.maker_txfee_contributions, 0)
        my_change_value = (my_total_in - self.cjamount - self.cjfee_total -
                           my_txfee)
        #Since we could not predict the maker's inputs, we may end up needing
        #too much such that the change value is negative or small. Note that
        #we have tried to avoid this based on over-estimating the needed amount
        #in SendPayment.create_tx(), but it is still a possibility if one maker
        #uses a *lot* of inputs.
        if self.my_change_addr:
            if my_change_value < -1:
                raise ValueError(
                    "Calculated transaction fee of: " +
                    btc.amount_to_str(self.total_txfee) +
                    " is too large for our inputs; Please try again.")
            if my_change_value <= jm_single().BITCOIN_DUST_THRESHOLD:
                jlog.info("Dynamically calculated change lower than dust: " +
                          btc.amount_to_str(my_change_value) + "; dropping.")
                self.my_change_addr = None
                my_change_value = 0
        jlog.info(
            'fee breakdown for me totalin=%d my_txfee=%d makers_txfee=%d cjfee_total=%d => changevalue=%d'
            % (my_total_in, my_txfee, self.maker_txfee_contributions,
               self.cjfee_total, my_change_value))
        if self.my_change_addr is None:
            if my_change_value != 0 and abs(my_change_value) != 1:
                # seems you wont always get exactly zero because of integer
                # rounding so 1 satoshi extra or fewer being spent as miner
                # fees is acceptable
                jlog.info(
                    ('WARNING CHANGE NOT BEING USED\nCHANGEVALUE = {}').format(
                        btc.amount_to_str(my_change_value)))
            # we need to check whether the *achieved* txfee-rate is outside
            # the range allowed by the user in config; if not, abort the tx.
            # this is done with using the same estimate fee function and comparing
            # the totals; this ratio will correspond to the ratio of the feerates.
            num_ins = len([u for u in sum(self.utxos.values(), [])])
            num_outs = len(self.outputs) + 2
            new_total_fee = estimate_tx_fee(
                num_ins, num_outs, txtype=self.wallet_service.get_txtype())
            feeratio = self.total_txfee / new_total_fee
            jlog.debug(
                "Ratio of actual to estimated sweep fee: {}".format(feeratio))
            sweep_delta = float(jm_single().config.get("POLICY",
                                                       "max_sweep_fee_change"))
            if feeratio < 1 - sweep_delta or feeratio > 1 + sweep_delta:
                jlog.warn(
                    "Transaction fee for sweep: {} too far from expected:"
                    " {}; check the setting 'max_sweep_fee_change'"
                    " in joinmarket.cfg. Aborting this attempt.".format(
                        self.total_txfee, new_total_fee))
                return (False, "Unacceptable feerate for sweep, giving up.")
        else:
            self.outputs.append({
                'address': self.my_change_addr,
                'value': my_change_value
            })
        self.utxo_tx = [u for u in sum(self.utxos.values(), [])]
        self.outputs.append({
            'address': self.coinjoin_address(),
            'value': self.cjamount
        })
        # pre-Nov-2020/v0.8.0: transactions used ver 1 and nlocktime 0
        # so only the new "pit" (using native segwit) will use the updated
        # version 2 and nlocktime ~ current block as per normal payments.
        # TODO makers do not check this; while there is no security risk,
        # it might be better for them to sanity check.
        if self.wallet_service.get_txtype() == "p2wpkh":
            n_version = 2
            locktime = compute_tx_locktime()
        else:
            n_version = 1
            locktime = 0
        self.latest_tx = btc.make_shuffled_tx(self.utxo_tx,
                                              self.outputs,
                                              version=n_version,
                                              locktime=locktime)
        jlog.info('obtained tx\n' +
                  btc.human_readable_transaction(self.latest_tx))

        self.taker_info_callback("INFO",
                                 "Built tx, sending to counterparties.")
        return (True, list(self.maker_utxo_data.keys()),
                bintohex(self.latest_tx.serialize()))
Esempio n. 9
0
def test_non_addresses(setup_addresses):
    #could flesh this out with other examples
    res, msg = validate_address(2)
    assert res == False, "Incorrectly accepted number"
    def receive_utxos(self, ioauth_data):
        """Triggered when the daemon returns utxo data from
        makers who responded; this is the completion of phase 1
        of the protocol
        """
        if self.aborted:
            return (False, "User aborted")

        #Temporary list used to aggregate all ioauth data that must be removed
        rejected_counterparties = []
        #Need to authorize against the btc pubkey first.
        for nick, nickdata in ioauth_data.iteritems():
            utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata
            if not self.auth_counterparty(btc_sig, auth_pub, maker_pk):
                jlog.debug(
                    "Counterparty encryption verification failed, aborting: " +
                    nick)
                #This counterparty must be rejected
                rejected_counterparties.append(nick)

            if not validate_address(cj_addr)[0] or not validate_address(
                    change_addr)[0]:
                jlog.warn("Counterparty provided invalid address: {}".format(
                    (cj_addr, change_addr)))
                # Interpreted as malicious
                self.add_ignored_makers([nick])
                rejected_counterparties.append(nick)

        for rc in rejected_counterparties:
            del ioauth_data[rc]

        self.maker_utxo_data = {}

        for nick, nickdata in ioauth_data.iteritems():
            utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata
            self.utxos[nick] = utxo_list
            utxo_data = jm_single().bc_interface.query_utxo_set(
                self.utxos[nick])
            if None in utxo_data:
                jlog.warn(('ERROR outputs unconfirmed or already spent. '
                           'utxo_data={}').format(pprint.pformat(utxo_data)))
                jlog.warn('Disregarding this counterparty.')
                del self.utxos[nick]
                continue

            #Complete maker authorization:
            #Extract the address fields from the utxos
            #Construct the Bitcoin address for the auth_pub field
            #Ensure that at least one address from utxos corresponds.
            input_addresses = [d['address'] for d in utxo_data]
            # FIXME: This only works if taker's commitment address is of same type
            # as our wallet.
            auth_address = self.wallet.pubkey_to_addr(unhexlify(auth_pub))
            if not auth_address in input_addresses:
                jlog.warn("ERROR maker's (" + nick + ")"
                          " authorising pubkey is not included "
                          "in the transaction: " + str(auth_address))
                #this will not be added to the transaction, so we will have
                #to recheck if we have enough
                continue
            total_input = sum([d['value'] for d in utxo_data])
            real_cjfee = calc_cj_fee(self.orderbook[nick]['ordertype'],
                                     self.orderbook[nick]['cjfee'],
                                     self.cjamount)
            change_amount = (total_input - self.cjamount -
                             self.orderbook[nick]['txfee'] + real_cjfee)

            # certain malicious and/or incompetent liquidity providers send
            # inputs totalling less than the coinjoin amount! this leads to
            # a change output of zero satoshis; this counterparty must be removed.
            if change_amount < jm_single().DUST_THRESHOLD:
                fmt = ('ERROR counterparty requires sub-dust change. nick={}'
                       'totalin={:d} cjamount={:d} change={:d}').format
                jlog.warn(fmt(nick, total_input, self.cjamount, change_amount))
                jlog.warn("Invalid change, too small, nick= " + nick)
                continue

            self.outputs.append({
                'address': change_addr,
                'value': change_amount
            })
            fmt = ('fee breakdown for {} totalin={:d} '
                   'cjamount={:d} txfee={:d} realcjfee={:d}').format
            jlog.info(
                fmt(nick, total_input, self.cjamount,
                    self.orderbook[nick]['txfee'], real_cjfee))
            self.outputs.append({'address': cj_addr, 'value': self.cjamount})
            self.cjfee_total += real_cjfee
            self.maker_txfee_contributions += self.orderbook[nick]['txfee']
            self.maker_utxo_data[nick] = utxo_data
            #We have succesfully processed the data from this nick:
            try:
                self.nonrespondants.remove(nick)
            except Exception as e:
                jlog.warn("Failure to remove counterparty from nonrespondants list: " + str(nick) + \
                          ", error message: " + repr(e))

        #Apply business logic of how many counterparties are enough; note that
        #this must occur after the above ioauth data processing, since we only now
        #know for sure that the data meets all business-logic requirements.
        if len(self.maker_utxo_data.keys()) < jm_single().config.getint(
                "POLICY", "minimum_makers"):
            self.taker_info_callback("INFO",
                                     "Not enough counterparties, aborting.")
            return (False,
                    "Not enough counterparties responded to fill, giving up")

        self.taker_info_callback("INFO", "Got all parts, enough to build a tx")

        #The list self.nonrespondants is now reset and
        #used to track return of signatures for phase 2
        self.nonrespondants = list(self.maker_utxo_data.keys())

        my_total_in = sum(
            [va['value'] for u, va in self.input_utxos.iteritems()])
        if self.my_change_addr:
            #Estimate fee per choice of next/3/6 blocks targetting.
            estimated_fee = estimate_tx_fee(len(sum(self.utxos.values(), [])),
                                            len(self.outputs) + 2,
                                            txtype=self.wallet.get_txtype())
            jlog.info("Based on initial guess: " + str(self.total_txfee) +
                      ", we estimated a miner fee of: " + str(estimated_fee))
            #reset total
            self.total_txfee = estimated_fee
        my_txfee = max(self.total_txfee - self.maker_txfee_contributions, 0)
        my_change_value = (my_total_in - self.cjamount - self.cjfee_total -
                           my_txfee)
        #Since we could not predict the maker's inputs, we may end up needing
        #too much such that the change value is negative or small. Note that
        #we have tried to avoid this based on over-estimating the needed amount
        #in SendPayment.create_tx(), but it is still a possibility if one maker
        #uses a *lot* of inputs.
        if self.my_change_addr and my_change_value <= 0:
            raise ValueError("Calculated transaction fee of: " +
                             str(self.total_txfee) +
                             " is too large for our inputs;Please try again.")
        elif self.my_change_addr and my_change_value <= jm_single(
        ).BITCOIN_DUST_THRESHOLD:
            jlog.info("Dynamically calculated change lower than dust: " +
                      str(my_change_value) + "; dropping.")
            self.my_change_addr = None
            my_change_value = 0
        jlog.info(
            'fee breakdown for me totalin=%d my_txfee=%d makers_txfee=%d cjfee_total=%d => changevalue=%d'
            % (my_total_in, my_txfee, self.maker_txfee_contributions,
               self.cjfee_total, my_change_value))
        if self.my_change_addr is None:
            if my_change_value != 0 and abs(my_change_value) != 1:
                # seems you wont always get exactly zero because of integer
                # rounding so 1 satoshi extra or fewer being spent as miner
                # fees is acceptable
                jlog.info(('WARNING CHANGE NOT BEING '
                           'USED\nCHANGEVALUE = {}').format(my_change_value))
        else:
            self.outputs.append({
                'address': self.my_change_addr,
                'value': my_change_value
            })
        self.utxo_tx = [
            dict([('output', u)]) for u in sum(self.utxos.values(), [])
        ]
        self.outputs.append({
            'address': self.coinjoin_address(),
            'value': self.cjamount
        })
        random.shuffle(self.utxo_tx)
        random.shuffle(self.outputs)
        tx = btc.mktx(self.utxo_tx, self.outputs)
        jlog.info('obtained tx\n' + pprint.pformat(btc.deserialize(tx)))

        self.latest_tx = btc.deserialize(tx)
        for index, ins in enumerate(self.latest_tx['ins']):
            utxo = ins['outpoint']['hash'] + ':' + str(
                ins['outpoint']['index'])
            if utxo not in self.input_utxos.keys():
                continue
            # placeholders required
            ins['script'] = 'deadbeef'
        self.taker_info_callback("INFO",
                                 "Built tx, sending to counterparties.")
        return (True, self.maker_utxo_data.keys(), tx)
def address_valid_somewhere(addr):
    for x in ["bitcoin", "bitcoin/testnet", "bitcoin/regtest"]:
        btc.select_chain_params(x)
        if validate_address(addr)[0]:
            return True
    return False