Browse Source

Merge #910: prevent taker stall on some error conditions

aab14f6 fix taker fee estimation on sweep after maker responses (undeath)
9b01d9a minor refactor of Taker.receive_utxos() (undeath)
master
Adam Gibson 5 years ago
parent
commit
5ddceb2199
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 196
      jmclient/jmclient/taker.py

196
jmclient/jmclient/taker.py

@ -3,6 +3,7 @@
import base64
import pprint
import random
from typing import Any, NamedTuple
from twisted.internet import reactor, task
import jmbitcoin as btc
@ -24,7 +25,22 @@ jlog = get_log()
class JMTakerError(Exception):
pass
class IoauthInputVerificationError(Exception):
def __init__(self, messages):
self.messages = messages
super().__init__(messages)
class Taker(object):
class _MakerTxData(NamedTuple):
nick: Any
utxo_data: Any
total_input: Any
change_amount: Any
real_cjfee: Any
utxo_list: Any = None
cj_addr: Any = None
change_addr: Any = None
def __init__(self,
wallet_service,
@ -109,6 +125,7 @@ class Taker(object):
self.txid = None
self.schedule_index = -1
self.utxos = {}
self.maker_utxo_data = {}
self.tdestaddrs = [] if not tdestaddrs else tdestaddrs
self.filter_orders_callback = callbacks[0]
self.taker_info_callback = callbacks[1]
@ -368,92 +385,33 @@ class Taker(object):
if self.aborted:
return (False, "User aborted")
#Temporary list used to aggregate all ioauth data that must be removed
rejected_counterparties = []
#Need to authorize against the btc pubkey first.
for nick, nickdata in ioauth_data.items():
utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata
if not self.auth_counterparty(btc_sig, auth_pub, maker_pk):
jlog.debug(
"Counterparty encryption verification failed, aborting: " + nick)
#This counterparty must be rejected
rejected_counterparties.append(nick)
if not validate_address(cj_addr)[0] or not validate_address(change_addr)[0]:
jlog.warn("Counterparty provided invalid address: {}".format(
(cj_addr, change_addr)))
# Interpreted as malicious
self.add_ignored_makers([nick])
rejected_counterparties.append(nick)
for rc in rejected_counterparties:
del ioauth_data[rc]
self.maker_utxo_data = {}
for nick, nickdata in ioauth_data.items():
utxo_list, auth_pub, cj_addr, change_addr, _, _ = nickdata
utxo_data = jm_single().bc_interface.query_utxo_set(utxo_list)
self.utxos[nick] = utxo_list
if None in utxo_data:
jlog.warn(('ERROR outputs unconfirmed or already spent. '
'utxo_data={}').format(pprint.pformat(utxo_data)))
jlog.warn('Disregarding this counterparty.')
del self.utxos[nick]
continue
#Complete maker authorization:
#Extract the address fields from the utxos
#Construct the Bitcoin address for the auth_pub field
#Ensure that at least one address from utxos corresponds.
for inp in utxo_data:
try:
if self.wallet_service.pubkey_has_script(
auth_pub, inp['script']):
break
except EngineError as e:
pass
else:
jlog.warn("ERROR maker's (" + nick + ")"
" authorising pubkey is not included "
"in the transaction!")
#this will not be added to the transaction, so we will have
#to recheck if we have enough
continue
total_input = sum([d['value'] for d in utxo_data])
real_cjfee = calc_cj_fee(self.orderbook[nick]['ordertype'],
self.orderbook[nick]['cjfee'],
self.cjamount)
change_amount = (total_input - self.cjamount -
self.orderbook[nick]['txfee'] + real_cjfee)
# certain malicious and/or incompetent liquidity providers send
# inputs totalling less than the coinjoin amount! this leads to
# a change output of zero satoshis; this counterparty must be removed.
if change_amount < jm_single().DUST_THRESHOLD:
fmt = ('ERROR counterparty requires sub-dust change. nick={}'
' totalin={:d} cjamount={:d} change={:d}').format
jlog.warn(fmt(nick, total_input, self.cjamount, change_amount))
jlog.warn("Invalid change, too small, nick= " + nick)
continue
verified_data = self._verify_ioauth_data(ioauth_data)
for maker_inputs in verified_data:
# We have succesfully processed the data from this nick
self.utxos[maker_inputs.nick] = maker_inputs.utxo_list
self.outputs.append({'address': maker_inputs.change_addr,
'value': maker_inputs.change_amount})
self.outputs.append({'address': maker_inputs.cj_addr,
'value': self.cjamount})
self.cjfee_total += maker_inputs.real_cjfee
self.maker_txfee_contributions +=\
self.orderbook[maker_inputs.nick]['txfee']
self.maker_utxo_data[maker_inputs.nick] = maker_inputs.utxo_data
jlog.info(
f"fee breakdown for {maker_inputs.nick} "
f"totalin={maker_inputs.total_input:d} "
f"cjamount={self.cjamount:d} "
f"txfee={self.orderbook[maker_inputs.nick]['txfee']:d} "
f"realcjfee={maker_inputs.real_cjfee:d}")
self.outputs.append({'address': change_addr,
'value': change_amount})
fmt = ('fee breakdown for {} totalin={:d} '
'cjamount={:d} txfee={:d} realcjfee={:d}').format
jlog.info(fmt(nick, total_input, self.cjamount, self.orderbook[
nick]['txfee'], real_cjfee))
self.outputs.append({'address': cj_addr, 'value': self.cjamount})
self.cjfee_total += real_cjfee
self.maker_txfee_contributions += self.orderbook[nick]['txfee']
self.maker_utxo_data[nick] = utxo_data
#We have succesfully processed the data from this nick:
try:
self.nonrespondants.remove(nick)
self.nonrespondants.remove(maker_inputs.nick)
except Exception as e:
jlog.warn("Failure to remove counterparty from nonrespondants list: " + str(nick) + \
", error message: " + repr(e))
jlog.warn(
"Failure to remove counterparty from nonrespondants list:"
f" {maker_inputs.nick}), error message: {repr(e)})")
#Apply business logic of how many counterparties are enough; note that
#this must occur after the above ioauth data processing, since we only now
@ -517,7 +475,7 @@ class Taker(object):
# this is done with using the same estimate fee function and comparing
# the totals; this ratio will correspond to the ratio of the feerates.
num_ins = len([u for u in sum(self.utxos.values(), [])])
num_outs = len(self.outputs) + 2
num_outs = len(self.outputs) + 1
new_total_fee = estimate_tx_fee(num_ins, num_outs,
txtype=self.wallet_service.get_txtype())
feeratio = new_total_fee/self.total_txfee
@ -557,6 +515,80 @@ class Taker(object):
return (True, list(self.maker_utxo_data.keys()),
bintohex(self.latest_tx.serialize()))
def _verify_ioauth_data(self, ioauth_data):
verified_data = []
# Need to authorize against the btc pubkey first.
for nick, nickdata in ioauth_data.items():
utxo_list, auth_pub, cj_addr, change_addr, btc_sig, maker_pk = nickdata
if not self.auth_counterparty(btc_sig, auth_pub, maker_pk):
jlog.debug(
"Counterparty encryption verification failed, aborting: " + nick)
# This counterparty must be rejected
continue
if not validate_address(cj_addr)[0]\
or not validate_address(change_addr)[0]:
jlog.warn("Counterparty provided invalid address: {}".format(
(cj_addr, change_addr)))
# Interpreted as malicious
self.add_ignored_makers([nick])
continue
try:
maker_inputs_data = self._verify_ioauth_inputs(
nick, utxo_list, auth_pub)
except IoauthInputVerificationError as e:
for msg in e.messages:
jlog.warning(msg)
continue
verified_data.append(maker_inputs_data._replace(
utxo_list=utxo_list, cj_addr=cj_addr, change_addr=change_addr))
return verified_data
def _verify_ioauth_inputs(self, nick, utxo_list, auth_pub):
utxo_data = jm_single().bc_interface.query_utxo_set(utxo_list)
if None in utxo_data:
raise IoauthInputVerificationError([
"ERROR: outputs unconfirmed or already spent. utxo_data="
f"{pprint.pformat(utxo_data)}",
"Disregarding this counterparty."])
# Complete maker authorization:
# Extract the address fields from the utxos
# Construct the Bitcoin address for the auth_pub field
# Ensure that at least one address from utxos corresponds.
for inp in utxo_data:
try:
if self.wallet_service.pubkey_has_script(
auth_pub, inp['script']):
break
except EngineError as e:
pass
else:
raise IoauthInputVerificationError([
f"ERROR maker's ({nick}) authorising pubkey is not included "
"in the transaction!"])
total_input = sum([d['value'] for d in utxo_data])
real_cjfee = calc_cj_fee(self.orderbook[nick]['ordertype'],
self.orderbook[nick]['cjfee'],
self.cjamount)
change_amount = (total_input - self.cjamount -
self.orderbook[nick]['txfee'] + real_cjfee)
# certain malicious and/or incompetent liquidity providers send
# inputs totalling less than the coinjoin amount! this leads to
# a change output of zero satoshis; this counterparty must be removed.
if change_amount < jm_single().DUST_THRESHOLD:
raise IoauthInputVerificationError([
f"ERROR counterparty requires sub-dust change. nick={nick} "
f"totalin={total_input:d} cjamount={self.cjamount:d} "
f"change={change_amount:d}",
f"Invalid change, too small, nick={nick}"])
return self._MakerTxData(nick, utxo_data, total_input, change_amount,
real_cjfee)
@hexbin
def auth_counterparty(self, btc_sig, auth_pub, maker_pk):
"""Validate the counterpartys claim to own the btc

Loading…
Cancel
Save