You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

181 lines
8.1 KiB

#! /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)
from twisted.internet import task
jlog = get_log()
class Maker(object):
def __init__(self, wallet):
self.active_orders = {}
self.wallet = wallet
self.nextoid = -1
self.offerlist = None
self.sync_wait_loop = task.LoopingCall(self.try_to_create_my_orders)
self.sync_wait_loop.start(2.0)
self.aborted = False
def try_to_create_my_orders(self):
if not jm_single().bc_interface.wallet_synced:
return
self.offerlist = self.create_my_orders()
self.sync_wait_loop.stop()
if not self.offerlist:
jlog.info("Failed to create offers, giving up.")
sys.exit(0)
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'] != self.wallet.pubkey_to_address(cr_dict['P']):
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,)
self.wallet.update_cache_index()
# 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.info('mycjaddr, mychange = {}, {}'.format(cjaddr, changeaddr))
times_seen_cj_addr = 0
times_seen_change_addr = 0
for outs in txd['outs']:
#FIXME: the type of address should be detected from the script (p2pkh/p2sh)
addr = self.wallet.script_to_address(outs['script'])
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.info(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