Esempio n. 1
0
def test_calculate_timelocked_fidelity_bond_value(setup_wallet):
    EPSILON = 0.000001
    YEAR = 60*60*24*356.25

    #the function should be flat anywhere before the locktime ends
    values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
        utxo_value=100000000,
        confirmation_time=0,
        locktime=6*YEAR,
        current_time=y*YEAR,
        interest_rate=0.01
        )
        for y in range(4)
    ]
    value_diff = [values[i] - values[i+1] for i in range(len(values)-1)]
    for vd in value_diff:
        assert abs(vd) < EPSILON

    #after locktime, the value should go down
    values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
        utxo_value=100000000,
        confirmation_time=0,
        locktime=6*YEAR,
        current_time=(6+y)*YEAR,
        interest_rate=0.01
        )
        for y in range(5)
    ]
    value_diff = [values[i+1] - values[i] for i in range(len(values)-1)]
    for vrd in value_diff:
        assert vrd < 0

    #value of a bond goes up as the locktime goes up
    values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
        utxo_value=100000000,
        confirmation_time=0,
        locktime=y*YEAR,
        current_time=0,
        interest_rate=0.01
        )
        for y in range(5)
    ]
    value_ratio = [values[i] / values[i+1] for i in range(len(values)-1)]
    value_ratio_diff = [value_ratio[i] - value_ratio[i+1] for i in range(len(value_ratio)-1)]
    for vrd in value_ratio_diff:
        assert vrd < 0

    #value of a bond locked into the far future is constant, clamped at the value of burned coins
    values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
        utxo_value=100000000,
        confirmation_time=0,
        locktime=(200+y)*YEAR,
        current_time=0,
        interest_rate=0.01
        )
        for y in range(5)
    ]
    value_diff = [values[i] - values[i+1] for i in range(len(values)-1)]
    for vd in value_diff:
        assert abs(vd) < EPSILON
def test_get_bond_values() -> None:
    load_test_config()
    # 1 BTC
    amount = pow(10, 8)
    months = 1
    interest = jm_single().config.getfloat("POLICY", "interest_rate")
    exponent = jm_single().config.getfloat("POLICY", "bond_value_exponent")
    parameters, results = get_bond_values(amount, months)
    assert parameters["amount"] == amount
    assert parameters["current_time"] == parameters["confirm_time"]
    assert parameters["interest"] == interest
    assert parameters["exponent"] == exponent
    assert len(results) == months
    locktime = datetime.fromtimestamp(results[0]["locktime"])
    assert locktime.month == get_next_locktime(datetime.now()).month
    value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
        parameters["amount"],
        parameters["confirm_time"],
        results[0]["locktime"],
        parameters["current_time"],
        parameters["interest"],
    )
    assert results[0]["value"] == value

    months = 12
    interest = 0.02
    exponent = 2
    confirm_time = datetime(2021, 12, 1).timestamp()
    parameters, results = get_bond_values(amount, months, confirm_time,
                                          interest, exponent)
    assert parameters["amount"] == amount
    assert parameters["current_time"] != parameters["confirm_time"]
    assert parameters["confirm_time"] == confirm_time
    assert parameters["interest"] == interest
    assert parameters["exponent"] == exponent
    assert len(results) == months
    current_time = datetime.now()
    # get_bond_values(), at the end, reset the exponent to the config one.
    # So we have to set the exponent here, otherwise the bond value calculation
    # won't match and the assert would fail.
    old_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
    jm_single().config.set("POLICY", "bond_value_exponent", str(exponent))
    for result in results:
        locktime = datetime.fromtimestamp(result["locktime"])
        assert locktime.month == get_next_locktime(current_time).month
        current_time = locktime
        value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
            parameters["amount"],
            parameters["confirm_time"],
            result["locktime"],
            parameters["current_time"],
            parameters["interest"],
        )
        assert result["value"] == value
    jm_single().config.set("POLICY", "bond_value_exponent", old_exponent)
Esempio n. 3
0
def test_timestamp_to_timenumber(setup_wallet, timestamp, timenumber):
    try:
        implied_timenumber = FidelityBondMixin.timestamp_to_time_number(
            timestamp)
        assert implied_timenumber == timenumber
    except ValueError:
        assert timenumber == None
