Skip to content
This repository has been archived by the owner on May 13, 2022. It is now read-only.

Commit

Permalink
Incentivise Network Diversity
Browse files Browse the repository at this point in the history
This commit makes naive sybil attacks more difficult by keying counterparty
deduplication off IRC hostmasks, disincentivising running multiple yield
generators from the same machine or network.

It also fixes a minor bug: the transaction initiator must deduplicate BEFORE
counting the number of liquid counterparties.

It also tidies a bit of suboptimal yapfage, and factors out a rhyming utility.
  • Loading branch information
adlai committed Dec 17, 2015
1 parent e0f7c59 commit e59b16b
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 35 deletions.
11 changes: 6 additions & 5 deletions joinmarket/irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def get_irc_text(line):
return line[line[1:].find(':') + 2:]


def get_irc_nick(source):
return source[1:source.find('!')]
def get_irc_nick(source): # Sybil annoyance through hostmask inclusion
return source[1:source.find('!') + 1] + source[source.find('@') + 1:]


class PingThread(threading.Thread):
Expand Down Expand Up @@ -143,8 +143,9 @@ def push_tx(self, nick, txhex):
def announce_orders(self, orderlist, nick=None):
# nick=None means announce publicly
order_keys = ['oid', 'minsize', 'maxsize', 'txfee', 'cjfee']
header = 'PRIVMSG ' + (nick if nick else self.channel) + ' :'
orderlines = []
target = nick[:nick.find('!')] if nick else self.channel # same header
header = 'PRIVMSG ' + target + ' :' # works for both pit announcements
orderlines = [] # and PM replies to !orderbook
for i, order in enumerate(orderlist):
orderparams = COMMAND_PREFIX + order['ordertype'] + \
' ' + ' '.join([str(order[k]) for k in order_keys])
Expand Down Expand Up @@ -192,7 +193,7 @@ def __privmsg(self, nick, cmd, message):
return
message = encrypt_encode(message, box)

header = "PRIVMSG " + nick + " :"
header = "PRIVMSG " + nick[:nick.find('!')] + " :" # remove hostmask
max_chunk_len = MAX_PRIVMSG_LEN - len(header) - len(cmd) - 4
# 1 for command prefix 1 for space 2 for trailer
if len(message) > max_chunk_len:
Expand Down
48 changes: 25 additions & 23 deletions joinmarket/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,14 @@ def pick_order(orders, n, feekey):
return orders[pickedOrderIndex]
pickedOrderIndex = -1

def offer_profit(offer):
"""
Calculates the net profit to the maker of the given offer.
Expects an offer in the form (counterparty, oid, cjfee, txfee)
where cjfee and txfee are integer amounts of satoshi.
"""
return offer[2] - offer[3]


def choose_orders(db, cj_amount, n, chooseOrdersBy, ignored_makers=None):
if ignored_makers is None:
Expand All @@ -246,10 +254,20 @@ def choose_orders(db, cj_amount, n, chooseOrdersBy, ignored_makers=None):
if o['minsize'] <= cj_amount <= o['maxsize'] and o[
'counterparty'] not in ignored_makers]

# function that returns the fee for a given order
def feekey(o):
return o[2] - o[3]
"""
restrict to one order per counterparty, choose the one with the lowest
cjfee this is done in advance of the order selection algo, so applies to
all of them. however, if orders are picked manually, allow duplicates.
"""
if chooseOrdersBy != pick_order:
orders = sorted( # index by hostmask only, as it's harder to
dict(((v[0])[v[0].find('!')+1:], v) # spoof than the nick field
for v in sorted(orders, key=offer_profit,
reverse=True)).values(), key=offer_profit)
else:
orders = sorted(orders, key=offer_profit) # sort by increasing cjfee

