diff --git a/jmclient/jmclient/maker.py b/jmclient/jmclient/maker.py index 82dbe91..2213598 100644 --- a/jmclient/jmclient/maker.py +++ b/jmclient/jmclient/maker.py @@ -1,7 +1,7 @@ -#! /usr/bin/env python import base64 import sys import abc +import atexit import jmbitcoin as btc from jmbase import bintohex, hexbin, get_log, EXIT_FAILURE, stop_reactor @@ -38,6 +38,7 @@ class Maker(object): """ if not self.wallet_service.synced: return + self.freeze_timelocked_utxos() self.offerlist = self.create_my_orders() self.fidelity_bond = self.get_fidelity_bond_template() self.sync_wait_loop.stop() @@ -253,6 +254,32 @@ class Maker(object): self.offerlist.remove(oldorder_s[0]) self.offerlist += to_announce + def freeze_timelocked_utxos(self): + """ + Freeze all wallet's timelocked UTXOs. These cannot be spent in a + coinjoin because of protocol limitations. + """ + if not hasattr(self.wallet_service.wallet, 'FIDELITY_BOND_MIXDEPTH'): + return + + frozen_utxos = [] + md_utxos = self.wallet_service.get_utxos_by_mixdepth() + for tx, details \ + in md_utxos[self.wallet_service.FIDELITY_BOND_MIXDEPTH].items(): + if self.wallet_service.is_timelocked_path(details['path']): + self.wallet_service.disable_utxo(*tx) + frozen_utxos.append(tx) + path_repr = self.wallet_service.get_path_repr(details['path']) + jlog.info( + f"Timelocked UTXO at '{path_repr}' has been " + f"auto-frozen. They cannot be spent by makers.") + + def unfreeze(): + for tx in frozen_utxos: + self.wallet_service.disable_utxo(*tx, disable=False) + + atexit.register(unfreeze) + @abc.abstractmethod def create_my_orders(self): """Must generate a set of orders to be displayed diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 5cad68a..0988986 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -2319,7 +2319,7 @@ class FidelityBondMixin(object): md = self.FIDELITY_BOND_MIXDEPTH address_type = self.BIP32_TIMELOCK_ID for timenumber in range(self.TIMENUMBER_COUNT): - path = self.get_path(md, address_type, timenumber, timenumber) + path = self.get_path(md, address_type, timenumber) script = self.get_script_from_path(path) self._script_map[script] = path @@ -2355,13 +2355,14 @@ class FidelityBondMixin(object): else: return super()._get_key_from_path(path) - def get_path(self, mixdepth=None, address_type=None, index=None, timenumber=None): + def get_path(self, mixdepth=None, address_type=None, index=None): if address_type == None or address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID, self.BIP32_BURN_ID) or index == None: return super().get_path(mixdepth, address_type, index) elif address_type == self.BIP32_TIMELOCK_ID: - assert timenumber != None - timestamp = self._time_number_to_timestamp(timenumber) + # index is re-purposed as timenumber + assert index is not None + timestamp = self._time_number_to_timestamp(index) return tuple(chain(self._get_bip32_export_path(mixdepth, address_type), (index, timestamp))) else: @@ -2399,12 +2400,12 @@ class FidelityBondMixin(object): def _get_default_used_indices(self): return {x: [0, 0, 0, 0] for x in range(self.max_mixdepth + 1)} - def get_script(self, mixdepth, address_type, index, timenumber=None): - path = self.get_path(mixdepth, address_type, index, timenumber) + def get_script(self, mixdepth, address_type, index): + path = self.get_path(mixdepth, address_type, index) return self.get_script_from_path(path) - def get_addr(self, mixdepth, address_type, index, timenumber=None): - script = self.get_script(mixdepth, address_type, index, timenumber) + def get_addr(self, mixdepth, address_type, index): + script = self.get_script(mixdepth, address_type, index) return self.script_to_addr(script) def add_burner_output(self, path, txhex, block_height, merkle_branch, diff --git a/jmclient/jmclient/wallet_service.py b/jmclient/jmclient/wallet_service.py index b02ba84..22046b1 100644 --- a/jmclient/jmclient/wallet_service.py +++ b/jmclient/jmclient/wallet_service.py @@ -925,7 +925,7 @@ class WalletService(Service): md = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH address_type = FidelityBondMixin.BIP32_TIMELOCK_ID for timenumber in range(FidelityBondMixin.TIMENUMBER_COUNT): - addresses.add(self.get_addr(md, address_type, timenumber, timenumber)) + addresses.add(self.get_addr(md, address_type, timenumber)) return addresses, saved_indices diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py index 68fc236..02b9446 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -474,7 +474,7 @@ def wallet_display(wallet_service, showprivkey, displayall=False, address_type = FidelityBondMixin.BIP32_TIMELOCK_ID entrylist = [] for timenumber in range(FidelityBondMixin.TIMENUMBER_COUNT): - path = wallet_service.get_path(m, address_type, timenumber, timenumber) + path = wallet_service.get_path(m, address_type, timenumber) addr = wallet_service.get_address_from_path(path) timelock = datetime.utcfromtimestamp(0) + timedelta(seconds=path[-1]) @@ -1226,9 +1226,8 @@ def wallet_gettimelockaddress(wallet, locktime_string): address_type = FidelityBondMixin.BIP32_TIMELOCK_ID lock_datetime = datetime.strptime(locktime_string, "%Y-%m") timenumber = FidelityBondMixin.datetime_to_time_number(lock_datetime) - index = timenumber - path = wallet.get_path(m, address_type, index, timenumber) + path = wallet.get_path(m, address_type, timenumber) jmprint("path = " + wallet.get_path_repr(path), "info") jmprint("Coins sent to this address will be not be spendable until " + lock_datetime.strftime("%B %Y") + ". Full date: " diff --git a/jmclient/test/test_maker.py b/jmclient/test/test_maker.py index 031779a..b3a6b93 100644 --- a/jmclient/test/test_maker.py +++ b/jmclient/test/test_maker.py @@ -1,7 +1,9 @@ #!/usr/bin/env python +import datetime import jmbitcoin as btc -from jmclient import Maker, load_test_config, jm_single, WalletService +from jmclient import Maker, load_test_config, jm_single, WalletService, VolatileStorage, \ + SegwitWalletFidelityBonds, get_network import jmclient from commontest import DummyBlockchainInterface from test_taker import DummyWallet @@ -164,6 +166,24 @@ def test_verify_unsigned_tx_nonsw_valid(setup_env_nodeps): assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with only p2sh outputs" +def test_freeze_timelocked_utxos(setup_env_nodeps): + storage = VolatileStorage() + SegwitWalletFidelityBonds.initialize(storage, get_network()) + wallet = SegwitWalletFidelityBonds(storage) + ts = wallet.datetime_to_time_number( + datetime.datetime.strptime("2021-07", "%Y-%m")) + tl_path = wallet.get_path( + wallet.FIDELITY_BOND_MIXDEPTH, wallet.BIP32_TIMELOCK_ID, ts) + tl_script = wallet.get_script_from_path(tl_path) + utxo = (b'a'*32, 0) + wallet.add_utxo(utxo[0], utxo[1], tl_script, 100000000) + assert not wallet._utxos.is_disabled(*utxo) + + maker = OfflineMaker(WalletService(wallet)) + maker.freeze_timelocked_utxos() + assert wallet._utxos.is_disabled(*utxo) + + @pytest.fixture def setup_env_nodeps(monkeypatch): monkeypatch.setattr(jmclient.configure, 'get_blockchain_interface_instance', diff --git a/jmclient/test/test_wallet.py b/jmclient/test/test_wallet.py index 4933ee9..ca7f928 100644 --- a/jmclient/test/test_wallet.py +++ b/jmclient/test/test_wallet.py @@ -242,13 +242,12 @@ def test_bip32_addresses_p2sh_p2wpkh(setup_wallet, mixdepth, internal, index, ad assert wif == wallet.get_wif(mixdepth, internal, index) assert address == wallet.get_addr(mixdepth, internal, index) -@pytest.mark.parametrize('index,timenumber,address,wif', [ - [0, 0, 'bcrt1qgysu2eynn6klarz200ctgev7gqhhp7hwsdaaec3c7h0ltmc3r68q87c2d3', 'cVASAS6bpC5yctGmnsKaDz7D8CxEwccUtpjSNBQzeV2fw8ox8RR9'], - [0, 50, 'bcrt1qyrdhyqzj87vq20e853x7gzhx9lp8ta6cd8mwp8haqex8r4vrg2wsf7rcxm', 'cVASAS6bpC5yctGmnsKaDz7D8CxEwccUtpjSNBQzeV2fw8ox8RR9'], - [5, 0, 'bcrt1quunmmsudhpsuksa2ke8m6aj7757mst966mqq50nckx9wdrs4y6fs9gjuww', 'cUgT5jRjYi6i8Fc7TirJrrvhbs7ceSqJ6USKboVrLYghJKDzEQHQ'], - [9, 1, 'bcrt1qvpgmrn5a7yc0h2j6fp8jhtwzd8eetlt7hsu3cn098qftzp4t2h6sp5p35p', 'cW7H2pv6Rr5NWaTAnDC6r7bviHwDsAwyqh4XdZTE4xf2H2DB2hmb'] +@pytest.mark.parametrize('timenumber,address,wif', [ + [0, 'bcrt1qgysu2eynn6klarz200ctgev7gqhhp7hwsdaaec3c7h0ltmc3r68q87c2d3', 'cVASAS6bpC5yctGmnsKaDz7D8CxEwccUtpjSNBQzeV2fw8ox8RR9'], + [50, 'bcrt1q0cnscj0hlf6xqzlqwk7swngd3kmvd6unn49j9h4zgg68kg8fd7gq0r87lf', 'cMtnaLzC2EW3URnmAapRnPQECGwGruxqXJpAnuRjKup3pkWfrxRE'], + [1, 'bcrt1q26vw0q28rz2r2ktehp8w5yfzkzskrc4fxqdhzjy0f88kzhjvlfrs7fyas6', 'cU8G1YAAxGZMqNsXxApBAahb8pbxhxryDshFdX5eRT9FV4gHNVXT'] ]) -def test_bip32_timelocked_addresses(setup_wallet, index, timenumber, address, wif): +def test_bip32_timelocked_addresses(setup_wallet, timenumber, address, wif): jm_single().config.set('BLOCKCHAIN', 'network', 'testnet') entropy = unhexlify('2e0339ba89b4a1272cdf78b27ee62669ee01992a59e836e2807051be128ca817') @@ -260,10 +259,10 @@ def test_bip32_timelocked_addresses(setup_wallet, index, timenumber, address, wi address_type = FidelityBondMixin.BIP32_TIMELOCK_ID #wallet needs to know about the script beforehand - wallet.get_script_and_update_map(mixdepth, address_type, index, timenumber) + wallet.get_script_and_update_map(mixdepth, address_type, timenumber) - assert address == wallet.get_addr(mixdepth, address_type, index, timenumber) - assert wif == wallet.get_wif_path(wallet.get_path(mixdepth, address_type, index, timenumber)) + assert address == wallet.get_addr(mixdepth, address_type, timenumber) + assert wif == wallet.get_wif_path(wallet.get_path(mixdepth, address_type, timenumber)) @pytest.mark.parametrize('timenumber,locktime_string', [ [0, "2020-01"], @@ -279,9 +278,7 @@ def test_gettimelockaddress_method(setup_wallet, timenumber, locktime_string): m = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH address_type = FidelityBondMixin.BIP32_TIMELOCK_ID - index = timenumber - script = wallet.get_script_and_update_map(m, address_type, index, - timenumber) + script = wallet.get_script_and_update_map(m, address_type, timenumber) addr = wallet.script_to_addr(script) addr_from_method = wallet_gettimelockaddress(wallet, locktime_string) @@ -405,11 +402,10 @@ def test_timelocked_output_signing(setup_wallet): SegwitWalletFidelityBonds.initialize(storage, get_network()) wallet = SegwitWalletFidelityBonds(storage) - index = 0 timenumber = 0 script = wallet.get_script_and_update_map( FidelityBondMixin.FIDELITY_BOND_MIXDEPTH, - FidelityBondMixin.BIP32_TIMELOCK_ID, index, timenumber) + FidelityBondMixin.BIP32_TIMELOCK_ID, timenumber) utxo = fund_wallet_addr(wallet, wallet.script_to_addr(script)) timestamp = wallet._time_number_to_timestamp(timenumber) @@ -931,7 +927,7 @@ def test_is_standard_wallet_script_nonstandard(setup_wallet): 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) + tl_path = wallet.get_path(0, wallet.BIP32_TIMELOCK_ID, ts) assert not wallet.is_standard_wallet_script(tl_path)