Browse Source

Completed coverage of jmdaemon package.

master
Adam Gibson 9 years ago
parent
commit
d710bf9d9e
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 4
      .coveragerc
  2. 21
      conftest.py
  3. 6
      jmdaemon/jmdaemon/__init__.py
  4. 3
      jmdaemon/jmdaemon/daemon_protocol.py
  5. 8
      jmdaemon/jmdaemon/irc.py
  6. 23
      jmdaemon/jmdaemon/message_channel.py
  7. 13
      jmdaemon/jmdaemon/orderbookwatch.py
  8. 69
      jmdaemon/test/dummy_mc.py
  9. 190
      jmdaemon/test/msgdata.py
  10. 318
      jmdaemon/test/test_daemon_protocol.py
  11. 9
      jmdaemon/test/test_enc_wrapper.py
  12. 154
      jmdaemon/test/test_irc_messaging.py
  13. 374
      jmdaemon/test/test_message_channel.py
  14. 129
      jmdaemon/test/test_orderbookwatch.py
  15. 2
      setup.cfg

4
.coveragerc

@ -10,3 +10,7 @@ omit =
jmbitcoin/setup.py
jmbase/test/*
jmbase/setup.py
jmdaemon/test/*
jmdaemon/setup.py
jmdaemon/jmdaemon/socks.py
jmdaemon/jmdaemon/irc.py

21
conftest.py

@ -7,6 +7,7 @@ bitcoin_path = None
bitcoin_conf = None
bitcoin_rpcpassword = None
bitcoin_rpcusername = None
miniircd_procs = []
def local_command(command, bg=False, redirect=''):
if redirect == 'NULL':
@ -46,8 +47,17 @@ def pytest_addoption(parser):
action="store",
default='bitcoinrpc',
help="the RPC username for your test bitcoin instance (default=bitcoinrpc)")
parser.addoption("--nirc",
type="int",
action="store",
default=1,
help="the number of local miniircd instances")
def teardown():
#didn't find a stop command in miniircd, so just kill
global miniircd_procs
for m in miniircd_procs:
m.kill()
#shut down bitcoin and remove the regtest dir
local_command([bitcoin_path + "bitcoin-cli", "-regtest", "-rpcuser=" + bitcoin_rpcusername,
"-rpcpassword=" + bitcoin_rpcpassword, "stop"])
@ -65,6 +75,17 @@ def setup(request):
bitcoin_rpcpassword = request.config.getoption("--btcpwd")
bitcoin_rpcusername = request.config.getoption("--btcuser")
#start up miniircd
#minor bug in miniircd (seems); need *full* unqualified path for motd file
cwd = os.getcwd()
n_irc = request.config.getoption("--nirc")
global miniircd_procs
for i in range(n_irc):
miniircd_proc = local_command(
["./miniircd/miniircd", "--ports=" + str(6667+i),
"--motd=" + cwd + "/miniircd/testmotd"],
bg=True)
miniircd_procs.append(miniircd_proc)
#start up regtest blockchain
btc_proc = subprocess.call([bitcoin_path + "bitcoind", "-regtest",
"-daemon", "-conf=" + bitcoin_conf])

6
jmdaemon/jmdaemon/__init__.py

@ -10,11 +10,13 @@ from .message_channel import MessageChannel, MessageChannelCollection
from .orderbookwatch import OrderbookWatch
from jmbase import commands
from .daemon_protocol import JMDaemonServerProtocolFactory
from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH,
NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER)
from .message_channel import MessageChannelCollection
# Set default logging handler to avoid "No handler found" warnings.
try:
from logging import NullHandler
except ImportError:
except ImportError: #pragma: no cover
class NullHandler(logging.Handler):
def emit(self, record):
pass

3
jmdaemon/jmdaemon/daemon_protocol.py

@ -56,6 +56,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.restart_mc_required = False
self.irc_configs = None
self.mcc = None
self.crypto_boxes = {}
self.sig_lock = threading.Lock()
def checkClientResponse(self, response):
@ -63,7 +64,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
is considered criticial.
"""
if 'accepted' not in response or not response['accepted']:
reactor.stop()
reactor.stop() #pragma: no cover
def defaultErrback(self, failure):
failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone, ConnectionLost)

8
jmdaemon/jmdaemon/irc.py

@ -404,6 +404,12 @@ class IRCMessageChannel(MessageChannel):
self.pingQ = Queue.Queue()
self.throttleQ = Queue.Queue()
self.obQ = Queue.Queue()
self.reconnect_interval = 30
def set_reconnect_interval(self, interval):
"""For testing reconnection functions.
"""
self.reconnect_interval = interval
def run(self):
self.give_up = False
@ -467,6 +473,6 @@ class IRCMessageChannel(MessageChannel):
log.info("disconnected from irc host %s" %
(self.hostid))
if not self.give_up:
time.sleep(30)
time.sleep(self.reconnect_interval)
log.info('ending irc')
self.give_up = True

23
jmdaemon/jmdaemon/message_channel.py

@ -148,7 +148,7 @@ class MessageChannelCollection(object):
for mc in self.mchannels:
mc.daemon = daemon
def add_channel(self, mchannel):
def add_channel(self, mchannel): #pragma: no cover
"""TODO Not currently in use,
may be some issues with intialization.
"""
@ -277,7 +277,7 @@ class MessageChannelCollection(object):
matching_channels = [x
for x in self.available_channels()
if mc == x.hostid]
if len(matching_channels) != 1:
if len(matching_channels) != 1: #pragma: no cover
#raise because implies logic error
raise Exception(
"Tried to privmsg on an unavailable message channel.")
@ -850,7 +850,6 @@ class MessageChannel(object):
def send_error(self, nick, errormsg):
log.info('error<%s> : %s' % (nick, errormsg))
self.privmsg(nick, 'error', errormsg)
raise CJPeerError()
def pubmsg(self, message):
log.debug('>>pubmsg ' + message)
@ -892,7 +891,7 @@ class MessageChannel(object):
elif _chunks[0] == 'orderbook':
if self.on_orderbook_requested:
self.on_orderbook_requested(nick, self)
else:
else: #pragma: no cover
# TODO this is for testing/debugging, should be removed, see taker.py
if hasattr(self, 'debug_on_pubmsg_cmd'):
self.debug_on_pubmsg_cmd(nick, _chunks)
@ -949,8 +948,8 @@ class MessageChannel(object):
to_decrypt = ''.join(_chunks[1:])
try:
decrypted = decode_decrypt(to_decrypt, box)
except ValueError as e:
log.debug('valueerror when decrypting, skipping: ' +
except (ValueError, TypeError) as e:
log.debug('Error when decrypting, skipping: ' +
repr(e))
return
#rebuild the chunks array as if it had been plaintext
@ -997,13 +996,13 @@ class MessageChannel(object):
commit = None
except (ValueError, IndexError) as e:
self.send_error(nick, str(e))
return
if self.on_order_fill:
self.on_order_fill(nick, oid, amount, taker_pk, commit)
elif _chunks[0] == 'auth':
try:
cr = _chunks[1]
except (ValueError, IndexError) as e:
self.send_error(nick, str(e))
#Note index error logically impossible, would have thrown
#in sig check (zero message after cmd not allowed)
cr = _chunks[1]
if self.on_seen_auth:
self.on_seen_auth(nick, cr)
elif _chunks[0] == 'tx':
@ -1012,6 +1011,7 @@ class MessageChannel(object):
txhex = base64.b64decode(b64tx).encode('hex')
except TypeError as e:
self.send_error(nick, 'bad base64 tx. ' + repr(e))
return
if self.on_seen_tx:
self.on_seen_tx(nick, txhex)
elif _chunks[0] == 'push':
@ -1020,9 +1020,10 @@ class MessageChannel(object):
txhex = base64.b64decode(b64tx).encode('hex')
except TypeError as e:
self.send_error(nick, 'bad base64 tx. ' + repr(e))
return
if self.on_push_tx:
self.on_push_tx(nick, txhex)
except CJPeerError:
except (IndexError, ValueError):
# TODO proper error handling
log.debug('cj peer error TODO handle')
continue

13
jmdaemon/jmdaemon/orderbookwatch.py

@ -65,7 +65,7 @@ class OrderbookWatch(object):
if int(oid) < 0 or int(oid) > sys.maxint:
log.debug("Got invalid order ID: " + oid + " from " +
counterparty)
return (False, [])
return
# delete orders eagerly, so in case a buggy maker sends an
# invalid offer, we won't accidentally !fill based on the ghost
# of its previous message.
@ -76,7 +76,7 @@ class OrderbookWatch(object):
if int(minsize) < 0 or int(minsize) > 21 * 10**14:
log.debug("Got invalid minsize: {} from {}".format(
minsize, counterparty))
return (False, [])
return
if int(minsize) < DUST_THRESHOLD:
minsize = DUST_THRESHOLD
log.debug("{} has dusty minsize, capping at {}".format(
@ -85,24 +85,24 @@ class OrderbookWatch(object):
if int(maxsize) < 0 or int(maxsize) > 21 * 10**14:
log.debug("Got invalid maxsize: " + maxsize + " from " +
counterparty)
return (False, [])
return
if int(txfee) < 0:
log.debug("Got invalid txfee: {} from {}".format(txfee,
counterparty))
return (False, [])
return
if int(minsize) > int(maxsize):
fmt = ("Got minsize bigger than maxsize: {} - {} "
"from {}").format
log.debug(fmt(minsize, maxsize, counterparty))
return (False, [])
return
if ordertype == 'absoffer' and not isinstance(cjfee, int):
try:
cjfee = int(cjfee)
except ValueError:
log.debug("Got non integer coinjoin fee: " + str(cjfee) +
" for an absoffer from " + counterparty)
return (False, [])
return
self.db.execute(
'INSERT INTO orderbook VALUES(?, ?, ?, ?, ?, ?, ?);',
(counterparty, oid, ordertype, minsize, maxsize, txfee,
@ -114,7 +114,6 @@ class OrderbookWatch(object):
log.debug("Exception was: " + repr(e))
finally:
self.dblock.release()
return (True, [])
def on_order_cancel(self, counterparty, oid):
with self.dblock:

69
jmdaemon/test/dummy_mc.py

@ -0,0 +1,69 @@
from __future__ import absolute_import, print_function
import base64
import random
import socket
import ssl
import threading
import time
from jmdaemon.message_channel import MessageChannel
from jmdaemon.protocol import *
from jmclient import get_log
from msgdata import *
log = get_log()
# handle one channel at a time
class DummyMessageChannel(MessageChannel):
def __init__(self,
configdata,
username='username',
realname='realname',
password=None,
daemon=None,
hostid=None):
MessageChannel.__init__(self, daemon=daemon)
self.give_up = False
self.counterparties = [x['counterparty'] for x in t_orderbook]
self.hostid = "dummy"
if hostid:
self.hostid = hostid
self.serverport = self.hostid
def __str__(self):
return self.hostid
def run(self):
"""Simplest possible event loop."""
i = 0
while True:
if self.give_up:
break
time.sleep(0.5)
if i == 1:
if self.on_welcome:
log.debug("Calling on welcome")
self.on_welcome(self)
i += 1
def shutdown(self):
self.give_up = True
def close(self):
self.shutdown()
def _pubmsg(self, msg):
pass
def _privmsg(self, nick, cmd, message):
"""As for pubmsg
"""
pass
def _announce_orders(self, orderlist):
pass
def change_nick(self, new_nick):
print("Changing nick supposedly")

190
jmdaemon/test/msgdata.py

@ -0,0 +1,190 @@
#orderbook
t_orderbook = [{u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}]
t_dest_addr = "mvw1NazKDRbeNufFANqpYNAANafsMC2zVU"
t_chosen_orders = {u'J559UPUSLLjHJpaB': {u'cjfee': u'0.0002',
u'counterparty': u'J559UPUSLLjHJpaB',
u'maxsize': 599972700,
u'minsize': 7500000,
u'oid': 0,
u'ordertype': u'reloffer',
u'txfee': 1000},
u'J55z23xdjxJjC7er': {u'cjfee': u'0.0002',
u'counterparty': u'J55z23xdjxJjC7er',
u'maxsize': 599972700,
u'minsize': 7500000,
u'oid': 0,
u'ordertype': u'reloffer',
u'txfee': 1000},
u'J5CFffuuewjG44UJ': {u'cjfee': u'0.0002',
u'counterparty': u'J5CFffuuewjG44UJ',
u'maxsize': 599972700,
u'minsize': 7500000,
u'oid': 0,
u'ordertype': u'reloffer',
u'txfee': 1000}}
"""
2016-12-01 15:27:33,351 [MainThread ] [INFO ] total cj fee = 63000
2016-12-01 15:27:33,351 [MainThread ] [INFO ] total coinjoin fee = 0.0573%
2016-12-01 15:27:34,887 [MainThread ] [DEBUG] INFO:Preparing bitcoin data..
2016-12-01 15:27:34,888 [MainThread ] [DEBUG] rpc: getaccount ['myzi6K9vt88rdiXpYayfJkU1x33G1wz2fP']
2016-12-01 15:27:34,889 [MainThread ] [DEBUG] total estimated amount spent = 110093000
"""
t_utxos_by_mixdepth = {0: {u'534b635ed8891f16c4ec5b8236ae86164783903e8e8bb47fa9ef2ca31f3c2d7a:0': {'address': u'mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ',
'value': 200000000}},
1: {u'0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1': {'address': u'mvtY8DVgn3TtvjHbVsauYoSQjAhNqVyqmM',
'value': 200000000},
u'7e574db96a4d43a99786b3ea653cda9e4388f377848f489332577e018380cff1:0': {'address': u'n3nELhmU2D7ebGYzJnGFWgVDK3cYErmTcQ',
'value': 200000000},
u'dd9711a2ef340750db21efb761f5f7d665d94b312332dc354e252c77e9c48349:0': {'address': u'mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf',
'value': 200000000}},
2: {},
3: {},
4: {}}
t_selected_utxos = [{'utxo': u'0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1',
'value': 200000000}]
t_generated_podle = {'P': '025a2e04dc6bd5f58fe4eb13045b27f0dd17c39524264639f48607347cf6d69c4e',
'P2': '0223e54e9917d8482f1b54ead8e941907c17051b95397e8bc110adc6681d8d44c8',
'commit': 'aa0545c9ed918e66f86df467c96a4978529b836aa4688df682a2db4e27d4ed9d',
'e': '5b7ab1fa21287bbf0df4a0c46f6c31c3f17887ee9ea6ae584fc3a861ae9f1e9d',
'sig': 'ebe25d7b2d667de802677c30c6fea07386f0cd67d4e4c795e4a6ebc39b21eb39',
'used': 'False',
'utxo': u'0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1'}
t_maker_response = {"J559UPUSLLjHJpaB":
[["03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6:1"],
"03a2d1cbe977b1feaf8d0d5cc28c686859563d1520b28018be0c2661cf1ebe4857",
"mrKTGvFfYUEqk52qPKUroumZJcpjHLQ6pn",
"mxPnzFkCQpPzVQdajNLoT4us5pTPsQZZZp",
"MEQCIBeGrtxxVrj5tSUX6vEetmzE8nRBG/guSXq3SrqypIt5AiAnIZzDUXu8DtODgF2p1Bo27L8VcG1GJSfatZbS23YZQQ==",
"5bcc7ae1a3530e454812668620aced47d774bf06a1f5870d531422a1a958b629"],
"J55z23xdjxJjC7er":
[["498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3:0"],
"02b4b749d54e96b04066b0803e372a43d6ffa16e75a001ae0ed4b235674ab286be",
"mhatyHdna3Qt5FtnfwWaMVV1dohCaDYF3T",
"mjJoVN2HCUGVDvNebiFnHdB3zF56bxQm5z",
"MEQCIBlMF7DRbhr14e74He9m+UYjR5y8jjvP7TvUh8valebmAiBoIGjl436fsYim9pKSTbCKiBmT82hQ98LvIOGSLprk0A==",
"8204d1cba30d4cdabab16a5e8d10d17464e24c78a6f887ae2d920b223c030d28"],
"J5CFffuuewjG44UJ":
[["3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c:1"],
"023bcbafb4f68455e0d1d117c178b0e82a84e66414f0987453d78da034b299c3a9",
"mpAEocXy8ckcJBo3fhQg9Mv1kfEzAuUivX",
"n29NWbsyq5MjCMC5ykjStd78zwfjvCvJJZ",
"MEUCIQDAM5Aa0aU5iKI0b9YnNtwH0m+6sz3zeTL8f398CPjuQAIgLeU9mCJX8SupNNMkA+bsUJeRYe3kiLnzq3OlmXTxck0=",
"7377d03477485884e0129dbdb2d79f4956f5b74366d805385b6f127509a8433f"]}
"""
2016-12-01 15:27:39,914 [MainThread ] [DEBUG] rpc: gettxout [u'03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6', 1, False]
2016-12-01 15:27:39,915 [MainThread ] [DEBUG] fee breakdown for J559UPUSLLjHJpaB totalin=200000000 cjamount=110000000 txfee=1000 realcjfee=22000
2016-12-01 15:27:39,915 [MainThread ] [DEBUG] rpc: gettxout [u'498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False]
2016-12-01 15:27:39,915 [MainThread ] [DEBUG] fee breakdown for J55z23xdjxJjC7er totalin=200000000 cjamount=110000000 txfee=1000 realcjfee=22000
2016-12-01 15:27:39,916 [MainThread ] [DEBUG] rpc: gettxout [u'3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c', 1, False]
2016-12-01 15:27:39,916 [MainThread ] [DEBUG] fee breakdown for J5CFffuuewjG44UJ totalin=200000000 cjamount=110000000 txfee=1000 realcjfee=22000
2016-12-01 15:27:39,916 [MainThread ] [DEBUG] INFO:Got all parts, enough to build a tx
2016-12-01 15:27:39,917 [MainThread ] [DEBUG] Estimated transaction size: 870
2016-12-01 15:27:39,917 [MainThread ] [DEBUG] rpc: estimatefee [3]
2016-12-01 15:27:39,917 [MainThread ] [DEBUG] got estimated tx bytes: 870
2016-12-01 15:27:39,917 [MainThread ] [INFO ] Based on initial guess: 30000, we estimated a miner fee of: 26100
2016-12-01 15:27:39,918 [MainThread ] [INFO ] fee breakdown for me totalin=200000000 my_txfee=23100 makers_txfee=3000 cjfee_total=66000 => changevalue=89910900
"""
t_obtained_tx = {'ins': [{'outpoint': {'hash': '03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6',
'index': 1},
'script': '',
'sequence': 4294967295},
{'outpoint': {'hash': '3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c',
'index': 1},
'script': '',
'sequence': 4294967295},
{'outpoint': {'hash': '498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3',
'index': 0},
'script': '',
'sequence': 4294967295},
{'outpoint': {'hash': '0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75',
'index': 1},
'script': '',
'sequence': 4294967295}],
'locktime': 0,
'outs': [{'script': '76a914767c956efe6092a775fea39a06d1cac9aae956d788ac',
'value': 110000000},
{'script': '76a914cab20e3270988ac99651b8f079a3b4c93b996a6888ac',
'value': 89910900},
{'script': '76a914b91f75254b5fa1510cc944a2206ed72235d0d88188ac',
'value': 90021000},
{'script': '76a914a916707952c2df28a3abf3ee692dfbbd5a4d74dc88ac',
'value': 110000000},
{'script': '76a914e245b480b46bcbc9d13e68766ad19909decd135288ac',
'value': 90021000},
{'script': '76a91416af241bb1db02dfd7c65989bbab190ac489ccc188ac',
'value': 110000000},
{'script': '76a9142994295a9d4d083eb792e669e3211007dc78928888ac',
'value': 90021000},
{'script': '76a9145ece2dac945c8ff5b2b6635360ca0478ade305d488ac',
'value': 110000000}],
'version': 1}
#signatures from makers
"""
nick=J5CFffuuewjG44UJ message=!sig xH9IAMo2fvG+g+DAbLNOPsGsJCDm6r+ZY5QM7p+SRsixbqSwXcBQAn7Mnw1rS+uGlrJkM8ossX5VHKjdKDhTXQVLawR7XgiVFFFiO+/FjdFhqVuS4Q/NgOlb7nCBe/UaBebd9NpuURG+8u/V+46jtqKRtVsSO1+QZQBt2nSpYCqxWIjxMowRxS4O/zlrOVbyjv/AjchOajufKJwckkrkJDyQDYlUdW+eqs43tf0XsJ9k4NHRVVHAQQ== 036558f550b1d398d2325d892e50ef25b0f663ae13f70d0b304a15f07030061ace MEUCIQCE9MgU+HfcHkKE8zNzNeCEdDBJuQatA6C2sTJ9mVKK7wIgX4w9r0tz4s9qeuW0UjNliDatJ4X7pS3/atADSqPat0U=
nick=J559UPUSLLjHJpaB message=!sig geHTf1n88eKeUnVOj7bIrJF1KFCN03IQhZD0cR17Q7jPSn2DZrrvMaRNkjZRyF+zGnWFwd69kwLRU0ftCaMf/3lw+05UovVCREiyXWUtPJa7XAY2NW4iMmTnGTp8f9RLgDcDhiZayKXTpzBDC9r6WAt6wiD0lej5uw7dmluKSUyfXW8sOYPmLm4iJAPcbGeJiQfiR9zBeX8w+6Kz4bkaiue41SzQP/h9avPV2XIX4kVQQ3jLfQyHww== 038f90ab260df440cef82a981146b509eb9df019884e145158230e8babc17d7be4 MEQCIEo5Pau9zqW2lw+B2AYTYuTO5TDbBkgsOk0bqT+SQctKAiBO1nbsmYTy7E0Qd7jAxko1Gq6Yk0Q6DerByuEuk5IBSQ==
nick=J55z23xdjxJjC7er message=!sig A5CWvqmYCOiZBEEi9iHVpQL0oO9B7VIIzuU9QhkzXOw+iD916C9b+Yk3eTxrtf+qaLARQ7eui6zdPNek95EdmqCEqM/myeeuBVSy9KrcB9xU0sdnuCu4+g13jVe9Pkvd1iizZ8GCNP7SejEzeltNr0a1lR+M0kKtj4XI+nDTxhisSzL8PDXsqoOMcrDjegna3TZsJeKviu8r/1T/zWwTQtRCXqruLnflqXNLtZoyFmoaO1GurgkNHA== 029a8beadec242f04f2295787ac0175b960e2d68d115ec65c4310de7ce3fa2cec0 MEQCIHpTxVkwtvm7agbp47Z5V0We8jxXkfZDUFsW2tZwTZdHAiA9JnYvo74hF3RihzHw2l+ufTOmC/3ddBpxkB9+AdZvzA==
"""
"""
2016-12-01 15:27:39,921 [MainThread ] [DEBUG] INFO:Built tx, sending to counterparties.
2016-12-01 15:27:39,968 [MainThread ] [DEBUG] rpc: gettxout ['03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6', 1, False]
2016-12-01 15:27:39,968 [MainThread ] [DEBUG] rpc: gettxout ['3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c', 1, False]
2016-12-01 15:27:39,969 [MainThread ] [DEBUG] rpc: gettxout ['498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False]
2016-12-01 15:27:39,971 [MainThread ] [DEBUG] found good sig at index=1
2016-12-01 15:27:39,971 [MainThread ] [DEBUG] nick = J5CFffuuewjG44UJ sent all sigs, removing from nonrespondant list
2016-12-01 15:27:39,971 [MainThread ] [DEBUG] rpc: gettxout ['03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6', 1, False]
2016-12-01 15:27:39,972 [MainThread ] [DEBUG] rpc: gettxout ['498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False]
2016-12-01 15:27:39,973 [MainThread ] [DEBUG] found good sig at index=0
2016-12-01 15:27:39,973 [MainThread ] [DEBUG] nick = J559UPUSLLjHJpaB sent all sigs, removing from nonrespondant list
2016-12-01 15:27:43,937 [MainThread ] [DEBUG] rpc: gettxout ['498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False]
2016-12-01 15:27:43,938 [MainThread ] [DEBUG] found good sig at index=2
2016-12-01 15:27:43,938 [MainThread ] [DEBUG] nick = J55z23xdjxJjC7er sent all sigs, removing from nonrespondant list
2016-12-01 15:27:43,938 [MainThread ] [DEBUG] all makers have sent their signatures
2016-12-01 15:27:43,938 [MainThread ] [DEBUG] INFO:Transaction is valid, signing..
2016-12-01 15:27:43,943 [MainThread ] [DEBUG]
"""
t_raw_signed_tx = "0100000004f65091c0d60f75020234f07dee9246db32af6a8f30f833138a279e654a3f2403010000006b483045022100ad522388ce9eacf2760e4d6bd6a114a0e15b88879b430fbb2e60df947494df2402201f49338726599eb0980873aef268d8d890de2792967ff28f0c11eb35e54ff07a012103a2d1cbe977b1feaf8d0d5cc28c686859563d1520b28018be0c2661cf1ebe4857ffffffff3c2abd571046e93a0471c6e47a061bcbfa982c392c1ddcd88ae006d720a83e3f010000006a473044022012bbfa6ef7b0416e00001d90b022d6663f5fd57d9a07bb70b887510f7c44902d022059b382bfb1ff5588a518fc69c55b2ff67d3facc11088e984d7c09c336d4875330121023bcbafb4f68455e0d1d117c178b0e82a84e66414f0987453d78da034b299c3a9ffffffffc3d450f208681405a0c7f0b46816f2121ef302e20c6b3c443b4f53228baa8f49000000006b48304502210081019ea7b68130da4230fd748668c776043004843de50e07bb5fcb42e7632aed022000a34878274e583eec64815d1b587e7fbcd9ac714e722773ac1e69f1209f2e10012102b4b749d54e96b04066b0803e372a43d6ffa16e75a001ae0ed4b235674ab286beffffffff755d83f964d24f33de8f193a10932400bafcb47b9919351af0bf81e3e5d68007010000006b483045022100add68e9532a50ca5585999290531f26e515bdf3d001519b0de8dd6b981daec7f02200b34c58ce61e6673c9efc5bf82cacd4d02673bc1c6cbaba45b5c65579776b8180121025a2e04dc6bd5f58fe4eb13045b27f0dd17c39524264639f48607347cf6d69c4effffffff0880778e06000000001976a914767c956efe6092a775fea39a06d1cac9aae956d788ac74ee5b05000000001976a914cab20e3270988ac99651b8f079a3b4c93b996a6888ac889c5d05000000001976a914b91f75254b5fa1510cc944a2206ed72235d0d88188ac80778e06000000001976a914a916707952c2df28a3abf3ee692dfbbd5a4d74dc88ac889c5d05000000001976a914e245b480b46bcbc9d13e68766ad19909decd135288ac80778e06000000001976a91416af241bb1db02dfd7c65989bbab190ac489ccc188ac889c5d05000000001976a9142994295a9d4d083eb792e669e3211007dc78928888ac80778e06000000001976a9145ece2dac945c8ff5b2b6635360ca0478ade305d488ac00000000"
t_txid = "4d5bfad9bbfb93eb1e25fb2e6c832323d1bf39e63f6ed2319b65e85354c7ca70"
t_dummy_ext = {"used": [], "external": {
"79f1b8df7d0978f30028487c6c4e0eae96d1aa18e01f13bb4cba6788590cd431:1": {
"reveal": {
"1": {
"P2": "0329d4b4bb28c1a0747c1a5daad59763a9021b5e1fa957887a90c7849789a683b6",
"s": "a303cad939fb773dd16a81c44f210afe0b985a2cf9a63b033139455b70c77be6",
"e": "64f5b9861b95434ab84bd044b93a28f85ea94b474237992d899bd4302eef3820"
},
"0": {
"P2": "02681ed66595daf98b12d6d69d8afb8d14a531eeaea1161bce8b9f2666ea55f157",
"s": "ed994ad173431bd0f53c82fee70d202e9c2adce492b6226d3cb4116cc3a08383",
"e": "1dd7f56fe83ca66e89b3ec3b73fa44edacab0ef4524652c415065dbf91500c85"
},
"2": {
"P2": "02cdd5ced7e79bdb651d6d1883e0047509793a9a9e3da4ae516b8a853b9cdd8e98",
"s": "39a19287c4bacc823559d0e1b907e311c31d8a13f45fe30d10b133561113515c",
"e": "a0e7cd319c7e51c6f9e503e95d08c3d2398f9b546c2d64178b6c113c63c29d78"
}
},
"P": "033749d513d0e0239a75892556a6ce01c3e48f82e75169129abe8ef370ab992c94"
}}}

318
jmdaemon/test/test_daemon_protocol.py

@ -0,0 +1,318 @@
#! /usr/bin/env python
from __future__ import absolute_import
'''test daemon-protocol interfacae.'''
import pytest
from jmdaemon import (JMDaemonServerProtocolFactory, MessageChannelCollection)
from jmdaemon.orderbookwatch import OrderbookWatch
from jmdaemon.daemon_protocol import JMDaemonServerProtocol
from jmdaemon.protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH,
NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER)
from jmclient import (load_program_config, get_log, jm_single, get_irc_mchannels,
JMTakerClientProtocolFactory, Taker, AbstractWallet)
import os
from twisted.python.log import startLogging, err
from twisted.python.log import msg as tmsg
from twisted.internet import protocol, reactor, task
from twisted.internet.protocol import ServerFactory, ClientCreator
from twisted.internet.error import (ConnectionLost, ConnectionAborted,
ConnectionClosed, ConnectionDone)
from twisted.protocols.amp import UnknownRemoteError
from twisted.python import failure
from twisted.protocols import amp
from twisted.trial import unittest
from jmbase.commands import *
from msgdata import *
import json
import time
import base64
from dummy_mc import DummyMessageChannel
test_completed = False
end_early = False
jlog = get_log()
class JMProtocolError(Exception):
pass
class JMBaseProtocol(amp.AMP):
def checkClientResponse(self, response):
"""A generic check of client acceptance; any failure
is considered criticial.
"""
if 'accepted' not in response or not response['accepted']:
reactor.stop()
def defaultErrback(self, failure):
failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone,
ConnectionLost, UnknownRemoteError)
reactor.stop()
def defaultCallbacks(self, d):
d.addCallback(self.checkClientResponse)
d.addErrback(self.defaultErrback)
class JMTestClientProtocol(JMBaseProtocol):
def connectionMade(self):
self.clientStart()
def clientStart(self):
self.sigs_received = 0
irc = get_irc_mchannels()
d = self.callRemote(JMInit,
bcsource="dummyblockchain",
network="dummynetwork",
irc_configs=json.dumps(irc),
minmakers=2,
maker_timeout_sec=3)
self.defaultCallbacks(d)
@JMInitProto.responder
def on_JM_INIT_PROTO(self, nick_hash_length, nick_max_encoded,
joinmarket_nick_header, joinmarket_version):
show_receipt("JMINITPROTO", nick_hash_length, nick_max_encoded,
joinmarket_nick_header, joinmarket_version)
d = self.callRemote(JMStartMC,
nick="dummynick")
self.defaultCallbacks(d)
return {'accepted': True}
@JMUp.responder
def on_JM_UP(self):
show_receipt("JMUP")
d = self.callRemote(JMSetup,
role="TAKER",
n_counterparties=4) #TODO this number should be set
self.defaultCallbacks(d)
return {'accepted': True}
@JMSetupDone.responder
def on_JM_SETUP_DONE(self):
show_receipt("JMSETUPDONE")
d = self.callRemote(JMRequestOffers)
self.defaultCallbacks(d)
return {'accepted': True}
@JMFillResponse.responder
def on_JM_FILL_RESPONSE(self, success, ioauth_data):
show_receipt("JMFILLRESPONSE", success, ioauth_data)
reactor.callLater(1, self.maketx, ioauth_data)
return {'accepted': True}
def maketx(self, ioauth_data):
ioauth_data = json.loads(ioauth_data)
nl = ioauth_data.keys()
d = self.callRemote(JMMakeTx,
nick_list= json.dumps(nl),
txhex="deadbeef")
self.defaultCallbacks(d)
@JMOffers.responder
def on_JM_OFFERS(self, orderbook):
if end_early:
return {'accepted': True}
jlog.debug("JMOFFERS" + str(orderbook))
#Trigger receipt of verified privmsgs, including unverified
nick = str(t_chosen_orders.keys()[0])
b64tx = base64.b64encode("deadbeef")
d1 = self.callRemote(JMMsgSignatureVerify,
verif_result=True,
nick=nick,
fullmsg="!push " + b64tx + " abc def",
hostid="dummy")
self.defaultCallbacks(d1)
#unverified
d2 = self.callRemote(JMMsgSignatureVerify,
verif_result=False,
nick=nick,
fullmsg="!push " + b64tx + " abc def",
hostid="dummy")
self.defaultCallbacks(d2)
d = self.callRemote(JMFill,
amount=100,
commitment="dummycommitment",
revelation="dummyrevelation",
filled_offers=json.dumps(t_chosen_orders))
self.defaultCallbacks(d)
return {'accepted': True}
@JMSigReceived.responder
def on_JM_SIG_RECEIVED(self, nick, sig):
show_receipt("JMSIGRECEIVED", nick, sig)
self.sigs_received += 1
if self.sigs_received == 3:
#end of test
reactor.callLater(1, end_test)
return {'accepted': True}
@JMRequestMsgSig.responder
def on_JM_REQUEST_MSGSIG(self, nick, cmd, msg, msg_to_be_signed, hostid):
show_receipt("JMREQUESTMSGSIG", nick, cmd, msg, msg_to_be_signed, hostid)
d = self.callRemote(JMMsgSignature,
nick=nick,
cmd=cmd,
msg_to_return="xxxcreatedsigxx",
hostid=hostid)
self.defaultCallbacks(d)
return {'accepted': True}
@JMRequestMsgSigVerify.responder
def on_JM_REQUEST_MSGSIG_VERIFY(self, msg, fullmsg, sig, pubkey, nick,
hashlen, max_encoded, hostid):
show_receipt("JMREQUESTMSGSIGVERIFY", msg, fullmsg, sig, pubkey,
nick, hashlen, max_encoded, hostid)
d = self.callRemote(JMMsgSignatureVerify,
verif_result=True,
nick=nick,
fullmsg=fullmsg,
hostid=hostid)
self.defaultCallbacks(d)
return {'accepted': True}
class JMTestClientProtocolFactory(protocol.ClientFactory):
protocol = JMTestClientProtocol
def show_receipt(name, *args):
tmsg("Received msgtype: " + name + ", args: " + ",".join([str(x) for x in args]))
def end_test():
global test_completed
test_completed = True
class JMDaemonTestServerProtocol(JMDaemonServerProtocol):
def __init__(self, factory):
super(JMDaemonTestServerProtocol, self).__init__(factory)
#respondtoioauths should do nothing unless jmstate = 2
self.respondToIoauths(True)
#calling on_JM_MAKE_TX should also do nothing in wrong state
assert super(JMDaemonTestServerProtocol, self).on_JM_MAKE_TX(
1, 2) == {'accepted': False}
#calling on_JM_FILL with negative amount should reject
assert super(JMDaemonTestServerProtocol, self).on_JM_FILL(
-1000, 2, 3, 4) == {'accepted': False}
#checkutxos also does nothing for rejection at the moment
self.checkUtxosAccepted(False)
#None should be returned requesting a cryptobox for an unknown cp
assert self.get_crypto_box_from_nick("notrealcp") == None
#does nothing yet
self.on_error()
@JMRequestOffers.responder
def on_JM_REQUEST_OFFERS(self):
for o in t_orderbook:
#counterparty, oid, ordertype, minsize, maxsize,txfee, cjfee):
self.on_order_seen(o["counterparty"], o["oid"], o["ordertype"],
o["minsize"], o["maxsize"],
o["txfee"], o["cjfee"])
return super(JMDaemonTestServerProtocol, self).on_JM_REQUEST_OFFERS()
@JMInit.responder
def on_JM_INIT(self, bcsource, network, irc_configs, minmakers,
maker_timeout_sec):
self.maker_timeout_sec = int(maker_timeout_sec)
self.minmakers = int(minmakers)
mcs = [DummyMessageChannel(None)]
self.mcc = MessageChannelCollection(mcs)
#The following is a hack to get the counterparties marked seen/active;
#note it must happen before callign set_msgchan for OrderbookWatch
self.mcc.on_order_seen = None
for c in [o['counterparty'] for o in t_orderbook]:
self.mcc.on_order_seen_trigger(mcs[0], c, "a", "b", "c", "d", "e", "f")
OrderbookWatch.set_msgchan(self, self.mcc)
#register taker-specific msgchan callbacks here
self.mcc.register_taker_callbacks(self.on_error, self.on_pubkey,
self.on_ioauth, self.on_sig)
self.mcc.set_daemon(self)
self.restart_mc_required = True
d = self.callRemote(JMInitProto,
nick_hash_length=NICK_HASH_LENGTH,
nick_max_encoded=NICK_MAX_ENCODED,
joinmarket_nick_header=JOINMARKET_NICK_HEADER,
joinmarket_version=JM_VERSION)
self.defaultCallbacks(d)
return {'accepted': True}
@JMFill.responder
def on_JM_FILL(self, amount, commitment, revelation, filled_offers):
tmpfo = json.loads(filled_offers)
dummypub = "073732a7ca60470f709f23c602b2b8a6b1ba62ee8f3f83a61e5484ab5cbf9c3d"
#trigger invalid on_pubkey conditions
reactor.callLater(1, self.on_pubkey, "notrealcp", dummypub)
reactor.callLater(2, self.on_pubkey, tmpfo.keys()[0], dummypub + "deadbeef")
#trigger invalid on_ioauth condition
reactor.callLater(2, self.on_ioauth, "notrealcp", 1, 2, 3, 4, 5)
#trigger msg sig verify request operation for a dummy message
#currently a pass-through
reactor.callLater(1, self.request_signature_verify, "1",
"!push abcd abc def", "3", "4",
str(tmpfo.keys()[0]), 6, 7, self.mcc.mchannels[0].hostid)
#send "valid" onpubkey, onioauth messages
for k, v in tmpfo.iteritems():
reactor.callLater(1, self.on_pubkey, k, dummypub)
reactor.callLater(2, self.on_ioauth, k, ['a', 'b'], "auth_pub",
"cj_addr", "change_addr", "btc_sig")
return super(JMDaemonTestServerProtocol, self).on_JM_FILL(amount,
commitment, revelation, filled_offers)
@JMMakeTx.responder
def on_JM_MAKE_TX(self, nick_list, txhex):
for n in nick_list:
reactor.callLater(1, self.on_sig, n, "dummytxsig")
return super(JMDaemonTestServerProtocol, self).on_JM_MAKE_TX(nick_list,
txhex)
class JMDaemonTestServerProtocolFactory(ServerFactory):
protocol = JMDaemonTestServerProtocol
def buildProtocol(self, addr):
return JMDaemonTestServerProtocol(self)
class TrialTestJMDaemonProto(unittest.TestCase):
def setUp(self):
load_program_config()
jm_single().maker_timeout_sec = 1
self.port = reactor.listenTCP(27184, JMDaemonTestServerProtocolFactory())
self.addCleanup(self.port.stopListening)
clientconn = reactor.connectTCP("localhost", 27184,
JMTestClientProtocolFactory())
self.addCleanup(clientconn.disconnect)
print("Got here")
def test_waiter(self):
print("test_main()")
return task.deferLater(reactor, 12, self._called_by_deffered)
def _called_by_deffered(self):
pass
class TestJMDaemonProtoInit(unittest.TestCase):
def setUp(self):
global end_early
end_early = True
print("setUp()")
load_program_config()
jm_single().maker_timeout_sec = 1
self.port = reactor.listenTCP(27184, JMDaemonServerProtocolFactory())
self.addCleanup(self.port.stopListening)
clientconn = reactor.connectTCP("localhost", 27184,
JMTestClientProtocolFactory())
self.addCleanup(clientconn.disconnect)
print("Got here")
def test_waiter(self):
print("test_main()")
return task.deferLater(reactor, 5, self._called_by_deffered)
def _called_by_deffered(self):
global end_early
end_early = False

9
jmdaemon/test/test_enc_wrapper.py

@ -5,7 +5,7 @@ import random
import pytest
from jmdaemon import (init_keypair, get_pubkey, init_pubkey, as_init_encryption,
NaclError)
NaclError, encrypt_encode, decode_decrypt)
@pytest.mark.parametrize("ab_message,ba_message,num_iterations",
@ -43,6 +43,7 @@ def test_enc_wrapper(alice_bob_boxes, ab_message, ba_message, num_iterations):
alice_ptext = alice_box.decrypt(otw_bmsg)
assert alice_ptext == ba_message, "Encryption test: FAILED. Bob sent: %s, Alice received: " % (
ba_message, alice_ptext)
assert decode_decrypt(encrypt_encode(ab_message, bob_box), bob_box) == ab_message
@pytest.mark.parametrize("invalid_pubkey",
[
@ -68,8 +69,8 @@ def test_invalid_nacl_keys(alice_bob_boxes, invalid_pubkey):
@pytest.fixture()
def alice_bob_boxes():
alice_kp = init_keypair()
bob_kp = init_keypair()
alice_kp = init_keypair("alicekey")
bob_kp = init_keypair("bobkey")
# this is the DH key exchange part
bob_otwpk = get_pubkey(bob_kp, True)
@ -77,7 +78,7 @@ def alice_bob_boxes():
bob_pk = init_pubkey(bob_otwpk)
alice_box = as_init_encryption(alice_kp, bob_pk)
alice_pk = init_pubkey(alice_otwpk)
alice_pk = init_pubkey(alice_otwpk, "alicepubkey")
bob_box = as_init_encryption(bob_kp, alice_pk)
# now Alice and Bob can use their 'box'

154
jmdaemon/test/test_irc_messaging.py

@ -0,0 +1,154 @@
#! /usr/bin/env python
from __future__ import absolute_import
'''Tests of joinmarket bots end-to-end (including IRC and bitcoin) '''
import subprocess
import signal
import os
import pytest
import time
import threading
import hashlib
import jmbitcoin as btc
from jmdaemon import (JOINMARKET_NICK_HEADER, NICK_HASH_LENGTH,
NICK_MAX_ENCODED, IRCMessageChannel)
from jmdaemon.message_channel import CJPeerError
import jmdaemon
#needed for test framework
from jmclient import (load_program_config, get_irc_mchannels, jm_single)
python_cmd = "python2"
yg_cmd = "yield-generator-basic.py"
yg_name = "ygtest"
si = 3
class DummyDaemon(object):
def request_signature_verify(self, a, b, c, d, e,
f, g, h):
return True
class DummyMC(IRCMessageChannel):
def __init__(self, configdata, nick, daemon):
super(DummyMC, self).__init__(configdata, daemon=daemon)
"""
#hacked in here to allow auth without mc-collection
nick_priv = hashlib.sha256(os.urandom(16)).hexdigest() + '01'
nick_pubkey = btc.privtopub(nick_priv)
nick_pkh_raw = hashlib.sha256(nick_pubkey).digest()[
:NICK_HASH_LENGTH]
nick_pkh = btc.changebase(nick_pkh_raw, 256, 58)
#right pad to maximum possible; b58 is not fixed length.
#Use 'O' as one of the 4 not included chars in base58.
nick_pkh += 'O' * (NICK_MAX_ENCODED - len(nick_pkh))
#The constructed length will be 1 + 1 + NICK_MAX_ENCODED
nick = JOINMARKET_NICK_HEADER + str(
jm_single().JM_VERSION) + nick_pkh
jm_single().nickname = nick
"""
self.daemon = daemon
self.set_nick(nick)
def on_connect(x):
print('simulated on-connect')
def on_welcome(x):
print('simulated on-welcome')
def on_disconnect(x):
print('simulated on-disconnect')
def on_order_seen(dummy, counterparty, oid, ordertype, minsize,
maxsize, txfee, cjfee):
global yg_name
yg_name = counterparty
def on_pubkey(pubkey):
print "received pubkey: " + pubkey
class RawIRCThread(threading.Thread):
def __init__(self, ircmsgchan):
threading.Thread.__init__(self, name='RawIRCThread')
self.daemon = True
self.ircmsgchan = ircmsgchan
def run(self):
self.ircmsgchan.run()
def test_junk_messages(setup_messaging):
#start a yg bot just to receive messages
"""
wallets = make_wallets(1,
wallet_structures=[[1,0,0,0,0]],
mean_amt=1)
wallet = wallets[0]['wallet']
ygp = local_command([python_cmd, yg_cmd,\
str(wallets[0]['seed'])], bg=True)
"""
#time.sleep(90)
#start a raw IRCMessageChannel instance in a thread;
#then call send_* on it with various errant messages
dm = DummyDaemon()
mc = DummyMC(get_irc_mchannels()[0], "irc_ping_test", dm)
mc.register_orderbookwatch_callbacks(on_order_seen=on_order_seen)
mc.register_taker_callbacks(on_pubkey=on_pubkey)
mc.on_connect = on_connect
mc.on_disconnect = on_disconnect
mc.on_welcome = on_welcome
RawIRCThread(mc).start()
#start up a fake counterparty
mc2 = DummyMC(get_irc_mchannels()[0], yg_name, dm)
RawIRCThread(mc2).start()
time.sleep(si)
mc.request_orderbook()
time.sleep(si)
#now try directly
mc.pubmsg("!orderbook")
time.sleep(si)
#should be ignored; can we check?
mc.pubmsg("!orderbook!orderbook")
time.sleep(si)
#assuming MAX_PRIVMSG_LEN is not something crazy
#big like 550, this should fail
with pytest.raises(AssertionError) as e_info:
mc.pubmsg("junk and crap"*40)
time.sleep(si)
#assuming MAX_PRIVMSG_LEN is not something crazy
#small like 180, this should succeed
mc.pubmsg("junk and crap"*15)
time.sleep(si)
#try a long order announcement in public
#because we don't want to build a real orderbook,
#call the underlying IRC announce function.
#TODO: how to test that the sent format was correct?
mc._announce_orders(["!abc def gh 0001"]*30)
time.sleep(si)
#send a fill with an invalid pubkey to the existing yg;
#this should trigger a NaclError but should NOT kill it.
mc._privmsg(yg_name, "fill", "0 10000000 abcdef")
#Test that null privmsg does not cause crash; TODO check maker log?
mc.send_raw("PRIVMSG " + yg_name + " :")
time.sleep(si)
#Try with ob flag
mc._pubmsg("!reloffer stuff")
time.sleep(si)
#Trigger throttling with large messages
mc._privmsg(yg_name, "tx", "aa"*5000)
time.sleep(si)
#with pytest.raises(CJPeerError) as e_info:
mc.send_error(yg_name, "fly you fools!")
time.sleep(si)
#Test the effect of shutting down the connection
mc.set_reconnect_interval(si-1)
mc.close()
mc._announce_orders(["!abc def gh 0001"]*30)
time.sleep(si+2)
#kill the connection at socket level
mc.shutdown()
@pytest.fixture(scope="module")
def setup_messaging():
#Trigger PING LAG sending artificially
jmdaemon.irc.PING_INTERVAL = 3
load_program_config()

374
jmdaemon/test/test_message_channel.py

@ -0,0 +1,374 @@
#! /usr/bin/env python
from __future__ import absolute_import
'''test messagechannel management code.'''
import pytest
from jmdaemon import (JMDaemonServerProtocolFactory, MessageChannelCollection)
from jmdaemon.message_channel import MChannelThread
from jmdaemon.orderbookwatch import OrderbookWatch
from jmdaemon.daemon_protocol import JMDaemonServerProtocol
from jmdaemon.protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH,
NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER)
from jmclient import (load_program_config, get_log, jm_single, get_irc_mchannels,
JMTakerClientProtocolFactory, Taker, AbstractWallet)
import os
from jmbase.commands import *
from msgdata import *
import json
import time
import hashlib
import base64
import traceback
import threading
import jmbitcoin as bitcoin
from dummy_mc import DummyMessageChannel
jlog = get_log()
def make_valid_nick(i=0):
nick_priv = hashlib.sha256(chr(i)*16).hexdigest() + '01'
nick_pubkey = bitcoin.privtopub(nick_priv)
nick_pkh_raw = hashlib.sha256(nick_pubkey).digest()[:NICK_HASH_LENGTH]
nick_pkh = bitcoin.changebase(nick_pkh_raw, 256, 58)
#right pad to maximum possible; b58 is not fixed length.
#Use 'O' as one of the 4 not included chars in base58.
nick_pkh += 'O' * (NICK_MAX_ENCODED - len(nick_pkh))
#The constructed length will be 1 + 1 + NICK_MAX_ENCODED
return JOINMARKET_NICK_HEADER + str(JM_VERSION) + nick_pkh
class DummyBox(object):
def encrypt(self, msg):
return msg
def decrypt(self, msg):
return msg
class DaemonForSigns(object):
"""The following functions handle requests and responses
from client for messaging signing and verifying.
"""
def __init__(self, mcc):
self.siglock = threading.Lock()
self.mcc = mcc
self.crypto_boxes = {}
def request_signed_message(self, nick, cmd, msg, msg_to_be_signed, hostid):
with self.siglock:
#Here we have to pretend we signed it and
#send it to privmsg
self.mcc.privmsg(nick, cmd, msg, mc=hostid)
def request_signature_verify(self, msg, fullmsg, sig, pubkey, nick, hashlen,
max_encoded, hostid):
with self.siglock:
#Here we must pretend we verified it and send it to on_verified_privmsg
self.mcc.on_verified_privmsg(nick, fullmsg, hostid)
def get_crypto_box_from_nick(self, nick):
if nick in self.crypto_boxes and self.crypto_boxes[nick] != None:
return self.crypto_boxes[nick][1] # libsodium encryption object
else:
jlog.debug('something wrong, no crypto object, nick=' + nick +
', message will be dropped')
return None
def dummy_on_welcome():
jlog.debug("On welcome called")
def don_error():
jlog.debug("called: " + traceback.extract_stack(None, 2)[0][2])
def don_ioauth(nick, utxo_list, auth_pub, cj_addr,
change_addr, btc_sig):
jlog.debug("onioauth callback")
jlog.debug("Args are: " + ",".join([str(x) for x in nick,
utxo_list, auth_pub, cj_addr,
change_addr, btc_sig]))
def don_sig(nick, sig):
jlog.debug("calledback on-sig")
don_pubkey = don_sig
def don_orderbook_requested(nick, mc):
jlog.debug("called oobr")
def don_commitment_seen(nick, cmt):
jlog.debug("called doncommitmentseen")
jlog.debug("Nick, cmt was: " + str(nick) + " , " + str(cmt))
def don_seen_auth(nick, cr):
jlog.debug("called donseen auth")
jlog.debug("Cr was: " + str(cr))
def don_push_tx(nick, txhex):
jlog.debug("called donpushtx with thex: " + str(txhex))
def don_seen_tx(nick, txhex):
jlog.debug("called donseentx with txhex: " + str(txhex))
def don_commitment_transferred(nick, cmt):
jlog.debug("called doncommitmenttransferred")
def don_order_fill(nick, oid, amount, taker_pk, commit):
jlog.debug("donorderfill called with: " + ",".join(
[str(x) for x in [nick, oid, amount, taker_pk, commit]]))
def test_setup_mc():
ob = OrderbookWatch()
ob.on_welcome = dummy_on_welcome
dmcs = [DummyMessageChannel(None, hostid="hostid"+str(x)) for x in range(3)]
mcc = MessageChannelCollection(dmcs)
#this sets orderbookwatch callbacks
ob.set_msgchan(mcc)
#we want to set all the callbacks, maker and taker
mcc.register_taker_callbacks(don_error, don_pubkey, don_ioauth, don_sig)
mcc.register_maker_callbacks(on_orderbook_requested=don_orderbook_requested,
on_order_fill=don_order_fill,
on_seen_auth=don_seen_auth, on_seen_tx=don_seen_tx,
on_push_tx=don_push_tx,
on_commitment_seen=don_commitment_seen,
on_commitment_transferred=don_commitment_transferred)
mcc.set_nick("testnick")
dummydaemon = DaemonForSigns(mcc)
mcc.set_daemon(dummydaemon)
for mc in dmcs:
mc.on_welcome(mc)
#instead of calling mcc.run, we'll start threads for mcs manually so we
#can probe them
for mc in dmcs:
MChannelThread(mc).start()
for m in dmcs:
m.on_pubmsg("testmaker", "!orderbook")
#receive invalid pubmsgs
for msg in ["!orderbook!orderbook", "!notacommand a b c", "no command prefix",
"!reloffer 0 4000 5000 100"]:
dmcs[2].on_pubmsg("testmaker", msg)
mcc.request_orderbook()
mcc.pubmsg("outward pubmsg")
#now create a verifiable counterparty nick;
#to get it into active state, need to receive an orderbook from it
cp1 = make_valid_nick()
#Simulate order receipt on 2 of 3 msgchans from this nick;
#note that it will have its active chan set to mc "1" because that
#is the last it was seen on:
dmcs[0].on_privmsg(cp1, "!reloffer 0 4000 5000 100 0.2 abc def")
dmcs[1].on_privmsg(cp1, "!reloffer 0 4000 5000 100 0.2 abc def")
time.sleep(0.5)
#send back a response
mcc.privmsg(cp1, "fill", "0")
#trigger failure to find nick in privmsg
mcc.privmsg(cp1+"XXX", "fill", "0")
#trigger check_privmsg decorator
mcc.send_sigs(cp1, ["abc", "def"])
mcc.send_pubkey(cp1, "testpubkey")
mcc.send_ioauth(cp1, ["abc", "def"], "testpub", "testaddr1", "testaddr2",
"testsig")
mcc.send_error(cp1, "errormsg")
mcc.push_tx(cp1, "deadbeef")
#kill the chan on which the cp is marked active;
#note dummychannel has no actual shutdown (call it anyway),
#so change its status manually.
dmcs[2].shutdown()
mcc.mc_status[dmcs[1]] = 2
time.sleep(0.5)
#Flush removes references to inactive channels (in this case dmcs[1]).
#Dynamic switching of cp1 should occur to the other seen channel (dmcs[0]).
mcc.flush_nicks()
#force cp1 to be unseen on mc 0:
mcc.unsee_nick(cp1, dmcs[0])
del mcc.active_channels[cp1]
#try sending a privmsg again; this time it should just print a warning,
#as cp1 is not seen anywhere
mcc.send_sigs(cp1, ["abc", "def"])
#simulate order cancels (even though we have none)
mcc.cancel_orders([0,1,2])
#let cp1 be seen on mc2 without having got into active channels;
#note that this is an illegal pubmsg and is ignored for everything *except*
#nick_seen (what we need here)
dmcs[2].on_pubmsg(cp1, "random")
mcc.send_sigs(cp1, ["abc", "def"])
#Try using the proper way of setting up privsmgs
#first try without box
mcc.prepare_privmsg(cp1, "auth", "a b c")
dummydaemon.crypto_boxes[cp1] = ["a", DummyBox()]
#now conditions are correct, should succeed:
mcc.prepare_privmsg(cp1, "auth", "a b c")
#try again but this time there is no active channel
del mcc.active_channels[cp1]
mcc.prepare_privmsg(cp1, "auth", "a b c")
#try announcing orders; first public
mcc.announce_orders(t_orderbook)
#try on fake mc
mcc.announce_orders(t_orderbook, new_mc="fakemc")
#direct to one cp
mcc.announce_orders(t_orderbook, nick=cp1)
#direct to one cp on one mc
mcc.announce_orders(t_orderbook, nick=cp1, new_mc=dmcs[0])
#Next, set up 6 counterparties and fill their offers,
#send txs to them
cps = [make_valid_nick(i) for i in range(1, 7)]
#reuse t_chosen_orders data, but swap out the counterparty names
offervals = t_chosen_orders.values()
new_offers = dict(zip(cps, offervals))
#first, pretend they all showed up on all 3 mcs:
for m in dmcs:
for cp in cps:
m.on_privmsg(cp, "!reloffer 0 400000 500000 100 0.002 abc def")
#next, call main fill function
mcc.fill_orders(new_offers, 1000, "dummypubkey", "dummycommit")
#now send a dummy transaction to this same set.
#first fails with no crypto box.
mcc.send_tx(cps, "deadbeef")
#Now initialize the boxes
for c in cps:
dummydaemon.crypto_boxes[c] = ["a", DummyBox()]
mcc.send_tx(cps, "deadbeef")
#try to send the transaction to a wrong cp:
mcc.send_tx(["notrealcp"], "deadbeef")
#At this stage, dmcs0,2 should be "up" and 1 should be "down"
assert mcc.mc_status[dmcs[0]] == 1
assert mcc.mc_status[dmcs[1]] == 2
assert mcc.mc_status[dmcs[2]] == 1
#simulate re-connection of dmcs[1] ; note that this code isn't used atm
mcc.on_connect_trigger(dmcs[1])
assert mcc.mc_status[dmcs[1]] == 1
#Now trigger disconnection code; each mc one by one; the last should trigger
#on_disconnect callback
for m in dmcs:
mcc.on_disconnect_trigger(m)
#reconnect; effect is all nick references are flushed
for m in dmcs:
mcc.on_connect_trigger(m)
assert mcc.active_channels == {}
#have the cps rearrive
for m in dmcs:
for cp in cps:
m.on_privmsg(cp, "!reloffer 0 4000 5000 100 0.2 abc def")
#####################################################################
#next series of messages are to test various normal and abnormal
#message receipts under normal connection conditions
#####################################################################
#simulate receipt of commitments
#valid
dmcs[0].on_pubmsg(cps[2], "!hp2 deadbeef")
#invalid missing field
dmcs[0].on_pubmsg(cps[2], "!hp2")
#receive commitment via privmsg to trigger commitment_transferred
dmcs[0].on_privmsg(cps[2], "!hp2 deadbeef abc def")
#simulate receipt of order cancellation
#valid
dmcs[0].on_pubmsg(cps[2], "!cancel 2")
#invalid oid
dmcs[0].on_pubmsg(cps[2], "!cancel x")
#too short privmsg (can't even have a signature)
dmcs[0].on_privmsg(cps[2], COMMAND_PREFIX)
#not using correct protocol start character
dmcs[0].on_privmsg(cps[2], "A B C")
#unrecognized command
dmcs[0].on_privmsg(cps[2], "!fakecommand A B C D")
#Perhaps dubious, but currently msg after command must be non-zero
dmcs[0].on_privmsg(cps[2], "!reloffer sig1 sig2")
#Simulating receipt of encrypted messages:
#ioauth
dummy_on_ioauth_msg = "deadbeef:0,deadbeef:1 XauthpubX XcjaddrX XchangeaddrX XbtcsigX"
b64dummyioauth = base64.b64encode(dummy_on_ioauth_msg)
dmcs[0].on_privmsg(cps[3], "!ioauth " + b64dummyioauth + " sig1 sig2")
#Try with a garbage b64 (but decodable); should throw index error at least
dmcs[0].on_privmsg(cps[3], "!ioauth _*_ sig1 sig2")
#Try also for receipt from an unknown counterparty; should fail with no enc box
dmcs[0].on_privmsg("notrealcp", "!ioauth " + b64dummyioauth + " sig1 sig2")
#Try same message from valid cp but with corrupted b64
b64dummyioauth = "999"
dmcs[0].on_privmsg(cps[3], "!ioauth " + b64dummyioauth + " sig1 sig2")
#sig
dummy_on_sig_msg = "dummysig"
b64dummysig = base64.b64encode(dummy_on_sig_msg)
dmcs[0].on_privmsg(cps[3], "!sig " + b64dummysig + " sig1 sig2")
#auth
dummy_auth_msg = "dummyauth"
b64dummyauth = base64.b64encode(dummy_auth_msg)
dmcs[0].on_privmsg(cps[2], "!auth " + b64dummyauth + " sig1 sig2")
#invalid auth (only no message is invalid)
dmcs[0].on_privmsg(cps[3], "!auth " +base64.b64encode("") + " sig1 sig2")
#tx
#valid
dummy_tx = "deadbeefdeadbeef"
b64dummytx = base64.b64encode(dummy_tx)
b642dummytx = base64.b64encode(b64dummytx)
dmcs[0].on_privmsg(cps[2], "!tx " + b642dummytx + " sig1 sig2")
badbase64tx = "999"
badbase64tx2 = base64.b64encode(badbase64tx)
#invalid txhex; here the first round will work (msg decryption), second shouldn't
dmcs[0].on_privmsg(cps[2], "!tx " + badbase64tx2 + " sig1 sig2")
#push
#valid
dmcs[0].on_privmsg(cps[2], "!push " + b642dummytx + " sig1 sig2")
#invalid
dmcs[0].on_privmsg(cps[2], "!push 999 sig1 sig2")
#fill
#valid, no commit
dmcs[0].on_privmsg(cps[4], "!fill 0 4000 dummypub sig1 sig2")
#valid with commit
dmcs[0].on_privmsg(cps[4], "!fill 0 4000 dummypub dummycommit sig1 sig2")
#invalid length
dmcs[0].on_privmsg(cps[4], "!fill 0 sig1 sig2")
#pubkey
dmcs[0].on_privmsg(cps[4], "!pubkey dummypub sig1 sig2")
##############################################################
#End message receipts
##############################################################
#simulate loss of conncetion to cp[0]
for m in dmcs[::-1]:
mcc.on_nick_leave_trigger(cps[0], m)
#call onnickleave for something not in the ac list
mcc.on_nick_leave_trigger("notrealcp", dmcs[0])
#make mcs 0,1 go down so that when cp[1] tries to dynamic switch, it fails
mcc.on_disconnect_trigger(dmcs[0])
mcc.on_disconnect_trigger(dmcs[1])
mcc.on_nick_leave_trigger(cps[1], dmcs[2])
mcc.shutdown()
@pytest.mark.parametrize(
"failuretype, mcindex, wait",
[("shutdown", 0, 1),
("break", 1, 1),
("bad", 1, 1),
])
def test_mc_run(failuretype, mcindex, wait):
ob = OrderbookWatch()
ob.on_welcome = dummy_on_welcome
dmcs = [DummyMessageChannel(None, hostid="hostid"+str(x)) for x in range(3)]
mcc = MessageChannelCollection(dmcs)
#this sets orderbookwatch callbacks
ob.set_msgchan(mcc)
dummydaemon = DaemonForSigns(mcc)
mcc.set_daemon(dummydaemon)
#to externally trigger give up condition, start mcc itself in a thread
MChannelThread(mcc).start()
time.sleep(0.2)
mcc.give_up = True
time.sleep(1.2)
#wipe state, this time use failure injections
mcc = MessageChannelCollection(dmcs)
#to test exception raise on bad failure inject, don't use thread:
if failuretype == "bad":
with pytest.raises(NotImplementedError) as e_info:
mcc.run(failures=[failuretype, mcindex, wait])
else:
#need to override thread run()
class FIThread(MChannelThread):
def run(self):
self.mc.run(failures=self.failures)
fi = FIThread(mcc)
fi.failures = [failuretype, mcindex, wait]
fi.start()
time.sleep(wait+0.5)

129
jmdaemon/test/test_orderbookwatch.py

@ -0,0 +1,129 @@
#!/usr/bin/env python
from __future__ import print_function
import pytest
from jmdaemon.orderbookwatch import OrderbookWatch
from jmdaemon import IRCMessageChannel
from jmclient import get_irc_mchannels, load_program_config
from jmdaemon.protocol import JM_VERSION, ORDER_KEYS
class DummyDaemon(object):
def request_signature_verify(self, a, b, c, d, e,
f, g, h):
return True
class DummyMC(IRCMessageChannel):
def __init__(self, configdata, nick, daemon):
super(DummyMC, self).__init__(configdata, daemon=daemon)
self.daemon = daemon
self.set_nick(nick)
def on_welcome(x):
print("Simulated on-welcome")
def get_ob():
load_program_config()
dm = DummyDaemon()
mc = DummyMC(get_irc_mchannels()[0], "test", dm)
ob = OrderbookWatch()
ob.on_welcome = on_welcome
ob.set_msgchan(mc)
return ob
@pytest.mark.parametrize(
"badtopic",
[("abc|"),
("abcd|def"),
("abc| 0 a qvd"),
])
def test_ob(badtopic):
ob = get_ob()
topic = ("JoinMarket open outcry pit. /r/joinmarket Discussion in #joinmarket"
"| 0 5 LATEST RELEASE v0.2.2. Useful new features. Update ASAP, and "
"do not use pre-0.2.0! https://bitcointalk.org/index.php?topic=91911"
"6.msg16714124#msg16714124")
ob.on_set_topic(topic)
#should not throw:
ob.on_set_topic(badtopic)
#test old version
future_ver = str(JM_VERSION + 2)
deprecated = topic.replace("| 0 5", "| 0 "+future_ver)
ob.on_set_topic(deprecated)
@pytest.mark.parametrize(
"counterparty, oid, ordertype, minsize, maxsize, txfee, cjfee, expected",
[
#good absoffer
("test", "0", "absoffer", "3000", "4000", "2", "300", True),
#good reloffer
("test", "0", "reloffer", "3000", "4000", "2", "0.3", True),
#dusty minsize OK
("test", "0", "reloffer", "1000", "4000", "2", "0.3", True),
#invalid oid
("test", "-2", "reloffer", "3000", "4000", "2", "0.3", False),
#invalid minsize
("test", "2", "reloffer", "-3000", "4000", "2", "0.3", False),
#invalid maxsize
("test", "2", "reloffer", "3000", "2200000000000000", "2", "0.3", False),
#invalid txfee
("test", "2", "reloffer", "3000", "4000", "-1", "0.3", False),
#min bigger than max
("test", "2", "reloffer", "4000", "3000", "2", "0.3", False),
#non-integer absoffer
("test", "2", "absoffer", "3000", "4000", "2", "0.3", False),
#invalid syntax for cjfee
("test", "2", "reloffer", "3000", "4000", "2", "0.-1", False),
#invalid type for oid
("test", "xxx", "reloffer", "3000", "4000", "2", "0.3", False),
])
def test_order_seen_cancel(counterparty, oid, ordertype, minsize, maxsize, txfee,
cjfee, expected):
ob = get_ob()
ob.on_order_seen(counterparty, oid, ordertype, minsize, maxsize,
txfee, cjfee)
if expected:
#offer should now be in the orderbook
rows = ob.db.execute('SELECT * FROM orderbook;').fetchall()
orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows]
assert len(orderbook) == 1
#test it can be removed
ob.on_order_cancel(counterparty, oid)
rows = ob.db.execute('SELECT * FROM orderbook;').fetchall()
orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows]
assert len(orderbook) == 0
def test_disconnect_leave():
ob = get_ob()
t_orderbook = [{u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'},
{u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', u'oid': 0,
u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}]
for o in t_orderbook:
ob.on_order_seen(o['counterparty'], o['oid'], o['ordertype'],
o['minsize'], o['maxsize'], o['txfee'], o['cjfee'])
rows = ob.db.execute('SELECT * FROM orderbook;').fetchall()
orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows]
assert len(orderbook) == 6
#simulate one cp leaves:
ob.on_nick_leave("J5cBx1FwUVh9zzoO")
rows = ob.db.execute('SELECT * FROM orderbook;').fetchall()
orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows]
assert len(orderbook) == 5
#simulate quit
ob.on_disconnect()
rows = ob.db.execute('SELECT * FROM orderbook;').fetchall()
orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows]
assert len(orderbook) == 0

2
setup.cfg

@ -1,4 +1,4 @@
# content of pytest.ini
[tool:pytest]
norecursedirs = test/*
testpaths = jmbitcoin jmclient jmbase
testpaths = jmbitcoin jmclient jmbase jmdaemon

Loading…
Cancel
Save