Browse Source

fix maker selection of ioauth input with expired timelocked addresses

master
undeath 4 years ago
parent
commit
8c3ae11cf6
No known key found for this signature in database
GPG Key ID: F0DF5443BD2F3520
  1. 7
      jmclient/jmclient/maker.py
  2. 61
      jmclient/jmclient/wallet.py
  3. 9
      jmclient/jmclient/wallet_service.py
  4. 47
      jmclient/jmclient/yieldgenerator.py
  5. 2
      jmclient/test/test_client_protocol.py
  6. 3
      jmclient/test/test_taker.py
  7. 31
      jmclient/test/test_wallet.py

7
jmclient/jmclient/maker.py

@ -107,9 +107,8 @@ class Maker(object):
self.wallet_service.save_wallet() self.wallet_service.save_wallet()
# Construct data for auth request back to taker. # Construct data for auth request back to taker.
# Need to choose an input utxo pubkey to sign with # 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 utxos and retrieve key from wallet.
# Just choose the first utxo in self.utxos and retrieve key from wallet. auth_address = next(iter(utxos.values()))['address']
auth_address = utxos[list(utxos.keys())[0]]['address']
auth_key = self.wallet_service.get_key_from_addr(auth_address) auth_key = self.wallet_service.get_key_from_addr(auth_address)
auth_pub = btc.privkey_to_pubkey(auth_key) auth_pub = btc.privkey_to_pubkey(auth_key)
# kphex was auto-converted by @hexbin but we actually need to sign the # kphex was auto-converted by @hexbin but we actually need to sign the
@ -262,7 +261,7 @@ class Maker(object):
""" """
@abc.abstractmethod @abc.abstractmethod
def oid_to_order(self, cjorder, oid, amount): def oid_to_order(self, cjorder, amount):
"""Must convert an order with an offer/order id """Must convert an order with an offer/order id
into a set of utxos to fill the order. into a set of utxos to fill the order.
Also provides the output addresses for the Taker. Also provides the output addresses for the Taker.

61
jmclient/jmclient/wallet.py

