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

Commit

Permalink
first blacklist version
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamISZ committed Nov 19, 2015
1 parent 8d0c6f0 commit 73a6f0b
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 18 deletions.
30 changes: 29 additions & 1 deletion lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
config_location = 'joinmarket.cfg'
# FIXME: Add rpc_* options here in the future!
required_options = {'BLOCKCHAIN':['blockchain_source', 'network'],
'MESSAGING':['host','channel','port']}
'MESSAGING':['host','channel','port'],
'LIMITS':['taker_utxo_retries']}

defaultconfig =\
"""
Expand Down Expand Up @@ -188,6 +189,33 @@ def debug_dump_object(obj, skip_fields=[]):
else:
debug(str(v))

def check_utxo_blacklist(utxo):
#write/read the file here; could be inferior
#performance-wise; should cache. TODO
#there also needs to be formatting error checking here,
#but not a high priority TODO
blacklist = {}
if os.path.isfile("logs/blacklist"):
with open("logs/blacklist","rb") as f:
blacklist_lines = f.readlines()
else:
blacklist_lines = []
for bl in blacklist_lines:
ut, ct = bl.split(',')
ut = ut.strip()
ct = int(ct.strip())
blacklist[ut]=ct
if utxo in blacklist.keys():
blacklist[utxo] += 1
if blacklist[utxo] >= config.getint("LIMITS","taker_utxo_retries"):
return False
else:
blacklist[utxo]=1
with open("logs/blacklist","wb") as f:
for k,v in blacklist.iteritems():
f.write(k+' , '+str(v)+'\n')
return True

def select_gradual(unspent, value):
'''
UTXO selection algorithm for gradual dust reduction
Expand Down
12 changes: 7 additions & 5 deletions lib/irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ def fill_orders(self, nickoid_dict, cj_amount, taker_pubkey):
msg = str(oid) + ' ' + str(cj_amount) + ' ' + taker_pubkey
self.__privmsg(c, 'fill', msg)

def send_auth(self, nick, pubkey, sig):
message = pubkey + ' ' + sig
def send_auth(self, nick, utxo, pubkey, sig):
message = utxo + ' ' + pubkey + ' ' + sig
self.__privmsg(nick, 'auth', message)

def send_tx(self, nick_list, txhex):
Expand Down Expand Up @@ -175,6 +175,7 @@ def __privmsg(self, nick, cmd, message):
trailer = ' ~' if m==message_chunks[-1] else ' ;'
if m==message_chunks[0]:
m = COMMAND_PREFIX + cmd + ' ' + m
debug("sending this message: "+header+m+trailer)
self.send_raw(header + m + trailer)

def send_raw(self, line):
Expand Down Expand Up @@ -246,12 +247,13 @@ def __on_privmsg(self, nick, message):
self.on_order_fill(nick, oid, amount, taker_pk)
elif chunks[0] == 'auth':
try:
i_utxo_pubkey = chunks[1]
btc_sig = chunks[2]
i_utxo = chunks[1]
i_utxo_pubkey = chunks[2]
btc_sig = chunks[3]
except (ValueError, IndexError) as e:
self.send_error(nick, str(e))
if self.on_seen_auth:
self.on_seen_auth(nick, i_utxo_pubkey, btc_sig)
self.on_seen_auth(nick, i_utxo, i_utxo_pubkey, btc_sig)
elif chunks[0] == 'tx':
b64tx = chunks[1]
try:
Expand Down
29 changes: 22 additions & 7 deletions lib/maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,27 @@ def __init__(self, maker, nick, oid, amount, taker_pk):
# orders to find out which addresses you use
self.maker.msgchan.send_pubkey(nick, self.kp.hex_pk())

def auth_counterparty(self, nick, i_utxo_pubkey, btc_sig):
def auth_counterparty(self, nick, i_utxo, i_utxo_pubkey, btc_sig):
self.i_utxo_pubkey = i_utxo_pubkey

if not common.check_utxo_blacklist(i_utxo):
common.debug("Taker input utxo is in blacklist, having been used "\
+ common.config.get("LIMITS","taker_utxo_retries")+ " times, rejecting.")
return False
if not btc.ecdsa_verify(self.taker_pk, btc_sig, binascii.unhexlify(self.i_utxo_pubkey)):
print 'signature didnt match pubkey and message'
common.debug("signature from nick: " + nick + "didnt match pubkey and message")
return False
#finally, check that the proffered utxo is real, and corresponds
#to the pubkey
res = common.bc_interface.query_utxo_set([i_utxo])
if len(res) != 1:
common.debug("Input utxo: "+str(i_utxo)+" from nick: "+nick+" is not valid.")
return False
real_utxo = res[0]
if real_utxo['address'] != btc.pubkey_to_address(i_utxo_pubkey, common.get_p2pk_vbyte()):
return False
#TODO: could add check for coin age and value here
#(need to edit query_utxo_set if we want coin age)

#authorisation of taker passed
#(but input utxo pubkey is checked in verify_unsigned_tx).
#Send auth request to taker
Expand Down Expand Up @@ -199,12 +214,12 @@ def on_order_fill(self, nick, oid, amount, taker_pubkey):
finally:
self.wallet_unspent_lock.release()

def on_seen_auth(self, nick, pubkey, sig):
def on_seen_auth(self, nick, utxo, pubkey, sig):
if nick not in self.active_orders or self.active_orders[nick] == None:
self.msgchan.send_error(nick, 'No open order from this nick')
self.active_orders[nick].auth_counterparty(nick, pubkey, sig)
#TODO if auth_counterparty returns false, remove this order from active_orders
# and send an error
if not self.active_orders[nick].auth_counterparty(nick, utxo, pubkey, sig):
self.active_orders[nick]=None
self.msgchan.send_error(nick, "Authorisation failed.")

def on_seen_tx(self, nick, txhex):
if nick not in self.active_orders or self.active_orders[nick] == None:
Expand Down
4 changes: 3 additions & 1 deletion lib/taker.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ def start_encryption(self, nick, maker_pk):
my_btc_addr = self.auth_addr
else:
my_btc_addr = self.input_utxos.itervalues().next()['address']
my_signing_utxo = self.input_utxos.iterkeys().next()
common.debug("Using this signing utxo: "+my_signing_utxo)
my_btc_priv = self.wallet.get_key_from_addr(my_btc_addr)
my_btc_pub = btc.privtopub(my_btc_priv, True)
my_btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), binascii.unhexlify(my_btc_priv))
self.msgchan.send_auth(nick, my_btc_pub, my_btc_sig)
self.msgchan.send_auth(nick, str(my_signing_utxo), my_btc_pub, my_btc_sig)

def auth_counterparty(self, nick, btc_sig, cj_pub):
'''Validate the counterpartys claim to own the btc
Expand Down
146 changes: 146 additions & 0 deletions test/blacklist-test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import sys
import os, time
data_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
sys.path.insert(0, os.path.join(data_dir, 'lib'))
import subprocess
import unittest
import common
import commontest
from blockchaininterface import *
import bitcoin as btc
import binascii

''' Just some random thoughts to motivate possible tests;
almost none of this has really been done:
Expectations
1. Any bot should run indefinitely irrespective of the input
messages it receives, except bots which perform a finite action
2. A bot must never spend an unacceptably high transaction fee.
3. A bot must explicitly reject interactions with another bot not
respecting the JoinMarket protocol for its version.
4. Bots must never send bitcoin data in the clear over the wire.
'''

'''helper functions put here to avoid polluting the main codebase.'''

import platform
OS = platform.system()
PINL = '\r\n' if OS == 'Windows' else '\n'

def local_command(command, bg=False, redirect=''):
if redirect=='NULL':
if OS=='Windows':
command.append(' > NUL 2>&1')
elif OS=='Linux':
command.extend(['>', '/dev/null', '2>&1'])
else:
print "OS not recognised, quitting."
elif redirect:
command.extend(['>', redirect])

if bg:
FNULL = open(os.devnull,'w')
return subprocess.Popen(command, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True)
else:
#in case of foreground execution, we can use the output; if not
#it doesn't matter
return subprocess.check_output(command)



class BlackListPassTests(unittest.TestCase):
'''This test case intends to simulate
a single join with a single counterparty. In that sense,
it's not realistic, because nobody (should) do joins with only 1 maker,
but this test has the virtue of being the simplest possible thing
that JoinMarket can do. '''
def setUp(self):
#create 2 new random wallets.
#put 10 coins into the first receive address
#to allow that bot to start.
self.wallets = commontest.make_wallets(2,
wallet_structures=[[1,0,0,0,0],[1,0,0,0,0]], mean_amt=10)


def blacklist_run(self, n, m, fake):
os.remove('logs/blacklist')
#start yield generator with wallet1
yigen_proc = local_command(['python','yield-generator.py',
str(self.wallets[0]['seed'])],bg=True)

#A significant delay is needed to wait for the yield generator to sync its wallet
time.sleep(10)

#run a single sendpayment call with wallet2
amt = n*100000000 #in satoshis
dest_address = btc.privkey_to_address(os.urandom(32), from_hex=False, magicbyte=common.get_p2pk_vbyte())
try:
for i in range(m):
sp_proc = local_command(['python','sendpayment.py','--yes','-N','1', self.wallets[1]['seed'],\
str(amt), dest_address])
except subprocess.CalledProcessError, e:
if yigen_proc:
yigen_proc.terminate()
print e.returncode
print e.message
raise

if yigen_proc:
yigen_proc.terminate()
if not fake:
received = common.bc_interface.get_received_by_addr([dest_address], None)['data'][0]['balance']
if received != amt*m:
common.debug('received was: '+str(received)+ ' but amount was: '+str(amt))
return False
#check sanity in blacklist
with open('logs/blacklist','rb') as f:
blacklist_lines = f.readlines()
if not fake:
required_bl_lines = m
bl_count = 1
else:
required_bl_lines = 1
bl_count = m
if len(blacklist_lines) != required_bl_lines:
common.debug('wrong number of blacklist lines: '+str(len(blacklist_lines)))
return False

for bl in blacklist_lines:
if len(bl.split(',')[0].strip()) != 66:
common.debug('malformed utxo: '+str(len(bl.split(',')[0].strip())))
return False
if int(bl.split(',')[1]) != bl_count:
common.debug('wrong blacklist count:'+str(bl.split(',')[1]))
return False
return True

def test_simple_send(self):
self.failUnless(self.blacklist_run(2, 2, False))




def main():
os.chdir(data_dir)
common.load_program_config()
unittest.main()

if __name__ == '__main__':
#Big kludge, but there is currently no way to inject this code:
print """this test is to be run in two modes, first
with no changes, then second adding a 'return' in
taker.CoinJoinTX.push() return (so it does nothing),
and further changing the third parameter to blacklist_run to 'True'
and the second parameter to '3' from '2'
In both cases the test should pass for success.
Also, WARNING! This test will delete your blacklist, better
not run it in a "real" repo or back it up.
"""
raw_input("OK?")
main()


5 changes: 1 addition & 4 deletions yield-generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,7 @@ def on_tx_unconfirmed(self, cjorder, txid, removed_utxos):
return ([], [neworders[0]])

def on_tx_confirmed(self, cjorder, confirmations, txid):
if cjorder.cj_addr in self.tx_unconfirm_timestamp:
confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr]
else:
confirm_time = 0
confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr]
timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
self.log_statement([timestamp, cjorder.cj_amount, len(cjorder.utxos),
sum([av['value'] for av in cjorder.utxos.values()]), cjorder.real_cjfee,
Expand Down

0 comments on commit 73a6f0b

Please sign in to comment.