From c6a9cc82dd0a6362962b5296fc362d36cbf402dc Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Wed, 2 Sep 2020 19:28:13 +0900 Subject: [PATCH 1/2] Update UnstakePatcher * Implement eventlog recording --- iconservice/icx/unstake_patcher.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/iconservice/icx/unstake_patcher.py b/iconservice/icx/unstake_patcher.py index bac53189d..e88350c31 100644 --- a/iconservice/icx/unstake_patcher.py +++ b/iconservice/icx/unstake_patcher.py @@ -10,8 +10,10 @@ from typing import TYPE_CHECKING, List, Dict, Any from iconcommons.logger import Logger + from .storage import AccountPartFlag -from ..base.address import Address +from ..base.address import Address, SYSTEM_SCORE_ADDRESS +from ..iconscore.icon_score_event_log import EventLogEmitter from ..icx.coin_part import CoinPartFlag if TYPE_CHECKING: @@ -140,6 +142,7 @@ def run(self, context: 'IconScoreContext'): assert stake_part.is_dirty() storage.put_stake_part(context, address, stake_part) + self._emit_event_log(context, target) self._add_success_item(target) Logger.info(tag=TAG, msg="UnstakePatcher.run() end") @@ -257,6 +260,18 @@ def _add_failure_item(self, target: Target): self._failure_targets.append(target) self._failure_unstake += target.total_unstake + @classmethod + def _emit_event_log(cls, context: 'IconScoreContext', target: Target): + for unstake in target.unstakes: + EventLogEmitter.emit_event_log( + context=context, + event_signature="InvalidUnstakeFixed(Address,int,int)", + score_address=SYSTEM_SCORE_ADDRESS, + arguments=[target.address, unstake.amount, unstake.block_height], + indexed_args_count=1, + fee_charge=False + ) + def write_result(self, path: str): Logger.info(tag=TAG, msg=f"UnstakePatcher.write_result() start: {path}") From 4c883b5dfed89d97e8db9d2161e16ca59a7ec56c Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Wed, 2 Sep 2020 20:04:57 +0900 Subject: [PATCH 2/2] Make UnstakePatcher robust * Although some exceptions are raised, keep going fixing invalid unstakes * Add unit-test for Target class --- iconservice/icon_service_engine.py | 2 ++ iconservice/icx/unstake_patcher.py | 36 ++++++++++++--------- tests/unit_test/icx/test_unstake_patcher.py | 35 ++++++++++++++++++-- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/iconservice/icon_service_engine.py b/iconservice/icon_service_engine.py index ea0162101..797fa94c1 100644 --- a/iconservice/icon_service_engine.py +++ b/iconservice/icon_service_engine.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Dict, Union, Any from iconcommons.logger import Logger + from iconservice.rollback import check_backup_exists from iconservice.rollback.backup_cleaner import BackupCleaner from iconservice.rollback.backup_manager import BackupManager @@ -2112,6 +2113,7 @@ def _run_unstake_patcher(self, context: 'IconScoreContext'): try: path: Optional[str] = self._conf.get( ConfigKey.INVALID_EXPIRED_UNSTAKES_PATH, None) + Logger.info(tag="UNSTAKE", msg=f"path: {path}") patcher = UnstakePatcher.from_path(path) patcher.run(context) diff --git a/iconservice/icx/unstake_patcher.py b/iconservice/icx/unstake_patcher.py index e88350c31..2988901f4 100644 --- a/iconservice/icx/unstake_patcher.py +++ b/iconservice/icx/unstake_patcher.py @@ -127,23 +127,27 @@ def run(self, context: 'IconScoreContext'): storage = context.storage.icx for target in self._targets: - address = target.address - coin_part = storage.get_part(context, AccountPartFlag.COIN, address) - stake_part = storage.get_part(context, AccountPartFlag.STAKE, address) - - result: Result = self._check_removable(coin_part, stake_part, target) - if result == Result.FALSE: - self._add_failure_item(target) - else: - if result == Result.REMOVABLE_V0: - stake_part = self._remove_invalid_expired_unstakes_v0(stake_part, target) + try: + address = target.address + coin_part = storage.get_part(context, AccountPartFlag.COIN, address) + stake_part = storage.get_part(context, AccountPartFlag.STAKE, address) + + result: Result = self._check_removable(coin_part, stake_part, target) + if result == Result.FALSE: + self._add_failure_item(target) else: - stake_part = self._remove_invalid_expired_unstakes_v1(stake_part, target) - - assert stake_part.is_dirty() - storage.put_stake_part(context, address, stake_part) - self._emit_event_log(context, target) - self._add_success_item(target) + if result == Result.REMOVABLE_V0: + stake_part = self._remove_invalid_expired_unstakes_v0(stake_part, target) + else: + stake_part = self._remove_invalid_expired_unstakes_v1(stake_part, target) + + assert stake_part.is_dirty() + storage.put_stake_part(context, address, stake_part) + self._emit_event_log(context, target) + self._add_success_item(target) + except BaseException as e: + # Although some unexpected errors happen, keep going + Logger.exception(tag=TAG, msg=str(e)) Logger.info(tag=TAG, msg="UnstakePatcher.run() end") diff --git a/tests/unit_test/icx/test_unstake_patcher.py b/tests/unit_test/icx/test_unstake_patcher.py index 728a46d3d..a62a1a726 100644 --- a/tests/unit_test/icx/test_unstake_patcher.py +++ b/tests/unit_test/icx/test_unstake_patcher.py @@ -4,12 +4,17 @@ import json import os import random -from typing import Dict, Any +from typing import Dict, Any, List import pytest +from iconservice.base.address import Address, AddressPrefix from iconservice.icx.stake_part import StakePart -from iconservice.icx.unstake_patcher import UnstakePatcher, INVALID_EXPIRED_UNSTAKES_FILENAME +from iconservice.icx.unstake_patcher import ( + INVALID_EXPIRED_UNSTAKES_FILENAME, + Target, + UnstakePatcher, +) def get_invalid_expired_unstakes_path() -> str: @@ -26,6 +31,32 @@ def unstake_patcher() -> UnstakePatcher: return UnstakePatcher.from_path(path) +class TestTarget: + def test_from_dict(self): + address = Address(AddressPrefix.EOA, os.urandom(20)) + count = random.randint(1, 5) + unstakes: List[List[int]] = [] + + for _ in range(count): + amount = random.randint(1, 1000) + block_height = random.randint(10000, 99999) + unstakes.append([amount, block_height]) + + data = { + "address": str(address), + "unstakes": unstakes + } + target = Target.from_dict(data) + + assert target.address == address + assert target.total_unstake == sum(unstake[0] for unstake in unstakes) + assert len(target.unstakes) == count + + for i, unstake in enumerate(target.unstakes): + assert unstake.amount == unstakes[i][0] + assert unstake.block_height == unstakes[i][1] + + class TestUnstakePatcher: def test_init(self, unstake_patcher): path = get_invalid_expired_unstakes_path()