You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
664 lines
26 KiB
664 lines
26 KiB
#! /usr/bin/env python |
|
from __future__ import print_function |
|
|
|
from .message_channel import MessageChannelCollection |
|
from .orderbookwatch import OrderbookWatch |
|
from .enc_wrapper import (as_init_encryption, init_keypair, init_pubkey, |
|
NaclError) |
|
from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH, |
|
NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER, |
|
COMMITMENT_PREFIXES) |
|
from .irc import IRCMessageChannel |
|
|
|
from jmbase.commands import * |
|
from jmbase import _byteify |
|
from twisted.protocols import amp |
|
from twisted.internet import reactor, ssl |
|
from twisted.internet.protocol import ServerFactory |
|
from twisted.internet.error import (ConnectionLost, ConnectionAborted, |
|
ConnectionClosed, ConnectionDone) |
|
from twisted.python import failure, log |
|
import json |
|
import threading |
|
import os |
|
import copy |
|
from functools import wraps |
|
|
|
"""Joinmarket application protocol control flow. |
|
For documentation on protocol (formats, message sequence) see |
|
https://github.com/JoinMarket-Org/JoinMarket-Docs/blob/master/ |
|
Joinmarket-messaging-protocol.md |
|
""" |
|
""" |
|
*** |
|
API |
|
*** |
|
The client-daemon two-way communication is documented in jmbase.commands.py |
|
""" |
|
|
|
"""Decorators for limiting which |
|
inbound callbacks trigger in the DaemonProtocol |
|
object. |
|
""" |
|
|
|
def maker_only(func): |
|
@wraps(func) |
|
def func_wrapper(inst, *args, **kwargs): |
|
if inst.role == "MAKER": |
|
return func(inst, *args, **kwargs) |
|
return None |
|
return func_wrapper |
|
|
|
def taker_only(func): |
|
@wraps(func) |
|
def func_wrapper(inst, *args, **kwargs): |
|
if inst.role == "TAKER": |
|
return func(inst, *args, **kwargs) |
|
return None |
|
return func_wrapper |
|
|
|
def check_utxo_blacklist(commitment, persist=False): |
|
"""Compare a given commitment with the persisted blacklist log file, |
|
which is hardcoded to this directory and name 'commitmentlist' (no |
|
security or privacy issue here). |
|
If the commitment has been used before, return False (disallowed), |
|
else return True. |
|
If flagged, persist the usage of this commitment to the above file. |
|
""" |
|
#TODO format error checking? |
|
fname = "commitmentlist" |
|
if os.path.isfile(fname): |
|
with open(fname, "rb") as f: |
|
blacklisted_commitments = [x.strip() for x in f.readlines()] |
|
else: |
|
blacklisted_commitments = [] |
|
if commitment in blacklisted_commitments: |
|
return False |
|
elif persist: |
|
blacklisted_commitments += [commitment] |
|
with open(fname, "wb") as f: |
|
f.write('\n'.join(blacklisted_commitments)) |
|
f.flush() |
|
#If the commitment is new and we are *not* persisting, nothing to do |
|
#(we only add it to the list on sending io_auth, which represents actual |
|
#usage). |
|
return True |
|
|
|
class JMProtocolError(Exception): |
|
pass |
|
|
|
class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): |
|
|
|
def __init__(self, factory): |
|
self.factory = factory |
|
self.jm_state = 0 |
|
self.restart_mc_required = False |
|
self.irc_configs = None |
|
self.mcc = None |
|
#Default role is TAKER; must be overriden to MAKER in JMSetup message. |
|
self.role = "TAKER" |
|
self.crypto_boxes = {} |
|
self.sig_lock = threading.Lock() |
|
self.active_orders = {} |
|
|
|
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() #pragma: no cover |
|
|
|
def defaultErrback(self, failure): |
|
"""TODO better network error handling. |
|
""" |
|
failure.trap(ConnectionAborted, ConnectionClosed, |
|
ConnectionDone, ConnectionLost) |
|
|
|
def defaultCallbacks(self, d): |
|
d.addCallback(self.checkClientResponse) |
|
d.addErrback(self.defaultErrback) |
|
|
|
@JMInit.responder |
|
def on_JM_INIT(self, bcsource, network, irc_configs, minmakers, |
|
maker_timeout_sec): |
|
"""Reads in required configuration from client for a new |
|
session; feeds back joinmarket messaging protocol constants |
|
(required for nick creation). |
|
If a new message channel configuration is required, the current |
|
one is shutdown in preparation. |
|
""" |
|
self.maker_timeout_sec = int(maker_timeout_sec) |
|
self.minmakers = int(minmakers) |
|
irc_configs = json.loads(irc_configs) |
|
#(bitcoin) network only referenced in channel name construction |
|
self.network = network |
|
if irc_configs == self.irc_configs: |
|
self.restart_mc_required = False |
|
log.msg("New init received did not require a new message channel" |
|
" setup.") |
|
else: |
|
if self.irc_configs: |
|
#close the existing connections |
|
self.mc_shutdown() |
|
self.irc_configs = irc_configs |
|
self.restart_mc_required = True |
|
mcs = [IRCMessageChannel(c, |
|
daemon=self, |
|
realname='btcint=' + bcsource) |
|
for c in self.irc_configs] |
|
self.mcc = MessageChannelCollection(mcs) |
|
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.register_maker_callbacks(self.on_orderbook_requested, |
|
self.on_order_fill, |
|
self.on_seen_auth, |
|
self.on_seen_tx, |
|
self.on_push_tx, |
|
self.on_commitment_seen, |
|
self.on_commitment_transferred) |
|
self.mcc.set_daemon(self) |
|
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} |
|
|
|
@JMStartMC.responder |
|
def on_JM_START_MC(self, nick): |
|
"""Starts message channel threads, if we are working with |
|
a new message channel configuration. Sets new nick if required. |
|
JM_UP will be called when the welcome messages are received. |
|
""" |
|
self.init_connections(nick) |
|
return {'accepted': True} |
|
|
|
@JMSetup.responder |
|
def on_JM_SETUP(self, role, initdata): |
|
assert self.jm_state == 0 |
|
self.role = role |
|
self.crypto_boxes = {} |
|
self.kp = init_keypair() |
|
d = self.callRemote(JMSetupDone) |
|
self.defaultCallbacks(d) |
|
#Request orderbook here, on explicit setup request from client, |
|
#assumes messagechannels are in "up" state. Orders are read |
|
#in the callback on_order_seen in OrderbookWatch. |
|
#TODO: pubmsg should not (usually?) fire if already up from previous run. |
|
if self.role == "TAKER": |
|
self.mcc.pubmsg(COMMAND_PREFIX + "orderbook") |
|
elif self.role == "MAKER": |
|
self.offerlist = json.loads(initdata) |
|
self.mcc.announce_orders(self.offerlist) |
|
self.jm_state = 1 |
|
return {'accepted': True} |
|
|
|
@JMMsgSignature.responder |
|
def on_JM_MSGSIGNATURE(self, nick, cmd, msg_to_return, hostid): |
|
self.mcc.privmsg(nick, cmd, msg_to_return, mc=hostid) |
|
return {'accepted': True} |
|
|
|
@JMMsgSignatureVerify.responder |
|
def on_JM_MSGSIGNATURE_VERIFY(self, verif_result, nick, fullmsg, hostid): |
|
if not verif_result: |
|
log.msg("Verification failed for nick: " + str(nick)) |
|
else: |
|
self.mcc.on_verified_privmsg(nick, fullmsg, hostid) |
|
return {'accepted': True} |
|
|
|
"""Taker specific responders |
|
""" |
|
|
|
@JMRequestOffers.responder |
|
def on_JM_REQUEST_OFFERS(self): |
|
"""Reports the current state of the orderbook. |
|
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) |
|
self.defaultCallbacks(d) |
|
return {'accepted': True} |
|
|
|
@JMFill.responder |
|
def on_JM_FILL(self, amount, commitment, revelation, filled_offers): |
|
"""Takes the necessary data from the Taker and initiates the Stage 1 |
|
interaction with the Makers. |
|
""" |
|
if not (self.jm_state == 1 and isinstance(amount, int) and amount >=0): |
|
return {'accepted': False} |
|
self.cjamount = amount |
|
self.commitment = commitment |
|
self.revelation = revelation |
|
#Reset utxo data to null for this new transaction |
|
self.ioauth_data = {} |
|
self.active_orders = json.loads(filled_offers) |
|
for nick, offer_dict in self.active_orders.iteritems(): |
|
offer_fill_msg = " ".join([str(offer_dict["oid"]), str(amount), str( |
|
self.kp.hex_pk()), str(commitment)]) |
|
self.mcc.prepare_privmsg(nick, "fill", offer_fill_msg) |
|
reactor.callLater(self.maker_timeout_sec, self.completeStage1) |
|
self.jm_state = 2 |
|
return {'accepted': True} |
|
|
|
@JMMakeTx.responder |
|
def on_JM_MAKE_TX(self, nick_list, txhex): |
|
"""Taker sends the prepared unsigned transaction |
|
to all the Makers in nick_list |
|
""" |
|
if not self.jm_state == 4: |
|
log.msg("Make tx was called in wrong state, rejecting") |
|
return {'accepted': False} |
|
nick_list = json.loads(nick_list) |
|
self.mcc.send_tx(nick_list, txhex) |
|
return {'accepted': True} |
|
|
|
@JMPushTx.responder |
|
def on_JM_PushTx(self, nick, txhex): |
|
self.mcc.push_tx(nick, txhex) |
|
return {'accepted': True} |
|
|
|
"""Maker specific responders |
|
""" |
|
|
|
@JMAnnounceOffers.responder |
|
def on_JM_ANNOUNCE_OFFERS(self, to_announce, to_cancel, offerlist): |
|
"""Called by Maker to reset his current offerlist; |
|
Daemon decides what messages (cancel, announce) to |
|
send to the message channel. |
|
""" |
|
if self.role != "MAKER": |
|
return |
|
to_announce = json.loads(to_announce) |
|
to_cancel = json.loads(to_cancel) |
|
self.offerlist = json.loads(offerlist) |
|
if len(to_cancel) > 0: |
|
self.mcc.cancel_orders(to_cancel) |
|
if len(to_announce) > 0: |
|
self.mcc.announce_orders(to_announce, None, None) |
|
return {"accepted": True} |
|
|
|
@JMIOAuth.responder |
|
def on_JM_IOAUTH(self, nick, utxolist, pubkey, cjaddr, changeaddr, pubkeysig): |
|
"""Daemon constructs full !ioauth message to be sent on message |
|
channel based on data from Maker. Relevant data (utxos, addresses) |
|
are stored in the active_orders dict keyed by the nick of the Taker. |
|
""" |
|
nick, utxolist, pubkey, cjaddr, changeaddr, pubkeysig = [_byteify( |
|
x) for x in nick, utxolist, pubkey, cjaddr, changeaddr, pubkeysig] |
|
if not self.role == "MAKER": |
|
return |
|
if not nick in self.active_orders: |
|
return |
|
utxos= json.loads(utxolist) |
|
#completed population of order/offer object |
|
self.active_orders[nick]["cjaddr"] = cjaddr |
|
self.active_orders[nick]["changeaddr"] = changeaddr |
|
self.active_orders[nick]["utxos"] = utxos |
|
msg = str(",".join(utxos.keys())) + " " + " ".join( |
|
[pubkey, cjaddr, changeaddr, pubkeysig]) |
|
self.mcc.prepare_privmsg(nick, "ioauth", msg) |
|
#In case of *blacklisted (ie already used) commitments, we already |
|
#broadcasted them on receipt; in case of valid, and now used commitments, |
|
#we broadcast them here, and not early - to avoid accidentally |
|
#blacklisting commitments that are broadcast between makers in real time |
|
#for the same transaction. |
|
self.transfer_commitment(self.active_orders[nick]["commit"]) |
|
#now persist the fact that the commitment is actually used. |
|
check_utxo_blacklist(self.active_orders[nick]["commit"], persist=True) |
|
return {"accepted": True} |
|
|
|
@JMTXSigs.responder |
|
def on_JM_TX_SIGS(self, nick, sigs): |
|
"""Signatures that the Maker has produced |
|
are passed here to the daemon as a list and |
|
broadcast one by one. TODO: could shorten this, |
|
have more than one sig per message. |
|
""" |
|
sigs = _byteify(json.loads(sigs)) |
|
for sig in sigs: |
|
self.mcc.prepare_privmsg(nick, "sig", sig) |
|
return {"accepted": True} |
|
|
|
"""Message channel callbacks |
|
""" |
|
|
|
def on_welcome(self): |
|
"""Fired when channel indicated state readiness |
|
""" |
|
d = self.callRemote(JMUp) |
|
self.defaultCallbacks(d) |
|
|
|
@maker_only |
|
def on_orderbook_requested(self, nick, mc=None): |
|
"""Dealt with by daemon, assuming offerlist is up to date |
|
""" |
|
self.mcc.announce_orders(self.offerlist, nick, mc) |
|
|
|
@maker_only |
|
def on_order_fill(self, nick, oid, amount, taker_pk, commit): |
|
"""Handled locally in daemon. This is the start of |
|
communication with the Taker. Does the following: |
|
|
|
* Immediately rejects if commitment is invalid or already used. |
|
* Checks that the fill is against a valid offer. |
|
* Establishes encryption with a new ephemeral keypair |
|
* Creates the amount, commitment and keypair fields in |
|
active_orders[nick] (or resets if already existing). |
|
|
|
Processing will only return to the Maker once the conversation |
|
up to !ioauth is complete. |
|
""" |
|
if nick in self.active_orders: |
|
log.msg("Restarting transaction for nick: " + nick) |
|
if not commit[0] in COMMITMENT_PREFIXES: |
|
self.mcc.send_error(nick, |
|
"Unsupported commitment type: " + str(commit[0])) |
|
return |
|
scommit = commit[1:] |
|
if not check_utxo_blacklist(scommit): |
|
log.msg("Taker utxo commitment is blacklisted, rejecting.") |
|
self.mcc.send_error(nick, "Commitment is blacklisted: " + str(scommit)) |
|
#Note that broadcast is happening here to reflect an already |
|
#consumed commitment; it can also be broadcast separately (earlier) on |
|
#valid usage |
|
self.transfer_commitment(scommit) |
|
return |
|
offer_s = [o for o in self.offerlist if o['oid'] == oid] |
|
if len(offer_s) == 0: |
|
self.mcc.send_error(nick, 'oid not found') |
|
offer = offer_s[0] |
|
if amount < offer['minsize'] or amount > offer['maxsize']: |
|
self.mcc.send_error(nick, 'amount out of range') |
|
#prepare a pubkey for this valid transaction |
|
kp = init_keypair() |
|
try: |
|
crypto_box = as_init_encryption(kp, init_pubkey(taker_pk)) |
|
except NaclError as e: |
|
log.msg("Unable to set up cryptobox with counterparty: " + repr(e)) |
|
self.mcc.send_error(nick, "Invalid nacl pubkey: " + taker_pk) |
|
return |
|
#Note this sets the *whole* dict, old entries (e.g. changeaddr) |
|
#are removed, so we can't have a conflict between old and new |
|
#versions of active_orders[nick] |
|
self.active_orders[nick] = {"crypto_box": crypto_box, |
|
"kp": kp, |
|
"offer": offer, |
|
"amount": amount, |
|
"commit": scommit} |
|
self.mcc.prepare_privmsg(nick, "pubkey", kp.hex_pk()) |
|
|
|
@maker_only |
|
def on_seen_auth(self, nick, commitment_revelation): |
|
"""Passes to Maker the !auth message from the Taker, |
|
for processing. This will include validating the PoDLE |
|
commitment revelation against the existing commitment, |
|
which was already stored in active_orders[nick]. |
|
""" |
|
if not nick in self.active_orders: |
|
return |
|
ao =self.active_orders[nick] |
|
#ask the client to validate the commitment and prepare the utxo data |
|
d = self.callRemote(JMAuthReceived, |
|
nick=nick, |
|
offer=json.dumps(ao["offer"]), |
|
commitment=ao["commit"], |
|
revelation=json.dumps(commitment_revelation), |
|
amount=ao["amount"], |
|
kphex=ao["kp"].hex_pk()) |
|
self.defaultCallbacks(d) |
|
|
|
@maker_only |
|
def on_commitment_seen(self, nick, commitment): |
|
"""Triggered when we see a commitment for blacklisting |
|
appear in the public pit channel. If the policy is set, |
|
we blacklist this commitment. |
|
""" |
|
if jm_single().config.has_option("POLICY", "accept_commitment_broadcasts"): |
|
blacklist_add = jm_single().config.getint("POLICY", |
|
"accept_commitment_broadcasts") |
|
else: |
|
blacklist_add = 0 |
|
if blacklist_add > 0: |
|
#just add if necessary, ignore return value. |
|
check_utxo_blacklist(commitment, persist=True) |
|
log.msg("Received commitment broadcast by other maker: " + str( |
|
commitment) + ", now blacklisted.") |
|
else: |
|
log.msg("Received commitment broadcast by other maker: " + str( |
|
commitment) + ", ignored.") |
|
|
|
@maker_only |
|
def on_commitment_transferred(self, nick, commitment): |
|
"""Triggered when a privmsg is received from another maker |
|
with a commitment to announce in public (obfuscation of source). |
|
We simply post it in public (not affected by whether we ourselves |
|
are *accepting* commitment broadcasts. |
|
""" |
|
self.mcc.pubmsg("!hp2 " + commitment) |
|
|
|
@maker_only |
|
def on_push_tx(self, nick, txhex): |
|
"""Not yet implemented; ignore rather than raise. |
|
""" |
|
log.msg('received pushtx message, ignoring, TODO') |
|
|
|
@maker_only |
|
def on_seen_tx(self, nick, txhex): |
|
"""Passes the txhex to the Maker for verification |
|
and signing. Note the security checks occur in Maker. |
|
""" |
|
if nick not in self.active_orders: |
|
return |
|
#we send a copy of the entire "active_orders" entry except the cryptobox, |
|
#so make a temporary copy |
|
ao = copy.deepcopy(self.active_orders[nick]) |
|
del ao["crypto_box"] |
|
del ao["kp"] |
|
d = self.callRemote(JMTXReceived, |
|
nick=nick, |
|
txhex=txhex, |
|
offer=json.dumps(ao)) |
|
self.defaultCallbacks(d) |
|
|
|
@taker_only |
|
def on_pubkey(self, nick, maker_pk): |
|
"""This is handled locally in the daemon; set up e2e |
|
encrypted messaging with this counterparty |
|
""" |
|
if nick not in self.active_orders.keys(): |
|
log.msg("Counterparty not part of this transaction. Ignoring") |
|
return |
|
try: |
|
self.crypto_boxes[nick] = [maker_pk, as_init_encryption( |
|
self.kp, init_pubkey(maker_pk))] |
|
except NaclError as e: |
|
print("Unable to setup crypto box with " + nick + ": " + repr(e)) |
|
self.mcc.send_error(nick, "invalid nacl pubkey: " + maker_pk) |
|
return |
|
self.mcc.prepare_privmsg(nick, "auth", str(self.revelation)) |
|
|
|
@taker_only |
|
def on_ioauth(self, nick, utxo_list, auth_pub, cj_addr, change_addr, |
|
btc_sig): |
|
"""Passes through to Taker the information from counterparties once |
|
they've all been received; note that we must also pass back the maker_pk |
|
so it can be verified against the btc-sigs for anti-MITM |
|
""" |
|
if nick not in self.active_orders.keys(): |
|
print("Got an unexpected ioauth from nick: " + str(nick)) |
|
return |
|
self.ioauth_data[nick] = [utxo_list, auth_pub, cj_addr, change_addr, |
|
btc_sig, self.crypto_boxes[nick][0]] |
|
if self.ioauth_data.keys() == self.active_orders.keys(): |
|
#Finish early if we got all |
|
self.respondToIoauths(True) |
|
|
|
@taker_only |
|
def on_sig(self, nick, sig): |
|
"""Pass signature through to Taker. |
|
""" |
|
d = self.callRemote(JMSigReceived, |
|
nick=nick, |
|
sig=sig) |
|
self.defaultCallbacks(d) |
|
|
|
def on_error(self, msg): |
|
log.msg("Received error: " + str(msg)) |
|
|
|
"""The following 2 functions handle requests and responses |
|
from client for messaging signing and verifying. |
|
""" |
|
|
|
def request_signed_message(self, nick, cmd, msg, msg_to_be_signed, hostid): |
|
"""The daemon passes the nick and cmd fields |
|
to the client so it can be echoed back to the privmsg |
|
after return (with signature); note that the cmd is already |
|
inside "msg" after having been parsed in MessageChannel; this |
|
duplication is so that the client does not need to know the |
|
message syntax. |
|
""" |
|
with self.sig_lock: |
|
d = self.callRemote(JMRequestMsgSig, |
|
nick=str(nick), |
|
cmd=str(cmd), |
|
msg=str(msg), |
|
msg_to_be_signed=str(msg_to_be_signed), |
|
hostid=str(hostid)) |
|
self.defaultCallbacks(d) |
|
|
|
def request_signature_verify(self, msg, fullmsg, sig, pubkey, nick, hashlen, |
|
max_encoded, hostid): |
|
with self.sig_lock: |
|
d = self.callRemote(JMRequestMsgSigVerify, |
|
msg=msg, |
|
fullmsg=fullmsg, |
|
sig=sig, |
|
pubkey=pubkey, |
|
nick=nick, |
|
hashlen=hashlen, |
|
max_encoded=max_encoded, |
|
hostid=hostid) |
|
self.defaultCallbacks(d) |
|
|
|
def init_connections(self, nick): |
|
"""Sets up message channel connections |
|
if they are not already up; re-sets joinmarket state to 0 |
|
for a new transaction; effectively means any previous |
|
incomplete transaction is wiped. |
|
""" |
|
self.jm_state = 0 #uninited |
|
if self.restart_mc_required: |
|
self.mcc.run() |
|
self.restart_mc_required = False |
|
else: |
|
#if we are not restarting the MC, |
|
#we must simulate the on_welcome message: |
|
self.on_welcome() |
|
self.mcc.set_nick(nick) |
|
|
|
def transfer_commitment(self, commit): |
|
"""Send this commitment via privmsg to one (random) |
|
other maker. |
|
""" |
|
crow = self.db.execute( |
|
'SELECT DISTINCT counterparty FROM orderbook ORDER BY ' + |
|
'RANDOM() LIMIT 1;' |
|
).fetchone() |
|
if crow is None: |
|
return |
|
counterparty = crow['counterparty'] |
|
#TODO de-hardcode hp2 |
|
log.msg("Sending commitment to: " + str(counterparty)) |
|
self.mcc.prepare_privmsg(counterparty, 'hp2', commit) |
|
|
|
def respondToIoauths(self, accepted): |
|
"""Sends the full set of data from the Makers to the |
|
Taker after processing of first stage is completed, |
|
using the JMFillResponse command. But if the responses |
|
were not accepted (including, not sufficient number |
|
of responses), we send the list of Makers who did not |
|
respond to the Taker, instead of the ioauth data, |
|
so that the Taker can keep track of non-responders |
|
(although note this code is not yet quite ideal, see |
|
comments below). |
|
""" |
|
if self.jm_state != 2: |
|
#this can be called a second time on timeout, in which case we |
|
#do nothing |
|
return |
|
self.jm_state = 3 |
|
if not accepted: |
|
#use ioauth data field to return the list of non-responsive makers |
|
nonresponders = [x for x in self.active_orders.keys() if x not |
|
in self.ioauth_data.keys()] |
|
ioauth_data = self.ioauth_data if accepted else nonresponders |
|
d = self.callRemote(JMFillResponse, |
|
success=accepted, |
|
ioauth_data = json.dumps(ioauth_data)) |
|
if not accepted: |
|
#Client simply accepts failure TODO |
|
self.defaultCallbacks(d) |
|
else: |
|
#Act differently if *we* provided utxos, but |
|
#client does not accept for some reason |
|
d.addCallback(self.checkUtxosAccepted) |
|
d.addErrback(self.defaultErrback) |
|
|
|
def completeStage1(self): |
|
"""Timeout of stage 1 requests; |
|
either send success + ioauth data if enough makers, |
|
else send failure to client. |
|
""" |
|
response = True if len(self.ioauth_data.keys()) >= self.minmakers else False |
|
self.respondToIoauths(response) |
|
|
|
def checkUtxosAccepted(self, accepted): |
|
if not accepted: |
|
log.msg("Taker rejected utxos provided; resetting.") |
|
#TODO create re-set function to start again |
|
else: |
|
#only update state if client accepted |
|
self.jm_state = 4 |
|
|
|
def get_crypto_box_from_nick(self, nick): |
|
"""Retrieve the libsodium box object for the counterparty; |
|
stored differently for Taker and Maker |
|
""" |
|
if nick in self.crypto_boxes and self.crypto_boxes[nick] != None: |
|
return self.crypto_boxes[nick][1] |
|
elif nick in self.active_orders and self.active_orders[nick] != None \ |
|
and "crypto_box" in self.active_orders[nick]: |
|
return self.active_orders[nick]["crypto_box"] |
|
else: |
|
log.msg('something wrong, no crypto object, nick=' + nick + |
|
', message will be dropped') |
|
return None |
|
|
|
def mc_shutdown(self): |
|
log.msg("Message channels being shutdown by daemon") |
|
if self.mcc: |
|
self.mcc.shutdown() |
|
|
|
|
|
class JMDaemonServerProtocolFactory(ServerFactory): |
|
protocol = JMDaemonServerProtocol |
|
|
|
def buildProtocol(self, addr): |
|
return JMDaemonServerProtocol(self) |
|
|
|
|
|
def start_daemon(host, port, factory, usessl=False, sslkey=None, sslcert=None): |
|
if usessl: |
|
assert sslkey |
|
assert sslcert |
|
reactor.listenSSL( |
|
port, factory, ssl.DefaultOpenSSLContextFactory(sslkey, sslcert), |
|
interface=host) |
|
else: |
|
reactor.listenTCP(port, factory, interface=host)
|
|
|