Esempio n. 4
0
def test_timenumber_to_timestamp(setup_wallet, timenumber, timestamp):
    try:
        implied_timestamp = FidelityBondMixin._time_number_to_timestamp(
            timenumber)
        assert implied_timestamp == timestamp
    except ValueError:
        #None means the timenumber is intentionally invalid
        assert timestamp == None
def get_fidelity_bond_data(taker):
    with taker.dblock:
        fbonds = taker.db.execute("SELECT * FROM fidelitybonds;").fetchall()

    blocks = jm_single().bc_interface.get_current_block_height()
    mediantime = jm_single().bc_interface.get_best_block_median_time()
    interest_rate = get_interest_rate()

    bond_utxo_set = set()
    fidelity_bond_data = []
    bond_outpoint_conf_times = []
    fidelity_bond_values = []
    for fb in fbonds:
        try:
            parsed_bond = FidelityBondProof.parse_and_verify_proof_msg(
                fb["counterparty"], fb["takernick"], fb["proof"])
        except ValueError:
            continue
        bond_utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo(
            parsed_bond.utxo, parsed_bond.utxo_pub, parsed_bond.locktime,
            parsed_bond.cert_expiry, blocks)
        if bond_utxo_data == None:
            continue
        #check for duplicated utxos i.e. two or more makers using the same UTXO
        # which is obviously not allowed, a fidelity bond must only be usable by one maker nick
        utxo_str = parsed_bond.utxo[0] + b":" + str(
            parsed_bond.utxo[1]).encode("ascii")
        if utxo_str in bond_utxo_set:
            continue
        bond_utxo_set.add(utxo_str)

        fidelity_bond_data.append((parsed_bond, bond_utxo_data))
        conf_time = jm_single().bc_interface.get_block_time(
            jm_single().bc_interface.get_block_hash(
                blocks - bond_utxo_data["confirms"] + 1))
        bond_outpoint_conf_times.append(conf_time)

        bond_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
            bond_utxo_data["value"], conf_time, parsed_bond.locktime,
            mediantime, interest_rate)
        fidelity_bond_values.append(bond_value)
    return (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times)
    def get_fidelity_bond_template(self):
        if not isinstance(self.wallet_service.wallet, FidelityBondMixin):
            jlog.info(
                "Not a fidelity bond wallet, not announcing fidelity bond")
            return None
        blocks = jm_single().bc_interface.get_current_block_height()
        mediantime = jm_single().bc_interface.get_best_block_median_time()

        BLOCK_COUNT_SAFETY = 2  #use this safety number to reduce chances of the proof expiring
        #before the taker gets a chance to verify it
        RETARGET_INTERVAL = 2016
        CERT_MAX_VALIDITY_TIME = 1
        cert_expiry = ((blocks + BLOCK_COUNT_SAFETY) //
                       RETARGET_INTERVAL) + CERT_MAX_VALIDITY_TIME

        utxos = self.wallet_service.wallet.get_utxos_by_mixdepth(
            include_disabled=True,
            includeheight=True)[FidelityBondMixin.FIDELITY_BOND_MIXDEPTH]
        timelocked_utxos = [
            (outpoint, info) for outpoint, info in utxos.items()
            if FidelityBondMixin.is_timelocked_path(info["path"])
        ]
        if len(timelocked_utxos) == 0:
            jlog.info(
                "No timelocked coins in wallet, not announcing fidelity bond")
            return
        timelocked_utxos_with_confirmation_time = [
            (outpoint, info, jm_single().bc_interface.get_block_time(
                jm_single().bc_interface.get_block_hash(info["height"])))
            for (outpoint, info) in timelocked_utxos
        ]

        interest_rate = get_interest_rate()
        max_valued_bond = max(
            timelocked_utxos_with_confirmation_time,
            key=lambda x: FidelityBondMixin.
            calculate_timelocked_fidelity_bond_value(x[1]["value"], x[2], x[1][
                "path"][-1], mediantime, interest_rate))
        (utxo_priv,
         locktime), engine = self.wallet_service.wallet._get_key_from_path(
             max_valued_bond[1]["path"])
        utxo_pub = engine.privkey_to_pubkey(utxo_priv)
        cert_priv = os.urandom(32) + b"\x01"
        cert_pub = btc.privkey_to_pubkey(cert_priv)
        cert_msg = b"fidelity-bond-cert|" + cert_pub + b"|" + str(
            cert_expiry).encode("ascii")
        cert_sig = base64.b64decode(btc.ecdsa_sign(cert_msg, utxo_priv))
        utxo = (max_valued_bond[0][0], max_valued_bond[0][1])
        fidelity_bond = FidelityBond(utxo, utxo_pub, locktime, cert_expiry,
                                     cert_priv, cert_pub, cert_sig)
        jlog.info("Announcing fidelity bond coin {}".format(fmt_utxo(utxo)))
        return fidelity_bond
