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

Commit

Permalink
blacklist first version
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamISZ committed Nov 19, 2015
1 parent 8d0c6f0 commit e19b7c8
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 50 deletions.
19 changes: 18 additions & 1 deletion lib/blockchaininterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ def sync_addresses(self, wallet):
wallet_addr_list.append(imported_addr)
imported_addr_list = self.rpc('getaddressesbyaccount', [wallet_name])
if not set(wallet_addr_list).issubset(set(imported_addr_list)):
common.debug("adding wallet address list to woadrs: "+str(wallet_addr_list))
self.add_watchonly_addresses(wallet_addr_list, wallet_name)
return

Expand All @@ -477,42 +478,58 @@ def sync_addresses(self, wallet):
buf = self.rpc('listtransactions', [wallet_name, 1000,
len(txs), True])
txs += buf
common.debug("Got these transactions: \n"+pprint.pformat(txs))
#TODO check whether used_addr_list can be a set, may be faster (if its a hashset) and allows
# using issubset() here and setdiff() for finding which addresses need importing
#TODO also check the fastest way to build up python lists, i suspect using += is slow
used_addr_list = [tx['address'] for tx in txs if tx['category'] == 'receive']
common.debug("Got this used address list: "+pprint.pformat(used_addr_list))
too_few_addr_mix_change = []
for mix_depth in range(wallet.max_mix_depth):
for forchange in [0, 1]:
common.debug("Working on depth: "+str(mix_depth)+" and forchange: "+str(forchange))
unused_addr_count = 0
last_used_addr = ''
breakloop = False
while not breakloop:
if unused_addr_count >= wallet.gaplimit and\
is_index_ahead_of_cache(wallet, mix_depth, forchange):
common.debug("Breaking out")
common.debug("wallet index is: "+pprint.pformat(wallet.index))
common.debug("unused address count is: "+str(unused_addr_count))
break
mix_change_addrs = [wallet.get_new_addr(mix_depth, forchange) for i in range(addr_req_count)]
common.debug("Generated mxchad: "+pprint.pformat(mix_change_addrs))
for mc_addr in mix_change_addrs:
if mc_addr not in imported_addr_list:
too_few_addr_mix_change.append((mix_depth, forchange))
common.debug("Breaking out")
common.debug("tfamc: "+pprint.pformat(too_few_addr_mix_change))
breakloop = True
break
if mc_addr in used_addr_list:
last_used_addr = mc_addr
common.debug("Reset last used addr to: "+last_used_addr)
unused_addr_count = 0
else:
common.debug("Incrementing unused addr count")
unused_addr_count += 1

if last_used_addr == '':
common.debug("last used addr was null, setting index to zero for:")
common.debug("mix depth: "+str(mix_depth)+" and forchange: "+str(forchange))
wallet.index[mix_depth][forchange] = 0
else:
common.debug("last used addr was not null, setting index to: ")
common.debug(str(wallet.addr_cache[last_used_addr][2] + 1))
common.debug("mix depth: "+str(mix_depth)+" and forchange: "+str(forchange))
wallet.index[mix_depth][forchange] = wallet.addr_cache[last_used_addr][2] + 1

wallet_addr_list = []
if len(too_few_addr_mix_change) > 0:
common.debug('too few addresses in ' + str(too_few_addr_mix_change))
for mix_depth, forchange in too_few_addr_mix_change:
wallet_addr_list += [wallet.get_new_addr(mix_depth, forchange) for i in range(addr_req_count*3)]
wallet_addr_list += [wallet.get_new_addr(mix_depth, forchange) for i in range(addr_req_count*10)]
self.add_watchonly_addresses(wallet_addr_list, wallet_name)
return

Expand Down
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()


Loading

0 comments on commit e19b7c8

Please sign in to comment.