diff --git a/jmbase/jmbase/commands.py b/jmbase/jmbase/commands.py index 21e1d6f..ec039c3 100644 --- a/jmbase/jmbase/commands.py +++ b/jmbase/jmbase/commands.py @@ -189,7 +189,8 @@ class JMOffers(JMCommand): """Return the entire contents of the orderbook to TAKER, as a json-ified dict. """ - arguments = [(b'orderbook', BigUnicode())] + arguments = [(b'orderbook', BigUnicode()), + (b'fidelitybonds', BigUnicode())] class JMFillResponse(JMCommand): """Returns ioauth data from MAKER if successful. diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index 741ab7c..1a900fa 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -24,7 +24,7 @@ from .configure import (load_test_config, process_shutdown, load_program_config, jm_single, get_network, update_persist_config, validate_address, is_burn_destination, get_irc_mchannels, get_blockchain_interface_instance, set_config, is_segwit_mode, - is_native_segwit_mode, JMPluginService, get_interest_rate) + is_native_segwit_mode, JMPluginService, get_interest_rate, get_bondless_makers_allowance) from .blockchaininterface import (BlockchainInterface, RegtestBitcoinCoreInterface, BitcoinCoreInterface) from .snicker_receiver import SNICKERError, SNICKERReceiver diff --git a/jmclient/jmclient/cli_options.py b/jmclient/jmclient/cli_options.py index 5840cd8..9dc5ece 100644 --- a/jmclient/jmclient/cli_options.py +++ b/jmclient/jmclient/cli_options.py @@ -16,7 +16,8 @@ options which are common to more than one script in a base class. order_choose_algorithms = { 'random_under_max_order_choose': '-R', 'cheapest_order_choose': '-C', - 'weighted_order_choose': '-W' + 'weighted_order_choose': '-W', + 'fidelity_bond_weighted_order_choose': '-F' } def add_base_options(parser): @@ -90,12 +91,12 @@ def add_common_options(parser): '--order-choose-algorithm', action='callback', type='string', - default=jmclient.support.random_under_max_order_choose, + default=jmclient.support.fidelity_bond_weighted_order_choose, callback=get_order_choose_algorithm, help="Set the algorithm to use for selecting orders from the order book.\n" "Default: {}\n" "Available options: {}" - .format('random_under_max_order_choose', + .format('fidelity_bond_weighted_order_choose', ', '.join(order_choose_algorithms.keys())), dest='order_choose_fn') add_order_choose_short_options(parser) diff --git a/jmclient/jmclient/client_protocol.py b/jmclient/jmclient/client_protocol.py index 97acb50..45155c9 100644 --- a/jmclient/jmclient/client_protocol.py +++ b/jmclient/jmclient/client_protocol.py @@ -395,7 +395,7 @@ class JMMakerClientProtocol(JMClientProtocol): d = self.callRemote(commands.JMSetup, role="MAKER", offers=json.dumps(self.client.offerlist), - use_fidelity_bond=(self.client.fidelity_bond != None)) + use_fidelity_bond=(self.client.fidelity_bond is not None)) self.defaultCallbacks(d) @commands.JMSetupDone.responder @@ -640,7 +640,7 @@ class JMTakerClientProtocol(JMClientProtocol): d = self.callRemote(commands.JMSetup, role="TAKER", offers="{}", - fidelity_bond=b'') + use_fidelity_bond=False) self.defaultCallbacks(d) return {'accepted': True} @@ -689,11 +689,12 @@ class JMTakerClientProtocol(JMClientProtocol): return {'accepted': True} @commands.JMOffers.responder - def on_JM_OFFERS(self, orderbook): + def on_JM_OFFERS(self, orderbook, fidelitybonds): self.orderbook = json.loads(orderbook) + fidelity_bonds_list = json.loads(fidelitybonds) #Removed for now, as judged too large, even for DEBUG: #jlog.debug("Got the orderbook: " + str(self.orderbook)) - retval = self.client.initialize(self.orderbook) + retval = self.client.initialize(self.orderbook, fidelity_bonds_list) #format of retval is: #True, self.cjamount, commitment, revelation, self.filtered_orderbook) if not retval[0]: diff --git a/jmclient/jmclient/configure.py b/jmclient/jmclient/configure.py index 900de39..97955c5 100644 --- a/jmclient/jmclient/configure.py +++ b/jmclient/jmclient/configure.py @@ -90,6 +90,8 @@ required_options = {'BLOCKCHAIN': ['blockchain_source', 'network'], _DEFAULT_INTEREST_RATE = "0.015" +_DEFAULT_BONDLESS_MAKERS_ALLOWANCE = "0.125" + defaultconfig = \ """ [DAEMON] @@ -300,6 +302,13 @@ max_sats_freeze_reuse = -1 # Set as a real number, i.e. 1 = 100% and 0.01 = 1% interest_rate = """ + _DEFAULT_INTEREST_RATE + """ +# Some makers run their bots to mix their funds not just to earn money +# So to improve privacy very slightly takers dont always choose a maker based +# on his fidelity bond but allow a certain small percentage to be chosen completely +# randomly without taking into account fidelity bonds +# This parameter sets how many makers on average will be chosen regardless of bonds +# A real number, i.e. 1 = 100%, 0.125 = 1/8 = 1 in every 8 makers on average will be bondless +bondless_makers_allowance = """ + _DEFAULT_BONDLESS_MAKERS_ALLOWANCE + """ ############################## #THE FOLLOWING SETTINGS ARE REQUIRED TO DEFEND AGAINST SNOOPERS. @@ -552,6 +561,10 @@ def get_interest_rate(): return float(global_singleton.config.get('POLICY', 'interest_rate', fallback=_DEFAULT_INTEREST_RATE)) +def get_bondless_makers_allowance(): + return float(global_singleton.config.get('POLICY', 'bondless_makers_allowance', + fallback=_DEFAULT_BONDLESS_MAKERS_ALLOWANCE)) + def remove_unwanted_default_settings(config): for section in config.sections(): if section.startswith('MESSAGING:'): diff --git a/jmclient/jmclient/support.py b/jmclient/jmclient/support.py index 4177eea..e8641a0 100644 --- a/jmclient/jmclient/support.py +++ b/jmclient/jmclient/support.py @@ -2,6 +2,7 @@ from functools import reduce import random from jmbase.support import get_log from decimal import Decimal +from .configure import get_bondless_makers_allowance from math import exp @@ -218,6 +219,26 @@ def cheapest_order_choose(orders, n): """ return orders[0] +def fidelity_bond_weighted_order_choose(orders, n): + """ + choose orders based on fidelity bond for improved sybil resistance + + * with probability `bondless_makers_allowance`: will revert to previous default + order choose (random_under_max_order_choose) + * with probability `1 - bondless_makers_allowance`: if there are no bond offerings, revert + to previous default as above. If there are, choose randomly from those, with weighting + being the fidelity bond values. + """ + + if random.random() < get_bondless_makers_allowance(): + return random_under_max_order_choose(orders, n) + #remove orders without fidelity bonds + filtered_orders = list(filter(lambda x: x[0]["fidelity_bond_value"] != 0, orders)) + if len(filtered_orders) == 0: + return random_under_max_order_choose(orders, n) + weights = list(map(lambda x: x[0]["fidelity_bond_value"], filtered_orders)) + weights = [x / sum(weights) for x in weights] + return filtered_orders[rand_weighted_choice(len(filtered_orders), weights)] def _get_is_within_max_limits(max_fee_rel, max_fee_abs, cjvalue): def check_max_fee(fee): diff --git a/jmclient/jmclient/taker.py b/jmclient/jmclient/taker.py index a018b27..2115c09 100644 --- a/jmclient/jmclient/taker.py +++ b/jmclient/jmclient/taker.py @@ -7,13 +7,14 @@ from typing import Any, NamedTuple from twisted.internet import reactor, task import jmbitcoin as btc -from jmclient.configure import jm_single, validate_address +from jmclient.configure import jm_single, validate_address, get_interest_rate from jmbase import get_log, bintohex, hexbin from jmclient.support import (calc_cj_fee, weighted_order_choose, choose_orders, choose_sweep_orders) -from jmclient.wallet import estimate_tx_fee, compute_tx_locktime +from jmclient.wallet import estimate_tx_fee, compute_tx_locktime, FidelityBondMixin from jmclient.podle import generate_podle, get_podle_commitments from jmclient.wallet_service import WalletService +from jmclient.fidelity_bond import FidelityBondProof from .output import generate_podle_error_string from .cryptoengine import EngineError from .schedule import NO_ROUNDING @@ -166,7 +167,7 @@ class Taker(object): return self.honest_only = truefalse - def initialize(self, orderbook): + def initialize(self, orderbook, fidelity_bonds_info): """Once the daemon is active and has returned the current orderbook, select offers, re-initialize variables and prepare a commitment, then send it to the protocol to fill offers. @@ -227,6 +228,11 @@ class Taker(object): self.latest_tx = None self.txid = None + fidelity_bond_values = calculate_fidelity_bond_values(fidelity_bonds_info) + for offer in orderbook: + #having no fidelity bond is like having a zero value fidelity bond + offer["fidelity_bond_value"] = fidelity_bond_values.get(offer["counterparty"], 0) + sweep = True if self.cjamount == 0 else False if not self.filter_orderbook(orderbook, sweep): return (False,) @@ -987,3 +993,41 @@ def round_to_significant_figures(d, sf): sigfiged = int(round(d/power10*sf_power10)*power10/sf_power10) return sigfiged raise RuntimeError() + +def calculate_fidelity_bond_values(fidelity_bonds_info): + if len(fidelity_bonds_info) == 0: + return {} + interest_rate = get_interest_rate() + blocks = jm_single().bc_interface.get_current_block_height() + mediantime = jm_single().bc_interface.get_best_block_median_time() + + validated_bonds = {} + for bond_data in fidelity_bonds_info: + try: + fb_proof = FidelityBondProof.parse_and_verify_proof_msg( + bond_data["counterparty"], bond_data["takernick"], bond_data["proof"]) + except ValueError: + continue + if fb_proof.utxo in validated_bonds: + continue + utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo( + fb_proof.utxo, fb_proof.utxo_pub, fb_proof.locktime, + fb_proof.cert_expiry, blocks) + if utxo_data is not None: + validated_bonds[fb_proof.utxo] = (fb_proof, utxo_data) + + fidelity_bond_values = { + bond_data.maker_nick: + 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 + ) + ), + bond_data.locktime, + mediantime, + interest_rate) + for bond_data, utxo_data in validated_bonds.values() + } + return fidelity_bond_values diff --git a/jmclient/test/test_client_protocol.py b/jmclient/test/test_client_protocol.py index b89c158..0b6d300 100644 --- a/jmclient/test/test_client_protocol.py +++ b/jmclient/test/test_client_protocol.py @@ -43,7 +43,7 @@ class DummyTaker(Taker): def default_taker_info_callback(self, infotype, msg): jlog.debug(infotype + ":" + msg) - def initialize(self, orderbook): + def initialize(self, orderbook, fidelity_bonds_info): """Once the daemon is active and has returned the current orderbook, select offers, re-initialize variables and prepare a commitment, then send it to the protocol to fill offers. @@ -184,8 +184,8 @@ class JMTestServerProtocol(JMBaseProtocol): return {'accepted': True} @JMSetup.responder - def on_JM_SETUP(self, role, offers, fidelity_bond): - show_receipt("JMSETUP", role, offers, fidelity_bond) + def on_JM_SETUP(self, role, offers, use_fidelity_bond): + show_receipt("JMSETUP", role, offers, use_fidelity_bond) d = self.callRemote(JMSetupDone) self.defaultCallbacks(d) return {'accepted': True} @@ -195,8 +195,10 @@ class JMTestServerProtocol(JMBaseProtocol): show_receipt("JMREQUESTOFFERS") #build a huge orderbook to test BigString Argument orderbook = ["aaaa" for _ in range(15)] + fidelitybonds = ["bbbb" for _ in range(15)] d = self.callRemote(JMOffers, - orderbook=json.dumps(orderbook)) + orderbook=json.dumps(orderbook), + fidelitybonds=json.dumps(fidelitybonds)) self.defaultCallbacks(d) return {'accepted': True} diff --git a/jmclient/test/test_coinjoin.py b/jmclient/test/test_coinjoin.py index b2e694d..431699f 100644 --- a/jmclient/test/test_coinjoin.py +++ b/jmclient/test/test_coinjoin.py @@ -80,7 +80,7 @@ def create_orders(makers): maker.try_to_create_my_orders() def init_coinjoin(taker, makers, orderbook, cj_amount): - init_data = taker.initialize(orderbook) + init_data = taker.initialize(orderbook, []) assert init_data[0], "taker.initialize error" active_orders = init_data[4] maker_data = {} diff --git a/jmclient/test/test_taker.py b/jmclient/test/test_taker.py index 31cd525..3734e3a 100644 --- a/jmclient/test/test_taker.py +++ b/jmclient/test/test_taker.py @@ -165,11 +165,11 @@ def test_filter_rejection(setup_taker): return False taker = get_taker(filter_orders=filter_orders_reject) taker.schedule = [[0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]] - res = taker.initialize(t_orderbook) + res = taker.initialize(t_orderbook, []) assert not res[0] taker = get_taker(filter_orders=filter_orders_reject) taker.schedule = [[0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]] - res = taker.initialize(t_orderbook) + res = taker.initialize(t_orderbook, []) assert not res[0] @pytest.mark.parametrize( @@ -258,7 +258,7 @@ def test_make_commitment(setup_taker, mixdepth, cjamt, failquery, external, def test_not_found_maker_utxos(setup_taker): taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)]) orderbook = copy.deepcopy(t_orderbook) - res = taker.initialize(orderbook) + res = taker.initialize(orderbook, []) taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same maker_response = copy.deepcopy(t_maker_response) jm_single().bc_interface.setQUSFail(True) @@ -270,7 +270,7 @@ def test_not_found_maker_utxos(setup_taker): def test_auth_pub_not_found(setup_taker): taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)]) orderbook = copy.deepcopy(t_orderbook) - res = taker.initialize(orderbook) + res = taker.initialize(orderbook, []) taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same maker_response = copy.deepcopy(t_maker_response) utxos = [utxostr_to_utxo(x)[1] for x in [ @@ -351,11 +351,11 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers, if schedule[0][1] == 0.2: #triggers calc-ing amount based on a fraction jm_single().mincjamount = 50000000 #bigger than 40m = 0.2 * 200m - res = taker.initialize(orderbook) + res = taker.initialize(orderbook, []) assert res[0] assert res[1] == jm_single().mincjamount return clean_up() - res = taker.initialize(orderbook) + res = taker.initialize(orderbook, []) if toomuchcoins or ignored: assert not res[0] return clean_up() @@ -427,7 +427,7 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers, assert res[0] #re-calling will trigger "finished" code, since schedule is "complete". - res = taker.initialize(orderbook) + res = taker.initialize(orderbook, []) assert not res[0] #some exception cases: no coinjoin address, no change address: @@ -454,7 +454,7 @@ def test_custom_change(setup_taker): for script, addr in zip(scripts, addrs): taker = get_taker(schedule, custom_change=addr) orderbook = copy.deepcopy(t_orderbook) - res = taker.initialize(orderbook) + res = taker.initialize(orderbook, []) taker.orderbook = copy.deepcopy(t_chosen_orders) maker_response = copy.deepcopy(t_maker_response) res = taker.receive_utxos(maker_response) diff --git a/jmdaemon/jmdaemon/daemon_protocol.py b/jmdaemon/jmdaemon/daemon_protocol.py index 7714030..c2992fd 100644 --- a/jmdaemon/jmdaemon/daemon_protocol.py +++ b/jmdaemon/jmdaemon/daemon_protocol.py @@ -594,10 +594,16 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): This call is stateless.""" rows = self.db.execute('SELECT * FROM orderbook;').fetchall() self.orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows] - log.msg("About to send orderbook of size: " + str(len(self.orderbook))) string_orderbook = json.dumps(self.orderbook) - d = self.callRemote(JMOffers, - orderbook=string_orderbook) + + fbond_rows = self.db.execute("SELECT * FROM fidelitybonds;").fetchall() + fidelitybonds = [fb for fb in fbond_rows] + string_fidelitybonds = json.dumps(fidelitybonds) + + log.msg("About to send orderbook (size=" + str(len(self.orderbook)) + + " with fidelity bonds (size=" + str(len(fidelitybonds))) + d = self.callRemote(JMOffers, orderbook=string_orderbook, + fidelitybonds=string_fidelitybonds) self.defaultCallbacks(d) return {'accepted': True} diff --git a/jmdaemon/test/test_daemon_protocol.py b/jmdaemon/test/test_daemon_protocol.py index 40a2415..8d44d47 100644 --- a/jmdaemon/test/test_daemon_protocol.py +++ b/jmdaemon/test/test_daemon_protocol.py @@ -85,7 +85,7 @@ class JMTestClientProtocol(JMBaseProtocol): d = self.callRemote(JMSetup, role="TAKER", offers="{}", - fidelity_bond=b'') + use_fidelity_bond=False) self.defaultCallbacks(d) return {'accepted': True}