Browse Source

Include maker and yieldgen modules. Fix tx notifications

for confirm and unconfirm callback; yg-basic now functioning.
Some rearrangement of daemon protocol module.
master
Adam Gibson 9 years ago
parent
commit
ea9eb4a32b
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 9
      jmbase/jmbase/commands.py
  2. 75
      jmclient/jmclient/client_protocol.py
  3. 168
      jmclient/jmclient/maker.py
  4. 153
      jmclient/jmclient/yieldgenerator.py
  5. 304
      jmdaemon/jmdaemon/daemon_protocol.py
  6. 137
      scripts/yield-generator-basic.py

9
jmbase/jmbase/commands.py

@ -101,9 +101,12 @@ class JMPushTx(JMCommand):
class JMAnnounceOffers(JMCommand):
"""Send list (actually dict) of offers
to the daemon
to the daemon, along with new announcement
and cancellation lists (deltas).
"""
arguments = [('offerlist', String())]
arguments = [('to_announce', String()),
('to_cancel', String()),
('offerlist', String())]
class JMIOAuth(JMCommand):
"""Send contents of !ioauth message after
@ -123,7 +126,7 @@ class JMTXSigs(JMCommand):
arguments = [('nick', String()),
('sigs', String())]
"""COMMANDS FROM CLIENT TO DAEMON
"""COMMANDS FROM DAEMON TO CLIENT
=================================
"""

75
jmclient/jmclient/client_protocol.py

@ -24,9 +24,10 @@ import time
import hashlib
import os
import sys
import pprint
from jmclient import (Taker, Wallet, jm_single, get_irc_mchannels,
load_program_config, get_log)
load_program_config, get_log, get_p2sh_vbyte)
from jmbase import _byteify
import btc
jlog = get_log()
@ -134,6 +135,8 @@ class JMClientProtocol(amp.AMP):
class JMMakerClientProtocol(JMClientProtocol):
def __init__(self, factory, maker, nick_priv=None):
self.factory = factory
#used for keeping track of transactions for the unconf/conf callbacks
self.finalized_offers = {}
JMClientProtocol.__init__(self, factory, maker, nick_priv)
@commands.JMUp.responder
@ -141,21 +144,9 @@ class JMMakerClientProtocol(JMClientProtocol):
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")
@ -209,18 +200,72 @@ class JMMakerClientProtocol(JMClientProtocol):
@commands.JMTXReceived.responder
def on_JM_TX_RECEIVED(self, nick, txhex, offer):
offer = json.loads(offer)
offer = _byteify(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]
self.finalized_offers[nick] = offer
tx = btc.deserialize(txhex)
self.finalized_offers[nick]["txd"] = tx
jm_single().bc_interface.add_tx_notify(tx, self.unconfirm_callback,
self.confirm_callback,
offer["cjaddr"],
vb=get_p2sh_vbyte())
d = self.callRemote(commands.JMTXSigs,
nick=nick,
sigs=json.dumps(sigs))
self.defaultCallbacks(d)
return {"accepted": True}
def unconfirm_callback(self, txd, txid):
#find the offer for this tx
offerinfo = None
for k,v in self.finalized_offers.iteritems():
#Tx considered defined by its output set
if v["txd"]["outs"] == txd["outs"]:
offerinfo = v
break
if not offerinfo:
jlog.info("Failed to find notified unconfirmed transaction: " + txid)
return
removed_utxos = self.client.wallet.remove_old_utxos(txd)
jlog.info('saw tx on network, removed_utxos=\n{}'.format(
pprint.pformat(removed_utxos)))
to_cancel, to_announce = self.client.on_tx_unconfirmed(offerinfo,
txid, removed_utxos)
self.client.modify_orders(to_cancel, to_announce)
d = self.callRemote(commands.JMAnnounceOffers,
to_announce=json.dumps(to_announce),
to_cancel=json.dumps(to_cancel),
offerlist=json.dumps(self.client.offerlist))
self.defaultCallbacks(d)
def confirm_callback(self, txd, txid, confirmations):
#find the offer for this tx
offerinfo = None
for k,v in self.finalized_offers.iteritems():
#Tx considered defined by its output set
if v["txd"]["outs"] == txd["outs"]:
offerinfo = v
break
if not offerinfo:
jlog.info("Failed to find notified unconfirmed transaction: " + txid)
return
jm_single().bc_interface.sync_unspent(self.client.wallet)
jlog.info('tx in a block: ' + txid)
#TODO track the earning
#jlog.info('earned = ' + str(self.real_cjfee - self.txfee))
to_cancel, to_announce = self.client.on_tx_confirmed(offerinfo,
confirmations, txid)
self.client.modify_orders(to_cancel, to_announce)
d = self.callRemote(commands.JMAnnounceOffers,
to_announce=json.dumps(to_announce),
to_cancel=json.dumps(to_cancel),
offerlist=json.dumps(self.client.offerlist))
self.defaultCallbacks(d)
class JMTakerClientProtocol(JMClientProtocol):
def __init__(self, factory, client, nick_priv=None):

