From aa878fb2993f1e9450e93a70746ac94419679038 Mon Sep 17 00:00:00 2001 From: Levente Pap Date: Tue, 29 Oct 2019 17:43:53 +0100 Subject: [PATCH 1/2] Implement is_promotable Extended API Command Checks if a list of tails can be promoted or not by calling checkConsistency core API and performing extra checks on the attachment_timestamp of the transactions fetched from the Tangle. --- docs/api.rst | 35 ++ iota/api.py | 37 ++ iota/commands/extended/__init__.py | 1 + iota/commands/extended/is_promotable.py | 115 ++++++ test/commands/extended/is_promotable_test.py | 407 +++++++++++++++++++ 5 files changed, 595 insertions(+) create mode 100644 iota/commands/extended/is_promotable.py create mode 100644 test/commands/extended/is_promotable_test.py diff --git a/docs/api.rst b/docs/api.rst index bcb3d186..16fe936d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -278,6 +278,41 @@ This method returns a ``dict`` with the following items: - ``bundles: List[Bundle]``: Matching bundles, sorted by tail transaction timestamp. +``is_promotable`` +------------------- + +This extended API function helps you to determine whether a tail transaction +(bundle) is promotable. +Example usage could be to determine if a transaction can be promoted or you +should reattach (``replay_bundle``). + +The method takes a list of tail transaction hashes, calls ``check_consistency`` +to verify consistency. If successful, fetches the transaction trytes from the +Tangle and checks if ``attachment_timestamp`` is within reasonable limits. + +Parameters +~~~~~~~~~~ + +- ``tails: List[TransactionHash]``: Tail transaction hashes to check. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``promotable: bool``: ``True``, if: + + - Tails are consistent. + See `API Reference `_. + - ``attachment_timestamp`` for all transactions are less than current time + and attachement happened no earlier than ``depth`` milestones. + By default, ``depth`` = 6. + + parameter is ``False`` otherwise. + +- ``info: Optional(List[String])``: If ``promotable`` is ``False``, contains information + about the error. + ``is_reattachable`` ------------------- diff --git a/iota/api.py b/iota/api.py index 3e0a55e1..6ea1ec6f 100644 --- a/iota/api.py +++ b/iota/api.py @@ -1002,6 +1002,43 @@ def get_transfers(self, start=0, stop=None, inclusion_states=False): inclusionStates=inclusion_states, ) + def is_promotable( + self, + tails, # type: Iterable[TransactionHash] + ): + # type: (Iterable(TransactionHash)] -> dict + """ + Checks if tail transaction(s) is promotable by calling + :py:meth:`check_consistency` and verifying that `attachmentTimestamp` + is above a lower bound. + Lower bound is calculated based on number of milestones issued + since transaction attachment. + + :param tails: + List of tail transaction hashes. + + :return: + The return type mimics that of :py:meth:`check_consistency`. + Dict with the following structure:: + + { + 'promotable': bool, + If true, all tails are promotable. If false, see `info` + field. + + 'info': Optional(List[String]) + If `promotable` is false, this contains info about what + went wrong. + + } + + References: + - https://github.com/iotaledger/iota.js/blob/next/api_reference.md#module_core.isPromotable + """ + return extended.IsPromotableCommand(self.adapter)( + tails=tails, + ) + def prepare_transfer( self, transfers, # type: Iterable[ProposedTransaction] diff --git a/iota/commands/extended/__init__.py b/iota/commands/extended/__init__.py index e9e55c02..c399a03a 100644 --- a/iota/commands/extended/__init__.py +++ b/iota/commands/extended/__init__.py @@ -21,6 +21,7 @@ from .get_new_addresses import * from .get_transaction_objects import * from .get_transfers import * +from .is_promotable import * from .is_reattachable import * from .prepare_transfer import * from .promote_transaction import * diff --git a/iota/commands/extended/is_promotable.py b/iota/commands/extended/is_promotable.py new file mode 100644 index 00000000..4e3fa2d1 --- /dev/null +++ b/iota/commands/extended/is_promotable.py @@ -0,0 +1,115 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter +from iota.commands.core import CheckConsistencyCommand, GetTrytesCommand +from iota.transaction import Transaction +from iota import TransactionHash +import filters as f +from iota.filters import Trytes +import time + +__all__ = [ + 'IsPromotableCommand', +] + +MILESTONE_INTERVAL = 2 * 60 * 1000 +""" +Approximate interval in which a milestone is issued. +Unit is in milliseconds, so it is roughly 2 minutes. +""" + +ONE_WAY_DELAY = 1 * 60 * 1000 +""" +Propagation delay of the network (ms). (really-really worst case scenario) +The time needed for the message to propegate from client to edges of +majority network. +""" + +DEPTH = 6 +""" +The number of milestones issued since `attachmentTimestamp`. +""" + +get_current_ms = lambda : int(round(time.time() * 1000)) +""" +Calculate current time in milliseconds. +""" + +class IsPromotableCommand(FilterCommand): + """ + Determines if a tail transaction is promotable. + + See :py:meth:`iota.api.Iota.is_promotable` for more info. + """ + command = 'isPromotable' + + def get_request_filter(self): + return IsPromotableRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + tails = request['tails'] + + # First, check consistency + # A transaction is consistent, if: + # - The node isn't missing the transaction's branch or trunk transactions + # - The transaction's bundle is valid + # - The transaction's branch and trunk transactions are valid + cc_response = CheckConsistencyCommand(self.adapter)( + tails=tails, + ) + + if not cc_response['state']: + # One or more transactions are inconsistent + return { + 'promotable' : False, + 'info' : cc_response['info'], + } + + transactions = [ + Transaction.from_tryte_string(x) for x in + GetTrytesCommand(self.adapter)(hashes=tails)['trytes'] + ] + + response = { + 'promotable' : True, + 'info' : [], + } + + # Check timestamps + for tx in transactions: + is_within = is_within_depth(tx.attachment_timestamp) + if not is_within: + # Inform the user about what went wrong. + response['info'].append('Transaction {tx_hash} is above max depth.'.format( + tx_hash=tx.hash + )) + # If one tx fails, response is false + response['promotable'] = response['promotable'] and is_within + + # If there are no problems, we don't need 'info' field + if response['promotable']: + del response['info'] + + return response + +class IsPromotableRequestFilter(RequestFilter): + def __init__(self): + super(IsPromotableRequestFilter, self).__init__({ + 'tails': + f.Required | + f.Array | + f.FilterRepeater(f.Required | Trytes(TransactionHash)), + }) + +def is_within_depth(attachment_timestamp, depth=DEPTH): + """ + Checks if `attachment_timestamp` is within limits of `depth`. + """ + now = get_current_ms() + return attachment_timestamp < now and \ + now - attachment_timestamp < depth * MILESTONE_INTERVAL - ONE_WAY_DELAY \ No newline at end of file diff --git a/test/commands/extended/is_promotable_test.py b/test/commands/extended/is_promotable_test.py new file mode 100644 index 00000000..04961b38 --- /dev/null +++ b/test/commands/extended/is_promotable_test.py @@ -0,0 +1,407 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +import filters as f +from filters.test import BaseFilterTestCase + +from iota import Iota, TransactionHash, TryteString, TransactionTrytes, \ + Transaction +from iota.adapter import MockAdapter +from iota.commands.extended.is_promotable import IsPromotableCommand, \ + get_current_ms, is_within_depth +from iota.filters import Trytes +from test import mock + +class IsPromotableRequestFilterTestCase(BaseFilterTestCase): + filter_type = IsPromotableCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(IsPromotableRequestFilterTestCase, self).setUp() + + self.hash1 = ( + 'TESTVALUE9DONTUSEINPRODUCTION99999DXSCAD' + 'YBVDCTTBLHFYQATFZPYPCBG9FOUKIGMYIGLHM9NEZ' + ) + + self.hash2 = ( + 'TESTVALUE9DONTUSEINPRODUCTION99999EMFYSM' + 'HWODIAPUTTFDLQRLYIDAUIPJXXEXZZSBVKZEBWGAN' + ) + + def test_pass_happy_path(self): + """ + Request is valid. + """ + request = { + # Raw trytes are extracted to match the IRI's JSON protocol. + 'tails': [self.hash1, self.hash2], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + 'tails': [ + # Any TrytesCompatible value can be used here. + TransactionHash(self.hash1), + bytearray(self.hash2.encode('ascii')), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + # Raw trytes are extracted to match the IRI's JSON protocol. + 'tails': [self.hash1, self.hash2], + }, + ) + + def test_fail_empty(self): + """ + Request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'tails': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'tails': [TransactionHash(self.hash1)], + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_tails_null(self): + """ + ``tails`` is null. + """ + self.assertFilterErrors( + { + 'tails': None, + }, + + { + 'tails': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_tails_wrong_type(self): + """ + ``tails`` is not an array. + """ + self.assertFilterErrors( + { + # It's gotta be an array, even if there's only one hash. + 'tails': TransactionHash(self.hash1), + }, + + { + 'tails': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_tails_empty(self): + """ + ``tails`` is an array, but it is empty. + """ + self.assertFilterErrors( + { + 'tails': [], + }, + + { + 'tails': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_tails_contents_invalid(self): + """ + ``tails`` is a non-empty array, but it contains invalid values. + """ + self.assertFilterErrors( + { + 'tails': [ + b'', + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.hash1), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'tails.0': [f.Required.CODE_EMPTY], + 'tails.1': [f.Type.CODE_WRONG_TYPE], + 'tails.2': [f.Required.CODE_EMPTY], + 'tails.3': [Trytes.CODE_NOT_TRYTES], + 'tails.5': [f.Type.CODE_WRONG_TYPE], + 'tails.6': [Trytes.CODE_WRONG_FORMAT], + }, + ) + + +class IsPromotableCommandTestCase(TestCase): + def setUp(self): + super(IsPromotableCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = IsPromotableCommand(self.adapter) + + # Define some tryte sequences that we can re-use across tests. + self.trytes1 = TransactionTrytes( + 'CCGCVADBEACCWCXCGDEAXCGDEAPCEAHDTCGDHDEAHDFDPCBDGDPCRCHDXCCDBDE' + 'ACDBD9DMDSA9999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999ETTEXDKDEUALTLRJVX' + 'RHCPRJDLGPJCEQBJOMOAGBZWZCWLNUEWHAUSYJMYPEZPYNBTPSPGUIPQ9VOUNQ9' + '999999999999999999999999999JVPROMOTABLETEST99999999999USHRPBD99' + '999999999999999999XFVLEXEJPTYI9TUA9ULFNHXBGDUCOEPDIBKSZFXEBO9HF' + 'EGLENBCOVKHZ99IWZVCVSTUGKTIBEOVFBJPCDYHBDEIIBLHRVQX9KVVRTUIQMOF' + 'XUUETRIQCCCLSMVREZSNEXLIZCIUYIYRBJIBOKNJCQAJTAHGNZ9999DYHBDEIIB' + 'LHRVQX9KVVRTUIQMOFXUUETRIQCCCLSMVREZSNEXLIZCIUYIYRBJIBOKNJCQAJT' + 'AHGNZ9999ISPROMOTABLETEST9999999999999BFQOIOF999999999MMMMMMMMM' + 'EL999999999AG99999999999999' + ) + + self.hash1 = TransactionHash( + 'MHNBILKFU9CADOPNWSFYOMILGKJAHEU9GSSOYUEAPBGOOLAIKGBYSACXMFQRJZE' + 'PBSHI9SDKMBRK99999' + ) + + self.trytes2 = TransactionTrytes( + 'CCGCVADBEACCWCXCGDEAXCGDEAPCEAHDTCGDHDEAHDFDPCBDGDPCRCHDXCCDBDEA' + 'CDBD9DMDSA999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '9999999999999999999999999999999999999999999999999999999999999999' + '99999999999ETTEXDKDEUALTLRJVXRHCPRJDLGPJCEQBJOMOAGBZWZCWLNUEWHAU' + 'SYJMYPEZPYNBTPSPGUIPQ9VOUNQ9999999999999999999999999999JVPROMOTA' + 'BLETEST99999999999USHRPBD99999999999999999999XFVLEXEJPTYI9TUA9UL' + 'FNHXBGDUCOEPDIBKSZFXEBO9HFEGLENBCOVKHZ99IWZVCVSTUGKTIBEOVFBJPCDA' + 'WCMHRLDQPBBGISNENMIXOGGYSRYXGAFEJC9FOLXLYIQVUHFCMVRPBIEAXDUYYPYN' + 'EZPHH9KB9HZ9999DAWCMHRLDQPBBGISNENMIXOGGYSRYXGAFEJC9FOLXLYIQVUHF' + 'CMVRPBIEAXDUYYPYNEZPHH9KB9HZ9999ISPROMOTABLETEST99999999999IOCFQ' + 'OIOF999999999MMMMMMMMMCAA9999999UYA99999999999999' + ) + + self.hash2 = TransactionHash( + 'FLNPRAOEYMBIXZBBFMQGCEWLRKTZTMWWTVUQRNUNMZR9EMVKETRMWHRMBFWHJHX' + 'ZOIMUWZALX9IVZ9999' + ) + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).isPromotable, + IsPromotableCommand, + ) + + def test_happy_path(self): + """ + Successfully checking promotability. + """ + # To pass timestamp check, we need a timestamp that defines + # a moment no earlier than 660s from the moment the function + # is called during test. To do this, we cheat a bit: + tx = Transaction.from_tryte_string(self.trytes1) + tx.attachment_timestamp = get_current_ms() + # self.trytes1 becomes invalid transaction since we modified + # attachment_timestamp, but it doesn't matter here + self.trytes1 = tx.as_tryte_string() + # The same for self.trytes2 + tx = Transaction.from_tryte_string(self.trytes2) + tx.attachment_timestamp = get_current_ms() + self.trytes2 = tx.as_tryte_string() + + self.adapter.seed_response('checkConsistency', { + 'state': True, + }) + self.adapter.seed_response('getTrytes', { + 'trytes': [self.trytes1, self.trytes2] + }) + + response = self.command(tails=[self.hash1, self.hash2]) + + self.assertDictEqual( + response, + + { + 'promotable': True, + } + ) + + def test_not_consistent(self): + """ + One of the tails is not consistent. + """ + + self.adapter.seed_response('checkConsistency', { + 'state': False, + 'info': 'Oops, something went wrong.', + }) + + # No need for mokcing `getTrytes` becasue we should not + # reach that part + + response = self.command(tails=[self.hash1, self.hash2]) + + self.assertDictEqual( + response, + + { + 'promotable': False, + 'info': 'Oops, something went wrong.', + } + ) + + def test_one_timestamp_invalid(self): + """ + Test invalid timestamp in one of the transactions. + """ + # Note that self.trytes2 will have the original and + # therefore invalid (too old) timestamp + tx = Transaction.from_tryte_string(self.trytes1) + tx.attachment_timestamp = get_current_ms() + self.trytes1 = tx.as_tryte_string() + + self.adapter.seed_response('checkConsistency', { + 'state': True, + }) + self.adapter.seed_response('getTrytes', { + 'trytes': [self.trytes1, self.trytes2] + }) + + response = self.command(tails=[self.hash1, self.hash2]) + + self.assertDictEqual( + response, + + { + 'promotable': False, + 'info': ['Transaction {tx_hash} is above max depth.'.format( + tx_hash=self.hash2 + )], + } + ) + + def test_is_within_depth(self): + """ + Test ``is_within_depth`` helper method. + """ + # Timestamp is too old (depth=3) + old_timestamp = get_current_ms() - 660000 + + self.assertEqual( + is_within_depth(old_timestamp), + False + ) + + # Timestamp points to the future + future_timestamp = get_current_ms() + 500000 + + self.assertEqual( + is_within_depth(future_timestamp), + False + ) + + # Timestamp is valid (one second 'old') + timestamp = get_current_ms() - 1000 + + self.assertEqual( + is_within_depth(timestamp), + True + ) \ No newline at end of file From d9a61a67aaced25b27a57333c98dba6a0a5e3b24 Mon Sep 17 00:00:00 2001 From: Levente Pap Date: Tue, 5 Nov 2019 11:26:52 +0100 Subject: [PATCH 2/2] is_promotable: Test improvements and optimization --- iota/commands/extended/is_promotable.py | 9 +-- test/commands/extended/is_promotable_test.py | 59 ++++++++++---------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/iota/commands/extended/is_promotable.py b/iota/commands/extended/is_promotable.py index 4e3fa2d1..11f4bb83 100644 --- a/iota/commands/extended/is_promotable.py +++ b/iota/commands/extended/is_promotable.py @@ -81,8 +81,9 @@ def _execute(self, request): } # Check timestamps + now = get_current_ms() for tx in transactions: - is_within = is_within_depth(tx.attachment_timestamp) + is_within = is_within_depth(tx.attachment_timestamp, now) if not is_within: # Inform the user about what went wrong. response['info'].append('Transaction {tx_hash} is above max depth.'.format( @@ -93,7 +94,7 @@ def _execute(self, request): # If there are no problems, we don't need 'info' field if response['promotable']: - del response['info'] + response['info'] = None return response @@ -106,10 +107,10 @@ def __init__(self): f.FilterRepeater(f.Required | Trytes(TransactionHash)), }) -def is_within_depth(attachment_timestamp, depth=DEPTH): +def is_within_depth(attachment_timestamp, now, depth=DEPTH): + # type (int, int, Optiona(int)) -> bool """ Checks if `attachment_timestamp` is within limits of `depth`. """ - now = get_current_ms() return attachment_timestamp < now and \ now - attachment_timestamp < depth * MILESTONE_INTERVAL - ONE_WAY_DELAY \ No newline at end of file diff --git a/test/commands/extended/is_promotable_test.py b/test/commands/extended/is_promotable_test.py index 04961b38..6710a35f 100644 --- a/test/commands/extended/is_promotable_test.py +++ b/test/commands/extended/is_promotable_test.py @@ -11,7 +11,7 @@ Transaction from iota.adapter import MockAdapter from iota.commands.extended.is_promotable import IsPromotableCommand, \ - get_current_ms, is_within_depth + get_current_ms, is_within_depth, MILESTONE_INTERVAL, ONE_WAY_DELAY from iota.filters import Trytes from test import mock @@ -281,6 +281,12 @@ def setUp(self): 'ZOIMUWZALX9IVZ9999' ) + # Tuesday, October 29, 2019 4:19:43.600 PM GMT+01:00 + self.valid_now = 1572362383600 + """ + Timestamp that is just greater than the later timestamp in self.trytes. + """ + def test_wireup(self): """ Verify that the command is wired up correctly. @@ -294,18 +300,6 @@ def test_happy_path(self): """ Successfully checking promotability. """ - # To pass timestamp check, we need a timestamp that defines - # a moment no earlier than 660s from the moment the function - # is called during test. To do this, we cheat a bit: - tx = Transaction.from_tryte_string(self.trytes1) - tx.attachment_timestamp = get_current_ms() - # self.trytes1 becomes invalid transaction since we modified - # attachment_timestamp, but it doesn't matter here - self.trytes1 = tx.as_tryte_string() - # The same for self.trytes2 - tx = Transaction.from_tryte_string(self.trytes2) - tx.attachment_timestamp = get_current_ms() - self.trytes2 = tx.as_tryte_string() self.adapter.seed_response('checkConsistency', { 'state': True, @@ -314,15 +308,18 @@ def test_happy_path(self): 'trytes': [self.trytes1, self.trytes2] }) - response = self.command(tails=[self.hash1, self.hash2]) + with mock.patch('iota.commands.extended.is_promotable.get_current_ms', + mock.MagicMock(return_value=self.valid_now)): + response = self.command(tails=[self.hash1, self.hash2]) - self.assertDictEqual( - response, + self.assertDictEqual( + response, - { - 'promotable': True, - } - ) + { + 'promotable': True, + 'info': None, + } + ) def test_not_consistent(self): """ @@ -365,6 +362,9 @@ def test_one_timestamp_invalid(self): 'trytes': [self.trytes1, self.trytes2] }) + # Here we don`t mock get_current_ms. + # Tx 1 will have updated, passing timestamp. + # Tx 2 has the old one, so should fail. response = self.command(tails=[self.hash1, self.hash2]) self.assertDictEqual( @@ -382,26 +382,27 @@ def test_is_within_depth(self): """ Test ``is_within_depth`` helper method. """ - # Timestamp is too old (depth=3) - old_timestamp = get_current_ms() - 660000 + # Timestamp is too old (depth=6) + now = get_current_ms() + old_timestamp = now - (6 * MILESTONE_INTERVAL - ONE_WAY_DELAY) self.assertEqual( - is_within_depth(old_timestamp), + is_within_depth(old_timestamp, now), False ) - # Timestamp points to the future - future_timestamp = get_current_ms() + 500000 + # Timestamp points to the future (any number would do) + future_timestamp = now + 10 self.assertEqual( - is_within_depth(future_timestamp), + is_within_depth(future_timestamp, now), False ) - # Timestamp is valid (one second 'old') - timestamp = get_current_ms() - 1000 + # Timestamp is valid ( appr. one second 'old') + timestamp = now - 1000 self.assertEqual( - is_within_depth(timestamp), + is_within_depth(timestamp, now), True ) \ No newline at end of file