diff --git a/lib/blockchaininterface.py b/lib/blockchaininterface.py index 6f448c0c..335dd766 100644 --- a/lib/blockchaininterface.py +++ b/lib/blockchaininterface.py @@ -467,6 +467,7 @@ def sync_addresses(self, wallet): wallet_addr_list.append(imported_addr) imported_addr_list = self.rpc('getaddressesbyaccount', [wallet_name]) if not set(wallet_addr_list).issubset(set(imported_addr_list)): + common.debug("adding wallet address list to woadrs: "+str(wallet_addr_list)) self.add_watchonly_addresses(wallet_addr_list, wallet_name) return @@ -477,42 +478,58 @@ def sync_addresses(self, wallet): buf = self.rpc('listtransactions', [wallet_name, 1000, len(txs), True]) txs += buf + common.debug("Got these transactions: \n"+pprint.pformat(txs)) #TODO check whether used_addr_list can be a set, may be faster (if its a hashset) and allows # using issubset() here and setdiff() for finding which addresses need importing #TODO also check the fastest way to build up python lists, i suspect using += is slow used_addr_list = [tx['address'] for tx in txs if tx['category'] == 'receive'] + common.debug("Got this used address list: "+pprint.pformat(used_addr_list)) too_few_addr_mix_change = [] for mix_depth in range(wallet.max_mix_depth): for forchange in [0, 1]: + common.debug("Working on depth: "+str(mix_depth)+" and forchange: "+str(forchange)) unused_addr_count = 0 last_used_addr = '' breakloop = False while not breakloop: if unused_addr_count >= wallet.gaplimit and\ is_index_ahead_of_cache(wallet, mix_depth, forchange): + common.debug("Breaking out") + common.debug("wallet index is: "+pprint.pformat(wallet.index)) + common.debug("unused address count is: "+str(unused_addr_count)) break mix_change_addrs = [wallet.get_new_addr(mix_depth, forchange) for i in range(addr_req_count)] + common.debug("Generated mxchad: "+pprint.pformat(mix_change_addrs)) for mc_addr in mix_change_addrs: if mc_addr not in imported_addr_list: too_few_addr_mix_change.append((mix_depth, forchange)) + common.debug("Breaking out") + common.debug("tfamc: "+pprint.pformat(too_few_addr_mix_change)) breakloop = True break if mc_addr in used_addr_list: last_used_addr = mc_addr + common.debug("Reset last used addr to: "+last_used_addr) unused_addr_count = 0 else: + common.debug("Incrementing unused addr count") unused_addr_count += 1 if last_used_addr == '': + common.debug("last used addr was null, setting index to zero for:") + common.debug("mix depth: "+str(mix_depth)+" and forchange: "+str(forchange)) wallet.index[mix_depth][forchange] = 0 else: + common.debug("last used addr was not null, setting index to: ") + common.debug(str(wallet.addr_cache[last_used_addr][2] + 1)) + common.debug("mix depth: "+str(mix_depth)+" and forchange: "+str(forchange)) wallet.index[mix_depth][forchange] = wallet.addr_cache[last_used_addr][2] + 1 wallet_addr_list = [] if len(too_few_addr_mix_change) > 0: common.debug('too few addresses in ' + str(too_few_addr_mix_change)) for mix_depth, forchange in too_few_addr_mix_change: - wallet_addr_list += [wallet.get_new_addr(mix_depth, forchange) for i in range(addr_req_count*3)] + wallet_addr_list += [wallet.get_new_addr(mix_depth, forchange) for i in range(addr_req_count*10)] self.add_watchonly_addresses(wallet_addr_list, wallet_name) return diff --git a/lib/common.py b/lib/common.py index 1a02904d..b04e12d6 100644 --- a/lib/common.py +++ b/lib/common.py @@ -25,7 +25,8 @@ config_location = 'joinmarket.cfg' # FIXME: Add rpc_* options here in the future! required_options = {'BLOCKCHAIN':['blockchain_source', 'network'], - 'MESSAGING':['host','channel','port']} + 'MESSAGING':['host','channel','port'], + 'LIMITS':['taker_utxo_retries']} defaultconfig =\ """ @@ -188,6 +189,33 @@ def debug_dump_object(obj, skip_fields=[]): else: debug(str(v)) +def check_utxo_blacklist(utxo): + #write/read the file here; could be inferior + #performance-wise; should cache. TODO + #there also needs to be formatting error checking here, + #but not a high priority TODO + blacklist = {} + if os.path.isfile("logs/blacklist"): + with open("logs/blacklist","rb") as f: + blacklist_lines = f.readlines() + else: + blacklist_lines = [] + for bl in blacklist_lines: + ut, ct = bl.split(',') + ut = ut.strip() + ct = int(ct.strip()) + blacklist[ut]=ct + if utxo in blacklist.keys(): + blacklist[utxo] += 1 + if blacklist[utxo] >= config.getint("LIMITS","taker_utxo_retries"): + return False + else: + blacklist[utxo]=1 + with open("logs/blacklist","wb") as f: + for k,v in blacklist.iteritems(): + f.write(k+' , '+str(v)+'\n') + return True + def select_gradual(unspent, value): ''' UTXO selection algorithm for gradual dust reduction diff --git a/lib/irc.py b/lib/irc.py index 6ff81682..bd7590c3 100644 --- a/lib/irc.py +++ b/lib/irc.py @@ -100,8 +100,8 @@ def fill_orders(self, nickoid_dict, cj_amount, taker_pubkey): msg = str(oid) + ' ' + str(cj_amount) + ' ' + taker_pubkey self.__privmsg(c, 'fill', msg) - def send_auth(self, nick, pubkey, sig): - message = pubkey + ' ' + sig + def send_auth(self, nick, utxo, pubkey, sig): + message = utxo + ' ' + pubkey + ' ' + sig self.__privmsg(nick, 'auth', message) def send_tx(self, nick_list, txhex): @@ -175,6 +175,7 @@ def __privmsg(self, nick, cmd, message): trailer = ' ~' if m==message_chunks[-1] else ' ;' if m==message_chunks[0]: m = COMMAND_PREFIX + cmd + ' ' + m + debug("sending this message: "+header+m+trailer) self.send_raw(header + m + trailer) def send_raw(self, line): @@ -246,12 +247,13 @@ def __on_privmsg(self, nick, message): self.on_order_fill(nick, oid, amount, taker_pk) elif chunks[0] == 'auth': try: - i_utxo_pubkey = chunks[1] - btc_sig = chunks[2] + i_utxo = chunks[1] + i_utxo_pubkey = chunks[2] + btc_sig = chunks[3] except (ValueError, IndexError) as e: self.send_error(nick, str(e)) if self.on_seen_auth: - self.on_seen_auth(nick, i_utxo_pubkey, btc_sig) + self.on_seen_auth(nick, i_utxo, i_utxo_pubkey, btc_sig) elif chunks[0] == 'tx': b64tx = chunks[1] try: diff --git a/lib/maker.py b/lib/maker.py index 1512eca9..1537b73b 100644 --- a/lib/maker.py +++ b/lib/maker.py @@ -57,12 +57,27 @@ def __init__(self, maker, nick, oid, amount, taker_pk): # orders to find out which addresses you use self.maker.msgchan.send_pubkey(nick, self.kp.hex_pk()) - def auth_counterparty(self, nick, i_utxo_pubkey, btc_sig): + def auth_counterparty(self, nick, i_utxo, i_utxo_pubkey, btc_sig): self.i_utxo_pubkey = i_utxo_pubkey - + if not common.check_utxo_blacklist(i_utxo): + common.debug("Taker input utxo is in blacklist, having been used "\ + + common.config.get("LIMITS","taker_utxo_retries")+ " times, rejecting.") + return False if not btc.ecdsa_verify(self.taker_pk, btc_sig, binascii.unhexlify(self.i_utxo_pubkey)): - print 'signature didnt match pubkey and message' + common.debug("signature from nick: " + nick + "didnt match pubkey and message") + return False + #finally, check that the proffered utxo is real, and corresponds + #to the pubkey + res = common.bc_interface.query_utxo_set([i_utxo]) + if len(res) != 1: + common.debug("Input utxo: "+str(i_utxo)+" from nick: "+nick+" is not valid.") return False + real_utxo = res[0] + if real_utxo['address'] != btc.pubkey_to_address(i_utxo_pubkey, common.get_p2pk_vbyte()): + return False + #TODO: could add check for coin age and value here + #(need to edit query_utxo_set if we want coin age) + #authorisation of taker passed #(but input utxo pubkey is checked in verify_unsigned_tx). #Send auth request to taker @@ -199,12 +214,12 @@ def on_order_fill(self, nick, oid, amount, taker_pubkey): finally: self.wallet_unspent_lock.release() - def on_seen_auth(self, nick, pubkey, sig): + def on_seen_auth(self, nick, utxo, pubkey, sig): if nick not in self.active_orders or self.active_orders[nick] == None: self.msgchan.send_error(nick, 'No open order from this nick') - self.active_orders[nick].auth_counterparty(nick, pubkey, sig) - #TODO if auth_counterparty returns false, remove this order from active_orders - # and send an error + if not self.active_orders[nick].auth_counterparty(nick, utxo, pubkey, sig): + self.active_orders[nick]=None + self.msgchan.send_error(nick, "Authorisation failed.") def on_seen_tx(self, nick, txhex): if nick not in self.active_orders or self.active_orders[nick] == None: diff --git a/lib/taker.py b/lib/taker.py index 68cb48e0..598ff1e9 100644 --- a/lib/taker.py +++ b/lib/taker.py @@ -60,10 +60,12 @@ def start_encryption(self, nick, maker_pk): my_btc_addr = self.auth_addr else: my_btc_addr = self.input_utxos.itervalues().next()['address'] + my_signing_utxo = self.input_utxos.iterkeys().next() + common.debug("Using this signing utxo: "+my_signing_utxo) my_btc_priv = self.wallet.get_key_from_addr(my_btc_addr) my_btc_pub = btc.privtopub(my_btc_priv, True) my_btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), binascii.unhexlify(my_btc_priv)) - self.msgchan.send_auth(nick, my_btc_pub, my_btc_sig) + self.msgchan.send_auth(nick, str(my_signing_utxo), my_btc_pub, my_btc_sig) def auth_counterparty(self, nick, btc_sig, cj_pub): '''Validate the counterpartys claim to own the btc diff --git a/test/blacklist-test.py b/test/blacklist-test.py new file mode 100644 index 00000000..47951af6 --- /dev/null +++ b/test/blacklist-test.py @@ -0,0 +1,146 @@ +import sys +import os, time +data_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +sys.path.insert(0, os.path.join(data_dir, 'lib')) +import subprocess +import unittest +import common +import commontest +from blockchaininterface import * +import bitcoin as btc +import binascii + +''' Just some random thoughts to motivate possible tests; +almost none of this has really been done: + +Expectations +1. Any bot should run indefinitely irrespective of the input +messages it receives, except bots which perform a finite action + +2. A bot must never spend an unacceptably high transaction fee. + +3. A bot must explicitly reject interactions with another bot not +respecting the JoinMarket protocol for its version. + +4. Bots must never send bitcoin data in the clear over the wire. +''' + +'''helper functions put here to avoid polluting the main codebase.''' + +import platform +OS = platform.system() +PINL = '\r\n' if OS == 'Windows' else '\n' + +def local_command(command, bg=False, redirect=''): + if redirect=='NULL': + if OS=='Windows': + command.append(' > NUL 2>&1') + elif OS=='Linux': + command.extend(['>', '/dev/null', '2>&1']) + else: + print "OS not recognised, quitting." + elif redirect: + command.extend(['>', redirect]) + + if bg: + FNULL = open(os.devnull,'w') + return subprocess.Popen(command, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True) + else: + #in case of foreground execution, we can use the output; if not + #it doesn't matter + return subprocess.check_output(command) + + + +class BlackListPassTests(unittest.TestCase): + '''This test case intends to simulate + a single join with a single counterparty. In that sense, + it's not realistic, because nobody (should) do joins with only 1 maker, + but this test has the virtue of being the simplest possible thing + that JoinMarket can do. ''' + def setUp(self): + #create 2 new random wallets. + #put 10 coins into the first receive address + #to allow that bot to start. + self.wallets = commontest.make_wallets(2, + wallet_structures=[[1,0,0,0,0],[1,0,0,0,0]], mean_amt=10) + + + def blacklist_run(self, n, m, fake): + os.remove('logs/blacklist') + #start yield generator with wallet1 + yigen_proc = local_command(['python','yield-generator.py', + str(self.wallets[0]['seed'])],bg=True) + + #A significant delay is needed to wait for the yield generator to sync its wallet + time.sleep(10) + + #run a single sendpayment call with wallet2 + amt = n*100000000 #in satoshis + dest_address = btc.privkey_to_address(os.urandom(32), from_hex=False, magicbyte=common.get_p2pk_vbyte()) + try: + for i in range(m): + sp_proc = local_command(['python','sendpayment.py','--yes','-N','1', self.wallets[1]['seed'],\ + str(amt), dest_address]) + except subprocess.CalledProcessError, e: + if yigen_proc: + yigen_proc.terminate() + print e.returncode + print e.message + raise + + if yigen_proc: + yigen_proc.terminate() + if not fake: + received = common.bc_interface.get_received_by_addr([dest_address], None)['data'][0]['balance'] + if received != amt*m: + common.debug('received was: '+str(received)+ ' but amount was: '+str(amt)) + return False + #check sanity in blacklist + with open('logs/blacklist','rb') as f: + blacklist_lines = f.readlines() + if not fake: + required_bl_lines = m + bl_count = 1 + else: + required_bl_lines = 1 + bl_count = m + if len(blacklist_lines) != required_bl_lines: + common.debug('wrong number of blacklist lines: '+str(len(blacklist_lines))) + return False + + for bl in blacklist_lines: + if len(bl.split(',')[0].strip()) != 66: + common.debug('malformed utxo: '+str(len(bl.split(',')[0].strip()))) + return False + if int(bl.split(',')[1]) != bl_count: + common.debug('wrong blacklist count:'+str(bl.split(',')[1])) + return False + return True + + def test_simple_send(self): + self.failUnless(self.blacklist_run(2, 2, False)) + + + + +def main(): + os.chdir(data_dir) + common.load_program_config() + unittest.main() + +if __name__ == '__main__': + #Big kludge, but there is currently no way to inject this code: + print """this test is to be run in two modes, first + with no changes, then second adding a 'return' in + taker.CoinJoinTX.push() return (so it does nothing), + and further changing the third parameter to blacklist_run to 'True' + and the second parameter to '3' from '2' + In both cases the test should pass for success. + Also, WARNING! This test will delete your blacklist, better + not run it in a "real" repo or back it up. + """ + raw_input("OK?") + main() + + diff --git a/yield-generator.py b/yield-generator.py index f57f71e2..b6bb9905 100644 --- a/yield-generator.py +++ b/yield-generator.py @@ -11,15 +11,20 @@ import common, blockchaininterface from socket import gethostname +mix_levels = 5 + +#CONFIGURATION -txfee = 1000 -cjfee = '0.002' # 0.2% fee +#miner fee contribution +txfee = 1000 +# fees for available mix levels from max to min amounts. +cjfee = ['0.00009', '0.00009', '0.00009', '0.00009', '0.00009'] +#cjfee = ["%0.5f" % (0.00015 - n*0.00001) for n in range(mix_levels)] nickname = random_nick() nickserv_password = '' -minsize = int(1.2 * txfee / float(cjfee)) #minimum size is such that you always net profit at least 20% of the miner fee -mix_levels = 5 - +#END CONFIGURATION +print cjfee #is a maker for the purposes of generating a yield from held # bitcoins without ruining privacy for the taker, the taker could easily check @@ -27,9 +32,13 @@ # to ruin the privacy for barely any more yield #sell-side algorithm: #add up the value of each utxo for each mixing depth, -# announce a relative-fee order of the highest balance -#spent from utxos that try to make the highest balance even higher -# so try to keep coins concentrated in one mixing depth +# announce a relative-fee order of the balance in each mixing depth +# amounts made to be non-overlapping +# minsize set by the miner fee contribution, so you never earn less in cjfee than miner fee +# cjfee drops as you go down to the lower-balance mixing depths, provides +# incentive for people to clump coins together for you in one mix depth +#announce an absolute fee order between the dust limit and minimum amount +# so that there is liquidity in the very low amounts too class YieldGenerator(Maker): statement_file = os.path.join('logs', 'yigen-statement.csv') @@ -60,27 +69,58 @@ def on_welcome(self): 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: + debug('mix_balance = ' + str(mix_balance)) + nondust_mix_balance = dict([(m, b) for m, b in mix_balance.iteritems() if b > common.DUST_THRESHOLD]) + if len(nondust_mix_balance) == 0: debug('do not have any coins left') return [] - - #print mix_balance - max_mix = max(mix_balance, key=mix_balance.get) - order = {'oid': 0, 'ordertype': 'relorder', 'minsize': minsize, - 'maxsize': mix_balance[max_mix] - common.DUST_THRESHOLD, 'txfee': txfee, 'cjfee': cjfee} - return [order] + #sorts the mixdepth_balance map by balance size + sorted_mix_balance = sorted(list(mix_balance.iteritems()), key=lambda a: a[1], reverse=True) + minsize = int(1.1 * txfee / float(min(cjfee))) #minimum size is such that you always net profit at least 50% of the miner fee + filtered_mix_balance = [f for f in sorted_mix_balance if f[1] > minsize] + debug('minsize=' + str(minsize) + ' calc\'d with cjfee=' + str(min(cjfee))) + lower_bound_balances = filtered_mix_balance[1:] + [(-1, minsize)] + mix_balance_min = [(mxb[0], mxb[1], minb[1]) for mxb, minb in zip(filtered_mix_balance, lower_bound_balances)] + mix_balance_min = mix_balance_min[::-1] #reverse list order + thecjfee = cjfee[::-1] + + debug('mixdepth_balance_min = ' + str(mix_balance_min)) + orders=[] + oid = 0 + for mix_bal_min in mix_balance_min: + mixdepth, balance, mins = mix_bal_min + #the maker class reads specific keys from the dict, but others + # are allowed in there and will be ignored + order = {'oid': oid+1, 'ordertype': 'relorder', 'minsize': max(mins - common.DUST_THRESHOLD, common.DUST_THRESHOLD) + 1, + 'maxsize': max(balance - common.DUST_THRESHOLD, common.DUST_THRESHOLD), 'txfee': txfee, 'cjfee': thecjfee[oid], + 'mixdepth': mixdepth} + oid += 1 + orders.append(order) + + absorder_size = min(minsize, sorted_mix_balance[0][1]) + if absorder_size != 0: + lowest_cjfee = thecjfee[min(oid, len(thecjfee)-1)] + absorder_fee = calc_cj_fee('relorder', lowest_cjfee, minsize) + debug('absorder fee = ' + str(absorder_fee) + ' uses cjfee=' + str(lowest_cjfee)) + #the absorder is always oid=0 + order = {'oid': 0, 'ordertype': 'absorder', 'minsize': common.DUST_THRESHOLD + 1, + 'maxsize': absorder_size - common.DUST_THRESHOLD, 'txfee': txfee, 'cjfee': absorder_fee} + orders = [order] + orders + debug('generated orders = \n' + '\n'.join([str(o) for o in orders])) + return orders def oid_to_order(self, cjorder, oid, amount): - mix_balance = self.wallet.get_balance_by_mixdepth() - max_mix = max(mix_balance, key=mix_balance.get) - - #algo attempts to make the largest-balance mixing depth get an even larger balance - debug('finding suitable mixdepth') - mixdepth = (max_mix - 1) % self.wallet.max_mix_depth - while True: - if mixdepth in mix_balance and mix_balance[mixdepth] >= amount: - break - mixdepth = (mixdepth - 1) % self.wallet.max_mix_depth + order = [o for o in self.orderlist if o['oid'] == oid][0] + if order['ordertype'] == 'relorder': + mixdepth = order['mixdepth'] + else: + #for the absolute fee order, take from the lowest balance higher than dust + mix_balance = self.wallet.get_balance_by_mixdepth() + filtered_mix_balance = dict([(m, b) for m, b in mix_balance.iteritems() if b > common.DUST_THRESHOLD]) + filtered_mix_balance = sorted(list(mix_balance.iteritems()), key=lambda a: a[1], reverse=True) + mixdepth = filtered_mix_balance[0][0] + debug('filling order, mixdepth=' + str(mixdepth)) + #mixdepth is the chosen depth we'll be spending from cj_addr = self.wallet.get_receive_addr((mixdepth + 1) % self.wallet.max_mix_depth) change_addr = self.wallet.get_change_addr(mixdepth) @@ -101,22 +141,76 @@ def oid_to_order(self, cjorder, oid, amount): def on_tx_unconfirmed(self, cjorder, txid, removed_utxos): self.tx_unconfirm_timestamp[cjorder.cj_addr] = int(time.time()) - #if the balance of the highest-balance mixing depth change then reannounce it - oldorder = self.orderlist[0] if len(self.orderlist) > 0 else None - neworders = self.create_my_orders() - if len(neworders) == 0: - return ([0], []) #cancel old order - if oldorder: #oldorder may not exist when this is called from on_tx_confirmed - if oldorder['maxsize'] == neworders[0]['maxsize']: - return ([], []) #change nothing - #announce new order, replacing the old order - return ([], [neworders[0]]) + + ''' + case 0 + the absorder will basically never get changed, unless there are no utxos left, when neworders==[] + case 1 + a single coin is split into two coins across levels + must announce a new order, plus modify the old order + case 2 + two existing mixdepths get modified + announce the modified new orders + case 3 + one existing mixdepth gets emptied into another + cancel it, modify the place it went + + algorithm + find all the orders which have changed, the length of that list tells us which case + ''' + + myorders = self.create_my_orders() + oldorders = self.orderlist + if len(myorders) == 0: + return ([o['oid'] for o in oldorders], []) + + cancel_orders = [] + ann_orders = [] + + neworders = [o for o in myorders if o['ordertype'] == 'relorder'] + oldorders = [o for o in oldorders if o['ordertype'] == 'relorder'] + #new_setdiff_old = The relative complement of `new` in `old` = members in `new` which are not in `old` + new_setdiff_old = [o for o in neworders if o not in oldorders] + old_setdiff_new = [o for o in oldorders if o not in neworders] + + debug('neworders = \n' + '\n'.join([str(o) for o in neworders])) + debug('oldorders = \n' + '\n'.join([str(o) for o in oldorders])) + debug('new_setdiff_old = \n' + '\n'.join([str(o) for o in new_setdiff_old])) + debug('old_setdiff_new = \n' + '\n'.join([str(o) for o in old_setdiff_new])) + if len(neworders) == len(oldorders): + ann_orders = new_setdiff_old + elif len(neworders) > len(oldorders): + ann_orders = new_setdiff_old + elif len(neworders) < len(oldorders): + ann_orders = new_setdiff_old + ann_oids = [o['oid'] for o in ann_orders] + cancel_orders = [o['oid'] for o in old_setdiff_new if o['oid'] not in ann_oids] + + #check if the absorder has changed, or if it needs to be newly announced + new_abs = [o for o in myorders if o['ordertype'] == 'absorder'] + old_abs = [o for o in oldorders if o['ordertype'] == 'absorder'] + if len(new_abs) > len(old_abs): + #announce an absorder where there wasnt one before + ann_orders = [new_abs[0]] + ann_orders + elif len(new_abs) == len(old_abs): + #maxsize is the only thing that changes, except cjfee but that changes at the same time + if new_abs[0]['maxsize'] != old_abs[0]['maxsize']: + ann_orders = [new_abs[0]] + ann_orders + + debug('can_orders = \n' + '\n'.join([str(o) for o in cancel_orders])) + debug('ann_orders = \n' + '\n'.join([str(o) for o in ann_orders])) + return (cancel_orders, ann_orders) def on_tx_confirmed(self, cjorder, confirmations, txid): +<<<<<<< Updated upstream if cjorder.cj_addr in self.tx_unconfirm_timestamp: confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr] else: confirm_time = 0 +======= + confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr] + del self.tx_unconfirm_timestamp[cjorder.cj_addr] +>>>>>>> Stashed changes timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") self.log_statement([timestamp, cjorder.cj_amount, len(cjorder.utxos), sum([av['value'] for av in cjorder.utxos.values()]), cjorder.real_cjfee,