168
jmclient/jmclient/maker.py

@ -0,0 +1,168 @@
#! /usr/bin/env python
from __future__ import print_function
import base64
import pprint
import random
import sys
import time
import copy
import btc
from jmclient.configure import jm_single, get_p2pk_vbyte, get_p2sh_vbyte
from jmbase.support import get_log
from jmclient.support import (calc_cj_fee)
from jmclient.wallet import estimate_tx_fee
from jmclient.podle import (generate_podle, get_podle_commitments, verify_podle,
PoDLE, PoDLEError, generate_podle_error_string)
jlog = get_log()
class Maker(object):
def __init__(self, wallet):
self.active_orders = {}
self.wallet = wallet
self.nextoid = -1
self.offerlist = self.create_my_orders()
self.aborted = False
def on_auth_received(self, nick, offer, commitment, cr, amount, kphex):
"""Receives data on proposed transaction offer from daemon, verifies
commitment, returns necessary data to send ioauth message (utxos etc)
"""
#deserialize the commitment revelation
cr_dict = PoDLE.deserialize_revelation(cr)
#check the validity of the proof of discrete log equivalence
tries = jm_single().config.getint("POLICY", "taker_utxo_retries")
def reject(msg):
jlog.info("Counterparty commitment not accepted, reason: " + msg)
return (False,)
if not verify_podle(str(cr_dict['P']), str(cr_dict['P2']), str(cr_dict['sig']),
str(cr_dict['e']), str(commitment),
index_range=range(tries)):
reason = "verify_podle failed"
return reject(reason)
#finally, check that the proffered utxo is real, old enough, large enough,
#and corresponds to the pubkey
res = jm_single().bc_interface.query_utxo_set([cr_dict['utxo']],
includeconf=True)
if len(res) != 1 or not res[0]:
reason = "authorizing utxo is not valid"
return reject(reason)
age = jm_single().config.getint("POLICY", "taker_utxo_age")
if res[0]['confirms'] < age:
reason = "commitment utxo not old enough: " + str(res[0]['confirms'])
return reject(reason)
reqd_amt = int(amount * jm_single().config.getint(
"POLICY", "taker_utxo_amtpercent") / 100.0)
if res[0]['value'] < reqd_amt:
reason = "commitment utxo too small: " + str(res[0]['value'])
return reject(reason)
if res[0]['address'] != btc.pubkey_to_p2sh_p2wpkh_address(cr_dict['P'],
self.wallet.get_vbyte()):
reason = "Invalid podle pubkey: " + str(cr_dict['P'])
return reject(reason)
# authorisation of taker passed
#Find utxos for the transaction now:
utxos, cj_addr, change_addr = self.oid_to_order(offer, amount)
if not utxos:
#could not find funds
return (False,)
# Construct data for auth request back to taker.
# Need to choose an input utxo pubkey to sign with
# (no longer using the coinjoin pubkey from 0.2.0)
# Just choose the first utxo in self.utxos and retrieve key from wallet.
auth_address = utxos[utxos.keys()[0]]['address']
auth_key = self.wallet.get_key_from_addr(auth_address)
auth_pub = btc.privtopub(auth_key)
btc_sig = btc.ecdsa_sign(kphex, auth_key)
return (True, utxos, auth_pub, cj_addr, change_addr, btc_sig)
def on_tx_received(self, nick, txhex, offerinfo):
try:
tx = btc.deserialize(txhex)
except IndexError as e:
return (False, 'malformed txhex. ' + repr(e))
jlog.info('obtained tx\n' + pprint.pformat(tx))
goodtx, errmsg = self.verify_unsigned_tx(tx, offerinfo)
if not goodtx:
jlog.info('not a good tx, reason=' + errmsg)
return (False, errmsg)
jlog.info('goodtx')
sigs = []
utxos = offerinfo["utxos"]
for index, ins in enumerate(tx['ins']):
utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index'])
if utxo not in utxos.keys():
continue
addr = utxos[utxo]['address']
amount = utxos[utxo]["value"]
txs = self.wallet.sign(txhex, index,
self.wallet.get_key_from_addr(addr),
amount=amount)
sigmsg = btc.deserialize(txs)["ins"][index]["script"].decode("hex")
if "txinwitness" in btc.deserialize(txs)["ins"][index].keys():
#We prepend the witness data since we want (sig, pub, scriptCode);
#also, the items in witness are not serialize_script-ed.
sigmsg = "".join([btc.serialize_script_unit(
x.decode("hex")) for x in btc.deserialize(
txs)["ins"][index]["txinwitness"]]) + sigmsg
sigs.append(base64.b64encode(sigmsg))
return (True, sigs)
def verify_unsigned_tx(self, txd, offerinfo):
tx_utxo_set = set(ins['outpoint']['hash'] + ':' + str(
ins['outpoint']['index']) for ins in txd['ins'])
utxos = offerinfo["utxos"]
cjaddr = offerinfo["cjaddr"]
changeaddr = offerinfo["changeaddr"]
amount = offerinfo["amount"]
cjfee = offerinfo["offer"]["cjfee"]
txfee = offerinfo["offer"]["txfee"]
ordertype = offerinfo["offer"]["ordertype"]
my_utxo_set = set(utxos.keys())
if not tx_utxo_set.issuperset(my_utxo_set):
return (False, 'my utxos are not contained')
my_total_in = sum([va['value'] for va in utxos.values()])
real_cjfee = calc_cj_fee(ordertype, cjfee, amount)
expected_change_value = (my_total_in - amount - txfee + real_cjfee)
jlog.info('potentially earned = {}'.format(real_cjfee - txfee))
jlog.debug('mycjaddr, mychange = {}, {}'.format(cjaddr, changeaddr))
times_seen_cj_addr = 0
times_seen_change_addr = 0
for outs in txd['outs']:
addr = btc.script_to_address(outs['script'], get_p2sh_vbyte())
if addr == cjaddr:
times_seen_cj_addr += 1
if outs['value'] != amount:
return (False, 'Wrong cj_amount. I expect ' + str(amount))
if addr == changeaddr:
times_seen_change_addr += 1
if outs['value'] != expected_change_value:
return (False, 'wrong change, i expect ' + str(
expected_change_value))
if times_seen_cj_addr != 1 or times_seen_change_addr != 1:
fmt = ('cj or change addr not in tx '
'outputs once, #cjaddr={}, #chaddr={}').format
return (False, (fmt(times_seen_cj_addr, times_seen_change_addr)))
return (True, None)
def modify_orders(self, to_cancel, to_announce):
jlog.info('modifying orders. to_cancel={}\nto_announce={}'.format(
to_cancel, to_announce))
for oid in to_cancel:
order = [o for o in self.offerlist if o['oid'] == oid]
if len(order) == 0:
fmt = 'didnt cancel order which doesnt exist, oid={}'.format
jlog.debug(fmt(oid))
self.offerlist.remove(order[0])
if len(to_announce) > 0:
for ann in to_announce:
oldorder_s = [order for order in self.offerlist
if order['oid'] == ann['oid']]
if len(oldorder_s) > 0:
self.offerlist.remove(oldorder_s[0])
self.offerlist += to_announce

