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)
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
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
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