forked from JoinMarket-Org/joinmarket
-
Notifications
You must be signed in to change notification settings - Fork 0
/
create-unsigned-tx-proxy.py
252 lines (228 loc) · 9.84 KB
/
create-unsigned-tx-proxy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
""" 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 flask import Flask, abort, jsonify, make_response, request
from optparse import OptionParser
import threading, pprint, sys, os
data_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, os.path.join(data_dir, 'lib'))
from common import *
import common
import taker as takermodule
from irc import IRCMessageChannel, random_nick
import bitcoin as btc
import libnacl.public
import enc_wrapper
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'})
common.load_program_config()
try:
nacl_sk_hex = 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:
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
self.ignored_makers += coinjointx.nonrespondants
debug('recreating the tx, ignored_makers=' + str(self.ignored_makers))
self.create_tx()
def sendpayment_choose_orders(self, cj_amount, makercount,
nonrespondants=[], active_nicks=[]):
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
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 = common.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
common.nickname = random_nick()
irc = IRCMessageChannel(common.nickname)
taker = CreateUnsignedTx(irc, auth_utxo, naclKeySig, cjamount, destaddr,
changeaddr, utxo_data, options, chooseOrdersFunc, kp, my_btc_pub)
try:
debug('starting irc')
irc.run()
debug('done irc')
return taker.tx
except:
debug('CRASHING, DUMPING EVERYTHING')
debug_dump_object(taker)
import traceback
debug(traceback.format_exc())
if __name__ == "__main__":
app.run()