Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #115 from scottbelden/check_consistency_api
Browse files Browse the repository at this point in the history
add check_consistency api
  • Loading branch information
todofixthis committed Dec 26, 2017
2 parents 8d2c2e8 + 8e0a18a commit 8e8351c
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 0 deletions.
28 changes: 28 additions & 0 deletions iota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,34 @@ def broadcast_transactions(self, trytes):
"""
return core.BroadcastTransactionsCommand(self.adapter)(trytes=trytes)

def check_consistency(self, tails):
# type: (Iterable[TransactionHash]) -> dict
"""
Used to ensure tail resolves to a consistent ledger which is necessary to
validate before attempting promotionChecks transaction hashes for
promotability.
This is called with a pending transaction (or more of them) and it will
tell you if it is still possible for this transaction (or all the
transactions simultaneously if you give more than one) to be confirmed, or
not (because it conflicts with another already confirmed transaction).
:param tails:
Transaction hashes. Must be tail transactions.
:return:
Dict containing the following::
{
'state': bool,
'info': str,
This field will only exist set if `state` is False.
}
"""
return core.CheckConsistencyCommand(self.adapter)(
tails = tails,
)

def find_transactions(
self,
bundles = None,
Expand Down
1 change: 1 addition & 0 deletions iota/commands/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .add_neighbors import *
from .attach_to_tangle import *
from .broadcast_transactions import *
from .check_consistency import *
from .find_transactions import *
from .get_balances import *
from .get_inclusion_states import *
Expand Down
38 changes: 38 additions & 0 deletions iota/commands/core/check_consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# coding=utf-8
from __future__ import absolute_import, division, print_function, \
unicode_literals

import filters as f
from iota import Transaction, TransactionHash
from iota.commands import FilterCommand, RequestFilter
from iota.filters import Trytes

__all__ = [
'CheckConsistencyCommand',
]


class CheckConsistencyCommand(FilterCommand):
"""
Executes ``checkConsistency`` extended API command.
See :py:meth:`iota.api.Iota.check_consistency` for more info.
"""
command = 'checkConsistency'

def get_request_filter(self):
return CheckConsistencyRequestFilter()

def get_response_filter(self):
pass


class CheckConsistencyRequestFilter(RequestFilter):
def __init__(self):
super(CheckConsistencyRequestFilter, self).__init__({
'tails': (
f.Required
| f.Array
| f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash))
),
})
247 changes: 247 additions & 0 deletions test/commands/core/check_consistency_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# 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
from iota.adapter import MockAdapter
from iota.commands.core.check_consistency import CheckConsistencyCommand
from iota.filters import Trytes


class CheckConsistencyRequestFilterTestCase(BaseFilterTestCase):
filter_type = CheckConsistencyCommand(MockAdapter()).get_request_filter
skip_value_check = True

# noinspection SpellCheckingInspection
def setUp(self):
super(CheckConsistencyRequestFilterTestCase, 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 CheckConsistencyCommandTestCase(TestCase):
# noinspection SpellCheckingInspection
def setUp(self):
super(CheckConsistencyCommandTestCase, self).setUp()

self.adapter = MockAdapter()
self.command = CheckConsistencyCommand(self.adapter)

# Define some tryte sequences that we can re-use across tests.
self.milestone =\
TransactionHash(
b'TESTVALUE9DONTUSEINPRODUCTION99999W9KDIH'
b'BALAYAFCADIDU9HCXDKIXEYDNFRAKHN9IEIDZFWGJ'
)

self.hash1 =\
TransactionHash(
b'TESTVALUE9DONTUSEINPRODUCTION99999TBPDM9'
b'ADFAWCKCSFUALFGETFIFG9UHIEFE9AYESEHDUBDDF'
)

self.hash2 =\
TransactionHash(
b'TESTVALUE9DONTUSEINPRODUCTION99999CIGCCF'
b'KIUFZF9EP9YEYGQAIEXDTEAAUGAEWBBASHYCWBHDX'
)

def test_wireup(self):
"""
Verify that the command is wired up correctly.
"""
self.assertIsInstance(
Iota(self.adapter).checkConsistency,
CheckConsistencyCommand,
)

def test_happy_path(self):
"""
Successfully checking consistency.
"""

self.adapter.seed_response('checkConsistency', {
'state': True,
})

response = self.command(tails=[self.hash1, self.hash2])

self.assertDictEqual(
response,

{
'state': True,
}
)

def test_info_with_false_state(self):
"""
`info` field exists when `state` is False.
"""

self.adapter.seed_response('checkConsistency', {
'state': False,
'info': 'Additional information',
})

response = self.command(tails=[self.hash1, self.hash2])

self.assertDictEqual(
response,

{
'state': False,
'info': 'Additional information',
}
)

0 comments on commit 8e8351c

Please sign in to comment.