Skip to content
This repository has been archived by the owner on Aug 23, 2020. It is now read-only.

fix broken getInclusionStates API call #1685

Merged
merged 8 commits into from
Jan 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion python-regression/tests/features/machine1/1_api_tests.feature
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,18 @@ Feature: Test API calls on Machine 1

Then the response for "getInclusionStates" should return with:
|keys |values |type |
|states |False |bool |
| states | True | boolListMixed |

#Values can be found in util/static_vals.py
Scenario: GetInclusionStates is called with transaction list
Given "getInclusionStates" is called on "nodeA-m1" with:
| keys | values | type |
| transactions | TEST_HASH_LIST | staticValue |
| tips | TEST_TIP_LIST | staticValue |

Then the response for "getInclusionStates" should return with:
| keys | values | type |
| states | True True False | boolListMixed |


#Address can be found in util/static_vals.py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,23 @@ Feature: Test transaction confirmation

And the response for "getInclusionStates" should return with:
|keys |values |type |
|states |True |bool |
| states | True True True True True True True True True True | boolListMixed |


When a transaction is generated and attached on "nodeA-m2" with:
| keys | values | type |
| address | TEST_ADDRESS | staticValue |
| value | 0 | int |

And "getInclusionStates" is called on "nodeA-m2" with:
| keys | values | type |
| transactions | TEST_STORE_ADDRESS | staticList |
| tips | latestMilestone | configValue |

Then the response for "getInclusionStates" should return with:
| keys | values | type |
| states | False | boolListMixed |


Scenario: Value Transactions are confirmed
In this test, a number of value transactions will be made to a specified node.
Expand Down Expand Up @@ -57,5 +71,19 @@ Feature: Test transaction confirmation

And the response for "getInclusionStates" should return with:
|keys |values |type |
|states |True |bool |
| states | True True True True True True True True True False | boolListMixed |

When a transaction is generated and attached on "nodeA-m2" with:
| keys | values | type |
| address | TEST_ADDRESS | staticValue |
| value | 0 | int |

And "getInclusionStates" is called on "nodeA-m2" with:
| keys | values | type |
| transactions | TEST_STORE_ADDRESS | staticList |
| tips | latestMilestone | configValue |

Then the response for "getInclusionStates" should return with:
| keys | values | type |
| states | False | boolListMixed |

4 changes: 2 additions & 2 deletions python-regression/tests/features/machine2/output.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
nodes:
nodeA:
nodeA-m2:
host: localhost
podip: localhost
ports:
Expand All @@ -10,7 +10,7 @@ nodes:
api: 14265
gossip-tcp: 15600
zmq-feed: 5556
nodeB:
nodeB-m2:
host: localhost
podip: localhost
ports:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from aloe import world, step
from util.response_logic import response_handling as response_handling
from util.test_logic import api_test_logic as api_utils
from util.test_logic import value_fetch_logic
from util.response_logic import response_handling as response_handling

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -84,7 +84,7 @@ def check_response_for_value(step, api_call):
expected_value = expected_values[expected_value_key]
response_value = response_values[expected_value_key]

if isinstance(response_value, list) and api_call != 'getTrytes':
if isinstance(response_value, list) and api_call != 'getTrytes' and api_call != 'getInclusionStates':
response_value = response_value[0]

assert expected_value == response_value, "The expected value {} does not match""\
Expand Down
4 changes: 3 additions & 1 deletion python-regression/util/static_vals.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
CONFIRMATION_REFERENCE_HASH = "ZSPRRUJRXHBSRFGCCPVHNXWJJKXRYZSAU9ZEGWFD9LPTWOJZARRLOEQYYWIKOPSXIBFD9ADNIVAHKG999"
NULL_HASH = "999999999999999999999999999999999999999999999999999999999999999999999999999999999"


