|
|
|
|
@ -9,7 +9,7 @@ from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH,
|
|
|
|
|
COMMITMENT_PREFIXES) |
|
|
|
|
from .irc import IRCMessageChannel |
|
|
|
|
|
|
|
|
|
from jmbase import (hextobin, is_hs_uri, get_tor_agent, JMHiddenService, |
|
|
|
|
from jmbase import (is_hs_uri, get_tor_agent, JMHiddenService, |
|
|
|
|
get_nontor_agent, BytesProducer, wrapped_urlparse, |
|
|
|
|
bdict_sdict_convert, JMHTTPResource) |
|
|
|
|
from jmbase.commands import * |
|
|
|
|
@ -32,7 +32,6 @@ import os
|
|
|
|
|
from io import BytesIO |
|
|
|
|
import copy |
|
|
|
|
from functools import wraps |
|
|
|
|
from numbers import Integral |
|
|
|
|
|
|
|
|
|
"""Joinmarket application protocol control flow. |
|
|
|
|
For documentation on protocol (formats, message sequence) see |
|
|
|
|
@ -166,15 +165,14 @@ class HTTPPassThrough(amp.AMP):
|
|
|
|
|
|
|
|
|
|
def on_INIT(self, netconfig): |
|
|
|
|
""" The network config must be passed in json |
|
|
|
|
and contains these fields: |
|
|
|
|
socks5_host |
|
|
|
|
socks5_proxy |
|
|
|
|
servers (comma separated list) |
|
|
|
|
tls_whitelist (comma separated list) |
|
|
|
|
filterconfig (not yet defined) |
|
|
|
|
credentials (not yet defined) |
|
|
|
|
""" |
|
|
|
|
netconfig = json.loads(netconfig) |
|
|
|
|
and contains these fields: |
|
|
|
|
socks5_host |
|
|
|
|
socks5_proxy |
|
|
|
|
servers (comma separated list) |
|
|
|
|
tls_whitelist (comma separated list) |
|
|
|
|
filterconfig (not yet defined) |
|
|
|
|
credentials (not yet defined) |
|
|
|
|
""" |
|
|
|
|
self.socks5_host = netconfig["socks5_host"] |
|
|
|
|
self.socks5_port = int(netconfig["socks5_port"]) |
|
|
|
|
self.servers = [a for a in netconfig["servers"] if a != ""] |
|
|
|
|
@ -272,7 +270,6 @@ class HTTPPassThrough(amp.AMP):
|
|
|
|
|
class BIP78ServerProtocol(HTTPPassThrough): |
|
|
|
|
@BIP78ReceiverInit.responder |
|
|
|
|
def on_BIP78_RECEIVER_INIT(self, netconfig): |
|
|
|
|
netconfig = json.loads(netconfig) |
|
|
|
|
self.serving_port = int(netconfig["port"]) |
|
|
|
|
self.tor_control_host = netconfig["tor_control_host"] |
|
|
|
|
self.tor_control_port = int(netconfig["tor_control_port"]) |
|
|
|
|
@ -325,7 +322,7 @@ class BIP78ServerProtocol(HTTPPassThrough):
|
|
|
|
|
""" |
|
|
|
|
self.post_request = request |
|
|
|
|
d = self.callRemote(BIP78ReceiverOriginalPSBT, body=body, |
|
|
|
|
params=json.dumps(bdict_sdict_convert(params))) |
|
|
|
|
params=bdict_sdict_convert(params)) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
|
|
|
|
|
@BIP78ReceiverSendProposal.responder |
|
|
|
|
@ -354,9 +351,9 @@ class BIP78ServerProtocol(HTTPPassThrough):
|
|
|
|
|
@BIP78SenderOriginalPSBT.responder |
|
|
|
|
def on_BIP78_SENDER_ORIGINAL_PSBT(self, body, params): |
|
|
|
|
self.postRequest(body, self.servers[0], |
|
|
|
|
self.bip78_receiver_response, |
|
|
|
|
params=json.loads(params), |
|
|
|
|
headers=Headers({"Content-Type": ["text/plain"]})) |
|
|
|
|
self.bip78_receiver_response, |
|
|
|
|
params=params, |
|
|
|
|
headers=Headers({"Content-Type": ["text/plain"]})) |
|
|
|
|
return {"accepted": True} |
|
|
|
|
|
|
|
|
|
def bip78_receiver_response(self, response, server): |
|
|
|
|
@ -475,6 +472,8 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
self.sig_lock = threading.Lock() |
|
|
|
|
self.active_orders = {} |
|
|
|
|
self.use_fidelity_bond = False |
|
|
|
|
self.offerlist = None |
|
|
|
|
self.kp = None |
|
|
|
|
|
|
|
|
|
def checkClientResponse(self, response): |
|
|
|
|
"""A generic check of client acceptance; any failure |
|
|
|
|
@ -502,11 +501,9 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
If a new message channel configuration is required, the current |
|
|
|
|
one is shutdown in preparation. |
|
|
|
|
""" |
|
|
|
|
self.maker_timeout_sec = int(maker_timeout_sec) |
|
|
|
|
# used in OrderbookWatch: |
|
|
|
|
self.maker_timeout_sec = maker_timeout_sec |
|
|
|
|
self.minmakers = minmakers |
|
|
|
|
self.dust_threshold = int(dust_threshold) |
|
|
|
|
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: |
|
|
|
|
@ -554,7 +551,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
return {'accepted': True} |
|
|
|
|
|
|
|
|
|
@JMSetup.responder |
|
|
|
|
def on_JM_SETUP(self, role, offers, use_fidelity_bond): |
|
|
|
|
def on_JM_SETUP(self, role, initdata, use_fidelity_bond): |
|
|
|
|
assert self.jm_state == 0 |
|
|
|
|
self.role = role |
|
|
|
|
self.crypto_boxes = {} |
|
|
|
|
@ -568,7 +565,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
if self.role == "TAKER": |
|
|
|
|
self.mcc.pubmsg(COMMAND_PREFIX + "orderbook") |
|
|
|
|
elif self.role == "MAKER": |
|
|
|
|
self.offerlist = json.loads(offers) |
|
|
|
|
self.offerlist = initdata |
|
|
|
|
self.use_fidelity_bond = use_fidelity_bond |
|
|
|
|
self.mcc.announce_orders(self.offerlist, None, None, None) |
|
|
|
|
self.jm_state = 1 |
|
|
|
|
@ -614,15 +611,14 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
"""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, Integral) |
|
|
|
|
and amount >= 0): |
|
|
|
|
if self.jm_state != 1 or 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) |
|
|
|
|
self.active_orders = filled_offers |
|
|
|
|
for nick, offer_dict in self.active_orders.items(): |
|
|
|
|
offer_fill_msg = " ".join([str(offer_dict["oid"]), str(amount), |
|
|
|
|
self.kp.hex_pk().decode('ascii'), str(commitment)]) |
|
|
|
|
@ -639,7 +635,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
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} |
|
|
|
|
|
|
|
|
|
@ -659,9 +654,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
""" |
|
|
|
|
if self.role != "MAKER": |
|
|
|
|
return |
|
|
|
|
to_announce = json.loads(to_announce) |
|
|
|
|
to_cancel = json.loads(to_cancel) |
|
|
|
|
self.offerlist = json.loads(offerlist) |
|
|
|
|
self.offerlist = offerlist |
|
|
|
|
if len(to_cancel) > 0: |
|
|
|
|
self.mcc.cancel_orders(to_cancel) |
|
|
|
|
if len(to_announce) > 0: |
|
|
|
|
@ -682,14 +675,13 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
""" |
|
|
|
|
if not self.role == "MAKER": |
|
|
|
|
return |
|
|
|
|
if not nick in self.active_orders: |
|
|
|
|
if nick not 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( |
|
|
|
|
self.active_orders[nick]["utxos"] = utxolist |
|
|
|
|
msg = str(",".join(utxolist)) + " " + " ".join( |
|
|
|
|
[pubkey, cjaddr, changeaddr, pubkeysig]) |
|
|
|
|
self.mcc.prepare_privmsg(nick, "ioauth", msg) |
|
|
|
|
#In case of *blacklisted (ie already used) commitments, we already |
|
|
|
|
@ -705,11 +697,10 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
@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 = json.loads(sigs) |
|
|
|
|
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. |
|
|
|
|
""" |
|
|
|
|
for sig in sigs: |
|
|
|
|
self.mcc.prepare_privmsg(nick, "sig", sig) |
|
|
|
|
return {"accepted": True} |
|
|
|
|
@ -794,19 +785,19 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
@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: |
|
|
|
|
for processing. This will include validating the PoDLE |
|
|
|
|
commitment revelation against the existing commitment, |
|
|
|
|
which was already stored in active_orders[nick]. |
|
|
|
|
""" |
|
|
|
|
if nick not 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"]), |
|
|
|
|
offer=ao["offer"], |
|
|
|
|
commitment=ao["commit"], |
|
|
|
|
revelation=json.dumps(commitment_revelation), |
|
|
|
|
revelation=commitment_revelation, |
|
|
|
|
amount=ao["amount"], |
|
|
|
|
kphex=ao["kp"].hex_pk().decode('ascii')) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
@ -831,20 +822,14 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
self.mcc.pubmsg("!hp2 " + commitment) |
|
|
|
|
|
|
|
|
|
@maker_only |
|
|
|
|
def on_push_tx(self, nick, txhex): |
|
|
|
|
"""Broadcast unquestioningly, except checking |
|
|
|
|
hex format. |
|
|
|
|
""" |
|
|
|
|
try: |
|
|
|
|
dummy = hextobin(txhex) |
|
|
|
|
except: |
|
|
|
|
return |
|
|
|
|
d = self.callRemote(JMTXBroadcast, |
|
|
|
|
txhex=txhex) |
|
|
|
|
def on_push_tx(self, nick, tx): |
|
|
|
|
"""Broadcast unquestioningly |
|
|
|
|
""" |
|
|
|
|
d = self.callRemote(JMTXBroadcast, tx=tx) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
|
|
|
|
|
@maker_only |
|
|
|
|
def on_seen_tx(self, nick, txhex): |
|
|
|
|
def on_seen_tx(self, nick, tx): |
|
|
|
|
"""Passes the txhex to the Maker for verification |
|
|
|
|
and signing. Note the security checks occur in Maker. |
|
|
|
|
""" |
|
|
|
|
@ -855,10 +840,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
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)) |
|
|
|
|
d = self.callRemote(JMTXReceived, nick=nick, tx=tx, offer=ao) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
|
|
|
|
|
@taker_only |
|
|
|
|
@ -898,9 +880,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
def on_sig(self, nick, sig): |
|
|
|
|
"""Pass signature through to Taker. |
|
|
|
|
""" |
|
|
|
|
d = self.callRemote(JMSigReceived, |
|
|
|
|
nick=nick, |
|
|
|
|
sig=sig) |
|
|
|
|
d = self.callRemote(JMSigReceived, nick=nick, sig=sig) |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
|
|
|
|
|
def on_error(self, msg): |
|
|
|
|
@ -990,12 +970,12 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
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()] |
|
|
|
|
nonresponders = [x for x in self.active_orders |
|
|
|
|
if x not in self.ioauth_data] |
|
|
|
|
ioauth_data = self.ioauth_data if accepted else nonresponders |
|
|
|
|
d = self.callRemote(JMFillResponse, |
|
|
|
|
success=accepted, |
|
|
|
|
ioauth_data = json.dumps(ioauth_data)) |
|
|
|
|
success=accepted, |
|
|
|
|
ioauth_data=ioauth_data) |
|
|
|
|
if not accepted: |
|
|
|
|
#Client simply accepts failure TODO |
|
|
|
|
self.defaultCallbacks(d) |
|
|
|
|
@ -1010,7 +990,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
|
|
|
|
|
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 |
|
|
|
|
response = True if len(self.ioauth_data) >= self.minmakers else False |
|
|
|
|
self.respondToIoauths(response) |
|
|
|
|
|
|
|
|
|
def checkUtxosAccepted(self, accepted): |
|
|
|
|
|