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. 302
      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): class JMAnnounceOffers(JMCommand):
"""Send list (actually dict) of offers """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): class JMIOAuth(JMCommand):
"""Send contents of !ioauth message after """Send contents of !ioauth message after
@ -123,7 +126,7 @@ class JMTXSigs(JMCommand):
arguments = [('nick', String()), arguments = [('nick', String()),
('sigs', 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 hashlib
import os import os
import sys import sys
import pprint
from jmclient import (Taker, Wallet, jm_single, get_irc_mchannels, 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 import btc
jlog = get_log() jlog = get_log()
@ -134,6 +135,8 @@ class JMClientProtocol(amp.AMP):
class JMMakerClientProtocol(JMClientProtocol): class JMMakerClientProtocol(JMClientProtocol):
def __init__(self, factory, maker, nick_priv=None): def __init__(self, factory, maker, nick_priv=None):
self.factory = factory self.factory = factory
#used for keeping track of transactions for the unconf/conf callbacks
self.finalized_offers = {}
JMClientProtocol.__init__(self, factory, maker, nick_priv) JMClientProtocol.__init__(self, factory, maker, nick_priv)
@commands.JMUp.responder @commands.JMUp.responder
@ -141,21 +144,9 @@ class JMMakerClientProtocol(JMClientProtocol):
d = self.callRemote(commands.JMSetup, d = self.callRemote(commands.JMSetup,
role="MAKER", role="MAKER",
initdata=json.dumps(self.client.offerlist)) 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) self.defaultCallbacks(d)
return {'accepted': True} 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 @commands.JMSetupDone.responder
def on_JM_SETUP_DONE(self): def on_JM_SETUP_DONE(self):
jlog.info("JM daemon setup complete") jlog.info("JM daemon setup complete")
@ -209,18 +200,72 @@ class JMMakerClientProtocol(JMClientProtocol):
@commands.JMTXReceived.responder @commands.JMTXReceived.responder
def on_JM_TX_RECEIVED(self, nick, txhex, offer): 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) retval = self.client.on_tx_received(nick, txhex, offer)
if not retval[0]: if not retval[0]:
jlog.info("Maker refuses to continue on receipt of tx") jlog.info("Maker refuses to continue on receipt of tx")
else: else:
sigs = retval[1] 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, d = self.callRemote(commands.JMTXSigs,
nick=nick, nick=nick,
sigs=json.dumps(sigs)) sigs=json.dumps(sigs))
self.defaultCallbacks(d) self.defaultCallbacks(d)
return {"accepted": True} 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): class JMTakerClientProtocol(JMClientProtocol):
def __init__(self, factory, client, nick_priv=None): 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)

302
jmdaemon/jmdaemon/daemon_protocol.py

