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

IS-1218: Eventlog for fixing invalid expired unstakes #514

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions iconservice/icon_service_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
51 changes: 35 additions & 16 deletions iconservice/icx/unstake_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -125,22 +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._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")

Expand Down Expand Up @@ -257,6 +264,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}")

Expand Down
35 changes: 33 additions & 2 deletions tests/unit_test/icx/test_unstake_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
Expand Down