Skip to content
This repository has been archived by the owner on Dec 15, 2021. It is now read-only.

IS-1021: DictDB iteration causes infinite loop #420

Merged
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
3 changes: 3 additions & 0 deletions iconservice/iconscore/icon_container_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ def __remove(self, key: K) -> None:
raise InvalidContainerAccessException('DictDB depth mismatch')
self._db.delete(get_encoded_key(key))

def __iter__(self):
raise InvalidContainerAccessException("Not Supported iter function on DictDB")


class ArrayDB(object):
"""
Expand Down
50 changes: 50 additions & 0 deletions tests/icon_score/test_icon_container_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,53 @@ def test_container_util(self):
with self.assertRaises(InvalidParamsException):
prefix: bytes = ContainerUtil.create_db_prefix(VarDB, 'vardb')


"""
Dict DB infinity infinite case
implement __getitem__ function means can be iter
https://www.python.org/dev/peps/pep-0234
https://docs.python.org/3/reference/datamodel.html#object.__getitem__
"""


class TestOnlyGetItemObj:
"""
Override getitem function
If you don't consider this case, this logic will always have an infinite loop.
https://stackoverflow.com/questions/926574/why-does-defining-getitem-on-a-class-make-it-iterable-in-python
"""
def __init__(self, limit):
self._limit = limit

def __getitem__(self, key):
if key > self._limit:
raise IndexError(key)


class TestIterObj(TestOnlyGetItemObj):
"""
Override the iter function.
This logic prevents the above infinite loop situation.
https://stackoverflow.com/questions/926574/why-does-defining-getitem-on-a-class-make-it-iterable-in-python
"""
def __iter__(self):
pass


class TestIsAvailableToIterator(unittest.TestCase):
def test_getitem(self):
# it is possible to use foreach by using getitem
limit = 100_000
datas = TestOnlyGetItemObj(limit)
index = 0
for index, e in enumerate(datas):
pass
self.assertEqual(index, limit)

def test_iter(self):
# prevent infinite loop by using empty __iter function
limit = 100_000
datas = TestIterObj(limit)
with self.assertRaises(TypeError) as e:
for e in datas:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"version": "0.0.1",
"main_file": "sample_dict_db",
"main_score": "IterableDictDB"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-

"""
Example

Problem with DictDB : cannot iterate
https://forum.icon.community/t/problem-with-dictdb-cannot-iterate/484
"""

from iconservice import *
TAG = 'IterableDictDB'


class IterableDictDB(IconScoreBase):
""" IterableDictDB SCORE Base implementation """

# ================================================
# Initialization
# ================================================
def __init__(self, db: IconScoreDatabase) -> None:
super().__init__(db)
self._dict = DictDB('DICT', db, value_type=int)

def on_install(self) -> None:
super().on_install()

def on_update(self) -> None:
super().on_update()

@external
def create_item(self, key: str, value: int) -> None:
self._dict[key] = value

@external(readonly=True)
def get_items(self) -> list:
items = []
for item in self._dict:
items.append(item)
return items
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from iconservice.base.address import GOVERNANCE_SCORE_ADDRESS
from iconservice.icon_constant import Revision
from iconservice.base.exception import InvalidContainerAccessException
from tests.integrate_test.test_integrate_base import TestIntegrateBase

if TYPE_CHECKING:
Expand Down Expand Up @@ -112,3 +113,45 @@ def test_array_db_patch(self):
)

self.assertEqual(len(response), 2)


class TestIntegrateDictDBPatch(TestIntegrateBase):
def test_dict_db_defective(self):
self.update_governance("0_0_4")

expected_status = {
"code": Revision.TWO.value,
"name": "1.1.0"
}

query_request = {
"version": self._version,
"from": self._accounts[0],
"to": GOVERNANCE_SCORE_ADDRESS,
"dataType": "call",
"data": {
"method": "getRevision",
"params": {}
}
}
response = self._query(query_request)
self.assertEqual(expected_status, response)

tx_results: List['TransactionResult'] = self.deploy_score("sample_scores",
"sample_dict_db",
self._accounts[0])
score_address: 'Address' = tx_results[0].score_address

self.score_call(self._accounts[0], score_address, "create_item", params={"key": "a", "value": hex(1)})

with self.assertRaises(InvalidContainerAccessException) as e:
self._query(
{
'to': score_address,
'dataType': 'call',
'data': {
'method': 'get_items'
}
}
)
self.assertEqual(e.exception.message, "Not Supported on DictDB")