From af7dee0957da587ee32cc259729d8e0b06f01b1a Mon Sep 17 00:00:00 2001 From: Philipp de Col Date: Thu, 10 Oct 2019 10:28:54 +0200 Subject: [PATCH] [#217] Snapshot proof implementation of getNewAddresses, getInputs, getTransfers and getAccountData --- docs/addresses.rst | 12 +- docs/api.rst | 22 +- iota/api.py | 8 +- iota/commands/extended/get_account_data.py | 2 +- iota/commands/extended/get_new_addresses.py | 25 +- iota/commands/extended/utils.py | 26 +- .../extended/get_account_data_test.py | 25 ++ test/commands/extended/get_inputs_test.py | 96 +++-- .../extended/get_new_addresses_test.py | 167 ++++++++- test/commands/extended/get_transfers_test.py | 42 ++- .../extended/prepare_transfer_test.py | 41 +- test/commands/extended/utils_test.py | 351 ++++++++++++++++++ 12 files changed, 738 insertions(+), 79 deletions(-) create mode 100644 test/commands/extended/utils_test.py diff --git a/docs/addresses.rst b/docs/addresses.rst index c3403ae..fbdbf8c 100644 --- a/docs/addresses.rst +++ b/docs/addresses.rst @@ -17,17 +17,11 @@ any other financial service. These performance issues will be fixed in a future version of the library; please bear with us! - In the meantime, if you are using Python 3, you can install a C extension + In the meantime, you can install a C extension that boosts PyOTA's performance significantly (speedups of 60x are common!). To install the extension, run ``pip install pyota[ccurl]``. - **Important:** The extension is not yet compatible with Python 2. - - If you are familiar with Python 2's C API, we'd love to hear from you! - Check the `GitHub issue `_ - for more information. - PyOTA provides two methods for generating addresses: Using the API @@ -60,7 +54,9 @@ method, using the following parameters: (defaults to 1). - If ``None``, the API will generate addresses until it finds one that has not been used (has no transactions associated with it on the - Tangle). It will then return the unused address and discard the rest. + Tangle, has no balance and was not spent from). This makes the command + safe to use even after a snapshot has been taken. It will then return the + unused address and discard the rest. - ``security_level: int``: Determines the security level of the generated addresses. See `Security Levels <#security-levels>`__ below. diff --git a/docs/api.rst b/docs/api.rst index d6eceac..6acef03 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -89,8 +89,10 @@ Parameters parameter behaves like the ``stop`` attribute in a ``slice`` object; the stop index is *not* included in the result. -- If ``None`` (default), then this method will check every address - until it finds one without any transfers. +- If ``None`` (default), then this method will not stop until it finds + an unused address. This is one without any transactions, that has no + balance and that was not spent from. Note that a snapshot, which removes + transactions, can cause this API call to stop earlier. - ``inclusion_states: bool`` Whether to also fetch the inclusion states of the transfers. This requires an additional API call to the node, @@ -145,7 +147,9 @@ Parameters - Note that this parameter behaves like the ``stop`` attribute in a ``slice`` object; the stop index is *not* included in the result. - If ``None`` (default), then this method will not stop until it finds - an unused address. + an unused address. This is one without any transactions, that has no + balance and that was not spent from. Note that a snapshot, which removes + transactions, can cause this API call to stop earlier. - ``threshold: Optional[int]``: If set, determines the minimum threshold for a successful result: - As soon as this threshold is reached, iteration will stop. @@ -199,11 +203,13 @@ Generates one or more new addresses from the seed. Parameters ~~~~~~~~~~ -- ``index: int``: Specify the index of the new address (must be >= 1). +- ``index: int``: Specify the index of the new address (must be >= 0). - ``count: Optional[int]``: Number of addresses to generate (must be >= 1). - If ``None``, this method will scan the Tangle to find the next - available unused address and return that. + available unused address and return that. This is one without any + transactions, that has no balance and that was not spent from. This makes + the command safe to use even after a snapshot has been taken. - ``security_level: int``: Number of iterations to use when generating new addresses. Lower values generate addresses faster, higher values result in more secure signatures in transactions. @@ -228,8 +234,10 @@ Parameters - ``stop: Optional[int]``: Stop before this index. - Note that this parameter behaves like the ``stop`` attribute in a ``slice`` object; the stop index is *not* included in the result. -- If ``None`` (default), then this method will check every address - until it finds one without any transfers. +- If ``None`` (default), then this method will not stop until it finds + an unused address. This is one without any transactions, that has no + balance and that was not spent from. Note that a snapshot, which removes + transactions, can cause this API call to stop earlier. Return ~~~~~~ diff --git a/iota/api.py b/iota/api.py index 9cfeb26..0d82b4f 100644 --- a/iota/api.py +++ b/iota/api.py @@ -830,8 +830,7 @@ def get_new_addresses( Generates one or more new addresses from the seed. :param index: - The key index of the first new address to generate (must be - >= 1). + The key index of the first new address to generate (must be >= 0). :param count: Number of addresses to generate (must be >= 1). @@ -841,8 +840,9 @@ def get_new_addresses( inside a loop. If ``None``, this method will progressively generate - addresses and scan the Tangle until it finds one that has no - transactions referencing it. + addresses and scan the Tangle until it finds one that is unused. + This is if no transactions are referencing it and it has no balance + and it was not spent from before. :param security_level: Number of iterations to use when generating new addresses. diff --git a/iota/commands/extended/get_account_data.py b/iota/commands/extended/get_account_data.py index 7235d10..46dd8d1 100644 --- a/iota/commands/extended/get_account_data.py +++ b/iota/commands/extended/get_account_data.py @@ -59,7 +59,7 @@ def _execute(self, request): my_hashes = ft_command(addresses=my_addresses).get('hashes') or [] account_balance = 0 - if my_hashes: + if my_addresses: # Load balances for the addresses that we generated. gb_response = ( GetBalancesCommand(self.adapter)(addresses=my_addresses) diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index fa9d088..3157a06 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -9,6 +9,9 @@ from iota import Address from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand +from iota.commands.core.get_balances import GetBalancesCommand +from iota.commands.core.were_addresses_spent_from import \ + WereAddressesSpentFromCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import SecurityLevel, Trytes @@ -58,17 +61,29 @@ def _find_addresses(self, seed, index, count, security_level, checksum): generator = AddressGenerator(seed, security_level, checksum) if count is None: - # Connect to Tangle and find the first address without any - # transactions. + # Connect to Tangle and find the first unused address. for addy in generator.create_iterator(start=index): - # We use addy.address here because FindTransactions does + # We use addy.address here because the commands do # not work on an address with a checksum + response = WereAddressesSpentFromCommand(self.adapter)( + addresses=[addy.address], + ) + if response['states'][0]: + continue + + response = GetBalancesCommand(self.adapter)( + addresses=[addy.address], + ) + if response['balances'][0] != 0: + continue + response = FindTransactionsCommand(self.adapter)( addresses=[addy.address], ) + if response.get('hashes'): + continue - if not response.get('hashes'): - return [addy] + return [addy] return generator.get_addresses(start=index, count=count) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index a8d3eb8..abe6d4e 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -8,7 +8,10 @@ TransactionHash from iota.adapter import BaseAdapter from iota.commands.core.find_transactions import FindTransactionsCommand +from iota.commands.core.get_balances import GetBalancesCommand from iota.commands.core.get_trytes import GetTrytesCommand +from iota.commands.core.were_addresses_spent_from import \ + WereAddressesSpentFromCommand from iota.commands.extended import FindTransactionObjectsCommand from iota.commands.extended.get_bundles import GetBundlesCommand from iota.commands.extended.get_latest_inclusion import \ @@ -25,15 +28,18 @@ def iter_used_addresses( ): # type: (...) -> Generator[Tuple[Address, List[TransactionHash]], None, None] """ - Scans the Tangle for used addresses. + Scans the Tangle for used addresses. A used address is an address that + was spent from or has a balance or has a transaction. This is basically the opposite of invoking ``getNewAddresses`` with - ``stop=None``. + ``count=None``. """ if security_level is None: security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL ft_command = FindTransactionsCommand(adapter) + wasf_command = WereAddressesSpentFromCommand(adapter) + gb_command = GetBalancesCommand(adapter) for addy in AddressGenerator(seed, security_level).create_iterator(start): ft_response = ft_command(addresses=[addy]) @@ -41,10 +47,20 @@ def iter_used_addresses( if ft_response['hashes']: yield addy, ft_response['hashes'] else: - break - - # Reset the command so that we can call it again. + wasp_response = wasf_command(addresses=[addy]) + if wasp_response['states'][0]: + yield addy, [] + else: + gb_response = gb_command(addresses=[addy]) + if gb_response['balances'][0] != 0: + yield addy, [] + else: + break + + # Reset the commands so that we can call them again. ft_command.reset() + wasf_command.reset() + gb_command.reset() def get_bundles_from_transaction_hashes( diff --git a/test/commands/extended/get_account_data_test.py b/test/commands/extended/get_account_data_test.py index fd743f7..383a6ac 100644 --- a/test/commands/extended/get_account_data_test.py +++ b/test/commands/extended/get_account_data_test.py @@ -435,3 +435,28 @@ def test_no_transactions(self): 'bundles': [], }, ) + + def test_balance_is_found_for_address_without_transaction(self): + """ + If an address has a balance but no transaction due to a snapshot the + balance should still be found and returned. + """ + with mock.patch( + 'iota.commands.extended.get_account_data.iter_used_addresses', + mock.Mock(return_value=[(self.addy1, [])]), + ): + self.adapter.seed_response('getBalances', { + 'balances': [42], + }) + + response = self.command(seed=Seed.random()) + + self.assertDictEqual( + response, + + { + 'addresses': [self.addy1], + 'balance': 42, + 'bundles': [], + }, + ) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index de62d9f..7674b75 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -590,12 +590,9 @@ def test_no_stop_threshold_met(self): """ No ``stop`` provided, balance meets ``threshold``. """ - self.adapter.seed_response('getBalances', { - 'balances': [42, 29], - }) - # ``getInputs`` uses ``findTransactions`` to identify unused - # addresses. + # ``getInputs`` uses ``findTransactions``, ``getBalances`` and + # ``wereAddressesSpentFrom`` to identify unused addresses. # noinspection SpellCheckingInspection self.adapter.seed_response('findTransactions', { 'hashes': [ @@ -620,6 +617,18 @@ def test_no_stop_threshold_met(self): 'hashes': [], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [42, 29], + }) + # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that # functionality, so we can get away with mocking it here. @@ -686,12 +695,9 @@ def test_no_stop_threshold_zero(self): """ No ``stop`` provided, ``threshold`` is 0. """ - # Note that the first address has a zero balance. - self.adapter.seed_response('getBalances', { - 'balances': [0, 1], - }) - # ``getInputs`` uses ``findTransactions`` to identify unused + # ``getInputs`` uses ``findTransactions``, ``getBalances`` and + # ``wereAddressesSpentFrom`` to identify unused addresses. # addresses. # noinspection SpellCheckingInspection self.adapter.seed_response('findTransactions', { @@ -717,6 +723,19 @@ def test_no_stop_threshold_zero(self): 'hashes': [], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + + # Note that the first address has a zero balance. + self.adapter.seed_response('getBalances', { + 'balances': [0, 1], + }) + # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that # functionality, so we can get away with mocking it here. @@ -750,12 +769,9 @@ def test_no_stop_no_threshold(self): """ No ``stop`` provided, no ``threshold``. """ - self.adapter.seed_response('getBalances', { - 'balances': [42, 29], - }) - # ``getInputs`` uses ``findTransactions`` to identify unused - # addresses. + # ``getInputs`` uses ``findTransactions``, ``getBalances`` and + # ``wereAddressesSpentFrom`` to identify unused addresses. # noinspection SpellCheckingInspection self.adapter.seed_response('findTransactions', { 'hashes': [ @@ -780,6 +796,18 @@ def test_no_stop_no_threshold(self): 'hashes': [], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [42, 29], + }) + # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that # functionality, so we can get away with mocking it here. @@ -818,12 +846,9 @@ def test_start(self): """ Using ``start`` to offset the key range. """ - self.adapter.seed_response('getBalances', { - 'balances': [86], - }) - # ``getInputs`` uses ``findTransactions`` to identify unused - # addresses. + # ``getInputs`` uses ``findTransactions``, ``getBalances`` and + # ``wereAddressesSpentFrom`` to identify unused addresses. # noinspection SpellCheckingInspection self.adapter.seed_response('findTransactions', { 'hashes': [ @@ -838,6 +863,18 @@ def test_start(self): 'hashes': [], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [86], + }) + # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that # functionality, so we can get away with mocking it here. @@ -926,11 +963,8 @@ def test_security_level_1_no_stop(self): seed = Seed.random() address = AddressGenerator(seed, security_level=1).get_addresses(0)[0] - self.adapter.seed_response('getBalances', { - 'balances': [86], - }) - # ``getInputs`` uses ``findTransactions`` to identify unused - # addresses. + # ``getInputs`` uses ``findTransactions``, ``getBalances`` and + # ``wereAddressesSpentFrom`` to identify unused addresses. # noinspection SpellCheckingInspection self.adapter.seed_response('findTransactions', { 'hashes': [ @@ -944,6 +978,18 @@ def test_security_level_1_no_stop(self): 'hashes': [], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + + self.adapter.seed_response('getBalances', { + 'balances': [86], + }) + response = GetInputsCommand(self.adapter)( seed=seed, securityLevel=1, diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index 735aa90..ccd8d4b 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -423,22 +423,23 @@ def test_security_level(self): }, ) - def test_get_addresses_online(self): + def test_get_addresses_online_already_spent_from(self): """ - Generate address in online mode (filtering used addresses). + Generate address in online mode (filtering used addresses). Test if an + address that was already spent from will not be returned. """ - # Pretend that ``self.addy1`` has already been used, but not - # ``self.addy2``. - # noinspection SpellCheckingInspection - self.adapter.seed_response('findTransactions', { - 'duration': 18, - - 'hashes': [ - 'TESTVALUE9DONTUSEINPRODUCTION99999ITQLQN' - 'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', - ], + # Pretend that ``self.addy1`` has already been spent from, but + # ``self.addy2`` is not used. + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [True], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) self.adapter.seed_response('findTransactions', { 'duration': 1, 'hashes': [], @@ -461,14 +462,152 @@ def test_get_addresses_online(self): self.assertListEqual( self.adapter.requests, - # The command issued two `findTransactions` API requests: one for - # each address generated, until it found an unused address. + # The command issued a `wereAddressesSpentFrom` API requests to check + # If the first address was already spent from. Then it calls + # `wereAddressesSpentFrom`, `getBalances` and `findTransactions` to + # verify that the second address is unused. [ + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.addy_1], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.addy_2], + }, + { + 'command': 'getBalances', + 'addresses': [self.addy_2], + 'threshold': 100, + }, { 'command': 'findTransactions', + 'addresses': [self.addy_2], + }, + ], + ) + + def test_get_addresses_online_has_balance(self): + """ + Generate address in online mode (filtering used addresses). Test if an + address that has a balance will not be returned. + """ + # Pretend that ``self.addy1`` has currently a balance, but + # ``self.addy2`` is not used. + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [42], + }) + + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + response = self.command(index=0, seed=self.seed) + + # The command determined that ``self.addy1`` was already used, so + # it skipped that one. + self.assertDictEqual(response, {'addresses': [self.addy_2]}) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'wereAddressesSpentFrom', 'addresses': [self.addy_1], }, + { + 'command': 'getBalances', + 'addresses': [self.addy_1], + 'threshold': 100, + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.addy_2], + }, + { + 'command': 'getBalances', + 'addresses': [self.addy_2], + 'threshold': 100, + }, + { + 'command': 'findTransactions', + 'addresses': [self.addy_2], + }, + ], + ) + def test_get_addresses_online_has_transaction(self): + """ + Generate address in online mode (filtering used addresses). Test if an + address that has a transaction will not be returned. + """ + # Pretend that ``self.addy1`` has a transaction, but + # ``self.addy2`` is not used. + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'duration': 18, + 'hashes': [ + 'TESTVALUE9DONTUSEINPRODUCTION99999ITQLQN' + 'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', + ], + }) + + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + response = self.command(index=0, seed=self.seed) + + # The command determined that ``self.addy1`` was already used, so + # it skipped that one. + self.assertDictEqual(response, {'addresses': [self.addy_2]}) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.addy_1], + }, + { + 'command': 'getBalances', + 'addresses': [self.addy_1], + 'threshold': 100, + }, + { + 'command': 'findTransactions', + 'addresses': [self.addy_1], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.addy_2], + }, + { + 'command': 'getBalances', + 'addresses': [self.addy_2], + 'threshold': 100, + }, { 'command': 'findTransactions', 'addresses': [self.addy_2], diff --git a/test/commands/extended/get_transfers_test.py b/test/commands/extended/get_transfers_test.py index ff2f4f6..e780924 100644 --- a/test/commands/extended/get_transfers_test.py +++ b/test/commands/extended/get_transfers_test.py @@ -372,7 +372,8 @@ def create_generator(ag, start, step=1): }, ) - # The second address is unused. + # The second address is unused. It has no transactions, was not spent from + # and has no balance. self.adapter.seed_response( 'findTransactions', @@ -381,6 +382,18 @@ def create_generator(ag, start, step=1): 'hashes': [], }, ) + self.adapter.seed_response( + 'wereAddressesSpentFrom', + { + 'states': [False], + }, + ) + self.adapter.seed_response( + 'getBalances', + { + 'balances': [0], + }, + ) self.adapter.seed_response( 'getTrytes', @@ -461,6 +474,18 @@ def create_generator(ag, start, step=1): 'hashes': [], }, ) + self.adapter.seed_response( + 'wereAddressesSpentFrom', + { + 'states': [False], + }, + ) + self.adapter.seed_response( + 'getBalances', + { + 'balances': [0], + }, + ) with mock.patch( 'iota.crypto.addresses.AddressGenerator.create_iterator', @@ -495,7 +520,8 @@ def create_generator(ag, start, step=1): }, ) - # The second address is unused. + # The second address is unused. It has no transactions, was not spent from + # and has no balance. self.adapter.seed_response( 'findTransactions', @@ -504,6 +530,18 @@ def create_generator(ag, start, step=1): 'hashes': [], }, ) + self.adapter.seed_response( + 'wereAddressesSpentFrom', + { + 'states': [False], + }, + ) + self.adapter.seed_response( + 'getBalances', + { + 'balances': [0], + }, + ) self.adapter.seed_response( 'getTrytes', diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index 91f3544..cad0646 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -1301,17 +1301,26 @@ def test_security_level(self): def mock_get_balances_execute(adapter, request): # returns balances of input addresses equal to SEND_VALUE + security_level * 11 addr = request["addresses"][0] - security_level = [l for l, a in mock_addresses.items() if str(a) == addr][0] - return dict(balances=[SEND_VALUE + security_level * 11], milestone=None) + if addr in [str(a) for a in mock_addresses.values()]: + security_level = [l for l, a in mock_addresses.items() if str(a) == addr][0] + balance = SEND_VALUE + security_level * 11 + else: + # Return 0 if getBalances is called in get_new_addresses + balance = 0 + return dict(balances=[balance], milestone=None) # testing for several security levels for security_level in SECURITY_LEVELS_TO_TEST: - # get_new_addresses uses `find_transactions` internaly. + # get_new_addresses uses `find_transactions`, `get_balances` and + # `were_addresses_spent_from` internally. # The following means requested address is considered unused self.adapter.seed_response('findTransactions', { 'hashes': [], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) self.command.reset() with mock.patch( @@ -1371,14 +1380,20 @@ def test_security_level_no_inputs(self): def mock_get_balances_execute(adapter, request): # returns balances of input addresses equal to SEND_VALUE + security_level * 11 addr = request["addresses"][0] - security_level = [l for l, a in addresses.items() if str(a) == addr][0] - return dict(balances=[SEND_VALUE + security_level * 11], milestone=None) + if addr in [str(a) for a in addresses.values()]: + security_level = [l for l, a in addresses.items() if str(a) == addr][0] + balance = SEND_VALUE + security_level * 11 + else: + # Return 0 if getBalances is called in iter_used_addresses or + # get_new_addresses + balance = 0 + return dict(balances=[balance], milestone=None) # testing several security levels for security_level in SECURITY_LEVELS_TO_TEST: - # get_inputs use iter_used_addresses and findTransactions. - # until address without tx found + # get_inputs uses iter_used_addresses, findTransactions, + # wereAddressesSpentfrom and getBalances until an unused address is found self.adapter.seed_response('findTransactions', { 'hashes': [ TransactionHash( @@ -1390,8 +1405,18 @@ def mock_get_balances_execute(adapter, request): self.adapter.seed_response('findTransactions', { 'hashes': [], }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) - # get_new_addresses uses `find_transactions` internaly. + # get_new_addresses uses `find_transactions`, `get_balances` and + # `were_addresses_spent_from` internally. + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) self.adapter.seed_response('findTransactions', { 'hashes': [], }) diff --git a/test/commands/extended/utils_test.py b/test/commands/extended/utils_test.py new file mode 100644 index 0000000..1970089 --- /dev/null +++ b/test/commands/extended/utils_test.py @@ -0,0 +1,351 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota.commands.extended.utils import iter_used_addresses + +from iota import MockAdapter +from iota.crypto.types import Seed +from test import mock + + +class IterUsedAddressesTestCase(TestCase): + def setUp(self): + super(IterUsedAddressesTestCase, self).setUp() + + self.adapter = MockAdapter() + self.seed = Seed(trytes='S' * 81) + self.address0 = 'A' * 81 + self.address1 = 'B' * 81 + self.address2 = 'C' * 81 + self.address3 = 'D' * 81 + + # To speed up the tests, we will mock the address generator. + def address_generator(ag, start, step=1): + for addy in [self.address0, self.address1, self.address2, + self.address3][start::step]: + yield addy + self.mock_address_generator = address_generator + + def seed_unused_address(self): + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [0], + }) + + def get_all_used_addresses(self, start=0): + return [address for address, _ + in iter_used_addresses(self.adapter, self.seed, start)] + + def test_fist_address_is_not_used(self): + """ + The very fist address is not used. No address is returned. + """ + # Address 0 + self.seed_unused_address() + + with mock.patch( + 'iota.crypto.addresses.AddressGenerator.create_iterator', + self.mock_address_generator, + ): + self.assertEqual([], self.get_all_used_addresses()) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'findTransactions', + 'addresses': [self.address0], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address0], + }, + { + 'command': 'getBalances', + 'addresses': [self.address0], + 'threshold': 100, + }, + ] + ) + + def test_transactions_are_considered_used(self): + """ + An address with a transaction is considered used. + """ + # Address 0 + self.adapter.seed_response('findTransactions', { + 'hashes': ['T' * 81], + }) + + # Address 1 + self.seed_unused_address() + + with mock.patch( + 'iota.crypto.addresses.AddressGenerator.create_iterator', + self.mock_address_generator, + ): + self.assertEqual([self.address0], self.get_all_used_addresses()) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'findTransactions', + 'addresses': [self.address0], + }, + { + 'command': 'findTransactions', + 'addresses': [self.address1], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address1], + }, + { + 'command': 'getBalances', + 'addresses': [self.address1], + 'threshold': 100, + }, + ] + ) + + def test_spent_from_is_considered_used(self): + """ + An address that was spent from is considered used. + """ + # Address 0 + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [True], + }) + + # Address 1 + self.seed_unused_address() + + with mock.patch( + 'iota.crypto.addresses.AddressGenerator.create_iterator', + self.mock_address_generator, + ): + self.assertEqual([self.address0], self.get_all_used_addresses()) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'findTransactions', + 'addresses': [self.address0], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address0], + }, + { + 'command': 'findTransactions', + 'addresses': [self.address1], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address1], + }, + { + 'command': 'getBalances', + 'addresses': [self.address1], + 'threshold': 100, + }, + ] + ) + + def test_balance_is_considered_used(self): + """ + An address that that has a balance is considered used + """ + # Address 0 + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [42], + }) + + # Address 1 + self.seed_unused_address() + + with mock.patch( + 'iota.crypto.addresses.AddressGenerator.create_iterator', + self.mock_address_generator, + ): + self.assertEqual([self.address0], self.get_all_used_addresses()) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'findTransactions', + 'addresses': [self.address0], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address0], + }, + { + 'command': 'getBalances', + 'addresses': [self.address0], + 'threshold': 100, + }, + { + 'command': 'findTransactions', + 'addresses': [self.address1], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address1], + }, + { + 'command': 'getBalances', + 'addresses': [self.address1], + 'threshold': 100, + }, + ] + ) + + def test_start_parameter_is_given(self): + """ + The correct address is returned if a start parameter is given + """ + # Address 1 + self.adapter.seed_response('findTransactions', { + 'hashes': ['T' * 81], + }) + + # Address 2 + self.seed_unused_address() + + with mock.patch( + 'iota.crypto.addresses.AddressGenerator.create_iterator', + self.mock_address_generator, + ): + self.assertEqual([self.address1], + self.get_all_used_addresses(start=1)) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'findTransactions', + 'addresses': [self.address1], + }, + { + 'command': 'findTransactions', + 'addresses': [self.address2], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address2], + }, + { + 'command': 'getBalances', + 'addresses': [self.address2], + 'threshold': 100, + }, + ] + ) + + def test_multiple_addresses_return(self): + """ + A larger test that combines multiple cases and more than one address + should be returned. + Address 0: Has a balance + Address 1: Was spent from + Address 2: Has a transaction + Address 3: Is not used. Should not be returned + """ + # Address 0 + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [False], + }) + self.adapter.seed_response('getBalances', { + 'balances': [42], + }) + + # Address 1 + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + self.adapter.seed_response('wereAddressesSpentFrom', { + 'states': [True], + }) + + # Address 2 + self.adapter.seed_response('findTransactions', { + 'hashes': ['T' * 81], + }) + + # Address 3 + self.seed_unused_address() + + with mock.patch( + 'iota.crypto.addresses.AddressGenerator.create_iterator', + self.mock_address_generator, + ): + self.assertEqual([self.address0, self.address1, self.address2], + self.get_all_used_addresses()) + + self.assertListEqual( + self.adapter.requests, + [ + { + 'command': 'findTransactions', + 'addresses': [self.address0], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address0], + }, + { + 'command': 'getBalances', + 'addresses': [self.address0], + 'threshold': 100, + }, + { + 'command': 'findTransactions', + 'addresses': [self.address1], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address1], + }, + { + 'command': 'findTransactions', + 'addresses': [self.address2], + }, + { + 'command': 'findTransactions', + 'addresses': [self.address3], + }, + { + 'command': 'wereAddressesSpentFrom', + 'addresses': [self.address3], + }, + { + 'command': 'getBalances', + 'addresses': [self.address3], + 'threshold': 100, + }, + ] + )