Esempio n. 7
0
def test_watchonly_wallet(setup_wallet):
    jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
    storage = VolatileStorage()
    SegwitLegacyWalletFidelityBonds.initialize(storage, get_network())
    wallet = SegwitLegacyWalletFidelityBonds(storage)

    paths = [
        "m/49'/1'/0'/0/0", "m/49'/1'/0'/1/0", "m/49'/1'/0'/2/0:1577836800",
        "m/49'/1'/0'/2/0:2314051200"
    ]
    burn_path = "m/49'/1'/0'/3/0"

    scripts = [
        wallet.get_script_from_path(wallet.path_repr_to_path(path))
        for path in paths
    ]
    privkey, engine = wallet._get_key_from_path(
        wallet.path_repr_to_path(burn_path))
    burn_pubkey = engine.privkey_to_pubkey(privkey)

    master_pub_key = wallet.get_bip32_pub_export(
        FidelityBondMixin.FIDELITY_BOND_MIXDEPTH)
    watchonly_storage = VolatileStorage()
    entropy = FidelityBondMixin.get_xpub_from_fidelity_bond_master_pub_key(
        master_pub_key).encode()
    FidelityBondWatchonlyWallet.initialize(watchonly_storage,
                                           get_network(),
                                           entropy=entropy)
    watchonly_wallet = FidelityBondWatchonlyWallet(watchonly_storage)

    watchonly_scripts = [
        watchonly_wallet.get_script_from_path(
            watchonly_wallet.path_repr_to_path(path)) for path in paths
    ]
    privkey, engine = wallet._get_key_from_path(
        wallet.path_repr_to_path(burn_path))
    watchonly_burn_pubkey = engine.privkey_to_pubkey(privkey)

    for script, watchonly_script in zip(scripts, watchonly_scripts):
        assert script == watchonly_script
    assert burn_pubkey == watchonly_burn_pubkey