@ -75,6 +75,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.restart_mc_required = False self.restart_mc_required = False
self.irc_configs = None self.irc_configs = None
self.mcc = None self.mcc = None
#Default role is TAKER; must be overriden to MAKER in JMSetup message.
self.role = "TAKER" self.role = "TAKER"
self.crypto_boxes = {} self.crypto_boxes = {}
self.sig_lock = threading.Lock() self.sig_lock = threading.Lock()
@ -88,7 +89,10 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
reactor.stop() #pragma: no cover reactor.stop() #pragma: no cover
def defaultErrback(self, failure): 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): def defaultCallbacks(self, d):
d.addCallback(self.checkClientResponse) d.addCallback(self.checkClientResponse)
@ -152,29 +156,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.init_connections(nick) self.init_connections(nick)
return {'accepted': True} 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 @JMSetup.responder
def on_JM_SETUP(self, role, initdata): def on_JM_SETUP(self, role, initdata):
assert self.jm_state == 0 assert self.jm_state == 0
@ -197,13 +178,21 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.jm_state = 1 self.jm_state = 1
return {'accepted': True} return {'accepted': True}
@JMTXSigs.responder @JMMsgSignature.responder
def on_JM_TX_SIGS(self, nick, sigs): def on_JM_MSGSIGNATURE(self, nick, cmd, msg_to_return, hostid):
sigs = _byteify(json.loads(sigs)) self.mcc.privmsg(nick, cmd, msg_to_return, mc=hostid)
print('sending sigs: ' + str(sigs)) return {'accepted': True}
for sig in sigs:
self.mcc.prepare_privmsg(nick, "sig", sig) @JMMsgSignatureVerify.responder
return {"accepted": True} 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 @JMRequestOffers.responder
def on_JM_REQUEST_OFFERS(self): def on_JM_REQUEST_OFFERS(self):
@ -236,13 +225,78 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.jm_state = 2 self.jm_state = 2
return {'accepted': True} 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 @JMAnnounceOffers.responder
def on_JM_ANNOUNCE_OFFERS(self, offerlist): def on_JM_ANNOUNCE_OFFERS(self, to_announce, to_cancel, offerlist):
if self.role != "MAKER": if self.role != "MAKER":
return return
to_announce = json.loads(to_announce)
to_cancel = json.loads(to_cancel)
self.offerlist = json.loads(offerlist) 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} 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): def on_orderbook_requested(self, nick, mc=None):
"""Dealt with by daemon, assuming offerlist is up to date """Dealt with by daemon, assuming offerlist is up to date
""" """
@ -278,14 +332,14 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
kp = init_keypair() kp = init_keypair()
try: try:
crypto_box = as_init_encryption(kp, init_pubkey(taker_pk)) crypto_box = as_init_encryption(kp, init_pubkey(taker_pk))
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, self.active_orders[nick] = {"crypto_box": crypto_box,
"kp": kp, "kp": kp,
"offer": offer, "offer": offer,
"amount": amount, "amount": amount,
"commit": scommit} "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()) self.mcc.prepare_privmsg(nick, "pubkey", kp.hex_pk())
def on_seen_auth(self, nick, commitment_revelation): def on_seen_auth(self, nick, commitment_revelation):
@ -304,47 +358,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
kphex=ao["kp"].hex_pk()) kphex=ao["kp"].hex_pk())
self.defaultCallbacks(d) 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): def on_commitment_seen(self, nick, commitment):
"""Triggered when we see a commitment for blacklisting """Triggered when we see a commitment for blacklisting
appear in the public pit channel. If the policy is set, 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 #Finish early if we got all
self.respondToIoauths(True) 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): def on_sig(self, nick, sig):
"""Pass signature through to Taker. """Pass signature through to Taker.
""" """
@ -478,9 +443,13 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
sig=sig) sig=sig)
self.defaultCallbacks(d) 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. from client for messaging signing and verifying.
""" """
def request_signed_message(self, nick, cmd, msg, msg_to_be_signed, hostid): def request_signed_message(self, nick, cmd, msg, msg_to_be_signed, hostid):
"""The daemon passes the nick and cmd fields """The daemon passes the nick and cmd fields
to the client so it can be echoed back to the privmsg to the client so it can be echoed back to the privmsg
@ -512,23 +481,75 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
hostid=hostid) hostid=hostid)
self.defaultCallbacks(d) self.defaultCallbacks(d)
@JMPushTx.responder def init_connections(self, nick):
def on_JM_PushTx(self, nick, txhex): """Sets up message channel connections
self.mcc.push_tx(nick, txhex) if they are not already up; re-sets joinmarket state to 0
return {'accepted': True} 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 transfer_commitment(self, commit):
def on_JM_MSGSIGNATURE(self, nick, cmd, msg_to_return, hostid): """Send this commitment via privmsg to one (random)
self.mcc.privmsg(nick, cmd, msg_to_return, mc=hostid) other maker.
return {'accepted': True} """
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 respondToIoauths(self, accepted):
def on_JM_MSGSIGNATURE_VERIFY(self, verif_result, nick, fullmsg, hostid): if self.jm_state != 2:
if not verif_result: #this can be called a second time on timeout, in which case we
log.msg("Verification failed for nick: " + str(nick)) #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: else:
self.mcc.on_verified_privmsg(nick, fullmsg, hostid) #Act differently if *we* provided utxos, but
return {'accepted': True} #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): def get_crypto_box_from_nick(self, nick):
"""Retrieve the libsodium box object for the counterparty; """Retrieve the libsodium box object for the counterparty;
@ -543,9 +564,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
', message will be dropped') ', message will be dropped')
return None return None
def on_error(self, msg):
log.msg("Received error: " + str(msg))
def mc_shutdown(self): def mc_shutdown(self):
log.msg("Message channels being shutdown by daemon") log.msg("Message channels being shutdown by daemon")
if self.mcc: 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