153
jmclient/jmclient/yieldgenerator.py

@ -0,0 +1,153 @@
#! /usr/bin/env python
from __future__ import absolute_import, print_function
import datetime
import os
import time
import abc
from optparse import OptionParser
from jmclient import (Maker, jm_single, get_network, load_program_config, get_log,
SegwitWallet, sync_wallet, JMClientProtocolFactory,
start_reactor)
jlog = get_log()
MAX_MIX_DEPTH = 5
# is a maker for the purposes of generating a yield from held
# bitcoins
class YieldGenerator(Maker):
__metaclass__ = abc.ABCMeta
statement_file = os.path.join('logs', 'yigen-statement.csv')
def __init__(self, wallet):
Maker.__init__(self, wallet)
self.tx_unconfirm_timestamp = {}
if not os.path.isfile(self.statement_file):
self.log_statement(
['timestamp', 'cj amount/satoshi', 'my input count',
'my input value/satoshi', 'cjfee/satoshi', 'earned/satoshi',
'confirm time/min', 'notes'])
timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
self.log_statement([timestamp, '', '', '', '', '', '', 'Connected'])
def log_statement(self, data):
if get_network() == 'testnet':
return
data = [str(d) for d in data]
self.income_statement = open(self.statement_file, 'a')
self.income_statement.write(','.join(data) + '\n')
self.income_statement.close()
@abc.abstractmethod
def create_my_orders(self):
"""Must generate a set of orders to be displayed
according to the contents of the wallet + some algo.
(Note: should be called "create_my_offers")
"""
@abc.abstractmethod
def oid_to_order(self, cjorder, oid, amount):
"""Must convert an order with an offer/order id
into a set of utxos to fill the order.
Also provides the output addresses for the Taker.
"""
@abc.abstractmethod
def on_tx_unconfirmed(self, cjorder, txid, removed_utxos):
"""Performs action on receipt of transaction into the
mempool in the blockchain instance (e.g. announcing orders)
"""
@abc.abstractmethod
def on_tx_confirmed(self, cjorder, confirmations, txid):
"""Performs actions on receipt of 1st confirmation of
a transaction into a block (e.g. announce orders)
"""
def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffer',
nickserv_password='', minsize=100000, gaplimit=6):
import sys
parser = OptionParser(usage='usage: %prog [options] [wallet file]')
parser.add_option('-o', '--ordertype', action='store', type='string',
dest='ordertype', default=ordertype,
help='type of order; can be either reloffer or absoffer')
parser.add_option('-t', '--txfee', action='store', type='int',
dest='txfee', default=txfee,
help='minimum miner fee in satoshis')
parser.add_option('-c', '--cjfee', action='store', type='string',
dest='cjfee', default='',
help='requested coinjoin fee in satoshis or proportion')
parser.add_option('-p', '--password', action='store', type='string',
dest='password', default=nickserv_password,
help='irc nickserv password')
parser.add_option('-s', '--minsize', action='store', type='int',
dest='minsize', default=minsize,
help='minimum coinjoin size in satoshis')
parser.add_option('-g', '--gap-limit', action='store', type="int",
dest='gaplimit', default=gaplimit,
help='gap limit for wallet, default='+str(gaplimit))
parser.add_option('--fast',
action='store_true',
dest='fastsync',
default=False,
help=('choose to do fast wallet sync, only for Core and '
'only for previously synced wallet'))
(options, args) = parser.parse_args()
if len(args) < 1:
parser.error('Needs a wallet')
sys.exit(0)
wallet_name = args[0]
ordertype = options.ordertype
txfee = options.txfee
if ordertype == 'swreloffer':
if options.cjfee != '':
cjfee_r = options.cjfee
# minimum size is such that you always net profit at least 20%
#of the miner fee
minsize = max(int(1.2 * txfee / float(cjfee_r)), options.minsize)
elif ordertype == 'swabsoffer':
if options.cjfee != '':
cjfee_a = int(options.cjfee)
minsize = options.minsize
else:
parser.error('You specified an incorrect order type which ' +\
'can be either reloffer or absoffer')
sys.exit(0)
nickserv_password = options.password
load_program_config()
if not os.path.exists(os.path.join('wallets', wallet_name)):
wallet = SegwitWallet(wallet_name, None, max_mix_depth=MAX_MIX_DEPTH,
gaplimit=options.gaplimit)
else:
while True:
try:
pwd = get_password("Enter wallet decryption passphrase: ")
wallet = SegwitWallet(wallet_name, pwd,
max_mix_depth=MAX_MIX_DEPTH,
gaplimit=options.gaplimit)
except WalletError:
print("Wrong password, try again.")
continue
except Exception as e:
print("Failed to load wallet, error message: " + repr(e))
sys.exit(0)
break
sync_wallet(wallet, fast=options.fastsync)
maker = ygclass(wallet, [options.txfee, cjfee_a, cjfee_r,
options.ordertype, options.minsize])
jlog.info('starting yield generator')
clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")
nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
daemon = True if nodaemon == 1 else False
start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
jm_single().config.getint("DAEMON", "daemon_port"),
clientfactory, daemon=daemon)

