diff --git a/VERSION b/VERSION index 83d4cf8a9..a7ccabdbf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.18 +1.5.20 diff --git a/iconservice/icon_constant.py b/iconservice/icon_constant.py index 95b6f7f52..38d75951a 100644 --- a/iconservice/icon_constant.py +++ b/iconservice/icon_constant.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import IntFlag, unique, IntEnum, Enum, auto +from enum import IntFlag, unique, IntEnum, Enum, auto, Flag GOVERNANCE_ADDRESS = "cx0000000000000000000000000000000000000001" @@ -104,20 +104,18 @@ class IssueDataKey: class Revision(Enum): - def _generate_next_value_(self, start, count, last_values): - if self != 'LATEST': - return start + count + 1 + TWO = 2 + THREE = 3 + FOUR = 4 + IISS = 5 + DECENTRALIZATION = 6 + FIX_TOTAL_ELECTED_PREP_DELEGATED = 7 - return last_values[-1] + # Revision 8 + REALTIME_P2P_ENDPOINT_UPDATE = 8 + OPTIMIZE_DIRTY_PREP_UPDATE = 8 - TWO = auto() - THREE = auto() - FOUR = auto() - IISS = auto() - DECENTRALIZATION = auto() - IS_1_5_16 = auto() - - LATEST = auto() + LATEST = 8 RC_DB_VERSION_0 = 0 @@ -165,7 +163,7 @@ class ConfigKey: STEP_TRACE_FLAG = 'stepTraceFlag' PRECOMMIT_DATA_LOG_FLAG = 'precommitDataLogFlag' - #icon rc + # Reward calculator executable path ICON_RC_DIR_PATH = 'iconRcPath' # IISS meta data @@ -344,3 +342,45 @@ class BlockVoteStatus(Enum): NONE = 0 TRUE = 1 FALSE = 2 + + +class PRepFlag(Flag): + """Setting flags to True means that PRep fields specified by the flags has been changed + + """ + NONE = 0 + STATUS = auto() + NAME = auto() + COUNTRY = auto() + CITY = auto() + EMAIL = auto() + WEBSITE = auto() + DETAILS = auto() + P2P_ENDPOINT = auto() + PENALTY = auto() + GRADE = auto() + STAKE = auto() + DELEGATED = auto() + LAST_GENERATE_BLOCK_HEIGHT = auto() + TOTAL_BLOCKS = auto() + VALIDATED_BLOCKS = auto() + UNVALIDATED_SEQUENCE_BLOCKS = auto() + IREP = auto() # irep, irep_block_height + IREP_BLOCK_HEIGHT = auto() + + BLOCK_STATISTICS = TOTAL_BLOCKS | VALIDATED_BLOCKS | UNVALIDATED_SEQUENCE_BLOCKS + ALL = 0xFFFFFFFF + + +class PRepContainerFlag(Flag): + NONE = 0 + DIRTY = auto() + + +class TermFlag(Flag): + NONE = 0 + MAIN_PREPS = auto() + SUB_PREPS = auto() + MAIN_PREP_P2P_ENDPOINT = auto() + + ALL = 0xFFFFFFFF diff --git a/iconservice/icon_inner_service.py b/iconservice/icon_inner_service.py index 304cb9dd2..25504759a 100644 --- a/iconservice/icon_inner_service.py +++ b/iconservice/icon_inner_service.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from asyncio import get_event_loop +import asyncio from concurrent.futures.thread import ThreadPoolExecutor from typing import Any, TYPE_CHECKING, Optional, Tuple from earlgrey import message_queue_task, MessageQueueStub, MessageQueueService - from iconcommons.logger import Logger + from iconservice.base.address import Address from iconservice.base.block import Block from iconservice.base.exception import ExceptionCode, IconServiceBaseException, InvalidBaseTransactionException, \ @@ -26,7 +26,7 @@ from iconservice.base.type_converter import TypeConverter, ParamType from iconservice.base.type_converter_templates import ConstantKeys from iconservice.icon_constant import ICON_INNER_LOG_TAG, ICON_SERVICE_LOG_TAG, \ - EnableThreadFlag, ENABLE_THREAD_FLAG, ConfigKey, RCStatus + EnableThreadFlag, ENABLE_THREAD_FLAG from iconservice.icon_service_engine import IconServiceEngine from iconservice.utils import check_error_response, to_camel_case @@ -39,6 +39,9 @@ THREAD_VALIDATE = 'validate' +_TAG = "IIS" + + class IconScoreInnerTask(object): def __init__(self, conf: 'IconConfig'): self._conf = conf @@ -75,7 +78,7 @@ async def hello(self): await ready_future if self._is_thread_flag_on(EnableThreadFlag.INVOKE): - loop = get_event_loop() + loop = asyncio.get_event_loop() ret = await loop.run_in_executor(self._thread_pool[THREAD_INVOKE], self._hello) else: ret = self._hello() @@ -88,17 +91,23 @@ def _hello(self): return self._icon_service_engine.hello() def _close(self): - Logger.info("icon_score_service close", ICON_INNER_LOG_TAG) + Logger.info(tag=_TAG, msg="_close() start") if self._icon_service_engine: self._icon_service_engine.close() self._icon_service_engine = None MessageQueueService.loop.stop() + Logger.info(tag=_TAG, msg="_close() end") + @message_queue_task async def close(self): + Logger.info(tag=_TAG, msg="close() start") + self._close() + Logger.info(tag=_TAG, msg="close() end") + @message_queue_task async def invoke(self, request: dict): Logger.info(f'invoke request with {request}', ICON_INNER_LOG_TAG) @@ -106,7 +115,7 @@ async def invoke(self, request: dict): self._check_icon_service_ready() if self._is_thread_flag_on(EnableThreadFlag.INVOKE): - loop = get_event_loop() + loop = asyncio.get_event_loop() return await loop.run_in_executor(self._thread_pool[THREAD_INVOKE], self._invoke, request) else: @@ -184,7 +193,7 @@ async def query(self, request: dict): self._check_icon_service_ready() if self._is_thread_flag_on(EnableThreadFlag.QUERY): - loop = get_event_loop() + loop = asyncio.get_event_loop() return await loop.run_in_executor(self._thread_pool[THREAD_QUERY], self._query, request) else: @@ -227,7 +236,7 @@ async def call(self, request: dict): self._check_icon_service_ready() if self._is_thread_flag_on(EnableThreadFlag.QUERY): - loop = get_event_loop() + loop = asyncio.get_event_loop() return await loop.run_in_executor(self._thread_pool[THREAD_QUERY], self._call, request) else: @@ -261,7 +270,7 @@ async def write_precommit_state(self, request: dict): self._check_icon_service_ready() if self._is_thread_flag_on(EnableThreadFlag.INVOKE): - loop = get_event_loop() + loop = asyncio.get_event_loop() return await loop.run_in_executor(self._thread_pool[THREAD_INVOKE], self._write_precommit_state, request) else: @@ -309,7 +318,7 @@ async def remove_precommit_state(self, request: dict): self._check_icon_service_ready() if self._is_thread_flag_on(EnableThreadFlag.INVOKE): - loop = get_event_loop() + loop = asyncio.get_event_loop() return await loop.run_in_executor(self._thread_pool[THREAD_INVOKE], self._remove_precommit_state, request) else: @@ -345,7 +354,7 @@ async def validate_transaction(self, request: dict): self._check_icon_service_ready() if self._is_thread_flag_on(EnableThreadFlag.VALIDATE): - loop = get_event_loop() + loop = asyncio.get_event_loop() return await loop.run_in_executor(self._thread_pool[THREAD_VALIDATE], self._validate_transaction, request) else: diff --git a/iconservice/icon_service_engine.py b/iconservice/icon_service_engine.py index da18fb992..a77d0c35a 100644 --- a/iconservice/icon_service_engine.py +++ b/iconservice/icon_service_engine.py @@ -2129,18 +2129,26 @@ def hello(self) -> dict: """ Logger.debug(tag=self.TAG, msg="hello() start") + iiss_engine: 'IISSEngine' = IconScoreContext.engine.iiss + + last_block: 'Block' = self._get_last_block() + if isinstance(self._wal_reader, WriteAheadLogReader): wal_state = WALState(self._wal_reader.state) # If only writing rc_db is done on commit without sending COMMIT_BLOCK to rc, # send COMMIT_BLOCK to rc prior to invoking a block if not (wal_state & WALState.SEND_COMMIT_BLOCK): - IconScoreContext.engine.iiss.send_commit( + iiss_engine.send_commit( self._wal_reader.block.height, self._wal_reader.instant_block_hash) + assert last_block == self._wal_reader.block + # No need to use self._wal_reader = None + # iiss_engine.init_reward_calculator(last_block) + Logger.debug(tag=self.TAG, msg="hello() end") return {} diff --git a/iconservice/iconscore/icon_score_base2.py b/iconservice/iconscore/icon_score_base2.py index df49038c9..f487ebfb3 100644 --- a/iconservice/iconscore/icon_score_base2.py +++ b/iconservice/iconscore/icon_score_base2.py @@ -329,7 +329,6 @@ def get_main_prep_info() -> Tuple[List[PRepInfo], int]: context = ContextContainer._get_context() assert context - # TODO: Fix an error on unittest first before removing the commit below (goldworm) term = context.term if term is None: return [], -1 diff --git a/iconservice/iconscore/icon_score_context.py b/iconservice/iconscore/icon_score_context.py index 17e4394d1..1aed17b0c 100644 --- a/iconservice/iconscore/icon_score_context.py +++ b/iconservice/iconscore/icon_score_context.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Optional, List from iconcommons.logger import Logger -from iconservice.icx.issue.regulator import Regulator + from .icon_score_mapper import IconScoreMapper from .icon_score_trace import Trace from ..base.block import Block @@ -29,8 +29,9 @@ from ..base.transaction import Transaction from ..database.batch import BlockBatch, TransactionBatch from ..icon_constant import ( - IconScoreContextType, IconScoreFuncType, TERM_PERIOD, PRepGrade, PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS -) + IconScoreContextType, IconScoreFuncType, TERM_PERIOD, PRepGrade, PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS, + Revision, PRepFlag) +from ..icx.issue.regulator import Regulator if TYPE_CHECKING: from .icon_score_base import IconScoreBase @@ -185,13 +186,6 @@ def preps(self) -> Optional['PRepContainer']: def term(self) -> Optional['Term']: return self._term - def is_term_updated(self) -> bool: - """Returns whether info in self._term is changed - - :return: - """ - return self._term and self._term.is_dirty() - def is_decentralized(self) -> bool: return self.engine.prep.term is not None @@ -231,7 +225,7 @@ def update_rc_db_batch(self): self.rc_tx_batch.clear() def update_dirty_prep_batch(self): - """Update context.preps when a tx is done + """Apply updated P-Rep data to context.preps every time when a tx is done Caution: call update_dirty_prep_batch before update_state_db_batch() """ @@ -240,6 +234,9 @@ def update_dirty_prep_batch(self): # we should update P-Reps in this term self._update_elected_preps_in_term(dirty_prep) + if self.revision >= Revision.REALTIME_P2P_ENDPOINT_UPDATE.value: + self._update_main_preps_in_term(dirty_prep) + self._preps.replace(dirty_prep) # Write serialized dirty_prep data into tx_batch self.storage.prep.put_prep(self, dirty_prep) @@ -266,10 +263,24 @@ def _update_elected_preps_in_term(self, dirty_prep: 'PRep'): # Just in case, reset the P-Rep grade one to CANDIDATE dirty_prep.grade = PRepGrade.CANDIDATE - self._term.update_preps(self.revision, [dirty_prep]) + self._term.update_invalid_elected_preps([dirty_prep]) Logger.info(tag=self.TAG, msg=f"Invalid main and sub prep: {dirty_prep}") + def _update_main_preps_in_term(self, dirty_prep: 'PRep'): + """ + + :param dirty_prep: dirty prep + """ + if self._term is None: + return + + if dirty_prep.is_flags_on(PRepFlag.P2P_ENDPOINT) and \ + self._term.is_main_prep(dirty_prep.address): + self._term.on_main_prep_p2p_endpoint_updated() + + Logger.info(tag=self.TAG, msg=f"_update_main_prep_endpoint_in_term: {dirty_prep}") + def clear_batch(self): if self.tx_batch: self.tx_batch.clear() @@ -295,10 +306,19 @@ def get_prep(self, address: 'Address', mutable: bool = False) -> Optional['PRep' def put_dirty_prep(self, prep: 'PRep'): Logger.debug(tag=self.TAG, msg=f"put_dirty_prep() start: {prep}") - if self._tx_dirty_preps is not None: - self._tx_dirty_preps[prep.address] = prep + if self._tx_dirty_preps is None: + Logger.warning(tag=self.TAG, msg="self._tx_dirty_preps is None") + Logger.debug(tag=self.TAG, msg="put_dirty_prep() end") + return + + if not prep.is_dirty() and self.revision >= Revision.OPTIMIZE_DIRTY_PREP_UPDATE.value: + Logger.info(tag=self.TAG, msg=f"No need to update an unchanged P-Rep: revision={self.revision}") + Logger.debug(tag=self.TAG, msg="put_dirty_prep() end") + return + + self._tx_dirty_preps[prep.address] = prep - Logger.debug(tag=self.TAG, msg=f"put_dirty_prep() end") + Logger.debug(tag=self.TAG, msg="put_dirty_prep() end") class IconScoreContextFactory(object): diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index f28708687..1565cd493 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -111,7 +111,10 @@ def _call(context: 'IconScoreContext', context.current_address: 'Address' = icon_score_address score_func = getattr(icon_score, ATTR_SCORE_CALL) - return score_func(func_name=func_name, kw_params=converted_params) + ret = score_func(func_name=func_name, kw_params=converted_params) + + # No problem even though ret is None + return deepcopy(ret) @staticmethod def _convert_score_params_by_annotations(icon_score: 'IconScoreBase', func_name: str, kw_params: dict) -> dict: diff --git a/iconservice/iiss/engine.py b/iconservice/iiss/engine.py index 8df969d9d..b87ca1172 100644 --- a/iconservice/iiss/engine.py +++ b/iconservice/iiss/engine.py @@ -48,6 +48,7 @@ from ..icx import IcxStorage from ..prep.data import Term from ..base.block import Block + from ..base.transaction import Transaction _TAG = IISS_LOG_TAG @@ -113,7 +114,19 @@ def remove_listener(self, listener: 'EngineListener'): @staticmethod def ready_callback(cb_data: 'ReadyNotification'): - Logger.debug(tag=_TAG, msg=f"ready callback called with {cb_data}") + Logger.debug(tag=_TAG, msg=f"ready_callback() start: {cb_data}") + Logger.debug(tag=_TAG, msg="ready_callback() end") + + def init_reward_calculator(self, block: 'Block'): + """Send a INIT request to RC to synchronize the block state with RC + + """ + Logger.debug(tag=_TAG, msg=f"_init_reward_calculator() start: block={block}") + + success, block_height = self._reward_calc_proxy.init_reward_calculator(block.height) + Logger.info(tag=_TAG, msg=f"success={success} block_height={block_height} last_block={block}") + + Logger.debug(tag=_TAG, msg=f"_init_reward_calculator() end") def get_ready_future(self): return self._reward_calc_proxy.get_ready_future() @@ -622,10 +635,11 @@ def _check_claim_tx(context: 'IconScoreContext') -> bool: def _claim_iscore(self, context: 'IconScoreContext') -> (int, int): address: 'Address' = context.tx.origin block: 'Block' = context.block + tx: 'Transaction' = context.tx if context.type == IconScoreContextType.INVOKE and self._check_claim_tx(context): iscore, block_height = self._reward_calc_proxy.claim_iscore( - address, block.height, block.hash) + address, block.height, block.hash, tx.index, tx.hash) else: # For debug_estimateStep request iscore, block_height = 0, 0 @@ -635,6 +649,7 @@ def _claim_iscore(self, context: 'IconScoreContext') -> (int, int): def _commit_claim(self, context: 'IconScoreContext', iscore: int): address: 'Address' = context.tx.origin block: 'Block' = context.block + tx: 'Transaction' = context.tx success = True try: @@ -654,7 +669,7 @@ def _commit_claim(self, context: 'IconScoreContext', iscore: int): success = False raise e finally: - self._reward_calc_proxy.commit_claim(success, address, block.height, block.hash) + self._reward_calc_proxy.commit_claim(success, address, block.height, block.hash, tx.index, tx.hash) def handle_query_iscore(self, _context: 'IconScoreContext', @@ -866,17 +881,17 @@ def _put_preps_to_rc_db(cls, context: 'IconScoreContext', revision: int, term: O else: block_height: int = context.block.height - if revision < Revision.IS_1_5_16.value: + if revision < Revision.FIX_TOTAL_ELECTED_PREP_DELEGATED.value: total_elected_prep_delegated: int = term.total_elected_prep_delegated_snapshot else: total_elected_prep_delegated: int = term.total_elected_prep_delegated Logger.info( tag=cls.TAG, - msg=f"put_preps_for_rc_db" - f"block_height={block_height}" - f"total_elected_prep_delegated={term.total_elected_prep_delegated}" - f"total_elected_prep_delegated_snapshot={total_elected_prep_delegated}") + msg=f"_put_preps_for_rc_db() " + f"block_height={block_height} " + f"total_elected_prep_delegated={term.total_elected_prep_delegated} " + f"total_elected_prep_delegated_snapshot={term.total_elected_prep_delegated_snapshot}") data: 'PRepsData' = RewardCalcDataCreator.create_prep_data(block_height, total_elected_prep_delegated, diff --git a/iconservice/iiss/reward_calc/ipc/message.py b/iconservice/iiss/reward_calc/ipc/message.py index 4aad8c871..d6fee7ba6 100644 --- a/iconservice/iiss/reward_calc/ipc/message.py +++ b/iconservice/iiss/reward_calc/ipc/message.py @@ -25,7 +25,17 @@ _next_msg_id: int = 1 -def _get_next_id() -> int: +def reset_next_msg_id(msg_id: int): + """Only used for unittest + + :param msg_id: + :return: + """ + global _next_msg_id + _next_msg_id = msg_id + + +def _get_next_msg_id() -> int: global _next_msg_id msg_id: int = _next_msg_id @@ -44,6 +54,8 @@ class MessageType(IntEnum): COMMIT_CLAIM = 5 QUERY_CALCULATE_STATUS = 6 QUERY_CALCULATE_RESULT = 7 + ROLLBACK = 8 + INIT = 9 READY = 100 CALCULATE_DONE = 101 @@ -51,7 +63,7 @@ class MessageType(IntEnum): class Request(metaclass=ABCMeta): def __init__(self, msg_type: 'MessageType'): self.msg_type = msg_type - self.msg_id = _get_next_id() + self.msg_id = _get_next_msg_id() @abstractmethod def _to_list(self) -> tuple: @@ -111,38 +123,55 @@ def from_list(items: list) -> 'VersionResponse': class ClaimRequest(Request): - def __init__(self, address: 'Address', block_height: int, block_hash: bytes): + def __init__(self, address: 'Address', block_height: int, block_hash: bytes, tx_index: int, tx_hash: bytes): super().__init__(MessageType.CLAIM) self.address = address self.block_height = block_height self.block_hash = block_hash + self.tx_index = tx_index + self.tx_hash = tx_hash def _to_list(self) -> tuple: return self.msg_type, self.msg_id,\ - (self.address.to_bytes_including_prefix(), self.block_height, self.block_hash) + ( + self.address.to_bytes_including_prefix(), + self.block_height, self.block_hash, + self.tx_index, self.tx_hash + ) def __str__(self) -> str: - return f"{self.msg_type.name}({self.msg_id}, " \ - f"{self.address}, {self.block_height}, {bytes_to_hex(self.block_hash)})" + return \ + f"{self.msg_type.name}({self.msg_id}, " \ + f"{self.address}, " \ + f"{self.block_height}, {bytes_to_hex(self.block_hash)}), " \ + f"{self.tx_index}, {bytes_to_hex(self.tx_hash)}" class ClaimResponse(Response): MSG_TYPE = MessageType.CLAIM def __init__(self, msg_id: int, address: 'Address', - block_height: int, block_hash: bytes, iscore: int): + block_height: int, block_hash: bytes, + tx_index: int, tx_hash: bytes, + iscore: int): super().__init__() self.msg_id = msg_id self.address: 'Address' = address self.block_height: int = block_height self.block_hash: bytes = block_hash + self.tx_index: int = tx_index + self.tx_hash: bytes = tx_hash self.iscore: int = iscore def __str__(self) -> str: - return f"CLAIM({self.msg_id}, " \ - f"{self.address}, {self.block_height}, {bytes_to_hex(self.block_hash)}, {self.iscore})" + return \ + f"CLAIM({self.msg_id}, " \ + f"{self.address}, " \ + f"{self.block_height}, {bytes_to_hex(self.block_hash)}, " \ + f"{self.tx_index}, {bytes_to_hex(self.tx_hash)}, " \ + f"{self.iscore})" @staticmethod def from_list(items: list) -> 'ClaimResponse': @@ -152,30 +181,51 @@ def from_list(items: list) -> 'ClaimResponse': address: 'Address' = MsgPackForIpc.decode(TypeTag.ADDRESS, payload[0]) block_height: int = payload[1] block_hash: bytes = payload[2] - iscore: int = MsgPackForIpc.decode(TypeTag.INT, payload[3]) + tx_index: int = payload[3] + tx_hash: bytes = payload[4] + iscore: int = MsgPackForIpc.decode(TypeTag.INT, payload[5]) - return ClaimResponse(msg_id, address, block_height, block_hash, iscore) + return ClaimResponse(msg_id, address, block_height, block_hash, tx_index, tx_hash, iscore) class CommitClaimRequest(Request): """Send the result of claimIScore tx to reward calculator No response for CommitClaimRequest """ - def __init__(self, success: bool, address: 'Address', block_height: int, block_hash: bytes): + def __init__(self, success: bool, address: 'Address', + block_height: int, block_hash: bytes, + tx_index: int, tx_hash: bytes): super().__init__(MessageType.COMMIT_CLAIM) self.success = success self.address = address self.block_height = block_height self.block_hash = block_hash + self.tx_index = tx_index + self.tx_hash = tx_hash def _to_list(self) -> tuple: - return self.msg_type, self.msg_id,\ - (self.success, self.address.to_bytes_including_prefix(), self.block_height, self.block_hash) + return self.msg_type, self.msg_id, \ + ( + self.success, + self.address.to_bytes_including_prefix(), + self.block_height, + self.block_hash, + self.tx_index, + self.tx_hash + ) def __str__(self) -> str: - return f"{self.msg_type.name}" \ - f"({self.msg_id}, {self.success}, {self.address}, {self.block_height}, {bytes_to_hex(self.block_hash)})" + return \ + f"{self.msg_type.name}(" \ + f"{self.msg_id}, " \ + f"{self.success}, " \ + f"{self.address}, " \ + f"{self.block_height}, " \ + f"{bytes_to_hex(self.block_hash)}, " \ + f"{self.tx_index}, " \ + f"{bytes_to_hex(self.tx_hash)}" \ + f")" class CommitClaimResponse(Response): @@ -399,18 +449,56 @@ def from_list(items: list) -> 'CommitBlockResponse': return CommitBlockResponse(msg_id, success, block_height, block_hash) +class InitRequest(Request): + def __init__(self, block_height: int): + super().__init__(MessageType.INIT) + + self.block_height = block_height + + def __str__(self): + return f"{self.msg_type.name}({self.msg_id}, {self.block_height})" + + def _to_list(self) -> tuple: + return self.msg_type, self.msg_id, self.block_height + + +class InitResponse(Response): + MSG_TYPE = MessageType.INIT + + def __init__(self, msg_id: int, success: bool, block_height: int): + super().__init__() + + self.msg_id: int = msg_id + self.success: bool = success + self.block_height: int = block_height + + def __str__(self): + return f"INIT({self.msg_id}, {self.success}, {self.block_height})" + + @staticmethod + def from_list(items: list) -> 'InitResponse': + msg_id: int = items[1] + payload: list = items[2] + + success: bool = payload[0] + block_height: int = payload[1] + + return InitResponse(msg_id, success, block_height) + + class ReadyNotification(Response): MSG_TYPE = MessageType.READY - def __init__(self, msg_id: int, version: int, block_height: int): + def __init__(self, msg_id: int, version: int, block_height: int, block_hash: bytes): super().__init__() self.msg_id = msg_id self.version = version self.block_height = block_height + self.block_hash = block_hash def __str__(self): - return f"READY({self.msg_id}, {self.version}, {self.block_height})" + return f"READY({self.msg_id}, {self.version}, {self.block_height}, {bytes_to_hex(self.block_hash)})" @staticmethod def from_list(items: list) -> 'ReadyNotification': @@ -419,8 +507,9 @@ def from_list(items: list) -> 'ReadyNotification': version: int = payload[0] block_height: int = payload[1] + block_hash: bytes = payload[2] - return ReadyNotification(msg_id, version, block_height) + return ReadyNotification(msg_id, version, block_height, block_hash) class CalculateDoneNotification(Response): @@ -453,9 +542,14 @@ def from_list(items: list) -> 'CalculateDoneNotification': class NoneRequest(Request): + """This request is used to stop ipc channel on iconservice stopping + """ def __init__(self): super().__init__(MessageType.NONE) + def __str__(self): + return f"NONE_REQUEST({self.msg_id})" + def _to_list(self) -> tuple: return self.msg_type, self.msg_id @@ -467,6 +561,9 @@ def __init__(self, msg_id: int): super().__init__() self.msg_id = msg_id + def __str__(self): + return f"NONE_RESPONSE({self.msg_id})" + @staticmethod def from_list(items: list) -> 'NoneResponse': msg_id: int = items[1] diff --git a/iconservice/iiss/reward_calc/ipc/message_unpacker.py b/iconservice/iiss/reward_calc/ipc/message_unpacker.py index 578332537..2bc55446f 100644 --- a/iconservice/iiss/reward_calc/ipc/message_unpacker.py +++ b/iconservice/iiss/reward_calc/ipc/message_unpacker.py @@ -29,6 +29,7 @@ def __init__(self): MessageType.COMMIT_CLAIM: CommitClaimResponse, MessageType.QUERY_CALCULATE_STATUS: QueryCalculateStatusResponse, MessageType.QUERY_CALCULATE_RESULT: QueryCalculateResultResponse, + MessageType.INIT: InitResponse, MessageType.READY: ReadyNotification, MessageType.CALCULATE_DONE: CalculateDoneNotification } diff --git a/iconservice/iiss/reward_calc/ipc/reward_calc_proxy.py b/iconservice/iiss/reward_calc/ipc/reward_calc_proxy.py index 0be0f923a..85ffe2fb3 100644 --- a/iconservice/iiss/reward_calc/ipc/reward_calc_proxy.py +++ b/iconservice/iiss/reward_calc/ipc/reward_calc_proxy.py @@ -19,17 +19,21 @@ import concurrent.futures import os from subprocess import Popen -from typing import Optional, Callable, Any, Tuple +from typing import TYPE_CHECKING, Optional, Callable, Any, Tuple from iconcommons.logger import Logger -from iconservice.icon_constant import RCStatus + from .message import * from .message_queue import MessageQueue from .server import IPCServer from ....base.address import Address from ....base.exception import TimeoutException +from ....icon_constant import RCStatus from ....utils import bytes_to_hex +if TYPE_CHECKING: + from .message import ReadyNotification, CalculateDoneNotification, NoneResponse + _TAG = "RCP" @@ -77,26 +81,38 @@ def open(self, log_dir: str, sock_path: str, iiss_db_path: str): def start(self): Logger.debug(tag=_TAG, msg="start() end") + self._ipc_server.start() + Logger.debug(tag=_TAG, msg="start() end") def stop(self): Logger.debug(tag=_TAG, msg="stop() start") + + self._stop_message_queue() self._ipc_server.stop() + Logger.debug(tag=_TAG, msg="stop() end") def close(self): Logger.debug(tag=_TAG, msg="close() start") self._ipc_server.close() + self.stop_reward_calc() self._message_queue = None self._loop = None - self.stop_reward_calc() - Logger.debug(tag=_TAG, msg="close() end") + def _stop_message_queue(self): + Logger.info(tag=_TAG, msg="_stop_message_queue() start") + + request = NoneRequest() + self._message_queue.put(request) + + Logger.info(tag=_TAG, msg="_stop_message_queue() end") + def is_reward_calculator_ready(self) -> bool: return self._ready_future.done() @@ -163,7 +179,8 @@ async def _calculate(self, db_path: str, block_height: int): return future.result() def claim_iscore(self, address: 'Address', - block_height: int, block_hash: bytes) -> Tuple[int, int]: + block_height: int, block_hash: bytes, + tx_index: int, tx_hash: bytes) -> Tuple[int, int]: """Claim IScore of a given address It is called on invoke thread @@ -171,6 +188,8 @@ def claim_iscore(self, address: 'Address', :param address: the address to claim :param block_height: the height of block which contains this claim tx :param block_hash: the hash of block which contains this claim tx + :param tx_index: the index of claimIScore transaction which is contained in a block + :param tx_hash: the hash of claimIScore transaction :return: [i-score(int), block_height(int)] :exception TimeoutException: The operation has timed-out """ @@ -181,7 +200,7 @@ def claim_iscore(self, address: 'Address', ) future: concurrent.futures.Future = asyncio.run_coroutine_threadsafe( - self._claim_iscore(address, block_height, block_hash), self._loop) + self._claim_iscore(address, block_height, block_hash, tx_index, tx_hash), self._loop) try: response: 'ClaimResponse' = future.result(self._ipc_timeout) @@ -194,12 +213,18 @@ def claim_iscore(self, address: 'Address', return response.iscore, response.block_height async def _claim_iscore(self, address: 'Address', - block_height: int, block_hash: bytes) -> int: + block_height: int, block_hash: bytes, + tx_index: int, tx_hash: bytes) -> 'ClaimResponse': Logger.debug( tag=_TAG, - msg=f"_claim_iscore() start: address({address}) block_height({block_height}) block_hash({block_hash.hex()})" + msg=f"_claim_iscore() start: " + f"address={address} " + f"block_height={block_height} " + f"block_hash={bytes_to_hex(block_hash)} " + f"tx_index={tx_index} " + f"tx_hash={bytes_to_hex(tx_hash)}" ) - request = ClaimRequest(address, block_height, block_hash) + request = ClaimRequest(address, block_height, block_hash, tx_index, tx_hash) future: asyncio.Future = self._message_queue.put(request) await future @@ -208,15 +233,24 @@ async def _claim_iscore(self, address: 'Address', return future.result() - def commit_claim(self, success: bool, address: 'Address', block_height: int, block_hash: bytes): + def commit_claim(self, success: bool, address: 'Address', + block_height: int, block_hash: bytes, + tx_index: int, tx_hash: bytes): Logger.debug( tag=_TAG, msg=f"commit_claim() start: " - f"success({success}) address({address}) block_height({block_height}) block_hash({block_hash.hex()})" + f"success={success} " + f"address={address} " + f"block_height={block_height} " + f"block_hash={bytes_to_hex(block_hash)} " + f"tx_index={tx_index} " + f"tx_hash={bytes_to_hex(tx_hash)}" ) future: concurrent.futures.Future = asyncio.run_coroutine_threadsafe( - self._commit_claim(success, address, block_height, block_hash), self._loop) + self._commit_claim(success, address, block_height, block_hash, tx_index, tx_hash), + self._loop + ) try: future.result(self._ipc_timeout) @@ -227,14 +261,21 @@ def commit_claim(self, success: bool, address: 'Address', block_height: int, blo Logger.debug(tag=_TAG, msg="commit_claim() end") - async def _commit_claim(self, success: bool, address: 'Address', block_height: int, block_hash: bytes): + async def _commit_claim(self, success: bool, address: 'Address', + block_height: int, block_hash: bytes, + tx_index: int, tx_hash: bytes) -> 'CommitClaimResponse': Logger.debug( tag=_TAG, msg=f"_commit_claim() start: " - f"success({success} address({address}) block_height({block_height}) block_hash({block_hash.hex()})" + f"success={success} " + f"address={address} " + f"block_height={block_height} " + f"block_hash={bytes_to_hex(block_hash)} " + f"tx_index={tx_index} " + f"tx_hash={bytes_to_hex(tx_hash)}" ) - request = CommitClaimRequest(success, address, block_height, block_hash) + request = CommitClaimRequest(success, address, block_height, block_hash, tx_index, tx_hash) future: asyncio.Future = self._message_queue.put(request) await future @@ -385,6 +426,34 @@ async def _commit_block(self, success: bool, block_height: int, block_hash: byte return future.result() + def init_reward_calculator(self, block_height: int) -> int: + Logger.debug(tag=_TAG, msg=f"init_reward_calculator() start: block_height={block_height}") + + future: concurrent.futures.Future = asyncio.run_coroutine_threadsafe( + self._init_reward_calculator(block_height), self._loop) + + try: + response: InitResponse = future.result(self._ipc_timeout) + except asyncio.TimeoutError: + future.cancel() + raise TimeoutException("query_calculate_result message to RewardCalculator has timed-out") + + Logger.debug(tag=_TAG, msg="query_calculate_result() end") + + return response.success + + async def _init_reward_calculator(self, block_height: int): + Logger.debug(tag=_TAG, msg=f"init_reward_calculator() start: block_height={block_height}") + + request = InitRequest(block_height) + + future: asyncio.Future = self._message_queue.put(request) + await future + + Logger.debug(tag=_TAG, msg="init_reward_calculator() end") + + return future.result() + def ready_handler(self, response: 'Response'): Logger.debug(tag=_TAG, msg=f"ready_handler() start {response}") diff --git a/iconservice/iiss/reward_calc/ipc/server.py b/iconservice/iiss/reward_calc/ipc/server.py index 8a943cbb6..26aca3d0c 100644 --- a/iconservice/iiss/reward_calc/ipc/server.py +++ b/iconservice/iiss/reward_calc/ipc/server.py @@ -19,78 +19,86 @@ from iconcommons import Logger -from .message import MessageType, Request, NoneRequest, NoneResponse +from .message import MessageType, Request from .message_queue import MessageQueue from .message_unpacker import MessageUnpacker - _TAG = "RCP" class IPCServer(object): def __init__(self): + self._running = False self._loop = None - self._server = None + self._path = None self._queue: Optional['MessageQueue'] = None self._unpacker: Optional['MessageUnpacker'] = MessageUnpacker() self._tasks = [] def open(self, loop, message_queue: 'MessageQueue', path: str): + Logger.info(tag=_TAG, msg="open() start") + assert loop assert message_queue assert isinstance(path, str) self._loop = loop self._queue = message_queue + self._path = path - server = asyncio.start_unix_server(self._on_accepted, path) - - self._server = server + Logger.info(tag=_TAG, msg="open() end") def start(self): - if self._server is None: + Logger.info(tag=_TAG, msg="start() start") + + if self._running: return - self._server = self._loop.run_until_complete(self._server) + self._running = True + co = asyncio.start_unix_server(self._on_accepted, self._path) + asyncio.ensure_future(co) + + Logger.info(tag=_TAG, msg="start() end") def stop(self): - for t in self._tasks: - t.cancel() + Logger.info(tag=_TAG, msg="stop() start") - if self._server is None: + if not self._running: return - self._server.close() + self._running = False + + for t in self._tasks: + t.cancel() + + Logger.info(tag=_TAG, msg="stop() end") def close(self): - if self._server is not None: - asyncio.wait_for(self._server.wait_closed(), 5) - self._server = None + Logger.info(tag=_TAG, msg="close() start") self._loop = None - self._queue = None self._unpacker = None + Logger.info(tag=_TAG, msg="close() end") + def _on_accepted(self, reader: 'StreamReader', writer: 'StreamWriter'): - Logger.debug(tag=_TAG, msg=f"on_accepted() start: {reader} {writer}") + Logger.info(tag=_TAG, msg=f"on_accepted() start: {reader} {writer}") self._tasks.append(asyncio.ensure_future(self._on_send(writer))) self._tasks.append(asyncio.ensure_future(self._on_recv(reader))) - Logger.debug(tag=_TAG, msg="on_accepted() end") + Logger.info(tag=_TAG, msg="on_accepted() end") async def _on_send(self, writer: 'StreamWriter'): - Logger.debug(tag=_TAG, msg="_on_send() start") + Logger.info(tag=_TAG, msg="_on_send() start") - while True: + while self._running: try: request: 'Request' = await self._queue.get() - if request.msg_type == MessageType.NONE: - self._queue.put_response( - NoneResponse.from_list([request.msg_type, request.msg_id]) - ) + self._queue.task_done() - self._queue.task_done() + if request.msg_type == MessageType.NONE: + # Stopping IPCServer break data: bytes = request.to_bytes() @@ -99,19 +107,19 @@ async def _on_send(self, writer: 'StreamWriter'): writer.write(data) await writer.drain() - self._queue.task_done() - + except asyncio.CancelledError: + pass except BaseException as e: - Logger.error(tag=_TAG, msg=str(e)) + Logger.warning(tag=_TAG, msg=str(e)) writer.close() - Logger.debug(tag=_TAG, msg="_on_send() end") + Logger.info(tag=_TAG, msg="_on_send() end") async def _on_recv(self, reader: 'StreamReader'): - Logger.debug(tag=_TAG, msg="_on_recv() start") + Logger.info(tag=_TAG, msg="_on_recv() start") - while True: + while self._running: try: data: bytes = await reader.read(1024) if not isinstance(data, bytes) or len(data) == 0: @@ -125,9 +133,9 @@ async def _on_recv(self, reader: 'StreamReader'): Logger.info(tag=_TAG, msg=f"Received Data : {response}") self._queue.message_handler(response) + except asyncio.CancelledError: + pass except BaseException as e: - Logger.error(tag=_TAG, msg=str(e)) - - await self._queue.put(NoneRequest()) + Logger.warning(tag=_TAG, msg=str(e)) - Logger.debug(tag=_TAG, msg="_on_recv() end") + Logger.info(tag=_TAG, msg="_on_recv() end") diff --git a/iconservice/prep/data/__init__.py b/iconservice/prep/data/__init__.py index 4fd463eb5..ef478e030 100644 --- a/iconservice/prep/data/__init__.py +++ b/iconservice/prep/data/__init__.py @@ -13,6 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .prep import PRep, PRepFlag, PRepStatus +from .prep import PRep from .term import Term, PRepSnapshot from .prep_container import PRepContainer diff --git a/iconservice/prep/data/prep.py b/iconservice/prep/data/prep.py index 3554c0357..ef30629e6 100644 --- a/iconservice/prep/data/prep.py +++ b/iconservice/prep/data/prep.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2019 ICON Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,28 +14,21 @@ # limitations under the License. import copy -from enum import auto, Flag, IntEnum, Enum +from enum import auto, IntEnum, Enum from typing import TYPE_CHECKING, Tuple, Any import iso3166 from .sorted_list import Sortable -from ... import utils from ...base.exception import AccessDeniedException from ...base.type_converter_templates import ConstantKeys -from ...icon_constant import PRepGrade, PRepStatus, PenaltyReason, Revision +from ...icon_constant import PRepGrade, PRepStatus, PenaltyReason, Revision, PRepFlag from ...utils.msgpack_for_db import MsgPackForDB if TYPE_CHECKING: from iconservice.base.address import Address -class PRepFlag(Flag): - NONE = 0 - DIRTY = auto() - FROZEN = auto() - - class PRepDictType(Enum): FULL = auto() # getPRep ABRIDGED = auto() # getPReps @@ -140,14 +134,14 @@ def __init__( self._grade: 'PRepGrade' = grade # registration info - self.name: str = name + self._name: str = name self._country: 'iso3166.Country' = self._get_country(country) - self.city: str = city - self.email: str = email - self.website: str = website - self.details: str = details + self._city: str = city + self._email: str = email + self._website: str = website + self._details: str = details # information required for PRep Consensus - self.p2p_endpoint: str = p2p_endpoint + self._p2p_endpoint: str = p2p_endpoint # Governance Variables self._irep: int = irep self._irep_block_height: int = irep_block_height @@ -164,18 +158,21 @@ def __init__( self._validated_blocks: int = validated_blocks self._unvalidated_sequence_blocks: int = unvalidated_sequence_blocks - # This field is used to save delegated amount at the beginning of a term - # DO NOT STORE THIS TO DB - self._voting_power: int = -1 + self._is_frozen: bool = False - # DO NOT STORE IT TO DB (MEMORY ONLY) - self.extension: Any = None + def is_flags_on(self, flags: 'PRepFlag') -> bool: + return (self._flags & flags) == flags def is_dirty(self) -> bool: - return utils.is_flag_on(self._flags, PRepFlag.DIRTY) + """It returns True if any PRepFlag is True - def _set_dirty(self, on: bool): - self._flags = utils.set_flag(self._flags, PRepFlag.DIRTY, on) + :return: + """ + return bool(self._flags & PRepFlag.ALL) + + @property + def flags(self) -> 'PRepFlag': + return self._flags @property def status(self) -> 'PRepStatus': @@ -183,13 +180,78 @@ def status(self) -> 'PRepStatus': @status.setter def status(self, value: 'PRepStatus'): - self._status = value - self._set_dirty(True) + self._set_property(name="_status", new_value=value, flags=PRepFlag.STATUS) @property def penalty(self) -> 'PenaltyReason': return self._penalty + @penalty.setter + def penalty(self, value: 'PenaltyReason'): + self._set_property(name="_penalty", new_value=value, flags=PRepFlag.PENALTY) + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._set_property(name="_name", new_value=value, flags=PRepFlag.NAME) + + @property + def country(self) -> str: + return self._country.alpha3 + + @country.setter + def country(self, alpha3_country_code: str): + value = self._get_country(alpha3_country_code) + self._set_property(name="_country", new_value=value, flags=PRepFlag.COUNTRY) + + @classmethod + def _get_country(cls, alpha3_country_code: str) -> 'iso3166.Country': + return iso3166.countries_by_alpha3.get( + alpha3_country_code.upper(), cls._UNKNOWN_COUNTRY) + + @property + def city(self) -> str: + return self._city + + @city.setter + def city(self, value: str): + self._set_property(name="_city", new_value=value, flags=PRepFlag.CITY) + + @property + def email(self) -> str: + return self._email + + @email.setter + def email(self, value: str): + self._set_property(name="_email", new_value=value, flags=PRepFlag.EMAIL) + + @property + def website(self) -> str: + return self._website + + @website.setter + def website(self, value: str): + self._set_property(name="_website", new_value=value, flags=PRepFlag.WEBSITE) + + @property + def details(self) -> str: + return self._details + + @details.setter + def details(self, value: str): + self._set_property(name="_details", new_value=value, flags=PRepFlag.DETAILS) + + @property + def p2p_endpoint(self) -> str: + return self._p2p_endpoint + + @p2p_endpoint.setter + def p2p_endpoint(self, value: str): + self._set_property(name="_p2p_endpoint", new_value=value, flags=PRepFlag.P2P_ENDPOINT) + def is_suspended(self) -> bool: """The suspended P-Rep cannot serve as Main P-Rep during this term @@ -204,11 +266,6 @@ def is_electable(self) -> bool: """ return self._status == PRepStatus.ACTIVE and self._penalty == PenaltyReason.NONE - @penalty.setter - def penalty(self, value: 'PenaltyReason'): - self._penalty = value - self._set_dirty(True) - @property def grade(self) -> 'PRepGrade': """The grade of P-Rep @@ -222,22 +279,7 @@ def grade(self) -> 'PRepGrade': @grade.setter def grade(self, value: 'PRepGrade'): - self._grade = value - self._set_dirty(True) - - @property - def country(self) -> str: - return self._country.alpha3 - - @country.setter - def country(self, alpha3_country_code: str): - self._country = self._get_country(alpha3_country_code) - self._set_dirty(True) - - @classmethod - def _get_country(cls, alpha3_country_code: str) -> 'iso3166.Country': - return iso3166.countries_by_alpha3.get( - alpha3_country_code.upper(), cls._UNKNOWN_COUNTRY) + self._set_property(name="_grade", new_value=value, flags=PRepFlag.GRADE) def update_block_statistics(self, is_validator: bool): """Update the block validation statistics of P-Rep @@ -246,15 +288,17 @@ def update_block_statistics(self, is_validator: bool): """ self._check_access_permission() - self._total_blocks += 1 + self._set_property("_total_blocks", self._total_blocks + 1, PRepFlag.TOTAL_BLOCKS, False) if is_validator: - self._validated_blocks += 1 - self._unvalidated_sequence_blocks = 0 + self._set_property("_validated_blocks", self._validated_blocks + 1, PRepFlag.VALIDATED_BLOCKS, False) + self._set_property("_unvalidated_sequence_blocks", 0, PRepFlag.UNVALIDATED_SEQUENCE_BLOCKS, False) else: - self._unvalidated_sequence_blocks += 1 - - self._set_dirty(True) + self._set_property( + "_unvalidated_sequence_blocks", + self._unvalidated_sequence_blocks + 1, + PRepFlag.UNVALIDATED_SEQUENCE_BLOCKS, + False) def reset_block_validation_penalty(self): """Reset block validation penalty and @@ -262,8 +306,9 @@ def reset_block_validation_penalty(self): :return: """ - self._penalty = PenaltyReason.NONE - self._unvalidated_sequence_blocks = 0 + self._check_access_permission() + self._set_property("_penalty", PenaltyReason.NONE, PRepFlag.PENALTY, False) + self._set_property("_unvalidated_sequence_blocks", 0, PRepFlag.UNVALIDATED_SEQUENCE_BLOCKS, False) @property def total_blocks(self) -> int: @@ -304,8 +349,7 @@ def stake(self, value: int): :return: """ assert value >= 0 - self._check_access_permission() - self._stake = value + self._set_property(name="_stake", new_value=value, flags=PRepFlag.STAKE) @property def delegated(self) -> int: @@ -319,8 +363,7 @@ def delegated(self, value: int): :return: """ assert value >= 0 - self._check_access_permission() - self._delegated = value + self._set_property(name="_delegated", new_value=value, flags=PRepFlag.DELEGATED) @property def irep(self) -> int: @@ -337,8 +380,9 @@ def last_generate_block_height(self) -> int: @last_generate_block_height.setter def last_generate_block_height(self, value: int): assert value >= 0 - self._last_generate_block_height = value - self._set_dirty(True) + self._set_property(name="_last_generate_block_height", + new_value=value, + flags=PRepFlag.LAST_GENERATE_BLOCK_HEIGHT) @property def block_height(self) -> int: @@ -353,14 +397,13 @@ def make_key(cls, address: 'Address') -> bytes: return cls.PREFIX + address.to_bytes_including_prefix() def is_frozen(self) -> bool: - return bool(self._flags & PRepFlag.FROZEN) + return self._is_frozen def freeze(self): """Make all member variables immutable - - :return: """ - self._flags |= PRepFlag.FROZEN + self._is_frozen = True + self._flags = PRepFlag.NONE def set(self, *, @@ -398,8 +441,6 @@ def set(self, if value is not None: setattr(self, key, value) - self._set_dirty(True) - def set_irep(self, irep: int, block_height: int): """Set incentive rep @@ -409,9 +450,8 @@ def set_irep(self, irep: int, block_height: int): """ self._check_access_permission() - self._irep = irep - self._irep_block_height = block_height - self._set_dirty(True) + self._set_property("_irep", irep, PRepFlag.IREP, False) + self._set_property("_irep_block_height", block_height, PRepFlag.IREP_BLOCK_HEIGHT, False) def __gt__(self, other: 'PRep') -> bool: return self.order() > other.order() @@ -562,13 +602,12 @@ def to_dict(self, dict_type: 'PRepDictType') -> dict: def __str__(self) -> str: info: dict = self.to_dict(PRepDictType.FULL) - info["votingPower"] = self._voting_power - return str(info) - def copy(self, flags: 'PRepFlag' = PRepFlag.NONE) -> 'PRep': + def copy(self) -> 'PRep': prep = copy.copy(self) - prep._flags = flags + prep._is_frozen = False + prep._flags = PRepFlag.NONE return prep @@ -576,15 +615,11 @@ def _check_access_permission(self): if self.is_frozen(): raise AccessDeniedException("P-Rep access denied") - @property - def voting_power(self): - return self._voting_power - - @voting_power.setter - def voting_power(self, value: int): - """This field is used for saving voting power which is fixed at the beginning of a term - USE IT ONLY IN Term class + def _set_property(self, name: str, new_value: Any, flags: 'PRepFlag', check_permission: bool = True): + if check_permission: + self._check_access_permission() - :return: - """ - self._voting_power = value + old_value = getattr(self, name) + if old_value != new_value: + setattr(self, name, new_value) + self._flags |= flags diff --git a/iconservice/prep/data/prep_container.py b/iconservice/prep/data/prep_container.py index 4b0e978a6..e8aa061da 100644 --- a/iconservice/prep/data/prep_container.py +++ b/iconservice/prep/data/prep_container.py @@ -18,11 +18,12 @@ from iconcommons import Logger -from .prep import PRep, PRepFlag, PRepStatus +from .prep import PRep, PRepStatus from .sorted_list import SortedList +from ... import utils from ...base.address import Address from ...base.exception import InvalidParamsException, AccessDeniedException -from ... import utils +from ...icon_constant import PRepContainerFlag class PRepContainer(object): @@ -33,25 +34,20 @@ class PRepContainer(object): """ _TAG = "PREP" - def __init__(self, flags: PRepFlag = PRepFlag.NONE, total_prep_delegated: int = 0): - self._flags: 'PRepFlag' = flags + def __init__(self, is_frozen: bool = False, total_prep_delegated: int = 0): + self._is_frozen: bool = is_frozen # Total amount of delegated which all active P-Reps have self._total_prep_delegated: int = total_prep_delegated # Active P-Rep list ordered by delegated amount self._active_prep_list = SortedList() self._prep_dict = {} + self._flags: 'PRepContainerFlag' = PRepContainerFlag.NONE def is_frozen(self) -> bool: - return bool(self._flags & PRepFlag.FROZEN) + return self._is_frozen def is_dirty(self) -> bool: - return utils.is_flag_on(self._flags, PRepFlag.DIRTY) - - def _set_dirty(self, on: bool): - self._flags |= utils.set_flag(self._flags, PRepFlag.DIRTY, on) - - def is_flag_on(self, flags: 'PRepFlag') -> bool: - return (self._flags & flags) == flags + return utils.is_all_flag_on(self._flags, PRepContainerFlag.DIRTY) def size(self, active_prep_only: bool = False) -> int: """Returns the number of active P-Reps @@ -85,7 +81,7 @@ def freeze(self): if not prep.is_frozen(): prep.freeze() - self._flags |= PRepFlag.FROZEN + self._is_frozen: bool = True def add(self, prep: 'PRep'): self._check_access_permission() @@ -94,7 +90,7 @@ def add(self, prep: 'PRep'): raise InvalidParamsException("P-Rep already exists") self._add(prep) - self._set_dirty(True) + self._flags |= PRepContainerFlag.DIRTY def _add(self, prep: 'PRep'): @@ -117,7 +113,7 @@ def remove(self, address: 'Address') -> Optional['PRep']: prep: Optional['PRep'] = self._remove(address) if prep is not None: - self._set_dirty(True) + self._flags |= PRepContainerFlag.DIRTY return prep @@ -147,7 +143,7 @@ def replace(self, new_prep: 'PRep') -> Optional['PRep']: self._remove(new_prep.address) self._add(new_prep) - self._set_dirty(True) + self._flags |= PRepContainerFlag.DIRTY return old_prep @@ -195,6 +191,20 @@ def get_preps(self, start_index: int, size: int) -> List['PRep']: """ return self._active_prep_list[start_index:start_index + size] + def get_inactive_preps(self) -> List['PRep']: + """Returns inactive P-Reps which is unregistered or receiving prep disqualification or low productivity penalty. + This method does not care about the order of P-Rep list + + :return: Inactive Prep list + """ + + # Collect P-Reps which is unregistered or receiving prep disqualification or low productivity penalty. + def _func(node: 'PRep') -> bool: + return node.status != PRepStatus.ACTIVE + + inactive_preps = list(filter(_func, self._prep_dict.values())) + return inactive_preps + def index(self, address: 'Address') -> int: """Returns the index of a given address in active_prep_list @@ -216,8 +226,7 @@ def copy(self, mutable: bool) -> 'PRepContainer': :param mutable: :return: """ - flags: 'PRepFlag' = PRepFlag.NONE if mutable else PRepFlag.FROZEN - preps = PRepContainer(flags, self._total_prep_delegated) + preps = PRepContainer(is_frozen=not mutable, total_prep_delegated=self._total_prep_delegated) preps._prep_dict.update(self._prep_dict) preps._active_prep_list.extend(self._active_prep_list) diff --git a/iconservice/prep/data/term.py b/iconservice/prep/data/term.py index 78e115265..4201326df 100644 --- a/iconservice/prep/data/term.py +++ b/iconservice/prep/data/term.py @@ -16,12 +16,13 @@ __all__ = ("Term", "PRepSnapshot") import copy -import enum from typing import TYPE_CHECKING, List, Iterable, Optional, Dict from iconcommons.logger import Logger + +from ... import utils from ...base.exception import AccessDeniedException -from ...icon_constant import PRepStatus, PenaltyReason +from ...icon_constant import PRepStatus, PenaltyReason, TermFlag from ...utils import bytes_to_hex from ...utils.hashing.hash_generator import RootHashGenerator @@ -30,12 +31,6 @@ from .prep import PRep -class _Flag(enum.Flag): - NONE = 0 - DIRTY = enum.auto() - FROZEN = enum.auto() - - class PRepSnapshot(object): """Contains P-Rep address and the delegated amount when this term started """ @@ -71,7 +66,6 @@ def __init__(self, irep: int, total_supply: int, total_delegated: int): - self._flag: _Flag = _Flag.NONE self._sequence = sequence self._start_block_height = start_block_height @@ -89,15 +83,25 @@ def __init__(self, # made from main P-Rep addresses self._merkle_root_hash: Optional[bytes] = None + self._is_frozen: bool = False + self._flags: 'TermFlag' = TermFlag.NONE - def is_frozen(self) -> bool: - return bool(self._flag & _Flag.FROZEN) + @property + def flags(self) -> 'TermFlag': + return self._flags + + def is_dirty(self): + return utils.is_any_flag_on(self._flags, TermFlag.ALL) - def is_dirty(self) -> bool: - return bool(self._flag & _Flag.DIRTY) + def on_main_prep_p2p_endpoint_updated(self): + self._flags |= TermFlag.MAIN_PREP_P2P_ENDPOINT + + def is_frozen(self) -> bool: + return self._is_frozen def freeze(self): - self._flag = _Flag.FROZEN + self._is_frozen = True + self._flags = TermFlag.NONE def _check_access_permission(self): if self.is_frozen(): @@ -144,6 +148,13 @@ def __eq__(self, other: 'Term') -> bool: and self._preps_dict == other._preps_dict \ and self._merkle_root_hash == other._merkle_root_hash + def is_main_prep(self, address: 'Address') -> bool: + for prep_snapshot in self._main_preps: + if address == prep_snapshot.address: + return True + + return False + @property def sequence(self) -> int: return self._sequence @@ -272,29 +283,34 @@ def set_preps(self, self._total_elected_prep_delegated = total_elected_prep_delegated self._generate_root_hash() + self._flags = TermFlag.NONE - def update_preps(self, revision: int, invalid_elected_preps: Iterable['PRep']): + def update_invalid_elected_preps(self, invalid_elected_preps: Iterable['PRep']): """Update main and sub P-Reps with invalid elected P-Reps - :param revision: :param invalid_elected_preps: elected P-Reps that cannot keep governance during this term as their penalties :return: """ self._check_access_permission() + flags: 'TermFlag' = TermFlag.NONE for prep in invalid_elected_preps: - if self._remove_invalid_main_prep(revision, prep) >= 0: + if self._remove_invalid_main_prep(prep) >= 0: + flags |= TermFlag.MAIN_PREPS | TermFlag.SUB_PREPS continue - if self._remove_invalid_sub_prep(revision, prep) >= 0: + if self._remove_invalid_sub_prep(prep) >= 0: + flags |= TermFlag.SUB_PREPS continue raise AssertionError(f"{prep.address} not in elected P-Reps: {self}") - if self.is_dirty(): + if utils.is_all_flag_on(flags, TermFlag.MAIN_PREPS): self._generate_root_hash() - def _remove_invalid_main_prep(self, revision: int, invalid_prep: 'PRep') -> int: + self._flags |= flags + + def _remove_invalid_main_prep(self, invalid_prep: 'PRep') -> int: """Replace an invalid main P-Rep with the top-ordered sub P-Rep :param invalid_prep: an invalid main P-Rep @@ -320,13 +336,12 @@ def _remove_invalid_main_prep(self, revision: int, invalid_prep: 'PRep') -> int: msg=f"Replace a main P-Rep: " f"index={index} {address} -> {self._main_preps[index].address}") - self._reduce_total_elected_prep_delegated(revision, invalid_prep, invalid_prep_snapshot.delegated) + self._reduce_total_elected_prep_delegated(invalid_prep, invalid_prep_snapshot.delegated) del self._preps_dict[address] - self._flag |= _Flag.DIRTY return index - def _remove_invalid_sub_prep(self, revision: int, invalid_prep: 'PRep') -> int: + def _remove_invalid_sub_prep(self, invalid_prep: 'PRep') -> int: """Remove an invalid sub P-Rep from self._sub_preps :param invalid_prep: an invalid sub P-Rep @@ -337,14 +352,12 @@ def _remove_invalid_sub_prep(self, revision: int, invalid_prep: 'PRep') -> int: if index >= 0: invalid_prep_snapshot = self._sub_preps.pop(index) - self._reduce_total_elected_prep_delegated(revision, invalid_prep, invalid_prep_snapshot.delegated) + self._reduce_total_elected_prep_delegated(invalid_prep, invalid_prep_snapshot.delegated) del self._preps_dict[invalid_prep.address] - self._flag |= _Flag.DIRTY - return index - def _reduce_total_elected_prep_delegated(self, revision: int, invalid_prep: 'PRep', delegated: int): + def _reduce_total_elected_prep_delegated(self, invalid_prep: 'PRep', delegated: int): """Reduce total_elected_prep_delegated by the delegated amount of the given invalid P-Rep :param invalid_prep: @@ -353,8 +366,8 @@ def _reduce_total_elected_prep_delegated(self, revision: int, invalid_prep: 'PRe """ # This code is preserved only for state backward compatibility. - # After revision 7, B2 reward is not provided to block-validation-penalty - # (consecutive 660 blocks validation failure) + # After revision 7, B2 reward is not provided to the P-Rep + # which got penalized for consecutive 660 blocks validation failure if invalid_prep.status != PRepStatus.ACTIVE \ or invalid_prep.penalty != PenaltyReason.BLOCK_VALIDATION: self._total_elected_prep_delegated_snapshot -= delegated @@ -434,7 +447,8 @@ def to_list(self) -> List: def copy(self) -> 'Term': term = copy.copy(self) - term._flag = _Flag.NONE + term._is_frozen = False + term._flags = TermFlag.NONE term._main_preps = list(self._main_preps) term._sub_preps = list(self._sub_preps) diff --git a/iconservice/prep/engine.py b/iconservice/prep/engine.py index c27e1e503..69d8ffa76 100644 --- a/iconservice/prep/engine.py +++ b/iconservice/prep/engine.py @@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any, Optional, List, Dict, Tuple from iconcommons.logger import Logger + from .data import Term from .data.prep import PRep, PRepDictType from .data.prep_container import PRepContainer @@ -26,7 +27,7 @@ from ..base.type_converter import TypeConverter, ParamType from ..base.type_converter_templates import ConstantKeys from ..icon_constant import IISS_MAX_DELEGATIONS, Revision, IISS_MIN_IREP, PREP_PENALTY_SIGNATURE, \ - PenaltyReason + PenaltyReason, TermFlag from ..icon_constant import PRepGrade, PRepResultState, PRepStatus from ..iconscore.icon_score_context import IconScoreContext from ..iconscore.icon_score_event_log import EventLogEmitter @@ -67,7 +68,8 @@ def __init__(self): "getMainPReps": self.handle_get_main_preps, "getSubPReps": self.handle_get_sub_preps, "getPReps": self.handle_get_preps, - "getPRepTerm": self.handle_get_prep_term + "getPRepTerm": self.handle_get_prep_term, + "getInactivePReps": self.handle_get_inactive_preps } self.preps = PRepContainer() @@ -224,7 +226,7 @@ def _on_term_ended(self, context: 'IconScoreContext') -> Tuple[dict, 'Term']: context.storage.meta.put_last_main_preps(context, main_preps) # All block validation penalties are released - self._release_block_validation_penalty(context) + self._reset_block_validation_penalty(context) # Create a term with context.preps whose grades are up-to-date new_term: 'Term' = self._create_next_term(context, self.term) @@ -251,7 +253,7 @@ def _on_term_updated(self, context: 'IconScoreContext') -> Tuple[Optional[dict], """Update term with invalid elected P-Rep list during this term (In-term P-Rep replacement) - We have to consider 4 cases below: + We have to consider 5 cases below: 1. No invalid elected P-Rep - Nothing to do 2. Only main P-Reps are invalidated @@ -262,6 +264,9 @@ def _on_term_updated(self, context: 'IconScoreContext') -> Tuple[Optional[dict], 4. Both of them are invalidated - Send new main P-Rep list to loopchain - Save the new term to DB + 5. p2pEndpoint of a Main P-Rep is updated + - Send new main P-Rep list to loopchain + - No need to save the new term to DB :param context: :return: @@ -270,19 +275,14 @@ def _on_term_updated(self, context: 'IconScoreContext') -> Tuple[Optional[dict], main_preps: List['Address'] = [prep.address for prep in self.term.main_preps] context.storage.meta.put_last_main_preps(context, main_preps) - if not context.is_term_updated(): - # No elected P-Rep is disqualified during this term - return None, None - new_term = context.term - assert new_term.is_dirty() + if not new_term.is_dirty(): + return None, None - if self.term.root_hash != new_term.root_hash: - # Case 2 or 4: Some main P-Reps are replaced or removed - main_preps_as_dict: Optional[dict] = \ + if bool(new_term.flags & (TermFlag.MAIN_PREPS | TermFlag.MAIN_PREP_P2P_ENDPOINT)): + main_preps_as_dict = \ self._get_updated_main_preps(context, new_term, PRepResultState.IN_TERM_UPDATED) else: - # Case 3: Only sub P-Reps are invalidated main_preps_as_dict = None return main_preps_as_dict, new_term @@ -346,17 +346,16 @@ def _update_prep_grades(cls, Logger.debug(tag=_TAG, msg="_update_prep_grades() end") - def _release_block_validation_penalty(self, context: 'IconScoreContext'): - """Release block validation penalty every term end + @classmethod + def _reset_block_validation_penalty(cls, context: 'IconScoreContext'): + """Reset block validation penalty in the end of every term :param context: :return: """ - old_preps = self.preps - - for prep in old_preps: - if prep.penalty == PenaltyReason.BLOCK_VALIDATION: + for prep in context.preps: + if prep.penalty == PenaltyReason.BLOCK_VALIDATION and prep.status == PRepStatus.ACTIVE: dirty_prep = context.get_prep(prep.address, mutable=True) dirty_prep.reset_block_validation_penalty() context.put_dirty_prep(dirty_prep) @@ -778,7 +777,8 @@ def handle_get_sub_preps(self, _context: 'IconScoreContext', _params: dict) -> d } def handle_get_preps(self, context: 'IconScoreContext', params: dict) -> dict: - """Returns P-Reps ranging in ranking from start_ranking to end_ranking + """ + Returns P-Reps ranging in ranking from start_ranking to end_ranking P-Rep means all P-Reps including main P-Reps and sub P-Reps @@ -804,7 +804,7 @@ def handle_get_preps(self, context: 'IconScoreContext', params: dict) -> dict: for i in range(start_ranking - 1, end_ranking): prep: 'PRep' = preps.get_by_index(i) - prep_list.append(prep.to_dict(PRepDictType.ABRIDGED)) + prep_list.append(prep.to_dict(PRepDictType.FULL)) return { "blockHeight": context.block.height, @@ -825,32 +825,56 @@ def handle_get_prep_term(self, context: 'IconScoreContext', _params: dict) -> di raise ServiceNotReadyException("Term is not ready") preps_data = [] + + # Collect Main and Sub P-Reps for prep_snapshot in self.term.preps: prep = self.preps.get_by_address(prep_snapshot.address) preps_data.append(prep.to_dict(PRepDictType.FULL)) - # preps_data.append( - # { - # "name": prep.name, - # "country": prep.country, - # "city": prep.city, - # "grade": prep.grade.value, - # "address": prep.address, - # "p2pEndpoint": prep.p2p_endpoint - # } - # ) + # Collect P-Reps which got penalized for consecutive 660 block validation failure + def _func(node: 'PRep') -> bool: + return node.penalty == PenaltyReason.BLOCK_VALIDATION and node.status == PRepStatus.ACTIVE + + # Sort preps in descending order by delegated + preps_on_block_validation_penalty = \ + sorted(filter(_func, self.preps), key=lambda x: x.order()) + + for prep in preps_on_block_validation_penalty: + preps_data.append(prep.to_dict(PRepDictType.FULL)) return { "blockHeight": context.block.height, "sequence": self.term.sequence, "startBlockHeight": self.term.start_block_height, "endBlockHeight": self.term.end_block_height, - "totalSupply": context.total_supply, + "totalSupply": self.term.total_supply, "totalDelegated": self.term.total_delegated, "irep": self.term.irep, "preps": preps_data } + def handle_get_inactive_preps(self, context: 'IconScoreContext', _param: dict) -> dict: + """Returns inactive P-Reps which is unregistered or receiving prep disqualification or low productivity penalty. + + :param context: IconScoreContext + :param _param: None + :return: inactive preps + """ + sorted_inactive_preps: List['PRep'] = \ + sorted(self.preps.get_inactive_preps(), key=lambda node: node.order()) + + total_delegated = 0 + inactive_preps_data = [] + for prep in sorted_inactive_preps: + inactive_preps_data.append(prep.to_dict(PRepDictType.FULL)) + total_delegated += prep.delegated + + return { + "blockHeight": context.block.height, + "totalDelegated": total_delegated, + "preps": inactive_preps_data + } + # IISSEngineListener implementation --------------------------- def on_set_stake(self, context: 'IconScoreContext', account: 'Account'): """Called on IISSEngine.handle_set_stake() diff --git a/iconservice/prep/penalty_imposer.py b/iconservice/prep/penalty_imposer.py index 94eede77f..b2224e0fa 100644 --- a/iconservice/prep/penalty_imposer.py +++ b/iconservice/prep/penalty_imposer.py @@ -52,16 +52,16 @@ def run(self, on_penalty_imposed: Callable[['IconScoreContext', 'Address', 'PenaltyReason'], None]) -> 'PenaltyReason': reason: 'PenaltyReason' = PenaltyReason.NONE - if self._check_low_productivity_penalty(prep): - Logger.info(f"PenaltyImposer statistics({PenaltyReason.LOW_PRODUCTIVITY}): " - f"prep_total_blocks: {prep.total_blocks} " - f"prep_unvalidated_sequence_blocks: {prep.unvalidated_sequence_blocks}") - reason = PenaltyReason.LOW_PRODUCTIVITY if self._check_block_validation_penalty(prep): Logger.info(f"PenaltyImposer statistics({PenaltyReason.BLOCK_VALIDATION}): " f"prep_total_blocks: {prep.total_blocks} " f"prep_block_validation_proportion: {prep.block_validation_proportion}") reason = PenaltyReason.BLOCK_VALIDATION + if self._check_low_productivity_penalty(prep): + Logger.info(f"PenaltyImposer statistics({PenaltyReason.LOW_PRODUCTIVITY}): " + f"prep_total_blocks: {prep.total_blocks} " + f"prep_unvalidated_sequence_blocks: {prep.unvalidated_sequence_blocks}") + reason = PenaltyReason.LOW_PRODUCTIVITY if on_penalty_imposed and reason != PenaltyReason.NONE: on_penalty_imposed(context, prep.address, reason) diff --git a/iconservice/utils/__init__.py b/iconservice/utils/__init__.py index 0e31e0354..ef26b0277 100644 --- a/iconservice/utils/__init__.py +++ b/iconservice/utils/__init__.py @@ -107,10 +107,14 @@ def is_builtin_score(score_address: str) -> bool: return score_address in BUILTIN_SCORE_ADDRESS_MAPPER.values() -def is_flag_on(src_flags: Flag, flag: Flag) -> bool: +def is_all_flag_on(src_flags: Flag, flag: Flag) -> bool: return src_flags & flag == flag +def is_any_flag_on(src_flags: Flag, flag: Flag) -> bool: + return bool(src_flags & flag) + + def set_flag(src_flags: Flag, flag: Flag, on: bool) -> Flag: if on: src_flags |= flag diff --git a/tests/iiss/ipc/test_message.py b/tests/iiss/ipc/test_message.py index a584d0c11..6dd20db2a 100644 --- a/tests/iiss/ipc/test_message.py +++ b/tests/iiss/ipc/test_message.py @@ -5,21 +5,23 @@ class TestMessage(unittest.TestCase): def test_get_next_id(self): + message.reset_next_msg_id(1) + assert message._next_msg_id == 1 - msg_id: int = message._get_next_id() + msg_id: int = message._get_next_msg_id() assert msg_id == 1 assert message._next_msg_id == 2 - msg_id: int = message._get_next_id() + msg_id: int = message._get_next_msg_id() assert msg_id == 2 assert message._next_msg_id == 3 message._next_msg_id = 0xffffffff - msg_id: int = message._get_next_id() + msg_id: int = message._get_next_msg_id() assert msg_id == 0xffffffff assert message._next_msg_id == 1 - msg_id: int = message._get_next_id() + msg_id: int = message._get_next_msg_id() assert msg_id == 1 assert message._next_msg_id == 2 diff --git a/tests/iiss/ipc/test_message_unpacker.py b/tests/iiss/ipc/test_message_unpacker.py index 0e71a0f86..d1b17f59d 100644 --- a/tests/iiss/ipc/test_message_unpacker.py +++ b/tests/iiss/ipc/test_message_unpacker.py @@ -32,6 +32,8 @@ def test_iterator(self): block_height: int = 100 state_hash: bytes = hashlib.sha3_256(b'').digest() block_hash: bytes = hashlib.sha3_256(b'block_hash').digest() + tx_index: int = 1 + tx_hash: bytes = hashlib.sha3_256(b"tx_hash").digest() address = Address.from_data(AddressPrefix.EOA, b'') iscore: int = 5000 success: bool = True @@ -71,6 +73,8 @@ def test_iterator(self): address.to_bytes_including_prefix(), block_height, block_hash, + tx_index, + tx_hash, int_to_bytes(iscore) ) ), @@ -92,7 +96,8 @@ def test_iterator(self): msg_id, ( version, - block_height + block_height, + block_hash ) ), @@ -105,6 +110,14 @@ def test_iterator(self): int_to_bytes(iscore), state_hash ) + ), + ( + MessageType.INIT, + msg_id, + ( + success, + block_height + ) ) ] @@ -129,6 +142,8 @@ def test_iterator(self): self.assertIsInstance(claim_response, ClaimResponse) self.assertEqual(iscore, claim_response.iscore) self.assertEqual(block_height, claim_response.block_height) + self.assertEqual(tx_index, claim_response.tx_index) + self.assertEqual(tx_hash, claim_response.tx_hash) commit_block_response = next(it) self.assertIsInstance(commit_block_response, CommitBlockResponse) @@ -142,6 +157,7 @@ def test_iterator(self): self.assertIsInstance(ready_notification, ReadyNotification) self.assertEqual(version, ready_notification.version) self.assertEqual(block_height, ready_notification.block_height) + self.assertEqual(block_hash, ready_notification.block_hash) calculate_done_notification = next(it) self.assertIsInstance(calculate_done_notification, CalculateDoneNotification) @@ -149,6 +165,11 @@ def test_iterator(self): self.assertEqual(block_height, calculate_done_notification.block_height) self.assertEqual(state_hash, calculate_done_notification.state_hash) + init_response = next(it) + self.assertIsInstance(init_response, InitResponse) + self.assertEqual(success, init_response.success) + self.assertEqual(block_height, init_response.block_height) + with self.assertRaises(StopIteration): next(it) diff --git a/tests/integrate_test/iiss/decentralized/test_decentralized2.py b/tests/integrate_test/iiss/decentralized/test_decentralized2.py index 93d38777f..92c5b4ca6 100644 --- a/tests/integrate_test/iiss/decentralized/test_decentralized2.py +++ b/tests/integrate_test/iiss/decentralized/test_decentralized2.py @@ -13,8 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Union + +from iconservice.base.address import Address from iconservice.icon_constant import Revision, \ - PREP_MAIN_PREPS, ICX_IN_LOOP, ConfigKey, IISS_MIN_IREP, IISS_INITIAL_IREP, PREP_MAIN_AND_SUB_PREPS + PREP_MAIN_PREPS, ICX_IN_LOOP, ConfigKey, PREP_MAIN_AND_SUB_PREPS from tests.integrate_test.iiss.test_iiss_base import TestIISSBase from tests.integrate_test.test_integrate_base import TOTAL_SUPPLY @@ -44,7 +47,7 @@ def _decentralized(self): minimum_delegate_amount_for_decentralization: int = total_supply * 2 // 1000 + 1 init_balance: int = minimum_delegate_amount_for_decentralization * 2 - # distribute icx PREP_MAIN_PREPS ~ PREP_MAIN_PREPS + PREP_MAIN_PREPS - 1 + # distribute icx to PREP_MAIN_PREPS ~ PREP_MAIN_PREPS + PREP_SUB_PREPS - 1 self.distribute_icx(accounts=self._accounts[PREP_MAIN_PREPS:PREP_MAIN_AND_SUB_PREPS], init_balance=init_balance) @@ -163,3 +166,105 @@ def test_get_IISS_info(self): } } self.assertEqual(expected_response, response) + + def test_check_update_endpoint1(self): + self.update_governance() + + # set Revision REV_IISS + self.set_revision(Revision.IISS.value) + self._decentralized() + + self.make_blocks_to_end_calculation() + self.make_blocks(self._block_height + 1) + + response: dict = self.get_main_prep_list() + address: 'Address' = response["preps"][0]["address"] + assert isinstance(address, Address) + + response: dict = self.get_prep(address) + old_p2p_endpoint: str = response["p2pEndpoint"] + new_p2p_endpoint: str = "192.168.0.1:7100" + assert old_p2p_endpoint != new_p2p_endpoint + + self.distribute_icx([address], ICX_IN_LOOP) + + # set prep 1 + tx: dict = self.create_set_prep_tx(from_=address, + set_data={"p2pEndpoint": new_p2p_endpoint}) + + _, _, _, _, main_prep_as_dict = self.debug_make_and_req_block(tx_list=[tx]) + self.assertIsNone(main_prep_as_dict) + + self.set_revision(Revision.FIX_TOTAL_ELECTED_PREP_DELEGATED.value) + self.set_revision(Revision.REALTIME_P2P_ENDPOINT_UPDATE.value) + + # set prep 2 + new_p2p_endpoint = "192.168.0.1:7200" + tx: dict = self.create_set_prep_tx(from_=address, + set_data={"p2pEndpoint": new_p2p_endpoint}) + + _, _, _, _, main_prep_as_dict = self.debug_make_and_req_block(tx_list=[tx]) + self.assertEqual(new_p2p_endpoint, main_prep_as_dict["preps"][0]["p2pEndpoint"]) + + # set prep with the same p2pEndpoint as the old one + tx: dict = self.create_set_prep_tx(from_=address, + set_data={"p2pEndpoint": old_p2p_endpoint}) + + # main_prep_as_dict should not be modified + _, _, _, _, main_prep_as_dict = self.debug_make_and_req_block(tx_list=[tx]) + assert main_prep_as_dict is None + + def test_check_update_endpoint2(self): + self.update_governance() + + # set Revision REV_IISS + self.set_revision(Revision.IISS.value) + self._decentralized() + + # register PRep + tx_list: list = [] + for account in self._accounts[PREP_MAIN_PREPS:]: + tx: dict = self.create_register_prep_tx(from_=account) + tx_list.append(tx) + self.process_confirm_block_tx(tx_list) + + self.make_blocks_to_end_calculation() + self.make_blocks(self._block_height + 1) + + self.set_revision(Revision.FIX_TOTAL_ELECTED_PREP_DELEGATED.value) + + main_preps_count: int = self._config[ConfigKey.PREP_MAIN_PREPS] + + self.distribute_icx(self._accounts[:main_preps_count], ICX_IN_LOOP) + + # Change p2pEndpoints of sub P-Reps + tx_list: list = [] + start = 100 + size = 20 + for i in range(size): + new_p2p_endpoint: str = f"192.168.0.{start + i}:7100" + + # set prep + tx: dict = self.create_set_prep_tx(from_=self._accounts[i + main_preps_count], + set_data={"p2pEndpoint": new_p2p_endpoint}) + tx_list.append(tx) + + # To change the p2pEndpoints of sub P-Reps cannot affect main_prep_as_dict + _, _, _, _, main_prep_as_dict = self.debug_make_and_req_block(tx_list) + assert main_prep_as_dict is None + + self.process_confirm_block_tx(tx_list) + + # Check if setPRep for some sub P-Reps works well + for i in range(size): + p2p_endpoint: str = f"192.168.0.{start + i}:7100" + account = self._accounts[main_preps_count + i] + + prep_info: Dict[str, Union[str, int]] = self.get_prep(account.address) + assert p2p_endpoint == prep_info["p2pEndpoint"] + + # Unregistered main P-Rep is replaced by the first sub P-Rep in descending order by delegated + tx: dict = self.create_unregister_prep_tx(self._accounts[0]) + _, _, _, _, main_prep_as_dict = self.debug_make_and_req_block(tx_list=[tx]) + + assert f"192.168.0.{start}:7100" == main_prep_as_dict["preps"][0]["p2pEndpoint"] diff --git a/tests/integrate_test/iiss/decentralized/test_in_term_prep_replacement.py b/tests/integrate_test/iiss/decentralized/test_in_term_prep_replacement.py index c1e5c7160..c727839e9 100644 --- a/tests/integrate_test/iiss/decentralized/test_in_term_prep_replacement.py +++ b/tests/integrate_test/iiss/decentralized/test_in_term_prep_replacement.py @@ -14,7 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""IconScoreEngine testcase +""" +IconScoreEngine Test Cases +Added checking if getPRepTerm API returns not only main and sub P-Reps but inactive ones. + """ from enum import Enum from typing import TYPE_CHECKING, List, Dict @@ -71,6 +74,28 @@ class TestPreps(TestIISSBase): LOW_PRODUCTIVITY_PENALTY_THRESHOLD = 80 PENALTY_GRACE_PERIOD = CALCULATE_PERIOD * 2 + BLOCK_VALIDATION_PENALTY_THRESHOLD + def _check_preps_on_get_prep_term(self, added_inactive_preps: List[Dict[str, str]]): + """ + Return bool value + checking if not only main P-Reps and sub ones but input added inactive preps are preps of 'getPRepTerm' API + + :param added_inactive_preps: expected added inactive prep list + :return: bool + """ + preps = self.get_prep_term()["preps"] + main_preps = self.get_main_prep_list()["preps"] + sub_preps = self.get_sub_prep_list()["preps"] + tmp_preps = main_preps + sub_preps + expected_preps = [] + for prep in tmp_preps: + expected_preps.append(self.get_prep(prep["address"])) + + preps_on_block_validation_penalty = \ + sorted(added_inactive_preps, + key=lambda x: (-x["delegated"], x["blockHeight"], x["txIndex"])) + expected_preps.extend(preps_on_block_validation_penalty) + assert expected_preps == preps + def _make_init_config(self) -> dict: return { ConfigKey.SERVICE: { @@ -157,6 +182,7 @@ def test_replace_prep(self): term_3: dict = self.get_prep_term() assert term_3["sequence"] == 3 preps = term_3["preps"] + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) main_preps_3: List['Address'] = _get_main_preps(preps, main_prep_count) @@ -184,6 +210,7 @@ def test_replace_prep(self): term_4: dict = self.get_prep_term() assert term_4["sequence"] == 4 preps = term_4["preps"] + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) main_preps_4: List['Address'] = _get_main_preps(preps, main_prep_count) assert main_preps_3 == main_preps_4 @@ -200,14 +227,16 @@ def test_replace_prep(self): term_4: dict = self.get_prep_term() assert term_4["sequence"] == 4 preps = term_4["preps"] - _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) + + # A main P-Rep got penalized for consecutive 660 block validation failure + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count - 1) main_preps_4: List['Address'] = _get_main_preps(preps, main_prep_count) # The first sub P-Rep replaced the the second main P-Rep assert main_preps_4[1] == accounts[main_prep_count].address assert main_preps_4[1] != account_on_block_validation_penalty - prep_on_penalty: dict = self.get_prep(account_on_block_validation_penalty.address) + assert prep_on_penalty["status"] == PRepStatus.ACTIVE.value assert prep_on_penalty["penalty"] == PenaltyReason.BLOCK_VALIDATION.value assert prep_on_penalty["unvalidatedSequenceBlocks"] == self.BLOCK_VALIDATION_PENALTY_THRESHOLD + 1 @@ -215,6 +244,9 @@ def test_replace_prep(self): prep_on_penalty["validatedBlocks"] + \ prep_on_penalty["unvalidatedSequenceBlocks"] + # checks if adding the prep receiving a block validation penalty on preps of getPRepTerm API + self._check_preps_on_get_prep_term([prep_on_penalty]) + count = term_4["endBlockHeight"] - term_4["blockHeight"] + 1 self.make_empty_blocks( count=count, @@ -227,6 +259,7 @@ def test_replace_prep(self): term_5 = self.get_prep_term() assert term_5["sequence"] == 5 preps = term_5["preps"] + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) main_preps_5: List['Address'] = _get_main_preps(preps, main_prep_count) assert main_preps_5[1] == account_on_block_validation_penalty.address @@ -261,7 +294,11 @@ def test_replace_prep(self): term_5 = self.get_prep_term() preps = term_5["preps"] - _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) + + # checks if adding the unregistered prep on preps of getPRepTerm API (1) + self._check_preps_on_get_prep_term([]) + + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count - 1) main_preps_5: List['Address'] = _get_main_preps(preps, main_prep_count) assert preps[index]["address"] != unregistered_account.address @@ -278,6 +315,10 @@ def test_replace_prep(self): term_6 = self.get_prep_term() assert term_6["sequence"] == 6 preps = term_6["preps"] + + # checks if adding the unregistered prep on preps of getPRepTerm API (2) + self._check_preps_on_get_prep_term([]) + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) main_preps_6: List['Address'] = _get_main_preps(preps, main_prep_count) @@ -300,7 +341,11 @@ def test_replace_prep(self): term_6 = self.get_prep_term() preps = term_6["preps"] - _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) + + # checks if adding the prep receiving a low productivity penalty on preps of getPRepTerm API + self._check_preps_on_get_prep_term([]) + + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count - 1) main_preps_6: List['Address'] = _get_main_preps(preps, main_prep_count) assert main_preps_6[1] != account_on_low_productivity_penalty.address assert main_preps_6[1] == accounts[main_prep_count + 1].address @@ -316,6 +361,7 @@ def test_replace_prep(self): term_7 = self.get_prep_term() assert term_7["sequence"] == 7 preps = term_7["preps"] + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) main_preps_7: List['Address'] = _get_main_preps(preps, main_prep_count) @@ -343,15 +389,19 @@ def test_replace_prep(self): ) assert tx_results[-1].status == TransactionResult.FAILURE - prep_on_penalty = self.get_prep(account_on_disqualification) - assert prep_on_penalty["status"] == PRepStatus.DISQUALIFIED.value - assert prep_on_penalty["grade"] == PRepGrade.CANDIDATE.value - assert prep_on_penalty["penalty"] == PenaltyReason.PREP_DISQUALIFICATION.value + prep_on_disqualification_penalty = self.get_prep(account_on_disqualification) + assert prep_on_disqualification_penalty["status"] == PRepStatus.DISQUALIFIED.value + assert prep_on_disqualification_penalty["grade"] == PRepGrade.CANDIDATE.value + assert prep_on_disqualification_penalty["penalty"] == PenaltyReason.PREP_DISQUALIFICATION.value term_7_1 = self.get_prep_term() assert term_7_1["sequence"] == 7 preps = term_7_1["preps"] - _check_elected_prep_grades(preps, main_prep_count, elected_prep_count) + + # checks if adding the prep receiving a disqualification penalty on preps of getPRepTerm API + self._check_preps_on_get_prep_term([]) + + _check_elected_prep_grades(preps, main_prep_count, elected_prep_count - 1) main_preps_7: List['Address'] = _get_main_preps(preps, main_prep_count) assert main_preps_7[-1] != account_on_disqualification.address # The first sub P-Rep replaces the main P-Rep which is disqualified by network proposal diff --git a/tests/integrate_test/iiss/decentralized/test_preps_replace_in_term.py b/tests/integrate_test/iiss/decentralized/test_preps_replace_in_term.py index ee16dc8fe..9e92ad0b1 100644 --- a/tests/integrate_test/iiss/decentralized/test_preps_replace_in_term.py +++ b/tests/integrate_test/iiss/decentralized/test_preps_replace_in_term.py @@ -346,154 +346,6 @@ def test_prep_replace_in_term2(self): self.assertEqual(1, response["totalBlocks"]) self.assertEqual(1, response["validatedBlocks"]) - def test_prep_replace_in_term3(self): - """ - scenario 3 - unregister prep half_prep_count on current preps - """ - - self.distribute_icx(accounts=self._accounts[:PREP_MAIN_PREPS], - init_balance=1 * ICX_IN_LOOP) - accounts: List['EOAAccount'] = self.create_eoa_accounts(PREP_MAIN_PREPS) - self.distribute_icx(accounts=accounts, - init_balance=3000 * ICX_IN_LOOP) - - # replace new PREPS - half_prep_count: int = PREP_MAIN_PREPS // 2 - tx_list = [] - for i in range(half_prep_count): - tx = self.create_register_prep_tx(from_=accounts[i]) - tx_list.append(tx) - tx = self.create_register_prep_tx(from_=accounts[i + half_prep_count]) - tx_list.append(tx) - - tx = self.create_set_stake_tx(from_=accounts[i], - value=1) - tx_list.append(tx) - tx = self.create_set_stake_tx(from_=accounts[i + half_prep_count], - value=1) - tx_list.append(tx) - tx = self.create_set_delegation_tx(from_=accounts[i], - origin_delegations=[ - ( - accounts[i], - 1 - ) - ]) - tx_list.append(tx) - self.process_confirm_block_tx(tx_list) - - response: dict = self.get_main_prep_list() - expected_preps: list = [] - for account in self._accounts[:PREP_MAIN_PREPS]: - expected_preps.append({ - 'address': account.address, - 'delegated': 0 - }) - expected_response: dict = { - "preps": expected_preps, - "totalDelegated": 0 - } - self.assertEqual(expected_response, response) - - self.make_blocks_to_end_calculation() - - # check new PREPS to MAIN_PREPS - response: dict = self.get_main_prep_list() - expected_preps: list = [] - for account in accounts[:half_prep_count]: - expected_preps.append({ - 'address': account.address, - 'delegated': 1 - }) - for account in self._accounts[:half_prep_count]: - expected_preps.append({ - 'address': account.address, - 'delegated': 0 - }) - expected_response: dict = { - "preps": expected_preps, - "totalDelegated": half_prep_count - } - self.assertEqual(expected_response, response) - - for i in range(half_prep_count): - response: dict = self.get_prep(accounts[i]) - self.assertEqual(0, response["totalBlocks"]) - self.assertEqual(0, response["validatedBlocks"]) - response: dict = self.get_prep(self._accounts[i]) - self.assertEqual(0, response["totalBlocks"]) - self.assertEqual(0, response["validatedBlocks"]) - - # maintain - block_count = 5 - self.make_blocks( - to=self._block_height + block_count, - prev_block_generator=accounts[0].address, - prev_block_validators=[account.address for account in accounts[1: half_prep_count]] - ) - - # check new PREPS to MAIN_PREPS - response: dict = self.get_main_prep_list() - expected_preps: list = [] - for account in accounts[:half_prep_count]: - expected_preps.append({ - 'address': account.address, - 'delegated': 1 - }) - for account in self._accounts[:half_prep_count]: - expected_preps.append({ - 'address': account.address, - 'delegated': 0 - }) - expected_response: dict = { - "preps": expected_preps, - "totalDelegated": half_prep_count - } - self.assertEqual(expected_response, response) - - for i in range(1, half_prep_count): - response: dict = self.get_prep(accounts[i]) - self.assertEqual(block_count - 1, response["totalBlocks"]) - self.assertEqual(block_count - 1, response["validatedBlocks"]) - response: dict = self.get_prep(self._accounts[i]) - self.assertEqual(block_count, response["totalBlocks"]) - self.assertEqual(0, response["validatedBlocks"]) - - # block 5 -> change term! - # so you should remove preps unitl 5 times. - # or you have to unregister preps on one time. - count = 2 - for i in range(count): - self.unregister_prep(accounts[i]) - - # check new PREPS to MAIN_PREPS - response: dict = self.get_main_prep_list() - expected_preps: list = [] - - # insert subpreps to unregister prep's position - for account in self._accounts[half_prep_count: half_prep_count + count]: - expected_preps.append({ - 'address': account.address, - 'delegated': 0 - }) - - for account in accounts[count: half_prep_count]: - expected_preps.append({ - 'address': account.address, - 'delegated': 1 - }) - - for account in self._accounts[0: half_prep_count]: - expected_preps.append({ - 'address': account.address, - 'delegated': 0 - }) - expected_response: dict = { - "preps": expected_preps, - "totalDelegated": half_prep_count - } - self.assertEqual(expected_response, response) def test_prep_replace_in_term4(self): PENALTY_GRACE_PERIOD = 35 diff --git a/tests/integrate_test/iiss/prevote/test_prep.py b/tests/integrate_test/iiss/prevote/test_prep.py index 62176ee33..64ee8afe8 100644 --- a/tests/integrate_test/iiss/prevote/test_prep.py +++ b/tests/integrate_test/iiss/prevote/test_prep.py @@ -232,6 +232,7 @@ def test_preps_and_delegated(self): preps: list = [] for i in range(IISS_MAX_DELEGATIONS): address: 'Address' = self._accounts[i].address + expected_params: dict = self.create_register_prep_params(self._accounts[i]) preps.append( { "status": 0, @@ -250,9 +251,14 @@ def test_preps_and_delegated(self): "penalty": PenaltyReason.NONE.value, "unvalidatedSequenceBlocks": 0, "blockHeight": register_block_height, - "txIndex": i + "txIndex": i, + "email": expected_params["email"], + "website": expected_params["website"], + "details": expected_params["details"], + "p2pEndpoint": expected_params["p2pEndpoint"] } ) + expected_response: dict = \ { "blockHeight": self._block_height, diff --git a/tests/integrate_test/iiss/test_iiss_base.py b/tests/integrate_test/iiss/test_iiss_base.py index 3e924613d..6d20cd8ad 100644 --- a/tests/integrate_test/iiss/test_iiss_base.py +++ b/tests/integrate_test/iiss/test_iiss_base.py @@ -23,6 +23,8 @@ from iconservice.base.type_converter_templates import ConstantKeys from iconservice.icon_constant import ConfigKey, Revision, PREP_MAIN_PREPS, \ PREP_MAIN_AND_SUB_PREPS +from iconservice.iconscore.icon_score_context import IconScoreContext +from iconservice.prep.data import Term from iconservice.utils import icx_to_loop from tests.integrate_test.test_integrate_base import TestIntegrateBase, TOTAL_SUPPLY, DEFAULT_STEP_LIMIT @@ -583,3 +585,6 @@ def init_decentralized(self): value=balance - fee) tx_list.append(tx) self.process_confirm_block_tx(tx_list) + + def get_debug_term(self) -> 'Term': + return IconScoreContext.engine.prep.term \ No newline at end of file diff --git a/tests/integrate_test/samples/sample_scores/sample_global_variable_score/global_variable_score.py b/tests/integrate_test/samples/sample_scores/sample_global_variable_score/global_variable_score.py new file mode 100644 index 000000000..91b43b9a3 --- /dev/null +++ b/tests/integrate_test/samples/sample_scores/sample_global_variable_score/global_variable_score.py @@ -0,0 +1,38 @@ +from iconservice import * + + +GLOBAL_DICT = {"a": 1, "b": [2, 3], "c": {"d": 4}} +GLOBAL_LIST = [1, {"a": 1}, ["c", 2]] +GLOBAL_TUPLE = ({"a": 1}, 2, ["c", 2]) + + +class GlobalVariableScore(IconScoreBase): + """Used to check if global score data is corrupted by calling score query api + """ + + def __init__(self, db: IconScoreDatabase) -> None: + super().__init__(db) + + def on_install(self) -> None: + super().on_install() + + def on_update(self) -> None: + super().on_update() + + @external(readonly=True) + def hello(self) -> str: + return "hello" + + @external(readonly=True) + def getGlobalDict(self) -> dict: + return GLOBAL_DICT + + @external(readonly=True) + def getGlobalList(self) -> list: + return GLOBAL_LIST + + @external(readonly=True) + def getGlobalTuple(self) -> list: + """The mismatch of return value type hint is intended for test_integrate_global_variable_score unittest + """ + return GLOBAL_TUPLE diff --git a/tests/integrate_test/samples/sample_scores/sample_global_variable_score/package.json b/tests/integrate_test/samples/sample_scores/sample_global_variable_score/package.json new file mode 100644 index 000000000..61f7aa6e3 --- /dev/null +++ b/tests/integrate_test/samples/sample_scores/sample_global_variable_score/package.json @@ -0,0 +1,5 @@ +{ + "version": "0.0.1", + "main_file": "global_variable_score", + "main_score": "GlobalVariableScore" +} diff --git a/tests/integrate_test/test_integrate_base.py b/tests/integrate_test/test_integrate_base.py index 8f78c8b1a..5958b1467 100644 --- a/tests/integrate_test/test_integrate_base.py +++ b/tests/integrate_test/test_integrate_base.py @@ -16,14 +16,14 @@ """IconServiceEngine testcase """ -from copy import deepcopy +import copy from typing import TYPE_CHECKING, Union, Optional, Any, List, Tuple from unittest import TestCase from unittest.mock import Mock +from iconcommons import IconConfig from iconsdk.wallet.wallet import KeyWallet -from iconcommons import IconConfig from iconservice.base.address import ZERO_SCORE_ADDRESS, GOVERNANCE_SCORE_ADDRESS, Address, MalformedAddress from iconservice.base.block import Block from iconservice.fee.engine import FIXED_TERM @@ -77,7 +77,7 @@ def setUp(self): self._block_height = -1 self._prev_block_hash = None - config = IconConfig("", deepcopy(default_icon_config)) + config = IconConfig("", copy.deepcopy(default_icon_config)) config.load() config.update_conf({ConfigKey.BUILTIN_SCORE_OWNER: str(self._admin.address)}) @@ -231,6 +231,9 @@ def debug_make_and_req_block(self, prev_block_votes: Optional[List[Tuple['Address', int]]] = None, block: 'Block' = None) -> tuple: + # Prevent a base transaction from being added to the original tx_list + tx_list = copy.copy(tx_list) + if block is None: block_height: int = self._block_height + 1 block_hash = create_block_hash() @@ -628,7 +631,7 @@ def create_vote_proposal_tx(self, @staticmethod def _convert_tx_for_estimating_step_from_origin_tx(tx: dict): - tx = deepcopy(tx) + tx = copy.deepcopy(tx) tx["method"] = "debug_estimateStep" del tx["params"]["nonce"] del tx["params"]["stepLimit"] diff --git a/tests/integrate_test/test_integrate_global_variable_score.py b/tests/integrate_test/test_integrate_global_variable_score.py new file mode 100644 index 000000000..794895aae --- /dev/null +++ b/tests/integrate_test/test_integrate_global_variable_score.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""IconScoreEngine testcase +""" + +from typing import TYPE_CHECKING, List + +from iconservice.icon_inner_service import MakeResponse +from tests.integrate_test.test_integrate_base import TestIntegrateBase + +if TYPE_CHECKING: + from iconservice.base.address import Address + from iconservice.iconscore.icon_score_result import TransactionResult + + +def _create_query_request(from_: 'Address', to_: 'Address', method: str): + return { + "version": 3, + "from": from_, + "to": to_, + "dataType": "call", + "data": {"method": method} + } + + +class TestScoreGlobalVariable(TestIntegrateBase): + + def setUp(self): + super().setUp() + + sender: 'Address' = self._accounts[0].address + + tx_results: List['TransactionResult'] = self.deploy_score( + score_root="sample_scores", + score_name="sample_global_variable_score", + from_=sender, + expected_status=True) + score_address: 'Address' = tx_results[0].score_address + + request = _create_query_request(sender, score_address, "hello") + response = self._query(request) + self.assertEqual(response, "hello") + + self.sender = sender + self.score_address = score_address + + def _create_query_request(self, method: str): + return _create_query_request(self.sender, self.score_address, method) + + def test_global_dict(self): + expected_response = {"a": 1, "b": [2, 3], "c": {"d": 4}} + expected_converted_response = {"a": "0x1", "b": ["0x2", "0x3"], "c": {"d": "0x4"}} + request: dict = self._create_query_request("getGlobalDict") + + # First score call for query + response_0 = self._query(request) + assert isinstance(response_0, dict) + assert response_0 == expected_response + + # make_response() does in-place value type conversion in response_0 + converted_response = MakeResponse.make_response(response_0) + assert converted_response == expected_converted_response + assert response_0 != expected_response + assert id(converted_response) == id(response_0) + + # Check if the response is deeply copied on every query call + response_1: dict = self._query(request) + assert isinstance(response_1, dict) + assert id(response_1) != id(response_0) + assert response_1 == expected_response + + def test_global_list(self): + expected_response = [1, {"a": 1}, ["c", 2]] + expected_converted_response = ["0x1", {"a": "0x1"}, ["c", "0x2"]] + request: dict = self._create_query_request("getGlobalList") + + # First score call for query + response_0: list = self._query(request) + assert isinstance(response_0, list) + assert response_0 == expected_response + + # Check if the response is deeply copied on every query call + converted_response = MakeResponse.make_response(response_0) + assert converted_response == expected_converted_response + assert id(converted_response) == id(response_0) + + response_1 = self._query(request) + assert isinstance(response_1, list) + assert id(response_1) != id(response_0) + assert response_1 == expected_response + + def test_global_tuple(self): + expected_response = ({"a": 1}, 2, ["c", 2]) + request: dict = self._create_query_request("getGlobalTuple") + + # First score call for query + response_0: tuple = self._query(request) + assert isinstance(response_0, tuple) + assert response_0 == expected_response + + converted_response = MakeResponse.make_response(response_0) + assert converted_response == expected_response + assert response_0 == expected_response + assert id(converted_response) == id(response_0) + + response_1 = self._query(request) + assert isinstance(response_1, tuple) + assert id(response_1) != id(response_0) + assert response_1 == expected_response diff --git a/tests/prep/test_engine.py b/tests/prep/test_engine.py index a102c6a7a..e65d5a51b 100644 --- a/tests/prep/test_engine.py +++ b/tests/prep/test_engine.py @@ -16,6 +16,7 @@ import os import random import unittest +from typing import List, Dict, Union from unittest.mock import Mock from iconservice.base.address import AddressPrefix, Address @@ -25,7 +26,8 @@ from iconservice.iconscore.icon_score_context import IconScoreContext, IconScoreContextFactory from iconservice.iconscore.icon_score_step import IconScoreStepCounterFactory from iconservice.prep import PRepEngine -from iconservice.prep.data import PRep, PRepContainer, Term +from iconservice.prep.data import PRepContainer, Term +from iconservice.prep.data.prep import PRep, PRepDictType from iconservice.utils import icx_to_loop @@ -142,7 +144,6 @@ def tearDown(self) -> None: def test_update_prep_grades_on_main_prep_unregistration(self): context = Mock() - revision: int = 0 old_term = self.term old_preps = self.preps @@ -163,7 +164,7 @@ def test_update_prep_grades_on_main_prep_unregistration(self): assert new_preps.get_by_index(0) != prep # Replace main P-Rep0 with sub P-Rep0 - new_term.update_preps(revision, [dirty_prep]) + new_term.update_invalid_elected_preps([dirty_prep]) PRepEngine._update_prep_grades(context, new_preps, old_term, new_term) assert len(new_term.main_preps) == self.main_prep_count assert len(new_term.sub_preps) == self.sub_prep_count - 1 @@ -175,7 +176,6 @@ def test_update_prep_grades_on_main_prep_unregistration(self): def test_update_prep_grades_on_sub_prep_unregistration(self): context = Mock() - revision: int = 0 old_term = self.term old_preps = self.preps @@ -196,7 +196,7 @@ def test_update_prep_grades_on_sub_prep_unregistration(self): assert old_preps.get_by_index(index) == prep assert new_preps.get_by_index(index) != prep - new_term.update_preps(revision, [dirty_prep]) + new_term.update_invalid_elected_preps([dirty_prep]) PRepEngine._update_prep_grades(context, new_preps, old_term, new_term) _check_prep_grades(new_preps, len(new_term.main_preps), len(new_term)) assert len(new_term.main_preps) == self.main_prep_count @@ -210,7 +210,6 @@ def test_update_prep_grades_on_sub_prep_unregistration(self): def test_update_prep_grades_on_disqualification(self): context = Mock() - revision: int = 0 states = [PRepStatus.DISQUALIFIED, PRepStatus.DISQUALIFIED, PRepStatus.ACTIVE] penalties = [ @@ -240,7 +239,7 @@ def test_update_prep_grades_on_disqualification(self): assert old_preps.get_by_index(index) == prep assert new_preps.get_by_index(index) != prep - new_term.update_preps(revision, [dirty_prep]) + new_term.update_invalid_elected_preps([dirty_prep]) PRepEngine._update_prep_grades(context, new_preps, old_term, new_term) if penalties[i] != PenaltyReason.BLOCK_VALIDATION: _check_prep_grades(new_preps, len(new_term.main_preps), len(new_term)) @@ -263,7 +262,6 @@ def test_update_prep_grades_on_disqualification(self): def test_update_prep_grades_on_multiple_cases(self): context = Mock() - revision: int = 0 old_term = self.term old_preps = self.preps @@ -291,7 +289,7 @@ def test_update_prep_grades_on_multiple_cases(self): assert new_preps.get_by_address(prep.address) != prep assert new_preps.get_by_address(prep.address) == dirty_prep - new_term.update_preps(revision, [dirty_prep]) + new_term.update_invalid_elected_preps([dirty_prep]) # Sub P-Rep main_prep_count = len(new_term.main_preps) @@ -314,7 +312,7 @@ def test_update_prep_grades_on_multiple_cases(self): assert new_preps.get_by_address(address) != prep assert new_preps.get_by_address(address) == dirty_prep - new_term.update_preps(revision, [dirty_prep]) + new_term.update_invalid_elected_preps([dirty_prep]) # Candidate P-Rep for _ in range(3): @@ -339,3 +337,245 @@ def test_update_prep_grades_on_multiple_cases(self): assert len(new_term.sub_preps) == self.sub_prep_count - 7 assert new_preps.size(active_prep_only=True) == old_preps.size(active_prep_only=True) - 9 assert new_preps.size() == old_preps.size() + + def test_handle_get_prep_term_with_electable_preps(self): + block_height = 100 + params = {} + term = self.term + + context = Mock() + context.block.height = block_height + + engine = PRepEngine() + engine.term = term + engine.preps = self.preps + + ret: dict = engine.handle_get_prep_term(context, params) + + assert ret["blockHeight"] == block_height + assert ret["sequence"] == term.sequence + assert ret["startBlockHeight"] == term.start_block_height + assert ret["endBlockHeight"] == term.end_block_height + assert ret["totalSupply"] == term.total_supply + assert ret["totalDelegated"] == term.total_delegated + assert ret["irep"] == term.irep + + # Main P-Reps: 22, Sub P-Reps: 78 + prep_list: List[Dict[str, Union[int, str, 'Address']]] = ret["preps"] + assert len(prep_list) == PREP_MAIN_AND_SUB_PREPS + + for i, prep_snapshot in enumerate(term.preps): + prep_item: Dict[str, Union[int, str, 'Address']] = prep_list[i] + assert prep_item["address"] == prep_snapshot.address + + def test_handle_get_prep_term_with_penalized_preps(self): + block_height = 200 + sequence = 78 + period = 43120 + start_block_height = 200 + end_block_height = 200 + period - 1 + irep = icx_to_loop(40000) + total_supply = icx_to_loop(800_460_000) + total_delegated = icx_to_loop(1000) + + params = {} + + context = Mock() + context.block.height = block_height + + main_prep_count = 22 + elected_prep_count = 100 + total_prep_count = 106 + + term = Term(sequence=sequence, + start_block_height=start_block_height, + period=period, + irep=irep, + total_supply=total_supply, + total_delegated=total_delegated) + + preps = PRepContainer() + for i in range(total_prep_count): + address = Address.from_prefix_and_int(AddressPrefix.EOA, i) + delegated = icx_to_loop(1000 - i) + penalty = PenaltyReason.NONE + status = PRepStatus.ACTIVE + + if 0 <= i <= 4: + # block validation penalty preps: 5 + penalty: 'PenaltyReason' = PenaltyReason.BLOCK_VALIDATION + elif i == 5: + # unregistered preps: 1 + status = PRepStatus.UNREGISTERED + elif 6 <= i <= 7: + # low productivity preps: 2 + status = PRepStatus.DISQUALIFIED + penalty = PenaltyReason.LOW_PRODUCTIVITY + elif 8 <= i <= 10: + # disqualified preps: 3 + status = PRepStatus.DISQUALIFIED + penalty = PenaltyReason.PREP_DISQUALIFICATION + + prep = PRep(address, block_height=i, delegated=delegated, penalty=penalty, status=status) + prep.freeze() + preps.add(prep) + + preps.freeze() + assert preps.size(active_prep_only=False) == total_prep_count + + electable_preps = filter(lambda x: x.is_electable(), preps) + term.set_preps(electable_preps, main_prep_count=main_prep_count, elected_prep_count=elected_prep_count) + + engine = PRepEngine() + engine.term = term + engine.preps = preps + + ret: dict = engine.handle_get_prep_term(context, params) + + assert ret["blockHeight"] == block_height + assert ret["sequence"] == sequence + assert ret["startBlockHeight"] == start_block_height + assert ret["endBlockHeight"] == end_block_height + assert ret["totalSupply"] == total_supply + assert ret["totalDelegated"] == total_delegated + assert ret["irep"] == irep + + prep_list: List[Dict[str, Union[int, str, 'Address']]] = ret["preps"] + assert len(prep_list) == elected_prep_count + + for i, prep_snapshot in enumerate(term.preps): + prep_item: Dict[str, Union[int, str, 'Address']] = prep_list[i] + assert prep_item["address"] == prep_snapshot.address + assert prep_item["status"] == PRepStatus.ACTIVE.value + assert prep_item["penalty"] == PenaltyReason.NONE.value + + # The P-Reps which got penalized for consecutive 660 block validation failure + # are located at the end of the P-Rep list + prev_delegated = -1 + for i, prep_item in enumerate(prep_list[-5:]): + assert prep_item["address"] == Address.from_prefix_and_int(AddressPrefix.EOA, i) + assert prep_item["status"] == PRepStatus.ACTIVE.value + assert prep_item["penalty"] == PenaltyReason.BLOCK_VALIDATION.value + + delegated: int = prep_item["delegated"] + if prev_delegated >= 0: + assert prev_delegated >= delegated + + prev_delegated = delegated + + def test_handle_get_inactive_preps(self): + expected_block_height = 1234 + + context = Mock() + context.block.height = expected_block_height + + old_term = self.term + old_preps = self.preps + new_term = old_term.copy() + new_preps = old_preps.copy(mutable=True) + expected_preps = [] + expected_total_delegated = 0 + + cases = ( + (PRepStatus.UNREGISTERED, PenaltyReason.NONE), + (PRepStatus.UNREGISTERED, PenaltyReason.BLOCK_VALIDATION), + (PRepStatus.DISQUALIFIED, PenaltyReason.PREP_DISQUALIFICATION), + (PRepStatus.DISQUALIFIED, PenaltyReason.LOW_PRODUCTIVITY), + (PRepStatus.ACTIVE, PenaltyReason.BLOCK_VALIDATION), + (PRepStatus.ACTIVE, PenaltyReason.NONE) + ) + + for case in cases: + index = random.randint(0, len(new_term.main_preps) - 1) + prep = new_preps.get_by_index(index) + + dirty_prep = prep.copy() + dirty_prep.status = case[0] + dirty_prep.penalty = case[1] + new_preps.replace(dirty_prep) + assert new_preps.is_dirty() + assert old_preps.get_by_address(prep.address) == prep + assert new_preps.get_by_address(prep.address) != prep + assert new_preps.get_by_address(prep.address) == dirty_prep + + new_term.update_invalid_elected_preps([dirty_prep]) + + if dirty_prep.status != PRepStatus.ACTIVE: + expected_preps.append(dirty_prep) + expected_total_delegated += dirty_prep.delegated + + expected_preps = sorted(expected_preps, key=lambda node: node.order()) + + engine = PRepEngine() + engine.term = new_term + engine.preps = new_preps + + params = {} + response: dict = engine.handle_get_inactive_preps(context, params) + inactive_preps: list = response["preps"] + for i, prep in enumerate(expected_preps): + expected_prep_data: dict = prep.to_dict(PRepDictType.FULL) + assert expected_prep_data == inactive_preps[i] + + assert len(expected_preps) == len(inactive_preps) + assert expected_block_height == response["blockHeight"] + assert expected_total_delegated == response["totalDelegated"] + + def test__reset_block_validation_penalty(self): + engine = PRepEngine() + engine.term = self.term + engine.preps = self.preps + + self.term.freeze() + self.preps.freeze() + + assert engine.term.is_frozen() + assert engine.preps.is_frozen() + + IconScoreContext.engine = Mock() + IconScoreContext.storage = Mock() + IconScoreContext.engine.prep = engine + context = _create_context() + + assert engine.preps.size(active_prep_only=False) == context.preps.size(active_prep_only=False) + for i in range(engine.preps.size(active_prep_only=True)): + assert engine.preps.get_by_index(i) == context._preps.get_by_index(i) + + # Impose block validation penalty on 5 Main P-Reps + indices = set() + for _ in range(5): + index = random.randint(0, self.main_prep_count - 1) + indices.add(index) + + # Impose the block validation penalty on randomly chosen 5 or less than P-Reps + for i in indices: + prep = context.preps.get_by_index(i) + dirty_prep = context.get_prep(prep.address, mutable=True) + assert not dirty_prep.is_frozen() + + dirty_prep.penalty = PenaltyReason.BLOCK_VALIDATION + dirty_prep.grade = PRepGrade.CANDIDATE + + context.put_dirty_prep(dirty_prep) + + context.update_dirty_prep_batch() + + for i in indices: + prep = context.preps.get_by_index(i) + assert prep.is_frozen() + assert prep.status == PRepStatus.ACTIVE + assert prep.penalty == PenaltyReason.BLOCK_VALIDATION + assert prep.grade == PRepGrade.CANDIDATE + + engine._reset_block_validation_penalty(context) + + # The penalties of P-Reps in context should be reset + # to PenaltyReason.NONE at the last block of the current term + for prep in context.preps: + assert prep.status == PRepStatus.ACTIVE + assert prep.penalty == PenaltyReason.NONE + + for i in indices: + prep = context.preps.get_by_index(i) + assert prep.status == PRepStatus.ACTIVE + assert prep.penalty == PenaltyReason.NONE diff --git a/tests/prep/test_penalty_imposer.py b/tests/prep/test_penalty_imposer.py index 8736f69f0..d07fe8ed4 100644 --- a/tests/prep/test_penalty_imposer.py +++ b/tests/prep/test_penalty_imposer.py @@ -176,3 +176,28 @@ def test_low_productivity_penalty(self): context=self.context, prep=prep, on_penalty_imposed=on_penalty_imposed) on_penalty_imposed.assert_called_with( self.context, prep.address, PenaltyReason.LOW_PRODUCTIVITY) + + def test_block_validation_and_low_productivity_penalty(self): + # Success case: when prep get block validation and low productivity penalty at the same time, + # should impose low productivity penalty + penalty_grace_period = 43120 * 2 + block_validation_penalty_threshold = 660 + low_productivity_penalty_threshold = 85 + + total_blocks = penalty_grace_period + 1 + unvalidated_sequence_blocks = block_validation_penalty_threshold + validated_blocks = penalty_grace_period * low_productivity_penalty_threshold // 100 + + prep = create_prep(total_blocks, validated_blocks, unvalidated_sequence_blocks) + + on_penalty_imposed = MagicMock() + penalty_imposer = PenaltyImposer( + penalty_grace_period=penalty_grace_period, + low_productivity_penalty_threshold=low_productivity_penalty_threshold, + block_validation_penalty_threshold=block_validation_penalty_threshold + ) + actual_reason: 'PenaltyReason' = penalty_imposer.run( + context=self.context, prep=prep, on_penalty_imposed=on_penalty_imposed) + on_penalty_imposed.assert_called_with( + self.context, prep.address, PenaltyReason.LOW_PRODUCTIVITY) + self.assertEqual(PenaltyReason.LOW_PRODUCTIVITY, actual_reason) diff --git a/tests/prep/test_prep.py b/tests/prep/test_prep.py index a8b887b7f..722386ec9 100644 --- a/tests/prep/test_prep.py +++ b/tests/prep/test_prep.py @@ -20,6 +20,7 @@ from iconservice.base.address import AddressPrefix, Address from iconservice.base.exception import AccessDeniedException from iconservice.icon_constant import IISS_INITIAL_IREP, PenaltyReason, Revision +from iconservice.icon_constant import PRepStatus, PRepFlag, PRepGrade from iconservice.prep.data.prep import PRep, PRepDictType NAME = "banana" @@ -38,7 +39,7 @@ TOTAL_BLOCKS = 0 VALIDATED_BLOCKS = 0 IREP_BLOCK_HEIGHT = BLOCK_HEIGHT -PENALTY = PenaltyReason.BLOCK_VALIDATION +PENALTY = PenaltyReason.NONE UNVALIDATED_SEQUENCE_BLOCKS = 10 @@ -94,13 +95,18 @@ def test_freeze(prep): fixed_name = "orange" prep.set(name=fixed_name) assert prep.name == fixed_name + assert prep.is_dirty() + assert prep.is_flags_on(PRepFlag.NAME) prep.freeze() assert prep.is_frozen() + assert not prep.is_dirty() with pytest.raises(AccessDeniedException): prep.set(name="candy") assert prep.name == fixed_name + assert not prep.is_dirty() + assert not prep.is_flags_on(PRepFlag.NAME) with pytest.raises(AccessDeniedException): prep.set_irep(10_000, 777) @@ -109,6 +115,18 @@ def test_freeze(prep): prep.update_block_statistics(is_validator=True) +def test_set_irep(prep): + assert not prep.is_flags_on(PRepFlag.IREP) + + prep.set_irep(10_001, 1000) + assert prep.is_flags_on(PRepFlag.IREP ) + assert prep.is_dirty() + + prep.freeze() + assert not prep.is_flags_on(PRepFlag.IREP) + assert not prep.is_dirty() + + def test_set_ok(prep): kwargs = { "name": "Best P-Rep", @@ -129,6 +147,8 @@ def test_set_ok(prep): assert prep.details == kwargs["details"] assert prep.p2p_endpoint == kwargs["p2p_endpoint"] + assert prep.is_dirty() + def test_set_error(prep): kwargs = { @@ -218,16 +238,26 @@ def test_update_block_statistics(prep): assert prep.total_blocks == 0 assert prep.validated_blocks == 0 assert prep.unvalidated_sequence_blocks == UNVALIDATED_SEQUENCE_BLOCKS + assert not prep.is_flags_on(PRepFlag.BLOCK_STATISTICS) + assert not prep.is_dirty() prep.update_block_statistics(is_validator=False) assert prep.total_blocks == 1 assert prep.validated_blocks == 0 assert prep.unvalidated_sequence_blocks == UNVALIDATED_SEQUENCE_BLOCKS + 1 + assert prep.is_flags_on(PRepFlag.TOTAL_BLOCKS | PRepFlag.UNVALIDATED_SEQUENCE_BLOCKS) + assert prep.is_dirty() prep.update_block_statistics(is_validator=True) assert prep.total_blocks == 2 assert prep.validated_blocks == 1 assert prep.unvalidated_sequence_blocks == 0 + assert prep.is_flags_on(PRepFlag.BLOCK_STATISTICS) + assert prep.is_dirty() + + prep.freeze() + assert not prep.is_flags_on(PRepFlag.BLOCK_STATISTICS) + assert not prep.is_dirty() def test_reset_block_validation_penalty(prep): @@ -239,13 +269,19 @@ def test_reset_block_validation_penalty(prep): assert prep.total_blocks == size assert prep.validated_blocks == 0 assert prep.unvalidated_sequence_blocks == UNVALIDATED_SEQUENCE_BLOCKS + size + assert prep.is_flags_on(PRepFlag.TOTAL_BLOCKS | PRepFlag.UNVALIDATED_SEQUENCE_BLOCKS) + assert prep.is_dirty() prep.penalty = PenaltyReason.BLOCK_VALIDATION assert prep.penalty == PenaltyReason.BLOCK_VALIDATION + assert prep.is_flags_on(PRepFlag.PENALTY) + assert prep.is_dirty() prep.reset_block_validation_penalty() assert prep.penalty == PenaltyReason.NONE assert prep.unvalidated_sequence_blocks == 0 + assert prep.is_flags_on(PRepFlag.PENALTY) + assert prep.is_dirty() def test_to_dict_with_full(prep): @@ -303,3 +339,34 @@ def test_to_dict_with_abridged(prep): # version, email, website, details and p2pEndpoint are not included # SIZE(20) - 5(version, email, website, details, p2pEndpoint) + 2(stake, delegated) assert len(info) == PRep.Index.SIZE - 3 + + +def test_setter(prep): + data = { + "status": (PRepStatus.UNREGISTERED, PRepFlag.STATUS), + "name": ("orange", PRepFlag.NAME), + "country": ("USA", PRepFlag.COUNTRY), + "city": ("New York", PRepFlag.CITY), + "email": ("orange@example.com", PRepFlag.EMAIL), + "website": ("https://orange.example.com/", PRepFlag.WEBSITE), + "details": ("https://orange.example.com/details.json", PRepFlag.DETAILS), + "p2p_endpoint": ("orange.example.com:7100", PRepFlag.P2P_ENDPOINT), + "penalty": (PenaltyReason.LOW_PRODUCTIVITY, PRepFlag.PENALTY), + "grade": (PRepGrade.MAIN, PRepFlag.GRADE), + "stake": (STAKE + 1, PRepFlag.STAKE), + "delegated": (DELEGATED + 1, PRepFlag.DELEGATED), + "last_generate_block_height": (LAST_GENERATE_BLOCK_HEIGHT + 1, PRepFlag.LAST_GENERATE_BLOCK_HEIGHT) + } + + for key in data: + new_value = data[key][0] + flag: 'PRepFlag' = data[key][1] + + # If new value is the same as the old one, flag should not be set + old_value = getattr(prep, key) + setattr(prep, key, old_value) + assert not prep.is_flags_on(flag) + + # If new value is different from the old one, flag should be set + setattr(prep, key, new_value) + assert prep.is_flags_on(flag) diff --git a/tests/prep/test_prep_container.py b/tests/prep/test_prep_container.py index 6ff5f4b60..5a6cfb824 100644 --- a/tests/prep/test_prep_container.py +++ b/tests/prep/test_prep_container.py @@ -19,9 +19,11 @@ # noinspection PyPackageRequirements import pytest + from iconservice.base.address import Address, AddressPrefix from iconservice.base.exception import AccessDeniedException, InvalidParamsException -from iconservice.prep.data import PRep, PRepContainer, PRepStatus +from iconservice.icon_constant import PRepStatus +from iconservice.prep.data import PRep, PRepContainer def _create_dummy_prep(index: int, status: 'PRepStatus' = PRepStatus.ACTIVE) -> 'PRep': diff --git a/tests/prep/test_term.py b/tests/prep/test_term.py index d0cc385c8..9390fb026 100644 --- a/tests/prep/test_term.py +++ b/tests/prep/test_term.py @@ -13,16 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import random import unittest -import copy from typing import List from unittest.mock import Mock +from iconservice import utils from iconservice.base.address import Address, AddressPrefix from iconservice.icon_constant import ( PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS, TERM_PERIOD, - IconScoreContextType, PenaltyReason, PRepStatus + IconScoreContextType, PenaltyReason, PRepStatus, TermFlag ) from iconservice.iconscore.icon_score_context import IconScoreContext from iconservice.icx import IcxStorage @@ -83,6 +84,7 @@ def setUp(self) -> None: self.total_elected_prep_delegated += delegated self.term.set_preps(self.preps, PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS) + assert not self.term.is_dirty() def test_set_preps(self): self.term.set_preps(self.preps, PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS) @@ -125,13 +127,17 @@ def test_update_preps_with_critical_penalty(self): invalid_main_prep.penalty = penalty invalid_elected_preps: List['PRep'] = [invalid_main_prep] - term.update_preps(revision, invalid_elected_preps) + term.update_invalid_elected_preps(invalid_elected_preps) + _check_prep_snapshots_in_term(term) assert len(term.main_preps) == PREP_MAIN_PREPS assert len(term.sub_preps) == PREP_MAIN_AND_SUB_PREPS - PREP_MAIN_PREPS - len(invalid_elected_preps) assert isinstance(term.root_hash, bytes) assert term.root_hash != self.term.root_hash assert term.total_elected_prep_delegated == self.total_elected_prep_delegated - invalid_main_prep.delegated + assert not term.flags & TermFlag.MAIN_PREP_P2P_ENDPOINT + assert utils.is_all_flag_on(term.flags, TermFlag.MAIN_PREPS | TermFlag.SUB_PREPS) + assert term.is_dirty() def test_update_preps_with_block_validation_penalty(self): revision: int = 0 @@ -147,13 +153,16 @@ def test_update_preps_with_block_validation_penalty(self): invalid_main_prep.penalty = PenaltyReason.BLOCK_VALIDATION invalid_elected_preps: List['PRep'] = [invalid_main_prep] - term.update_preps(revision, invalid_elected_preps) + term.update_invalid_elected_preps(invalid_elected_preps) _check_prep_snapshots_in_term(term) assert len(term.main_preps) == PREP_MAIN_PREPS assert len(term.sub_preps) == PREP_MAIN_AND_SUB_PREPS - PREP_MAIN_PREPS - len(invalid_elected_preps) assert isinstance(term.root_hash, bytes) assert term.root_hash != self.term.root_hash assert term.total_elected_prep_delegated == self.total_elected_prep_delegated - invalid_main_prep.delegated + assert not term.flags & TermFlag.MAIN_PREP_P2P_ENDPOINT + assert utils.is_all_flag_on(term.flags, TermFlag.MAIN_PREPS | TermFlag.SUB_PREPS) + assert term.is_dirty() def test_update_preps_with_unregistered_prep(self): revision: int = 0 @@ -169,13 +178,16 @@ def test_update_preps_with_unregistered_prep(self): invalid_main_prep.status = PRepStatus.UNREGISTERED invalid_elected_preps: List['PRep'] = [invalid_main_prep] - term.update_preps(revision, invalid_elected_preps) + term.update_invalid_elected_preps(invalid_elected_preps) _check_prep_snapshots_in_term(term) assert len(term.main_preps) == PREP_MAIN_PREPS assert len(term.sub_preps) == PREP_MAIN_AND_SUB_PREPS - PREP_MAIN_PREPS - len(invalid_elected_preps) assert isinstance(term.root_hash, bytes) assert term.root_hash != self.term.root_hash assert term.total_elected_prep_delegated == self.total_elected_prep_delegated - invalid_main_prep.delegated + assert not term.flags & TermFlag.MAIN_PREP_P2P_ENDPOINT + assert utils.is_all_flag_on(term.flags, TermFlag.MAIN_PREPS | TermFlag.SUB_PREPS) + assert term.is_dirty() def test_update_preps_with_sub_preps_only(self): revision: int = 0 @@ -191,11 +203,15 @@ def test_update_preps_with_sub_preps_only(self): invalid_elected_preps.append(prep) assert len(invalid_elected_preps) == PREP_MAIN_AND_SUB_PREPS - PREP_MAIN_PREPS - term.update_preps(revision, invalid_elected_preps) + term.update_invalid_elected_preps(invalid_elected_preps) assert len(term.main_preps) == PREP_MAIN_PREPS assert len(term.sub_preps) == 0 assert isinstance(term.root_hash, bytes) assert term.root_hash == self.term.root_hash + assert not term.flags & TermFlag.MAIN_PREP_P2P_ENDPOINT + assert not term.flags & TermFlag.MAIN_PREPS + assert utils.is_all_flag_on(term.flags, TermFlag.SUB_PREPS) + assert term.is_dirty() def test_update_preps_2(self): # Remove all main P-Reps @@ -204,7 +220,7 @@ def test_update_preps_2(self): _check_prep_snapshots_in_term(term) invalid_elected_preps: List['PRep'] = [prep for prep in self.preps[:PREP_MAIN_PREPS]] - term.update_preps(revision, invalid_elected_preps) + term.update_invalid_elected_preps(invalid_elected_preps) assert len(term.main_preps) == PREP_MAIN_PREPS assert len(term.sub_preps) == PREP_MAIN_AND_SUB_PREPS - PREP_MAIN_PREPS * 2 assert isinstance(term.root_hash, bytes) @@ -215,14 +231,20 @@ def test_update_preps_2(self): _check_prep_snapshots_in_term(term) invalid_elected_preps: List['PRep'] = [prep for prep in self.preps[1:PREP_MAIN_AND_SUB_PREPS]] - term.update_preps(revision, invalid_elected_preps) + term.update_invalid_elected_preps(invalid_elected_preps) assert len(term.main_preps) == 1 assert len(term.sub_preps) == 0 assert isinstance(term.root_hash, bytes) assert term.root_hash != self.term.root_hash + assert term.flags == term.flags, TermFlag.MAIN_PREPS | TermFlag.SUB_PREPS + assert not term.flags & TermFlag.MAIN_PREP_P2P_ENDPOINT + assert utils.is_all_flag_on(term.flags, TermFlag.MAIN_PREPS | TermFlag.SUB_PREPS) + assert term.is_dirty() def test_to_list_and_from_list(self): self.term.set_preps(self.preps, PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS) + assert not self.term.is_dirty() + assert self.term.flags == TermFlag.NONE _check_prep_snapshots_in_term(self.term) new_term = Term.from_list(self.term.to_list(), self.term.total_elected_prep_delegated_snapshot) @@ -233,8 +255,21 @@ def test_to_list_and_from_list(self): def test__contain__(self): term = self.term term.set_preps(self.preps, PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS) + assert not term.is_dirty() + assert term.flags == TermFlag.NONE + _check_prep_snapshots_in_term(term) assert len(term.main_preps) == PREP_MAIN_PREPS assert len(term.sub_preps) == PREP_MAIN_AND_SUB_PREPS - PREP_MAIN_PREPS assert len(term) == PREP_MAIN_AND_SUB_PREPS + + def test_on_main_prep_p2p_endpoint_updated(self): + term = self.term + term.set_preps(self.preps, PREP_MAIN_PREPS, PREP_MAIN_AND_SUB_PREPS) + assert not term.is_dirty() + assert term.flags == TermFlag.NONE + + term.on_main_prep_p2p_endpoint_updated() + assert term.is_dirty() + assert term.flags == TermFlag.MAIN_PREP_P2P_ENDPOINT