Browse Source

Client-server protocol can now re-init without shutting down

message channels.
Sendpayment POC now runs multiple transactions with -b setting.
master
Adam Gibson 9 years ago
parent
commit
8129880e82
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 2
      README.md
  2. 10
      jmbase/commands.py
  3. 15
      jmclient/client_protocol.py
  4. 16
      jmclient/taker.py
  5. 12
      jmdaemon/message_channel.py
  6. 98
      joinmarketd.py
  7. 58
      sendpayment.py

2
README.md

@ -28,3 +28,5 @@ Next, you can install in 3 different modes:
supported, see https://github.com/AdamISZ/electrum-joinmarket-plugin for details):
`python setup.py --client-only install`
You can then access the library via `import jmclient`.

10
jmbase/commands.py

@ -15,7 +15,12 @@ class JMCommand(Command):
#commands from client to daemon
class JMInit(JMCommand):
class JMInit(JMCommand):
"""Communicates the client's required setup
configuration.
Blockchain source is communicated only as a naming
tag for messagechannels (currently IRC 'realname' field).
"""
arguments = [('bcsource', String()),
('network', String()),
('irc_configs', String()),
@ -24,6 +29,9 @@ class JMInit(JMCommand):
errors = {DaemonNotReady: 'daemon is not ready'}
class JMStartMC(JMCommand):
"""Will restart message channel connections if config
has changed; otherwise will only change nym/nick on MCs.
"""
arguments = [('nick', String())]
class JMSetup(JMCommand):

15
jmclient/client_protocol.py

@ -47,13 +47,18 @@ class JMTakerClientProtocol(amp.AMP):
reactor.stop()
def connectionMade(self):
self.factory.setClient(self)
self.clientStart()
def clientStart(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
#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()
minmakers = jm_single().config.getint("POLICY", "minimum_makers")
@ -66,9 +71,6 @@ class JMTakerClientProtocol(amp.AMP):
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()[
@ -220,6 +222,11 @@ class JMTakerClientProtocolFactory(protocol.ClientFactory):
def __init__(self, taker):
self.taker = taker
self.proto_client = None
def setClient(self, client):
self.proto_client = client
def getClient(self):
return self.proto_client
def buildProtocol(self, addr):
return JMTakerClientProtocol(self, self.taker)

16
jmclient/taker.py

@ -48,20 +48,32 @@ class Taker(object):
#allow custom wallet-based clients to use their own signing code;
#currently only setting "wallet" is allowed, calls wallet.sign_tx(tx)
self.sign_method = sign_method
#External callers can set any of the 3 callbacks for filtering orders,
#sending info messages to client, and action on completion.
if callbacks:
self.filter_orders_callback, self.taker_info_callback = callbacks
self.filter_orders_callback, self.taker_info_callback, self.on_finished_callback = callbacks
if not self.taker_info_callback:
self.taker_info_callback = self.default_taker_info_callback
if not self.on_finished_callback:
self.on_finished_callback = self.default_on_finished_callback
else:
self.filter_orders_callback = None
self.taker_info_callback = self.default_taker_info_callback
self.on_finished_callback = self.default_on_finished_callback
def default_taker_info_callback(self, infotype, msg):
jlog.debug(infotype + ":" + msg)
def default_on_finished_callback(self, result):
jlog.debug("Taker default on finished callback: " + str(result))
def initialize(self, orderbook):
"""Once the daemon is active and has returned the current orderbook,
select offers and prepare a commitment, then send it to the protocol
to fill offers.
"""
#reset destinations
self.outputs = []
if not self.filter_orderbook(orderbook):
return (False,)
#choose coins to spend
@ -555,7 +567,7 @@ class Taker(object):
self.txid = btc.txhash(tx)
jlog.debug('txid = ' + self.txid)
pushed = jm_single().bc_interface.pushtx(tx)
return pushed
self.on_finished_callback(pushed)
def self_sign_and_push(self):
self.self_sign()

12
jmdaemon/message_channel.py

@ -106,13 +106,15 @@ class MessageChannelCollection(object):
self.welcomed = False
#control access
self.mc_lock = threading.Lock()
self.nick=None
def set_nick(self, nick):
self.nick = nick
#protocol level var:
nickname = self.nick
for mc in self.mchannels:
mc.set_nick(self.nick)
if nick != self.nick:
self.nick = nick
#protocol level var:
nickname = self.nick
for mc in self.mchannels:
mc.set_nick(self.nick)
def available_channels(self):
return [x for x in self.mchannels if self.mc_status[x] == 1]

98
joinmarketd.py

@ -49,12 +49,11 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
def __init__(self, factory):
self.factory = factory
#Set of messages we can receive from a client:
self.supported_messages = ["JM_INIT", "JM_SETUP", "JM_FILL",
"JM_MAKE_TX", "JM_REQUEST_OFFERS",
"JM_MAKE_TX", "JM_MSGSIGNATURE",
"JM_MSGSIGNATURE_VERIFY", "JM_START_MC"]
self.jm_state = 0
self.restart_mc_required = False
self.irc_configs = None
self.mcc = None
self.sig_lock = threading.Lock()
def checkClientResponse(self, response):
"""A generic check of client acceptance; any failure
@ -66,21 +65,37 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
@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)
mcs = [IRCMessageChannel(c,
daemon=self,
realname='btcint=' + bcsource)
for c in irc_configs]
#(bitcoin) network only referenced in channel name construction
self.network = network
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.set_daemon(self)
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.set_daemon(self)
d = self.callRemote(JMInitProto,
nick_hash_length=NICK_HASH_LENGTH,
nick_max_encoded=NICK_MAX_ENCODED,
@ -91,7 +106,8 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
@JMStartMC.responder
def on_JM_START_MC(self, nick):
"""Starts message channel threads;
"""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)
@ -99,8 +115,15 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
def init_connections(self, nick):
self.jm_state = 0 #uninited
if self.restart_mc_required:
MCThread(self.mcc).start()
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)
MCThread(self.mcc).start()
def on_welcome(self):
"""Fired when channel indicated state readiness
@ -241,26 +264,28 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
duplication is so that the client does not need to know the
message syntax.
"""
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))
d.addCallback(self.checkClientResponse)
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))
d.addCallback(self.checkClientResponse)
def request_signature_verify(self, msg, fullmsg, sig, pubkey, nick, hashlen,
max_encoded, hostid):
d = self.callRemote(JMRequestMsgSigVerify,
msg=msg,
fullmsg=fullmsg,
sig=sig,
pubkey=pubkey,
nick=nick,
hashlen=hashlen,
max_encoded=max_encoded,
hostid=hostid)
d.addCallback(self.checkClientResponse)
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)
d.addCallback(self.checkClientResponse)
@JMMsgSignature.responder
def on_JM_MSGSIGNATURE(self, nick, cmd, msg_to_return, hostid):
@ -287,8 +312,9 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
log.msg("Unimplemented on_error")
def mc_shutdown(self):
log.msg("Message channels shut down in proto")
self.mcc.shutdown()
log.msg("Message channels being shutdown by daemon")
if self.mcc:
self.mcc.shutdown()
class JMDaemonServerProtocolFactory(ServerFactory):

58
sendpayment.py

@ -4,9 +4,26 @@ from __future__ import absolute_import, print_function
"""
A sample implementation of a single coinjoin script,
adapted from `sendpayment.py` in Joinmarket-Org/joinmarket.
This is primitive and not yet well tested, but it is designed
to illustrate the main functionality of the new architecture:
this code can be run in a separate environment (but not safely
over the internet, better on one machine) to the joinmarketdaemon.
Moreover, it can run several transactions using the -b option, e.g.:
`python sendpayment.py -b 3 -N 3 -m 1 walletseed amount address`;
note here only one destination address for multiple transactions,
only one mixdepth and other settings; this is just a proof of concept.
The idea is that the "backend" (daemon) will keep its orderbook and stay
connected on the message channel between runs, only shutting down
after all are complete.
It should be very easy to extend this further, of course.
More complex applications can extend from Taker and add
more features, such as repeated joins. This will also allow
easier coding of non-CLI interfaces.
more features. This will also allow
easier coding of non-CLI interfaces. A plugin for Electrum is in process
and already working.
Other potential customisations of the Taker object instantiation
include:
@ -22,7 +39,7 @@ import random
import sys
import threading
from optparse import OptionParser
from twisted.internet import reactor
import time
from jmclient import (Taker, load_program_config,
@ -36,7 +53,8 @@ from jmclient import (Taker, load_program_config,
from jmbase.support import get_log, debug_dump_object
log = get_log()
txcount = 1
wallet = None
def check_high_fee(total_fee_pc):
WARNING_THRESHOLD = 0.02 # 2%
@ -94,6 +112,13 @@ def main():
dest='daemonport',
help='port on which joinmarketd is running',
default='12345')
parser.add_option('-b',
'--txcount',
type='int',
dest='txcount',
help=('optionally do more than 1 transaction to the '
'same destination, of the same amount'),
default=1)
parser.add_option(
'-C',
'--choose-cheapest',
@ -181,19 +206,40 @@ def main():
assert (options.txfee >= 0)
log.debug('starting sendpayment')
global wallet
if not options.userpcwallet:
wallet = Wallet(wallet_name, options.amtmixdepths, options.gaplimit)
else:
wallet = BitcoinCoreWallet(fromaccount=wallet_name)
jm_single().bc_interface.sync_wallet(wallet)
def taker_finished(res):
global wallet
global txcount
txcount += 1
if res:
log.debug("Transaction finished OK, result was: ")
else:
log.info("A transaction failed, quitting")
sys.exit(1)
if txcount > options.txcount:
log.debug("Shutting down")
reactor.stop()
else:
#need to update for new transactions; only working for
#regtest at the moment (otherwise too slow)
jm_single().bc_interface.sync_wallet(wallet)
time.sleep(3) #for blocks to mine
#restarts from the entry point of the client-server protocol (JMInit)
#with the *same* Taker object.
clientfactory.getClient().clientStart()
taker = Taker(wallet,
options.mixdepth,
amount,
options.makercount,
order_chooser=chooseOrdersFunc,
external_addr=destaddr)
external_addr=destaddr,
callbacks=(None, None, taker_finished))
clientfactory = JMTakerClientProtocolFactory(taker)
start_reactor("localhost", options.daemonport, clientfactory)

Loading…
Cancel
Save