# after deduplication, ensure we have enough distinct counterparties
counterparties = set([o[0] for o in orders])
if n > len(counterparties):
log.debug(('ERROR not enough liquidity in the orderbook n=%d '
Expand All @@ -258,24 +276,11 @@ def feekey(o):
# TODO handle not enough liquidity better, maybe an Exception
return None, 0

"""
restrict to one order per counterparty, choose the one with the lowest
cjfee this is done in advance of the order selection algo, so applies to
all of them. however, if orders are picked manually, allow duplicates.
"""
if chooseOrdersBy != pick_order:
orders = sorted(
dict((v[0], v) for v in sorted(
orders, key=feekey, reverse=True)).values(), key=feekey)
else:
orders = sorted(orders,
key=feekey) # sort from smallest to biggest cj fee

log.debug('considered orders = \n' + '\n'.join([str(o) for o in orders]))
total_cj_fee = 0
chosen_orders = []
for i in range(n):
chosen_order = chooseOrdersBy(orders, n, feekey)
chosen_order = chooseOrdersBy(orders, n, offer_profit)
orders = [o for o in orders if o[0] != chosen_order[0]
] # remove all orders from that same counterparty
chosen_orders.append(chosen_order)
Expand All @@ -302,7 +307,7 @@ def choose_sweep_orders(db,
=> cjamount = (totalin - mytxfee - sum(absfee)) / (1 + sum(relfee))
"""
total_txfee = txfee*n

if ignored_makers is None:
ignored_makers = []

Expand Down Expand Up @@ -344,19 +349,16 @@ def calc_zero_change_cj_amount(ordercombo):
total_input_value), o['txfee'])
for o in orderlist]

def feekey(o):
return o[2] - o[3]

# sort from smallest to biggest cj fee
available_orders = sorted(available_orders, key=feekey)
available_orders = sorted(available_orders, key=offer_profit)
chosen_orders = []
while len(chosen_orders) < n:
if len(available_orders) < n - len(chosen_orders):
log.debug('ERROR not enough liquidity in the orderbook')
# TODO handle not enough liquidity better, maybe an Exception
return None, 0
for i in range(n - len(chosen_orders)):
chosen_order = chooseOrdersBy(available_orders, n, feekey)
chosen_order = chooseOrdersBy(available_orders, n, offer_profit)
log.debug('chosen = ' + str(chosen_order))
# remove all orders from that same counterparty
available_orders = [
Expand Down
10 changes: 3 additions & 7 deletions ob-watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,28 +107,24 @@ def get_graph_html(fig):


# callback functions for displaying order data
def do_nothing(arg, order, btc_unit, rel_unit):
return arg

def trim_host(nickhost, order, btc_unit, rel_unit):
return nickhost[nickhost.find('!')+1:]

def ordertype_display(ordertype, order, btc_unit, rel_unit):
ordertypes = {'absorder': 'Absolute Fee', 'relorder': 'Relative Fee'}
return ordertypes[ordertype]


def cjfee_display(cjfee, order, btc_unit, rel_unit):
if order['ordertype'] == 'absorder':
return satoshi_to_unit(cjfee, order, btc_unit, rel_unit)
elif order['ordertype'] == 'relorder':
return str(float(cjfee) * rel_unit_to_factor[rel_unit]) + rel_unit


def satoshi_to_unit(sat, order, btc_unit, rel_unit):
power = unit_to_power[btc_unit]
return ("%." + str(power) + "f") % float(
Decimal(sat) / Decimal(10 ** power))


def order_str(s, order, btc_unit, rel_unit):
return str(s)

Expand All @@ -139,7 +135,7 @@ def create_orderbook_table(db, btc_unit, rel_unit):
if not rows:
return 0, result
order_keys_display = (('ordertype', ordertype_display),
('counterparty', do_nothing), ('oid', order_str),
('counterparty', trim_host), ('oid', order_str),
('cjfee', cjfee_display), ('txfee', satoshi_to_unit),
('minsize', satoshi_to_unit),
('maxsize', satoshi_to_unit))
Expand Down

0 comments on commit e59b16b

Please sign in to comment.