|
|
|
|
@ -34,22 +34,16 @@ 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 |
|
|
|
|
class JMClientProtocol(amp.AMP): |
|
|
|
|
def __init__(self, factory, client, nick_priv=None): |
|
|
|
|
self.client = client |
|
|
|
|
self.factory = factory |
|
|
|
|
if not nick_priv: |
|
|
|
|
self.nick_priv = hashlib.sha256(os.urandom(16)).hexdigest() + '01' |
|
|
|
|
else: |
|
|
|
|
self.nick_priv = nick_priv |
|
|
|
|
|
|
|
|
|
self.shutdown_requested = False |
|
|
|
|
self.shutdown_requested = False |
|
|
|
|
|
|
|
|
|
def checkClientResponse(self, response): |
|
|
|
|
"""A generic check of client acceptance; any failure |
|
|
|
|
@ -72,12 +66,173 @@ class JMTakerClientProtocol(amp.AMP):
|
|
|
|
|
self.factory.setClient(self) |
|
|
|
|
self.clientStart() |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
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) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
@commands.JMRequestMsgSigVerify.responder |
|
|
|
|
def on_JM_REQUEST_MSGSIG_VERIFY(self, msg, fullmsg, sig, pubkey, nick, |
|
|
|
|
hashlen, max_encoded, hostid): |
|
|
|
|
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 |
|
|
|
|
d = self.callRemote(commands.JMMsgSignatureVerify, |
|
|
|
|
verif_result=verif_result, |
|
|
|
|
nick=nick, |
|
|
|
|
fullmsg=fullmsg, |
|
|
|
|
hostid=hostid) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
class JMMakerClientProtocol(JMClientProtocol): |
|
|
|
|
def __init__(self, factory, maker, nick_priv=None): |
|
|
|
|
self.factory = factory |
|
|
|
|
JMClientProtocol.__init__(self, factory, maker, nick_priv) |
|
|
|
|
|
|
|
|
|
@commands.JMUp.responder |
|
|
|
|
def on_JM_UP(self): |
|
|
|
|
d = self.callRemote(commands.JMSetup, |
|
|
|
|
role="MAKER", |
|
|
|
|
initdata=json.dumps(self.client.offerlist)) |
|
|
|
|
#for as long as the maker is up, it can asynchronously pass through |
|
|
|
|
#its updated offer list |
|
|
|
|
offer_loop = LoopingCall(self.get_offers) |
|
|
|
|
offer_loop.start(3.0) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
def get_offers(self): |
|
|
|
|
"""Feeds through current offers from Maker obect, to the daemon |
|
|
|
|
""" |
|
|
|
|
offerlist = self.client.offerlist |
|
|
|
|
d = self.callRemote(commands.JMAnnounceOffers, |
|
|
|
|
offerlist=json.dumps(offerlist)) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
|
|
|
|
|
@commands.JMSetupDone.responder |
|
|
|
|
def on_JM_SETUP_DONE(self): |
|
|
|
|
jlog.info("JM daemon setup complete") |
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
def clientStart(self): |
|
|
|
|
"""Upon confirmation of network connection |
|
|
|
|
to daemon, request message channel initialization |
|
|
|
|
with relevant config data for our message channels |
|
|
|
|
""" |
|
|
|
|
if self.taker.aborted: |
|
|
|
|
if self.client.aborted: |
|
|
|
|
return |
|
|
|
|
#needed only for naming convention in IRC currently |
|
|
|
|
blockchain_source = jm_single().config.get("BLOCKCHAIN", |
|
|
|
|
"blockchain_source") |
|
|
|
|
#needed only for channel naming convention |
|
|
|
|
network = jm_single().config.get("BLOCKCHAIN", "network") |
|
|
|
|
irc_configs = get_irc_mchannels() |
|
|
|
|
#only here because Init message uses this field; not used by makers TODO |
|
|
|
|
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) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
|
|
|
|
|
@commands.JMAuthReceived.responder |
|
|
|
|
def on_JM_AUTH_RECEIVED(self, nick, offer, commitment, revelation, amount, |
|
|
|
|
kphex): |
|
|
|
|
offer = json.loads(offer) |
|
|
|
|
revelation = json.loads(revelation) |
|
|
|
|
retval = self.client.on_auth_received(nick, offer, |
|
|
|
|
commitment, revelation, amount, kphex) |
|
|
|
|
if not retval[0]: |
|
|
|
|
jlog.info("Maker refuses to continue on receiving auth.") |
|
|
|
|
else: |
|
|
|
|
utxos, auth_pub, cj_addr, change_addr, btc_sig = retval[1:] |
|
|
|
|
d = self.callRemote(commands.JMIOAuth, |
|
|
|
|
nick=nick, |
|
|
|
|
utxolist=json.dumps(utxos), |
|
|
|
|
pubkey=auth_pub, |
|
|
|
|
cjaddr=cj_addr, |
|
|
|
|
changeaddr=change_addr, |
|
|
|
|
pubkeysig=btc_sig) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {"accepted": True} |
|
|
|
|
|
|
|
|
|
@commands.JMTXReceived.responder |
|
|
|
|
def on_JM_TX_RECEIVED(self, nick, txhex, offer): |
|
|
|
|
offer = json.loads(offer) |
|
|
|
|
retval = self.client.on_tx_received(nick, txhex, offer) |
|
|
|
|
if not retval[0]: |
|
|
|
|
jlog.info("Maker refuses to continue on receipt of tx") |
|
|
|
|
else: |
|
|
|
|
sigs = retval[1] |
|
|
|
|
d = self.callRemote(commands.JMTXSigs, |
|
|
|
|
nick=nick, |
|
|
|
|
sigs=json.dumps(sigs)) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {"accepted": True} |
|
|
|
|
|
|
|
|
|
class JMTakerClientProtocol(JMClientProtocol): |
|
|
|
|
|
|
|
|
|
def __init__(self, factory, client, nick_priv=None): |
|
|
|
|
self.orderbook = None |
|
|
|
|
JMClientProtocol.__init__(self, factory, client, nick_priv) |
|
|
|
|
|
|
|
|
|
def clientStart(self): |
|
|
|
|
"""Upon confirmation of network connection |
|
|
|
|
to daemon, request message channel initialization |
|
|
|
|
with relevant config data for our message channels |
|
|
|
|
""" |
|
|
|
|
if self.client.aborted: |
|
|
|
|
return |
|
|
|
|
#needed only for naming convention in IRC currently |
|
|
|
|
blockchain_source = jm_single().config.get("BLOCKCHAIN", |
|
|
|
|
@ -90,9 +245,9 @@ class JMTakerClientProtocol(amp.AMP):
|
|
|
|
|
|
|
|
|
|
#To avoid creating yet another config variable, we set the timeout |
|
|
|
|
#to 20 * maker_timeout_sec. |
|
|
|
|
if not hasattr(self.taker, 'testflag'): #pragma: no cover |
|
|
|
|
if not hasattr(self.client, 'testflag'): #pragma: no cover |
|
|
|
|
reactor.callLater(20*maker_timeout_sec, self.stallMonitor, |
|
|
|
|
self.taker.schedule_index+1) |
|
|
|
|
self.client.schedule_index+1) |
|
|
|
|
|
|
|
|
|
d = self.callRemote(commands.JMInit, |
|
|
|
|
bcsource=blockchain_source, |
|
|
|
|
@ -112,60 +267,31 @@ class JMTakerClientProtocol(amp.AMP):
|
|
|
|
|
on to the next item before we were woken up. |
|
|
|
|
""" |
|
|
|
|
jlog.info("STALL MONITOR:") |
|
|
|
|
if self.taker.aborted: |
|
|
|
|
if self.client.aborted: |
|
|
|
|
jlog.info("Transaction was aborted.") |
|
|
|
|
return |
|
|
|
|
if not self.taker.schedule_index == schedule_index: |
|
|
|
|
if not self.client.schedule_index == schedule_index: |
|
|
|
|
#TODO pre-initialize() ? |
|
|
|
|
jlog.info("No stall detected, continuing") |
|
|
|
|
return |
|
|
|
|
if self.taker.waiting_for_conf: |
|
|
|
|
if self.client.waiting_for_conf: |
|
|
|
|
#Don't restart if the tx is already on the network! |
|
|
|
|
jlog.info("No stall detected, continuing") |
|
|
|
|
return |
|
|
|
|
if not self.taker.txid: |
|
|
|
|
if not self.client.txid: |
|
|
|
|
#txid is set on pushing; if it's not there, we have failed. |
|
|
|
|
jlog.info("Stall detected. Regenerating transactions and retrying.") |
|
|
|
|
self.taker.on_finished_callback(False, True, 0.0) |
|
|
|
|
self.client.on_finished_callback(False, True, 0.0) |
|
|
|
|
else: |
|
|
|
|
#This shouldn't really happen; if the tx confirmed, |
|
|
|
|
#the finished callback should already be called. |
|
|
|
|
jlog.info("Tx was already pushed; ignoring") |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
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 |
|
|
|
|
initdata="none") |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
@ -189,11 +315,11 @@ class JMTakerClientProtocol(amp.AMP):
|
|
|
|
|
if not success: |
|
|
|
|
nonresponders = ioauth_data |
|
|
|
|
jlog.info("Makers didnt respond: " + str(nonresponders)) |
|
|
|
|
self.taker.add_ignored_makers(nonresponders) |
|
|
|
|
self.client.add_ignored_makers(nonresponders) |
|
|
|
|
return {'accepted': True} |
|
|
|
|
else: |
|
|
|
|
jlog.info("Makers responded with: " + json.dumps(ioauth_data)) |
|
|
|
|
retval = self.taker.receive_utxos(ioauth_data) |
|
|
|
|
retval = self.client.receive_utxos(ioauth_data) |
|
|
|
|
if not retval[0]: |
|
|
|
|
jlog.info("Taker is not continuing, phase 2 abandoned.") |
|
|
|
|
jlog.info("Reason: " + str(retval[1])) |
|
|
|
|
@ -208,7 +334,7 @@ class JMTakerClientProtocol(amp.AMP):
|
|
|
|
|
self.orderbook = json.loads(orderbook) |
|
|
|
|
#Removed for now, as judged too large, even for DEBUG: |
|
|
|
|
#jlog.debug("Got the orderbook: " + str(self.orderbook)) |
|
|
|
|
retval = self.taker.initialize(self.orderbook) |
|
|
|
|
retval = self.client.initialize(self.orderbook) |
|
|
|
|
#format of retval is: |
|
|
|
|
#True, self.cjamount, commitment, revelation, self.filtered_orderbook) |
|
|
|
|
if not retval[0]: |
|
|
|
|
@ -225,48 +351,12 @@ class JMTakerClientProtocol(amp.AMP):
|
|
|
|
|
|
|
|
|
|
@commands.JMSigReceived.responder |
|
|
|
|
def on_JM_SIG_RECEIVED(self, nick, sig): |
|
|
|
|
retval = self.taker.on_sig(nick, sig) |
|
|
|
|
retval = self.client.on_sig(nick, sig) |
|
|
|
|
if retval: |
|
|
|
|
nick_to_use, txhex = retval |
|
|
|
|
self.push_tx(nick_to_use, txhex) |
|
|
|
|
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) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
@commands.JMRequestMsgSigVerify.responder |
|
|
|
|
def on_JM_REQUEST_MSGSIG_VERIFY(self, msg, fullmsg, sig, pubkey, nick, |
|
|
|
|
hashlen, max_encoded, hostid): |
|
|
|
|
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 |
|
|
|
|
d = self.callRemote(commands.JMMsgSignatureVerify, |
|
|
|
|
verif_result=verif_result, |
|
|
|
|
nick=nick, |
|
|
|
|
fullmsg=fullmsg, |
|
|
|
|
hostid=hostid) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
def get_offers(self): |
|
|
|
|
d = self.callRemote(commands.JMRequestOffers) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
@ -282,23 +372,28 @@ class JMTakerClientProtocol(amp.AMP):
|
|
|
|
|
txhex=str(txhex_to_push)) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
|
|
|
|
|
class JMTakerClientProtocolFactory(protocol.ClientFactory): |
|
|
|
|
class JMClientProtocolFactory(protocol.ClientFactory): |
|
|
|
|
protocol = JMTakerClientProtocol |
|
|
|
|
|
|
|
|
|
def __init__(self, taker): |
|
|
|
|
self.taker = taker |
|
|
|
|
def __init__(self, client, proto_type="TAKER"): |
|
|
|
|
self.client = client |
|
|
|
|
self.proto_client = None |
|
|
|
|
self.proto_type = proto_type |
|
|
|
|
if self.proto_type == "MAKER": |
|
|
|
|
self.protocol = JMMakerClientProtocol |
|
|
|
|
|
|
|
|
|
def setClient(self, client): |
|
|
|
|
self.proto_client = client |
|
|
|
|
def getClient(self): |
|
|
|
|
return self.proto_client |
|
|
|
|
|
|
|
|
|
def buildProtocol(self, addr): |
|
|
|
|
return JMTakerClientProtocol(self, self.taker) |
|
|
|
|
|
|
|
|
|
return self.protocol(self, self.client) |
|
|
|
|
|
|
|
|
|
def start_reactor(host, port, factory, ish=True, daemon=False): #pragma: no cover |
|
|
|
|
#(Cannot start the reactor in tests) |
|
|
|
|
#Not used in prod (twisted logging): |
|
|
|
|
#startLogging(stdout) |
|
|
|
|
usessl = True if jm_single().config.get("DAEMON", "use_ssl") != 'false' else False |
|
|
|
|
if daemon: |
|
|
|
|
try: |
|
|
|
|
|