@ -23,7 +23,7 @@ from math import exp
from .configure import jm_single from .configure import jm_single
from .blockchaininterface import INF_HEIGHT from .blockchaininterface import INF_HEIGHT
from .support import select_gradual, select_greedy, select_greediest, \ from .support import select_gradual, select_greedy, select_greediest, \
select select, NotEnoughFundsException
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH,\ from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH,\
TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS,\ TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS,\
TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH,\ TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH,\
@ -662,7 +662,8 @@ class BaseWallet(object):
return (removed_utxos, added_utxos) return (removed_utxos, added_utxos)
def select_utxos(self, mixdepth, amount, utxo_filter=None, def select_utxos(self, mixdepth, amount, utxo_filter=None,
select_fn=None, maxheight=None, includeaddr=False): select_fn=None, maxheight=None, includeaddr=False,
require_auth_address=False):
""" """
Select a subset of available UTXOS for a given mixdepth whose value is Select a subset of available UTXOS for a given mixdepth whose value is
greater or equal to amount. If `includeaddr` is True, adds an `address` greater or equal to amount. If `includeaddr` is True, adds an `address`
@ -674,10 +675,17 @@ class BaseWallet(object):
amount: int, total minimum amount of all selected utxos amount: int, total minimum amount of all selected utxos
utxo_filter: list of (txid, index), utxos not to select utxo_filter: list of (txid, index), utxos not to select
maxheight: only select utxos with blockheight <= this. maxheight: only select utxos with blockheight <= this.
require_auth_address: if True, output utxos must include a
standard wallet address. The first item of the output dict is
guaranteed to be a suitable utxo. Result will be empty if no
such utxo set could be found.
returns: returns:
{(txid, index): {'script': bytes, 'path': tuple, 'value': int}} {(txid, index): {'script': bytes, 'path': tuple, 'value': int}}
raises:
NotEnoughFundsException: if mixdepth does not have utxos with
enough value to satisfy amount
""" """
assert isinstance(mixdepth, numbers.Integral) assert isinstance(mixdepth, numbers.Integral)
assert isinstance(amount, numbers.Integral) assert isinstance(amount, numbers.Integral)
@ -688,14 +696,33 @@ class BaseWallet(object):
assert len(i) == 2 assert len(i) == 2
assert isinstance(i[0], bytes) assert isinstance(i[0], bytes)
assert isinstance(i[1], numbers.Integral) assert isinstance(i[1], numbers.Integral)
ret = self._utxos.select_utxos( utxos = self._utxos.select_utxos(
mixdepth, amount, utxo_filter, select_fn, maxheight=maxheight) mixdepth, amount, utxo_filter, select_fn, maxheight=maxheight)
for data in ret.values(): total_value = 0
standard_utxo = None
for key, data in utxos.items():
if self.is_standard_wallet_script(data['path']):
standard_utxo = key
total_value += data['value']
data['script'] = self.get_script_from_path(data['path']) data['script'] = self.get_script_from_path(data['path'])
if includeaddr: if includeaddr:
data["address"] = self.get_address_from_path(data["path"]) data["address"] = self.get_address_from_path(data["path"])
return ret
if require_auth_address and not standard_utxo:
# try to select more utxos, hoping for a standard one
try:
return self.select_utxos(
mixdepth, total_value + 1, utxo_filter, select_fn,
maxheight, includeaddr, require_auth_address)
except NotEnoughFundsException:
# recursive utxo selection was unsuccessful, give up
return {}
elif require_auth_address:
utxos = collections.OrderedDict(utxos)
utxos.move_to_end(standard_utxo, last=False)
return utxos
def disable_utxo(self, txid, index, disable=True): def disable_utxo(self, txid, index, disable=True):
self._utxos.disable_utxo(txid, index, disable) self._utxos.disable_utxo(txid, index, disable)
@ -889,6 +916,16 @@ class BaseWallet(object):
""" """
return iter([]) return iter([])
def is_standard_wallet_script(self, path):
"""
Check if the path's script is of the same type as the standard wallet
key type.
return:
bool
"""
raise NotImplementedError()
def is_known_addr(self, addr): def is_known_addr(self, addr):
""" """
Check if address is known to belong to this wallet. Check if address is known to belong to this wallet.
@ -1725,6 +1762,12 @@ class ImportWalletMixin(object):
def _is_imported_path(cls, path): def _is_imported_path(cls, path):
return len(path) == 3 and path[0] == cls._IMPORTED_ROOT_PATH return len(path) == 3 and path[0] == cls._IMPORTED_ROOT_PATH
def is_standard_wallet_script(self, path):
if self._is_imported_path(path):
engine = self._get_key_from_path(path)[1]
return engine == self._ENGINE
return super().is_standard_wallet_script(path)
def path_repr_to_path(self, pathstr): def path_repr_to_path(self, pathstr):
spath = pathstr.encode('ascii').split(b'/') spath = pathstr.encode('ascii').split(b'/')
if not self._is_imported_path(spath): if not self._is_imported_path(spath):
@ -2030,6 +2073,9 @@ class BIP32Wallet(BaseWallet):
def _is_my_bip32_path(self, path): def _is_my_bip32_path(self, path):
return path[0] == self._key_ident return path[0] == self._key_ident
def is_standard_wallet_script(self, path):
return self._is_my_bip32_path(path)
def get_new_script(self, mixdepth, address_type): def get_new_script(self, mixdepth, address_type):
if self.disable_new_scripts: if self.disable_new_scripts:
raise RuntimeError("Obtaining new wallet addresses " raise RuntimeError("Obtaining new wallet addresses "
@ -2256,6 +2302,11 @@ class FidelityBondMixin(object):
pub = engine.privkey_to_pubkey(priv) pub = engine.privkey_to_pubkey(priv)
return sha256(sha256(pub).digest()).digest()[:3] return sha256(sha256(pub).digest()).digest()[:3]
def is_standard_wallet_script(self, path):
if self.is_timelocked_path(path):
return False
return super().is_standard_wallet_script(path)
@classmethod @classmethod
def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk): def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk):
if mpk.startswith(cls._BIP32_PUBKEY_PREFIX): if mpk.startswith(cls._BIP32_PUBKEY_PREFIX):

