Browse Source

Consider fidelity bonds when choosing makers

Verify the fidelity bond proof on the taker client side.
master
chris-belcher 5 years ago
parent
commit
199b5711a9
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 3
      jmbase/jmbase/commands.py
  2. 2
      jmclient/jmclient/__init__.py
  3. 7
      jmclient/jmclient/cli_options.py
  4. 9
      jmclient/jmclient/client_protocol.py
  5. 13
      jmclient/jmclient/configure.py
  6. 21
      jmclient/jmclient/support.py
  7. 50
      jmclient/jmclient/taker.py
  8. 10
      jmclient/test/test_client_protocol.py
  9. 2
      jmclient/test/test_coinjoin.py
  10. 16
      jmclient/test/test_taker.py
  11. 12
      jmdaemon/jmdaemon/daemon_protocol.py
  12. 2
      jmdaemon/test/test_daemon_protocol.py

3
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.

2
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

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

9
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]:

13
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:'):

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

50
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

10
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}

2
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 = {}

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

12
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}

2
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}

Loading…
Cancel
Save