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.
 
 
 
 

230 lines
9.4 KiB

#! /usr/bin/env python
from __future__ import print_function
from twisted.python.log import startLogging, err
from twisted.internet import protocol, reactor
from twisted.protocols import amp
from twisted.internet.protocol import ClientFactory
from twisted.internet.endpoints import TCP4ClientEndpoint
import commands
from sys import stdout
import json
import random
import string
import time
import hashlib
import os
from client import (Taker, Wallet, jm_single, get_irc_mchannels,
load_program_config, get_log)
import btc
jlog = get_log()
class JMProtocolError(Exception):
pass
class JMTakerClientProtocol(amp.AMP):
def __init__(self, factory, taker, nick_priv=None):
self.taker = taker
self.factory = factory
self.orderbook = None
self.supported_messages = ["JM_UP", "JM_SETUP_DONE", "JM_FILL_RESPONSE",
"JM_OFFERS", "JM_SIG_RECEIVED",
"JM_REQUEST_MSGSIG",
"JM_REQUEST_MSGSIG_VERIFY", "JM_INIT_PROTO"]
if not nick_priv:
self.nick_priv = hashlib.sha256(os.urandom(16)).hexdigest() + '01'
else:
self.nick_priv = nick_priv
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 connectionMade(self):
"""Upon confirmation of network connection
to daemon, request message channel initialization
with relevant config data for our message channels
"""
#needed only for channel naming convention
blockchain_source = jm_single().config.get("BLOCKCHAIN",
"blockchain_source")
network = jm_single().config.get("BLOCKCHAIN", "network")
irc_configs = get_irc_mchannels()
minmakers = jm_single().config.getint("POLICY", "minimum_makers")
maker_timeout_sec = jm_single().maker_timeout_sec
d = self.callRemote(commands.JMInit,
bcsource=blockchain_source,
network=network,
irc_configs=json.dumps(irc_configs),
minmakers=minmakers,
maker_timeout_sec=maker_timeout_sec)
d.addCallback(self.checkClientResponse)
def send_data(self, cmd, data):
JMProtocol.send_data(self, cmd, data)
def set_nick(self):
self.nick_pubkey = btc.privtopub(self.nick_priv)
self.nick_pkh_raw = hashlib.sha256(self.nick_pubkey).digest()[
:self.nick_hashlen]
self.nick_pkh = btc.changebase(self.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.
self.nick_pkh += 'O' * (self.nick_maxencoded - len(self.nick_pkh))
#The constructed length will be 1 + 1 + NICK_MAX_ENCODED
self.nick = self.nick_header + str(self.jm_version) + self.nick_pkh
jm_single().nickname = self.nick
@commands.JMInitProto.responder
def on_JM_INIT_PROTO(self, nick_hash_length, nick_max_encoded,
joinmarket_nick_header, joinmarket_version):
"""Daemon indicates init-ed status and passes back protocol constants.
Use protocol settings to set actual nick from nick private key,
then call setup to instantiate message channel connections in the daemon.
"""
self.nick_hashlen = nick_hash_length
self.nick_maxencoded = nick_max_encoded
self.nick_header = joinmarket_nick_header
self.jm_version = joinmarket_version
self.set_nick()
d = self.callRemote(commands.JMStartMC,
nick=self.nick)
d.addCallback(self.checkClientResponse)
return {'accepted': True}
@commands.JMUp.responder
def on_JM_UP(self):
d = self.callRemote(commands.JMSetup,
role="TAKER",
n_counterparties=4) #TODO this number should be set
d.addCallback(self.checkClientResponse)
return {'accepted': True}
@commands.JMSetupDone.responder
def on_JM_SETUP_DONE(self):
jlog.info("JM daemon setup complete")
#The daemon is ready and has requested the orderbook
#from the pit; we can request the entire orderbook
#and filter it as we choose.
reactor.callLater(jm_single().maker_timeout_sec, self.get_offers)
return {'accepted': True}
@commands.JMFillResponse.responder
def on_JM_FILL_RESPONSE(self, success, ioauth_data):
"""Receives the entire set of phase 1 data (principally utxos)
from the counterparties and passes through to the Taker for
tx construction, if successful. Then passes back the phase 2
initiating data to the daemon.
"""
ioauth_data = json.loads(ioauth_data)
if not success:
jlog.info("Makers didnt respond blah blah")
return {'accepted': True}
else:
jlog.info("Makers responded with: " + json.dumps(ioauth_data))
retval = self.taker.receive_utxos(ioauth_data)
if not retval[0]:
jlog.info("Taker is not continuing, phase 2 abandoned.")
jlog.info("Reason: " + str(retval[1]))
return {'accepted': False}
else:
nick_list, txhex = retval[1:]
reactor.callLater(0, self.make_tx, nick_list, txhex)
return {'accepted': True}
@commands.JMOffers.responder
def on_JM_OFFERS(self, orderbook):
self.orderbook = json.loads(orderbook)
jlog.info("Got the orderbook: " + str(self.orderbook))
retval = self.taker.initialize(self.orderbook)
#format of retval is:
#True, self.cjamount, commitment, revelation, self.filtered_orderbook)
if not retval[0]:
jlog.info("Taker not continuing after receipt of orderbook")
return
amt, cmt, rev, foffers = retval[1:]
d = self.callRemote(commands.JMFill,
amount=amt,
commitment=str(cmt),
revelation=str(rev),
filled_offers=json.dumps(foffers))
d.addCallback(self.checkClientResponse)
return {'accepted': True}
@commands.JMSigReceived.responder
def on_JM_SIG_RECEIVED(self, nick, sig):
retval = self.taker.on_sig(nick, sig)
if retval:
#flag indicating completion; but Taker
#handles tx pushing, just update state
self.state = 4
return {'accepted': True}
@commands.JMRequestMsgSig.responder
def on_JM_REQUEST_MSGSIG(self, nick, cmd, msg, msg_to_be_signed, hostid):
sig = btc.ecdsa_sign(str(msg_to_be_signed), self.nick_priv)
msg_to_return = str(msg) + " " + self.nick_pubkey + " " + sig
d = self.callRemote(commands.JMMsgSignature,
nick=nick,
cmd=cmd,
msg_to_return=msg_to_return,
hostid=hostid)
d.addCallback(self.checkClientResponse)
return {'accepted': True}
@commands.JMRequestMsgSigVerify.responder
def on_JM_REQUEST_MSGSIG_VERIFY(self, msg, fullmsg, sig, pubkey, nick,
hashlen, max_encoded, hostid):
jlog.info("Got a request to verify a signature")
verif_result = True
if not btc.ecdsa_verify(str(msg), sig, pubkey):
jlog.debug("nick signature verification failed, ignoring.")
verif_result = False
#check that nick matches hash of pubkey
nick_pkh_raw = hashlib.sha256(pubkey).digest()[:hashlen]
nick_stripped = nick[2:2 + max_encoded]
#strip right padding
nick_unpadded = ''.join([x for x in nick_stripped if x != 'O'])
if not nick_unpadded == btc.changebase(nick_pkh_raw, 256, 58):
jlog.debug("Nick hash check failed, expected: " + str(nick_unpadded)
+ ", got: " + str(btc.changebase(nick_pkh_raw, 256, 58)))
verif_result = False
jlog.info("Sending a verifcation result: " + str(verif_result))
d = self.callRemote(commands.JMMsgSignatureVerify,
verif_result=verif_result,
nick=nick,
fullmsg=fullmsg,
hostid=hostid)
d.addCallback(self.checkClientResponse)
return {'accepted': True}
def get_offers(self):
d = self.callRemote(commands.JMRequestOffers)
d.addCallback(self.checkClientResponse)
def make_tx(self, nick_list, txhex):
d = self.callRemote(commands.JMMakeTx,
nick_list= json.dumps(nick_list),
txhex=txhex)
d.addCallback(self.checkClientResponse)
class JMTakerClientProtocolFactory(protocol.ClientFactory):
protocol = JMTakerClientProtocol
def __init__(self, taker):
self.taker = taker
def buildProtocol(self, addr):
return JMTakerClientProtocol(self, self.taker)
def start_reactor(host, port, factory, ish=True):
reactor.connectTCP(host, port, factory)
reactor.run(installSignalHandlers=ish)