TEST_TIP_LIST = ["SBKWTQWCFTF9DBZHJKQJKU9LXMZD9BMWJIJLZCCZYJFWIBGYYQBJOWWFWIHDEDTIHUB9PMOWZVCPKV999"]
TEST_HASH_LIST = ["NMPXODIWSCTMWRTQ9AI9VROYNFACWCQDXDSJLNC9HKCECBCGQTBUBKVROXZLQWAZRQUGIJTLTMAMSH999",
"IEDSAXC9PQUEHTLDDWMXZXCQYMPRRDAGTBAWHF9VMGQMMVWHYODHVZZMUKXWLIM9WUBOXRKBHBFAWZ999",
"INVALIDWSCTMWRTQ9AI9VROYNFACWCQDXDSJLNC9HKCECBCGQTBUBKVROXZLQWAZRQUGIJTLTMAMSH999"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the second tx is a made up transaction.
Can we also add a real tx that is not included?

I think the scenarios should include:

  1. A bundle tail that is approved
  2. A bundle tail that is not approved
  3. A non-tail that is approved
  4. A non-tail that is not apporved
  5. a made up tx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping scenario 2 and 4 as agreed with @galrogo .


TEST_ADDRESS = "TEST9TRANSACTION9TEST9TRANSACTION9TEST9TRANSACTION9TEST9TRANSACTION9TEST999999999"
TEST_EMPTY_ADDRESS = "EMPTY9BALANCE9TEST999999999999999999999999999999999999999999999999999999999999999"
Expand Down
8 changes: 5 additions & 3 deletions python-regression/util/test_logic/api_test_logic.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from aloe import world
from iota import Iota, Address, Tag, TryteString
import json
import logging
import urllib3
from aloe import world
from iota import Iota, Address, Tag, TryteString

from . import value_fetch_logic as value_fetch

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,6 +90,7 @@ def prepare_options(args, option_list):
'configValue': value_fetch.fetch_config_value,
'configList': value_fetch.fetch_config_list,
'boolList': value_fetch.fetch_bool_list,
'boolListMixed': value_fetch.fetch_bool_list_mixed,
# TODO: remove the need for this logic
'ignore': value_fetch.fetch_string
}
Expand Down
10 changes: 10 additions & 0 deletions python-regression/util/test_logic/value_fetch_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ def fetch_bool_list(value):
return [True] * len(response)


def fetch_bool_list_mixed(value):
"""
Returns a list filled with bool conversions of the input string separated by space".
:param value: The input value
:return: The list of bool values
"""

bool_list = value.split()
return [True if x == "True" else False for x in bool_list]