9
jmclient/jmclient/wallet_service.py

@ -864,16 +864,17 @@ class WalletService(Service):
return self.current_blockheight - minconfs + 1 return self.current_blockheight - minconfs + 1
def select_utxos(self, mixdepth, amount, utxo_filter=None, select_fn=None, def select_utxos(self, mixdepth, amount, utxo_filter=None, select_fn=None,
minconfs=None, includeaddr=False): minconfs=None, includeaddr=False, require_auth_address=False):
""" Request utxos from the wallet in a particular mixdepth to satisfy """ Request utxos from the wallet in a particular mixdepth to satisfy
a certain total amount, optionally set the selector function (or use a certain total amount, optionally set the selector function (or use
the currently configured function set by the wallet, and optionally the currently configured function set by the wallet, and optionally
require a minimum of minconfs confirmations (default none means require a minimum of minconfs confirmations (default none means
unconfirmed are allowed). unconfirmed are allowed).
""" """
return self.wallet.select_utxos(mixdepth, amount, utxo_filter=utxo_filter, return self.wallet.select_utxos(
select_fn=select_fn, maxheight=self.minconfs_to_maxheight(minconfs), mixdepth, amount, utxo_filter=utxo_filter, select_fn=select_fn,
includeaddr=includeaddr) maxheight=self.minconfs_to_maxheight(minconfs),
includeaddr=includeaddr, require_auth_address=require_auth_address)
def get_balance_by_mixdepth(self, verbose=True, def get_balance_by_mixdepth(self, verbose=True,
include_disabled=False, include_disabled=False,

47
jmclient/jmclient/yieldgenerator.py

@ -22,6 +22,11 @@ jlog = get_log()
MAX_MIX_DEPTH = 5 MAX_MIX_DEPTH = 5
class NoIoauthInputException(Exception):
pass
class YieldGenerator(Maker): class YieldGenerator(Maker):
"""A maker for the purposes of generating a yield from held """A maker for the purposes of generating a yield from held
bitcoins, offering from the maximum mixdepth and trying to offer bitcoins, offering from the maximum mixdepth and trying to offer
@ -172,9 +177,18 @@ class YieldGeneratorBasic(YieldGenerator):
if not filtered_mix_balance: if not filtered_mix_balance:
return None, None, None return None, None, None
jlog.debug('mix depths that have enough = ' + str(filtered_mix_balance)) jlog.debug('mix depths that have enough = ' + str(filtered_mix_balance))
mixdepth = self.select_input_mixdepth(filtered_mix_balance, offer, amount)
if mixdepth is None: try:
mixdepth, utxos = self._get_order_inputs(
filtered_mix_balance, offer, required_amount)
except NoIoauthInputException:
jlog.error(
'unable to fill order, no suitable IOAUTH UTXO found. In '
'order to spend coins (UTXOs) from a mixdepth using coinjoin,'
' there needs to be at least one standard wallet UTXO (not '
'fidelity bond or different address type).')
return None, None, None return None, None, None
jlog.info('filling offer, mixdepth=' + str(mixdepth) + ', amount=' + str(amount)) jlog.info('filling offer, mixdepth=' + str(mixdepth) + ', amount=' + str(amount))
cj_addr = self.select_output_address(mixdepth, offer, amount) cj_addr = self.select_output_address(mixdepth, offer, amount)
@ -183,11 +197,34 @@ class YieldGeneratorBasic(YieldGenerator):
jlog.info('sending output to address=' + str(cj_addr)) jlog.info('sending output to address=' + str(cj_addr))
change_addr = self.wallet_service.get_internal_addr(mixdepth) change_addr = self.wallet_service.get_internal_addr(mixdepth)
return utxos, cj_addr, change_addr
utxos = self.wallet_service.select_utxos(mixdepth, required_amount, def _get_order_inputs(self, filtered_mix_balance, offer, required_amount):
minconfs=1, includeaddr=True) """
Select inputs from some applicable mixdepth that has a utxo suitable
for ioauth.
return utxos, cj_addr, change_addr params:
filtered_mix_balance: see get_available_mixdepths() output
offer: offer dict
required_amount: int, total inputs value in sat
returns:
mixdepth, utxos (int, dict)
raises:
NoIoauthInputException: if no provided mixdepth has a suitable utxo
"""
while filtered_mix_balance:
mixdepth = self.select_input_mixdepth(
filtered_mix_balance, offer, required_amount)
utxos = self.wallet_service.select_utxos(
mixdepth, required_amount, minconfs=1, includeaddr=True,
require_auth_address=True)
if utxos:
return mixdepth, utxos
filtered_mix_balance.pop(mixdepth)
raise NoIoauthInputException()
def on_tx_confirmed(self, offer, txid, confirmations): def on_tx_confirmed(self, offer, txid, confirmations):
if offer["cjaddr"] in self.tx_unconfirm_timestamp: if offer["cjaddr"] in self.tx_unconfirm_timestamp:

2
jmclient/test/test_client_protocol.py

@ -123,7 +123,7 @@ class DummyMaker(Maker):
'txfee': 0 'txfee': 0
}] }]
def oid_to_order(self, cjorder, oid, amount): def oid_to_order(self, cjorder, amount):
# utxos, cj_addr, change_addr # utxos, cj_addr, change_addr
return [], '', '' return [], '', ''

