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 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_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
def create_sybil_resistance_page(self, btc_unit): if jm_single().bc_interface == None: return "", "Calculations unavailable, requires configured bitcoin node." (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\ get_fidelity_bond_data(self.taker) choose_units_form = ( '<form method="get" action="">' + '<select name="btcunit" onchange="this.form.submit();">' + ''.join( ('<option>' + u + ' </option>' for u in sorted_units)) + '</select></form>') choose_units_form = choose_units_form.replace( '<option>' + btc_unit, '<option selected="selected">' + btc_unit) mainbody = choose_units_form honest_weight = sum(fidelity_bond_values) mainbody += ( "Assuming the makers in the offerbook right now are not sybil attackers, " + "how much would a sybil attacker starting now have to sacrifice to succeed in their" + " attack with 95% probability. Honest weight=" + satoshi_to_unit_power(honest_weight, 2 * unit_to_power[btc_unit]) + " " + btc_unit + "<sup>" + bond_exponent + "</sup><br/>Also assumes that takers " + "are not price-sensitive and that their max " + "coinjoin fee is configured high enough that they dont exclude any makers." ) heading2 = "Sybil attacks from external enemies." mainbody += ('<table class="tftable" border="1"><tr>' + '<th>Maker count</th>' + '<th>6month locked coins / ' + btc_unit + '</th>' + '<th>1y locked coins / ' + btc_unit + '</th>' + '<th>2y locked coins / ' + btc_unit + '</th>' + '<th>5y locked coins / ' + btc_unit + '</th>' + '<th>10y locked coins / ' + btc_unit + '</th>' + '<th>Required burned coins / ' + btc_unit + '</th>' + '</tr>') timelocks = [0.5, 1.0, 2.0, 5.0, 10.0, None] interest_rate = get_interest_rate() for makercount, unit_success_sybil_weight in sybil.successful_attack_95pc_sybil_weight.items( ): success_sybil_weight = unit_success_sybil_weight * honest_weight row = "<tr><td>" + str(makercount) + "</td>" for timelock in timelocks: if timelock != None: coins_per_sybil = sybil.weight_to_locked_coins( success_sybil_weight, interest_rate, timelock) else: coins_per_sybil = sybil.weight_to_burned_coins( success_sybil_weight) row += ("<td>" + satoshi_to_unit(coins_per_sybil * makercount, None, btc_unit, 0) + "</td>") row += "</tr>" mainbody += row mainbody += "</table>" mainbody += ( "<h2>Sybil attacks from enemies within</h2>Assume a sybil attack is ongoing" + " right now and that the counterparties with the most valuable fidelity bonds are " + " actually controlled by the same entity. Then, what is the probability of a " + " successful sybil attack for a given makercount, and what is the fidelity bond " + " value being foregone by not putting all bitcoins into just one maker." ) mainbody += ('<table class="tftable" border="1"><tr>' + '<th>Maker count</th>' + '<th>Success probability</th>' + '<th>Foregone value / ' + btc_unit + '<sup>' + bond_exponent + '</sup></th>' + '</tr>') #limited because calculation is slow, so this avoids server being too slow to respond MAX_MAKER_COUNT_INTERNAL = 10 weights = sorted(fidelity_bond_values)[::-1] for makercount in range(1, MAX_MAKER_COUNT_INTERNAL + 1): makercount_str = ( str(makercount) + " - " + str(MAX_MAKER_COUNT_INTERNAL) if makercount == len(fidelity_bond_data) and len(fidelity_bond_data) != MAX_MAKER_COUNT_INTERNAL else str(makercount)) success_prob = sybil.calculate_top_makers_sybil_attack_success_probability( weights, makercount) total_sybil_weight = sum(weights[:makercount]) sacrificed_values = [ sybil.weight_to_burned_coins(w) for w in weights[:makercount] ] foregone_value = ( sybil.coins_burned_to_weight(sum(sacrificed_values)) - total_sybil_weight) mainbody += ("<tr><td>" + makercount_str + "</td><td>" + str(round(success_prob * 100.0, 5)) + "%</td><td>" + satoshi_to_unit_power( foregone_value, 2 * unit_to_power[btc_unit]) + "</td></tr>") if makercount == len(weights): break mainbody += "</table>" return heading2, mainbody