def get_bond_values(amount: int,
                    months: int,
                    confirm_time: Optional[float] = None,
                    interest: Optional[float] = None,
                    exponent: Optional[float] = None,
                    orderbook: Optional[Mapping[str, Any]] = None) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
    """
    Conveniently generate values [and statistics] for multiple possible fidelity bonds.

    Args:
        amount: Fidelity bond UTXO amount in satoshi
        months: For how many months to calculate the results
        confirm_time: Fidelity bond UTXO confirmation time as timestamp, if None, current time is used.
                      I.e., like if the fidelity bond UTXO with given amount has just confirmed on the blockchain.
        interest: Interest rate, if None, value is taken from config
        exponent: Exponent, if None, value is taken from config
        orderbook: Orderbook data, if given, additional statistics are included in the results.
    Returns:
        A tuple with 2 elements.
         First is a dictionary with all the parameters used to perform fidelity bond calculations.
         Second is a list of dictionaries, one for each month, with the results.
    """
    current_time = datetime.now().timestamp()
    if confirm_time is None:
        confirm_time = current_time
    if interest is None:
        interest = get_interest_rate()
    if exponent is None:
        exponent = jm_single().config.getfloat("POLICY", "bond_value_exponent")
        use_config_exp = True
    else:
        old_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
        jm_single().config.set("POLICY", "bond_value_exponent", str(exponent))
        use_config_exp = False
    if orderbook:
        bond_values = [fb["bond_value"] for fb in orderbook["fidelitybonds"]]
        bonds_sum = sum(bond_values)
        percentiles = quantiles(bond_values, n=100, method="inclusive")

    parameters = {
        "amount": amount,
        "confirm_time": confirm_time,
        "current_time": current_time,
        "interest": interest,
        "exponent": exponent,
    }
    locktime = get_next_locktime(datetime.fromtimestamp(current_time))
    results = []
    for _ in range(months):
        fb_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
            amount,
            confirm_time,
            locktime.timestamp(),
            current_time,
            interest,
        )
        result = {"locktime": locktime.timestamp(),
                  "value": fb_value}
        if orderbook:
            result["weight"] = fb_value / (bonds_sum + fb_value)
            result["percentile"] = 100 - bisect_left(percentiles, fb_value)
        results.append(result)
        locktime = get_next_locktime(locktime)
    if not use_config_exp:
        # We don't want the modified exponent value to persist in memory, so we reset to whatever it was before
        jm_single().config.set("POLICY", "bond_value_exponent", old_exponent)
    return parameters, results
    def create_orderbook_table(self, btc_unit, rel_unit):
        result = ''
        try:
            self.taker.dblock.acquire(True)
            rows = self.taker.db.execute('SELECT * FROM orderbook;').fetchall()
        finally:
            self.taker.dblock.release()
        if not rows:
            return 0, result
        rows = [o for o in rows if o["ordertype"] in filtered_offername_list]

        if jm_single().bc_interface == None:
            for row in rows:
                row["bondvalue"] = "No data"
        else:
            blocks = jm_single().bc_interface.get_current_block_height()
            mediantime = jm_single().bc_interface.get_best_block_median_time()
            interest_rate = get_interest_rate()
            for row in rows:
                with self.taker.dblock:
                    fbond_data = self.taker.db.execute(
                        "SELECT * FROM fidelitybonds WHERE counterparty=?;",
                        (row["counterparty"], )).fetchall()
                if len(fbond_data) == 0:
                    row["bondvalue"] = "0"
                    continue
                else:
                    try:
                        parsed_bond = FidelityBondProof.parse_and_verify_proof_msg(
                            fbond_data[0]["counterparty"],
                            fbond_data[0]["takernick"], fbond_data[0]["proof"])
                    except ValueError:
                        row["bondvalue"] = "0"
                        continue
                    utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo(
                        parsed_bond.utxo, parsed_bond.utxo_pub,
                        parsed_bond.locktime, parsed_bond.cert_expiry, blocks)
                    if utxo_data == None:
                        row["bondvalue"] = "0"
                        continue
                    bond_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
                        utxo_data["value"],
                        jm_single().bc_interface.get_block_time(
                            jm_single().bc_interface.get_block_hash(
                                blocks - utxo_data["confirms"] + 1)),
                        parsed_bond.locktime, mediantime, interest_rate)
                    row["bondvalue"] = satoshi_to_unit_power(
                        bond_value, 2 * unit_to_power[btc_unit])

        order_keys_display = (('ordertype', ordertype_display),
                              ('counterparty', do_nothing), ('oid', order_str),
                              ('cjfee', cjfee_display),
                              ('txfee', satoshi_to_unit), ('minsize',
                                                           satoshi_to_unit),
                              ('maxsize', satoshi_to_unit), ('bondvalue',
                                                             do_nothing))

        # somewhat complex sorting to sort by cjfee but with swabsoffers on top

        def orderby_cmp(x, y):
            if x['ordertype'] == y['ordertype']:
                return cmp(Decimal(x['cjfee']), Decimal(y['cjfee']))
            return cmp(offername_list.index(x['ordertype']),
                       offername_list.index(y['ordertype']))

        for o in sorted(rows, key=cmp_to_key(orderby_cmp)):
            result += ' <tr>\n'
            for key, displayer in order_keys_display:
                result += '  <td>' + displayer(o[key], o, btc_unit,
                                               rel_unit) + '</td>\n'
            result += ' </tr>\n'
        return len(rows), result