3
jmclient/test/test_taker.py

@ -79,7 +79,8 @@ class DummyWallet(SegwitWallet):
return retval return retval
def select_utxos(self, mixdepth, amount, utxo_filter=None, select_fn=None, def select_utxos(self, mixdepth, amount, utxo_filter=None, select_fn=None,
maxheight=None, includeaddr=False): maxheight=None, includeaddr=False,
require_auth_address=False):
if amount > self.get_balance_by_mixdepth()[mixdepth]: if amount > self.get_balance_by_mixdepth()[mixdepth]:
raise NotEnoughFundsException(amount, self.get_balance_by_mixdepth()[mixdepth]) raise NotEnoughFundsException(amount, self.get_balance_by_mixdepth()[mixdepth])
# comment as for get_utxos_by_mixdepth: # comment as for get_utxos_by_mixdepth:

31
jmclient/test/test_wallet.py

@ -1,5 +1,5 @@
'''Wallet functionality tests.''' '''Wallet functionality tests.'''
import datetime
import os import os
import json import json
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
@ -906,6 +906,35 @@ def test_create_wallet(setup_wallet, password, wallet_cls):
os.remove(wallet_name) os.remove(wallet_name)
btc.select_chain_params("bitcoin/regtest") btc.select_chain_params("bitcoin/regtest")
@pytest.mark.parametrize('wallet_cls', [
SegwitLegacyWallet, SegwitWallet, SegwitWalletFidelityBonds
])
def test_is_standard_wallet_script(setup_wallet, wallet_cls):
storage = VolatileStorage()
wallet_cls.initialize(
storage, get_network(), max_mixdepth=0)
wallet = wallet_cls(storage)
script = wallet.get_new_script(0, 1)
assert wallet.is_known_script(script)
path = wallet.script_to_path(script)
assert wallet.is_standard_wallet_script(path)
def test_is_standard_wallet_script_nonstandard(setup_wallet):
storage = VolatileStorage()
SegwitWalletFidelityBonds.initialize(
storage, get_network(), max_mixdepth=0)
wallet = SegwitWalletFidelityBonds(storage)
import_path = wallet.import_private_key(
0, 'cRAGLvPmhpzJNgdMT4W2gVwEW3fusfaDqdQWM2vnWLgXKzCWKtcM')
assert wallet.is_standard_wallet_script(import_path)
ts = wallet.datetime_to_time_number(
datetime.datetime.strptime("2021-07", "%Y-%m"))
tl_path = wallet.get_path(0, wallet.BIP32_TIMELOCK_ID, 0, ts)
assert not wallet.is_standard_wallet_script(tl_path)
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def setup_wallet(request): def setup_wallet(request):
load_test_config() load_test_config()

Loading…
Cancel
Save