From 8eb9d1cbb1b6680bc459b0dee8a50509541a7d58 Mon Sep 17 00:00:00 2001 From: Leo Wandersleb Date: Thu, 17 Dec 2015 03:23:39 -0300 Subject: [PATCH 1/2] moved the proxy stuff over to the develop branch. work in progress --- create-unsigned-tx-proxy.py | 285 +++++++++++++++++++++++++++++++ joinmarket/taker.py | 42 +++-- test-create-unsigned-tx-proxy.py | 42 +++++ 3 files changed, 358 insertions(+), 11 deletions(-) create mode 100644 create-unsigned-tx-proxy.py create mode 100644 test-create-unsigned-tx-proxy.py diff --git a/create-unsigned-tx-proxy.py b/create-unsigned-tx-proxy.py new file mode 100644 index 00000000..c2522d37 --- /dev/null +++ b/create-unsigned-tx-proxy.py @@ -0,0 +1,285 @@ +#! /usr/bin/env python + +""" create-unsigned-tx-proxy is desigend to do the joinmarket heavy-lifting on + behalf of a client. The client allows this proxy to do so by signing a nacl + key pair with one of the used UTXO's addresses private keys. +""" + +from __future__ import absolute_import + +import sys +import threading +import time +from flask import Flask, abort, jsonify, make_response, request +from optparse import OptionParser +import pprint + +from joinmarket import taker as takermodule +from joinmarket import load_program_config, validate_address, \ + jm_single, get_p2pk_vbyte, random_nick +from joinmarket import get_log, choose_sweep_orders, choose_orders, \ + pick_order, cheapest_order_choose, weighted_order_choose +from joinmarket import AbstractWallet, IRCMessageChannel, debug_dump_object + +import bitcoin as btc +import sendpayment + +import libnacl.public +from joinmarket import enc_wrapper + +log = get_log() + +app = Flask(__name__) + +@app.errorhandler(400) +def bad_request(error): + return make_response(jsonify({'error': 'Bad Request'}), 400) + +@app.errorhandler(404) +def not_found(error): + return make_response(jsonify({'error': 'Not found'}), 404) + +@app.route('/joinmarket/v1/ping') +def ping(): + return jsonify({'ping': 'pong'}) + +load_program_config() +try: + nacl_sk_hex = jm_single().config.get("JM_PROXY", "nacl_sk_hex") + kp = libnacl.public.SecretKey(nacl_sk_hex.decode('hex')) +except: + print('\n\nNo key for the joinmarket proxy found') + print('please add these lines to your config:\n') + kp = enc_wrapper.init_keypair() + sk = kp.sk.encode('hex') + pk = kp.pk.encode('hex') + print('[JM_PROXY]') + print("# generated by enc_wrapper.init_keypair().sk.encode('hex')") + print("# clients have to use\n# {0}\n# as public key.".format(pk)) + print('nacl_sk_hex = {0}\n\n'.format(sk)) + exit() + +@app.route('/joinmarket/v1/getAuthKey', methods = ['GET']) +def get_auth_key(): + """returns a libnacl public key for the client to sign in order to approve + this proxy. + """ + # TODO: with one more node there is one more edge for a MITM to attack. + # We could/should? use pk for encryption with the client, too, at least + # optionally. + # TODO: kp getting created per server start is one option but the client + # doesn't know if it can trust it. Maybe there should be one key pair per + # server instance that signs the per session keys, so the IRC can't + # (trivially) know it's the same proxy but the client can trust the proxy + # even without https. + return jsonify({'pk': kp.pk.encode('hex')}) + +@app.route('/joinmarket/v1/getUnsignedTransaction', methods = ['POST']) +def get_unsigned_transaction(): + print(request.json) + if (not request.json or + not 'authUtxo' in request.json or + not 'authUtxoPK' in request.json or + not 'naclKeySig' in request.json or + not 'utxos' in request.json or + not 'change' in request.json or + not 'recipient' in request.json or + not 'amount' in request.json): + abort(400) + auth_utxo = request.json['authUtxo'] + authPK = str(request.json['authUtxoPK']) + naclKeySig = request.json['naclKeySig'].decode('hex') + if btc.ecdsa_verify(kp.pk.encode('hex'), naclKeySig, authPK.decode('hex')): + print('good sig found') + # TODO: check if the public key matches the authUtxo + else: + print('bad sig. aborting.') + abort(400) + makerCount = request.json['makerCount'] + cold_utxos = request.json['utxos'] + changeaddr = request.json['change'] + destaddr = request.json['recipient'] + cjamount = request.json['amount'] + options = type('Options', (object,), { + 'testnet': request.json['testnet'], + 'txfee': 100000, # total miner fee in satoshis + 'waittime': 5, # wait time in seconds to allow orders to arrive + 'makercount': 1, # how many makers to coinjoin with + 'choosecheapest': True, # override weightened offers picking and choose + # cheapest + 'pickorders': False, # manually pick which orders to take + 'answeryes': True # answer yes to everything + }) + tx = get_unsigned_tx(auth_utxo, naclKeySig, cjamount, destaddr, changeaddr, + cold_utxos, options, kp, authPK) + return jsonify({'result': tx}) + +#thread which does the buy-side algorithm +# chooses which coinjoins to initiate and when +class PaymentThread(threading.Thread): + def __init__(self, taker): + threading.Thread.__init__(self) + self.daemon = True + self.taker = taker + self.ignored_makers = [] + + def create_tx(self): + crow = self.taker.db.execute( + 'SELECT COUNT(DISTINCT counterparty) FROM orderbook;' + ).fetchone() + + counterparty_count = crow['COUNT(DISTINCT counterparty)'] + counterparty_count -= len(self.ignored_makers) + if counterparty_count < self.taker.options.makercount: + print 'not enough counterparties to fill order, ending' + self.taker.msgchan.shutdown() + return + + utxos = self.taker.utxo_data + orders = None + cjamount = 0 + change_addr = None + choose_orders_recover = None + if self.taker.cjamount == 0: + total_value = sum([va['value'] for va in utxos.values()]) + orders, cjamount = choose_sweep_orders( + self.taker.db, total_value, self.taker.options.txfee, + self.taker.options.makercount, self.taker.chooseOrdersFunc, + self.ignored_makers) + else: + orders, total_cj_fee = self.sendpayment_choose_orders( + self.taker.cjamount, self.taker.options.makercount) + if not orders: + log.debug( + 'ERROR not enough liquidity in the orderbook, exiting') + self.taker.msgchan.shutdown() + return + total_amount = self.taker.cjamount + total_cj_fee + \ + self.taker.options.txfee + print 'total amount spent = ' + str(total_amount) + cjamount = self.taker.cjamount + change_addr = self.taker.changeaddr + choose_orders_recover = self.sendpayment_choose_orders + + auth_addr = self.taker.utxo_data[self.taker.auth_utxo]['address'] + kp = self.taker.kp + my_btc_sig = self.taker.naclKeySig + my_btc_pub = self.taker.my_btc_pub + self.taker.start_cj(None, cjamount, orders, utxos, + self.taker.destaddr, change_addr, + self.taker.options.txfee, self.finishcallback, + choose_orders_recover, auth_addr, kp, + my_btc_sig, my_btc_pub) + + def finishcallback(self, coinjointx): + if coinjointx.all_responded: + tx = btc.serialize(coinjointx.latest_tx) + print 'unsigned tx = \n\n' + tx + '\n' + self.taker.msgchan.shutdown() + self.taker.tx = tx + return + self.ignored_makers += coinjointx.nonrespondants + log.debug( + 'recreating the tx, ignored_makers=' + str(self.ignored_makers)) + self.create_tx() + + def sendpayment_choose_orders(self, + cj_amount, + makercount, + nonrespondants=None, + active_nicks=None): + if active_nicks is None: + active_nicks = [] + if nonrespondants is None: + nonrespondants = [] + self.ignored_makers += nonrespondants + orders, total_cj_fee = choose_orders( + self.taker.db, cj_amount, makercount, + self.taker.chooseOrdersFunc, + self.ignored_makers + active_nicks) + if not orders: + return None, 0 + print 'chosen orders to fill: {0}\ntotalcjfee: {1}'.format(str(orders), + str(total_cj_fee)) + total_fee_pc = 1.0 * total_cj_fee / cj_amount + log.debug(' coinjoin fee = ' + str(float('%.3g' % (100.0 * total_fee_pc))) + '%') + if total_fee_pc > 0.02: + # TODO: do something meaningful here. Also fees configurable. + pass + return orders, total_cj_fee + + def run(self): + print 'waiting for all orders to certainly arrive' + debug_dump_object(self.taker) + time.sleep(self.taker.options.waittime) + self.create_tx() + + +class CreateUnsignedTx(takermodule.Taker): + def __init__(self, msgchan, auth_utxo, naclKeySig, cjamount, destaddr, + changeaddr, utxo_data, options, chooseOrdersFunc, kp, my_btc_pub): + super(CreateUnsignedTx, self).__init__(msgchan) + self.auth_utxo = auth_utxo + self.naclKeySig = naclKeySig + self.cjamount = cjamount + self.destaddr = destaddr + self.changeaddr = changeaddr + self.utxo_data = utxo_data + self.options = options + self.chooseOrdersFunc = chooseOrdersFunc + self.kp = kp + self.my_btc_pub = my_btc_pub + self.tx = None + + def on_welcome(self): + takermodule.Taker.on_welcome(self) + PaymentThread(self).start() + +def get_unsigned_tx(auth_utxo, naclKeySig, cjamount, destaddr, changeaddr, + cold_utxos, options, kp, my_btc_pub): + addr_valid1, errormsg1 = validate_address(destaddr) + #if amount = 0 dont bother checking changeaddr so user can write any junk + # TODO: cjamount == 0 is the sweep option. I already partially removed it + # but it actually makes sense to add it again. doh. + if cjamount != 0: + addr_valid2, errormsg2 = validate_address(changeaddr) + else: + addr_valid2 = True + if not addr_valid1 or not addr_valid2: + if not addr_valid1: + print 'ERROR: Address invalid. ' + errormsg1 + else: + print 'ERROR: Address invalid. ' + errormsg2 + return + + all_utxos = [auth_utxo] + cold_utxos + query_result = jm_single().bc_interface.query_utxo_set(all_utxos) + if None in query_result: + print query_result + utxo_data = {} + for utxo, data in zip(all_utxos, query_result): + utxo_data[utxo] = {'address': data['address'], 'value': data['value']} + + chooseOrdersFunc = cheapest_order_choose + + jm_single().nickname = random_nick() + log.debug('starting sendpayment') + + irc = IRCMessageChannel(jm_single().nickname) + taker = CreateUnsignedTx(irc, auth_utxo, naclKeySig, cjamount, destaddr, + changeaddr, utxo_data, options, chooseOrdersFunc, + kp, my_btc_pub) + try: + log.debug('starting irc') + irc.run() + log.debug('done irc') + return taker.tx + except: + log.debug('CRASHING, DUMPING EVERYTHING') + debug_dump_object(taker) + import traceback + log.debug(traceback.format_exc()) + + +if __name__ == "__main__": + app.run() diff --git a/joinmarket/taker.py b/joinmarket/taker.py index cd892ae0..fa088837 100644 --- a/joinmarket/taker.py +++ b/joinmarket/taker.py @@ -33,7 +33,10 @@ def __init__(self, total_txfee, finishcallback, choose_orders_recover, - auth_addr=None): + auth_addr=None, + kp=None, + my_btc_sig=None, + my_btc_pub=None): """ if my_change is None then there wont be a change address thats used if you want to entirely coinjoin one utxo with no change left over @@ -72,6 +75,26 @@ def __init__(self, self.outputs = [] # create DH keypair on the fly for this Tx object self.kp = init_keypair() + if kp: + print('kp is {0}'.format(kp)) + self.kp = kp + if my_btc_sig and my_btc_pub: + self.my_btc_sig = my_btc_sig + self.my_btc_pub = my_btc_pub + else: + print('ERROR: if key pair is provided, a btc sig and pub has to be provided, too.') + return + else: + #create DH keypair on the fly for this Tx object + self.kp = enc_wrapper.init_keypair() + if my_btc_sig == None: + if self.auth_addr: + self.my_btc_addr = self.auth_addr + else: + self.my_btc_addr = self.input_utxos.itervalues().next()['address'] + my_btc_priv = self.wallet.get_key_from_addr(self.my_btc_addr) + self.my_btc_pub = btc.privtopub(my_btc_priv) + self.my_btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), my_btc_priv) self.crypto_boxes = {} self.msgchan.fill_orders(self.active_orders, self.cj_amount, self.kp.hex_pk()) @@ -83,14 +106,7 @@ def start_encryption(self, nick, maker_pk): self.crypto_boxes[nick] = [maker_pk, as_init_encryption( self.kp, init_pubkey(maker_pk))] # send authorisation request - if self.auth_addr: - my_btc_addr = self.auth_addr - else: - my_btc_addr = self.input_utxos.itervalues().next()['address'] - my_btc_priv = self.wallet.get_key_from_addr(my_btc_addr) - my_btc_pub = btc.privtopub(my_btc_priv) - my_btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), my_btc_priv) - self.msgchan.send_auth(nick, my_btc_pub, my_btc_sig) + self.msgchan.send_auth(nick, self.my_btc_pub, self.my_btc_sig) def auth_counterparty(self, nick, btc_sig, cj_pub): """Validate the counterpartys claim to own the btc @@ -507,12 +523,16 @@ def start_cj(self, total_txfee, finishcallback=None, choose_orders_recover=None, - auth_addr=None): + auth_addr=None, + kp=None, + my_btc_sig=None, + my_btc_pub=None): self.cjtx = CoinJoinTX( self.msgchan, wallet, self.db, cj_amount, orders, input_utxos, my_cj_addr, my_change_addr, total_txfee, finishcallback, - choose_orders_recover, auth_addr) + choose_orders_recover, auth_addr, + kp, my_btc_sig, my_btc_pub) def on_error(self): pass # TODO implement diff --git a/test-create-unsigned-tx-proxy.py b/test-create-unsigned-tx-proxy.py new file mode 100644 index 00000000..4eb1695d --- /dev/null +++ b/test-create-unsigned-tx-proxy.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import +import requests + +import bitcoin as btc +import libnacl.public +from joinmarket import get_log +from joinmarket import load_program_config +from joinmarket.configure import jm_single + +log = get_log() + +address1 = 'n1aUo5P6mij5fJSGaefmdCTgjM7xxWhbVs' +sk1 = btc.b58check_to_bin('L5cFx7NvWSWidwNtKnBQw5VrguTax2dGYajuz12S81UhX2ewRehH') +pk1 = btc.privkey_to_pubkey(sk1) + +authUtxo = 'cb2caca7500d2d943b4cd03db20ee67fcabc6afa9548e930e81632d164f6bfa1:0' + +load_program_config() +nacl_sk_hex = jm_single().config.get("JM_PROXY", "nacl_sk_hex") +auth_pkey = libnacl.public.SecretKey(nacl_sk_hex.decode('hex')).pk.encode('hex') + +utxos = [authUtxo] + +naclKeySig = btc.ecdsa_sign(auth_pkey, sk1) + +data = {'testnet': True, + 'authUtxo': authUtxo, + 'authUtxoPK': pk1.encode('hex'), + 'naclKeySig': naclKeySig.encode('hex'), + 'makerCount': 1, + 'utxos': utxos, + 'change': 'mtrxCNUMYXRy2hQh2bBPeLjtRHWFKZcRkU', + 'recipient': 'mzGNyEEJNYtRi7fGnMAKTV5EZDFN8J9jTX', + 'amount': 600000} +log.debug(data) + +authPK = pk1 +sigGood = btc.ecdsa_verify(auth_pkey, naclKeySig, authPK) +log.debug('sig is good: {0}'.format(sigGood)) + +r = requests.post('http://127.0.0.1:5000/joinmarket/v1/getUnsignedTransaction', json=data) +log.debug(r.json()) From 5f7ccb198c23ee10422d0a53bc8a91481f6f558f Mon Sep 17 00:00:00 2001 From: Leo Wandersleb Date: Mon, 28 Dec 2015 19:25:10 -0300 Subject: [PATCH 2/2] fixed a missing import error --- joinmarket/taker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joinmarket/taker.py b/joinmarket/taker.py index 5f0cf93e..37b331aa 100644 --- a/joinmarket/taker.py +++ b/joinmarket/taker.py @@ -87,7 +87,7 @@ def __init__(self, return else: #create DH keypair on the fly for this Tx object - self.kp = enc_wrapper.init_keypair() + self.kp = init_keypair() if my_btc_sig == None: if self.auth_addr: self.my_btc_addr = self.auth_addr