def fetch_response_value(value):
"""
Expand Down
106 changes: 6 additions & 100 deletions src/main/java/com/iota/iri/service/API.java
Original file line number Diff line number Diff line change
Expand Up @@ -753,122 +753,29 @@ private AbstractResponse getNodeAPIConfigurationStatement() {
* <p>
* Get the inclusion states of a set of transactions.
* This endpoint determines if a transaction is confirmed by the network (referenced by a valid milestone).
* You can search for multiple tips (and thus, milestones) to get past inclusion states of transactions.
* </p>
* <p>
* This API call returns a list of boolean values in the same order as the submitted transactions.
* Boolean values will be <tt>true</tt> for confirmed transactions, otherwise <tt>false</tt>.
* </p>
* Returns an {@link com.iota.iri.service.dto.ErrorResponse} if a tip is missing or the subtangle is not solid
*
* @param transactions List of transactions you want to get the inclusion state for.
* @param tips List of tip transaction hashes (including milestones) you want to search for
* @return {@link com.iota.iri.service.dto.GetInclusionStatesResponse}
* @throws Exception When a transaction cannot be loaded from hash
**/
@Document(name="getInclusionStates")
private AbstractResponse getInclusionStatesStatement(
final List<String> transactions,
final List<String> tips) throws Exception {
private AbstractResponse getInclusionStatesStatement(final List<String> transactions ) throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you test what happens when someone passes tips still?
(it should work, but just checking)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works. No difference


final List<Hash> trans = transactions.stream()
.map(HashFactory.TRANSACTION::create)
.collect(Collectors.toList());

final List<Hash> tps = tips.stream().
map(HashFactory.TRANSACTION::create)
.collect(Collectors.toList());

int numberOfNonMetTransactions = trans.size();
final byte[] inclusionStates = new byte[numberOfNonMetTransactions];

List<Integer> tipsIndex = new LinkedList<>();
{
for(Hash tip: tps) {
TransactionViewModel tx = TransactionViewModel.fromHash(tangle, tip);
if (tx.getType() != TransactionViewModel.PREFILLED_SLOT) {
tipsIndex.add(tx.snapshotIndex());
}
}
}

// Finds the lowest tips index, or 0
int minTipsIndex = tipsIndex.stream().reduce((a, b) -> a < b ? a : b).orElse(0);

// If the lowest tips index (minTipsIndex) is 0 (or lower),
// we can't check transactions against snapshots because there were no tips,
// or tips have not been confirmed by a snapshot yet
if (minTipsIndex > 0) {
// Finds the highest tips index, or 0
int maxTipsIndex = tipsIndex.stream().reduce((a, b) -> a > b ? a : b).orElse(0);
int count = 0;

// Checks transactions with indexes of tips, and sets inclusionStates byte to 1 or -1 accordingly
// Sets to -1 if the transaction is only known by hash,
// or has no index, or index is above the max tip index (not included).

// Sets to 1 if the transaction index is below the max index of tips (included).
for(Hash hash: trans) {
TransactionViewModel transaction = TransactionViewModel.fromHash(tangle, hash);
if(transaction.getType() == TransactionViewModel.PREFILLED_SLOT || transaction.snapshotIndex() == 0) {
inclusionStates[count] = -1;
} else if (transaction.snapshotIndex() > maxTipsIndex) {
inclusionStates[count] = -1;
} else if (transaction.snapshotIndex() < maxTipsIndex) {
inclusionStates[count] = 1;
}
count++;
}
}

Set<Hash> analyzedTips = new HashSet<>();
Map<Integer, Integer> sameIndexTransactionCount = new HashMap<>();
Map<Integer, Queue<Hash>> sameIndexTips = new HashMap<>();

// Sorts all tips per snapshot index. Stops if a tip is not in our database, or just as a hash.
for (final Hash tip : tps) {
TransactionViewModel transactionViewModel = TransactionViewModel.fromHash(tangle, tip);
if (transactionViewModel.getType() == TransactionViewModel.PREFILLED_SLOT){
return ErrorResponse.create("One of the tips is absent");
}
int snapshotIndex = transactionViewModel.snapshotIndex();
sameIndexTips.putIfAbsent(snapshotIndex, new LinkedList<>());
sameIndexTips.get(snapshotIndex).add(tip);
}

// Loop over all transactions without a state, and counts the amount per snapshot index
for(int i = 0; i < inclusionStates.length; i++) {
if(inclusionStates[i] == 0) {
TransactionViewModel transactionViewModel = TransactionViewModel.fromHash(tangle, trans.get(i));
int snapshotIndex = transactionViewModel.snapshotIndex();
sameIndexTransactionCount.putIfAbsent(snapshotIndex, 0);
sameIndexTransactionCount.put(snapshotIndex, sameIndexTransactionCount.get(snapshotIndex) + 1);
}
}

// Loop over all snapshot indexes of transactions that were not confirmed.
// If we encounter an invalid tangle, stop this function completely.
for (Integer index : sameIndexTransactionCount.keySet()) {
// Get the tips from the snapshot indexes we are missing
Queue<Hash> sameIndexTip = sameIndexTips.get(index);

// We have tips on the same level as transactions, do a manual search.
if (sameIndexTip != null && !exhaustiveSearchWithinIndex(
sameIndexTip, analyzedTips, trans,
inclusionStates, sameIndexTransactionCount.get(index), index)) {

return ErrorResponse.create(INVALID_SUBTANGLE);
}
}
final boolean[] inclusionStatesBoolean = new boolean[inclusionStates.length];
for (int i = 0; i < inclusionStates.length; i++) {
// If a state is 0 by now, we know nothing so assume not included
inclusionStatesBoolean[i] = inclusionStates[i] == 1;
boolean[] inclusionStates = new boolean[trans.size()];
for(int i = 0; i < trans.size(); i++){
inclusionStates[i] = TransactionViewModel.fromHash(tangle, trans.get(i)).snapshotIndex() > 0;
}

{
return GetInclusionStatesResponse.create(inclusionStatesBoolean);
}
return GetInclusionStatesResponse.create(inclusionStates);
}

/**
Expand Down Expand Up @@ -1598,10 +1505,9 @@ private Function<Map<String, Object>, AbstractResponse> getInclusionStates() {
return ErrorResponse.create(INVALID_SUBTANGLE);
}
final List<String> transactions = getParameterAsList(request, "transactions", HASH_SIZE);
final List<String> tips = getParameterAsList(request, "tips", HASH_SIZE);

try {
return getInclusionStatesStatement(transactions, tips);
return getInclusionStatesStatement(transactions);
} catch (Exception e) {
throw new IllegalStateException(e);
}
Expand Down