304
jmdaemon/jmdaemon/daemon_protocol.py

@ -75,6 +75,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
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()
@ -88,7 +89,10 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
reactor.stop() #pragma: no cover
def defaultErrback(self, failure):
failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone, ConnectionLost)
"""TODO better network error handling.
"""
failure.trap(ConnectionAborted, ConnectionClosed,
ConnectionDone, ConnectionLost)
def defaultCallbacks(self, d):
d.addCallback(self.checkClientResponse)
@ -152,29 +156,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.init_connections(nick)
return {'accepted': True}
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 on_welcome(self):
"""Fired when channel indicated state readiness
"""
d = self.callRemote(JMUp)
self.defaultCallbacks(d)
@JMSetup.responder
def on_JM_SETUP(self, role, initdata):
assert self.jm_state == 0
@ -197,13 +178,21 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.jm_state = 1
return {'accepted': True}
@JMTXSigs.responder
def on_JM_TX_SIGS(self, nick, sigs):
sigs = _byteify(json.loads(sigs))
print('sending sigs: ' + str(sigs))
for sig in sigs:
self.mcc.prepare_privmsg(nick, "sig", sig)
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):
@ -236,13 +225,78 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.jm_state = 2
return {'accepted': True}
@JMMakeTx.responder
def on_JM_MAKE_TX(self, nick_list, txhex):
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, offerlist):
def on_JM_ANNOUNCE_OFFERS(self, to_announce, to_cancel, offerlist):
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):
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):
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)
def on_orderbook_requested(self, nick, mc=None):
"""Dealt with by daemon, assuming offerlist is up to date
"""
@ -278,14 +332,14 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
kp = init_keypair()
try:
crypto_box = as_init_encryption(kp, init_pubkey(taker_pk))
self.active_orders[nick] = {"crypto_box": crypto_box,
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)
self.active_orders[nick] = {"crypto_box": crypto_box,
"kp": kp,
"offer": offer,
"amount": amount,
"commit": scommit}
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)
self.mcc.prepare_privmsg(nick, "pubkey", kp.hex_pk())
def on_seen_auth(self, nick, commitment_revelation):
@ -304,47 +358,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
kphex=ao["kp"].hex_pk())
self.defaultCallbacks(d)
@JMIOAuth.responder
def on_JM_IOAUTH(self, nick, utxolist, pubkey, cjaddr, changeaddr, pubkeysig):
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}
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 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,
@ -422,54 +435,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
#Finish early if we got all
self.respondToIoauths(True)
def respondToIoauths(self, accepted):
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
@JMMakeTx.responder
def on_JM_MAKE_TX(self, nick_list, txhex):
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}
def on_sig(self, nick, sig):
"""Pass signature through to Taker.
"""
@ -478,9 +443,13 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
sig=sig)
self.defaultCallbacks(d)
"""The following functions handle requests and responses
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
@ -512,23 +481,75 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
hostid=hostid)
self.defaultCallbacks(d)
@JMPushTx.responder
def on_JM_PushTx(self, nick, txhex):
self.mcc.push_tx(nick, txhex)
return {'accepted': True}
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)
@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}
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)
@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))
def respondToIoauths(self, accepted):
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:
self.mcc.on_verified_privmsg(nick, fullmsg, hostid)
return {'accepted': True}
#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;
@ -543,9 +564,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
', message will be dropped')
return None
def on_error(self, msg):
log.msg("Received error: " + str(msg))
def mc_shutdown(self):
log.msg("Message channels being shutdown by daemon")
if self.mcc:

137
scripts/yield-generator-basic.py

@ -0,0 +1,137 @@
#! /usr/bin/env python
from __future__ import absolute_import, print_function
import datetime
import os
import time
from jmclient import YieldGenerator, ygmain, get_log, jm_single, calc_cj_fee
txfee = 1000
cjfee_a = 200
cjfee_r = '0.0002'
ordertype = 'swreloffer' #'swreloffer' or 'swabsoffer'
nickserv_password = ''
max_minsize = 100000
gaplimit = 6
jlog = get_log()
# is a maker for the purposes of generating a yield from held
# bitcoins, offering from the maximum mixdepth and trying to offer
# the largest amount within the constraints of mixing depth isolation.
# It will often (but not always) reannounce orders after transactions,
# thus is somewhat suboptimal in giving more information to spies.
class YieldGeneratorBasic(YieldGenerator):
def __init__(self, wallet, offerconfig):
self.txfee, self.cjfee_a, self.cjfee_r, self.ordertype, self.minsize \
= offerconfig
super(YieldGeneratorBasic,self).__init__(wallet)
def create_my_orders(self):
mix_balance = self.wallet.get_balance_by_mixdepth()
if len([b for m, b in mix_balance.iteritems() if b > 0]) == 0:
jlog.error('do not have any coins left')
return []
# print mix_balance
max_mix = max(mix_balance, key=mix_balance.get)
f = '0'
if self.ordertype == 'swreloffer':
f = self.cjfee_r
#minimum size bumped if necessary such that you always profit
#least 50% of the miner fee
self.minsize = max(int(1.5 * self.txfee / float(self.cjfee_r)),
max_minsize)
elif self.ordertype == 'swabsoffer':
f = str(self.txfee + self.cjfee_a)
order = {'oid': 0,
'ordertype': self.ordertype,
'minsize': self.minsize,
'maxsize': mix_balance[max_mix] - max(
jm_single().DUST_THRESHOLD,txfee),
'txfee': self.txfee,
'cjfee': f}
# sanity check
assert order['minsize'] >= 0
assert order['maxsize'] > 0
if order['minsize'] > order['maxsize']:
jlog.info('minsize (' + str(order['minsize']) + ') > maxsize (' + str(
order['maxsize']) + ')')
return []
return [order]
def oid_to_order(self, offer, amount):
total_amount = amount + offer["txfee"]
mix_balance = self.wallet.get_balance_by_mixdepth()
max_mix = max(mix_balance, key=mix_balance.get)
filtered_mix_balance = [m
for m in mix_balance.iteritems()
if m[1] >= total_amount]
if not filtered_mix_balance:
return None, None, None
jlog.debug('mix depths that have enough = ' + str(filtered_mix_balance))
filtered_mix_balance = sorted(filtered_mix_balance, key=lambda x: x[0])
mixdepth = filtered_mix_balance[0][0]
jlog.info('filling offer, mixdepth=' + str(mixdepth))
# mixdepth is the chosen depth we'll be spending from
cj_addr = self.wallet.get_internal_addr((mixdepth + 1) %
self.wallet.max_mix_depth)
change_addr = self.wallet.get_internal_addr(mixdepth)
utxos = self.wallet.select_utxos(mixdepth, total_amount)
my_total_in = sum([va['value'] for va in utxos.values()])
real_cjfee = calc_cj_fee(offer["ordertype"], offer["cjfee"], amount)
change_value = my_total_in - amount - offer["txfee"] + real_cjfee
if change_value <= jm_single().DUST_THRESHOLD:
jlog.debug(('change value={} below dust threshold, '
'finding new utxos').format(change_value))
try:
utxos = self.wallet.select_utxos(
mixdepth, total_amount + jm_single().DUST_THRESHOLD)
except Exception:
jlog.info('dont have the required UTXOs to make a '
'output above the dust threshold, quitting')
return None, None, None
return utxos, cj_addr, change_addr
def on_tx_unconfirmed(self, offer, txid, removed_utxos):
self.tx_unconfirm_timestamp[offer["cjaddr"]] = int(time.time())
# if the balance of the highest-balance mixing depth change then
# reannounce it
oldoffer = self.offerlist[0] if len(self.offerlist) > 0 else None
newoffers = self.create_my_orders()
if len(newoffers) == 0:
return [0], [] # cancel old order
if oldoffer:
if oldoffer['maxsize'] == newoffers[0]['maxsize']:
return [], [] # change nothing
# announce new order, replacing the old order
return [], [newoffers[0]]
def on_tx_confirmed(self, offer, confirmations, txid):
if offer["cjaddr"] in self.tx_unconfirm_timestamp:
confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[
offer["cjaddr"]]
else:
confirm_time = 0
timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
real_cjfee = calc_cj_fee(offer["offer"]["ordertype"],
offer["offer"]["cjfee"], offer["amount"])
self.log_statement([timestamp, offer["amount"], len(
offer["utxos"]), sum([av['value'] for av in offer["utxos"].values(
)]), real_cjfee, real_cjfee - offer["offer"]["txfee"], round(
confirm_time / 60.0, 2), ''])
return self.on_tx_unconfirmed(offer, txid, None)
if __name__ == "__main__":
ygmain(YieldGeneratorBasic, txfee=txfee, cjfee_a=cjfee_a,
cjfee_r=cjfee_r, ordertype=ordertype,
nickserv_password=nickserv_password,
minsize=max_minsize, gaplimit=gaplimit)
print('done')
Loading…
Cancel
Save