Пример #1
0
class Worker(NucypherTokenActor):
    """
    Ursula baseclass for blockchain operations, practically carrying a pickaxe.
    """
    class WorkerError(NucypherTokenActor.ActorError):
        pass

    class DetachedWorker(WorkerError):
        """Raised when the worker address is not assigned an on-chain stake in the StakingEscrow contract."""

    def __init__(self,
                 is_me: bool,
                 stake_tracker: StakeTracker = None,
                 worker_address: str = None,
                 start_working_loop: bool = True,
                 *args,
                 **kwargs) -> None:

        super().__init__(*args, **kwargs)

        self.log = Logger("worker")

        self.__worker_address = worker_address
        self.is_me = is_me

        # Agency
        self.token_agent = NucypherTokenAgent(blockchain=self.blockchain)
        self.staking_agent = StakingEscrowAgent(blockchain=self.blockchain)

        # Stakes
        self.__start_time = WORKER_NOT_RUNNING
        self.__uptime_period = WORKER_NOT_RUNNING

        # Workers cannot be started without being assigned a stake first.
        if is_me:
            self.stake_tracker = stake_tracker or StakeTracker(
                checksum_addresses=[self.checksum_address])

            if not self.stake_tracker.stakes(
                    checksum_address=self.checksum_address):
                raise self.DetachedWorker
            else:
                self.stake_tracker.add_action(self._confirm_period)
                if start_working_loop:
                    self.stake_tracker.start()

    @property
    def last_active_period(self) -> int:
        period = self.staking_agent.get_last_active_period(
            address=self.checksum_address)
        return period

    @only_me
    @save_receipt
    def confirm_activity(self) -> str:
        """For each period that the worker confirms activity, the staker is rewarded"""
        receipt = self.staking_agent.confirm_activity(
            worker_address=self.__worker_address)
        return receipt

    @only_me
    def _confirm_period(self) -> None:
        # TODO: Follow-up actions for downtime
        # TODO: Check for stake expiration and exit
        missed_periods = self.stake_tracker.current_period - self.last_active_period
        if missed_periods:
            self.log.warn(
                f"MISSED CONFIRMATIONS - {missed_periods} missed staking confirmations detected!"
            )
        self.confirm_activity()  # < --- blockchain WRITE
        self.log.info("Confirmed activity for period {}".format(
            self.stake_tracker.current_period))
Пример #2
0
def test_sampling_distribution(testerchain, token, deploy_contract):

    #
    # SETUP
    #

    max_allowed_locked_tokens = 5 * 10 ** 8
    _staking_coefficient = 2 * 10 ** 7
    staking_escrow_contract, _ = deploy_contract(
        contract_name=STAKING_ESCROW_CONTRACT_NAME,
        _token=token.address,
        _hoursPerPeriod=1,
        _miningCoefficient=4 * _staking_coefficient,
        _lockedPeriodsCoefficient=4,
        _rewardedPeriods=4,
        _minLockedPeriods=2,
        _minAllowableLockedTokens=100,
        _maxAllowableLockedTokens=max_allowed_locked_tokens,
        _minWorkerPeriods=1,
        _isTestContract=False
    )
    staking_agent = StakingEscrowAgent(registry=None, contract=staking_escrow_contract)

    policy_manager, _ = deploy_contract(
        'PolicyManagerForStakingEscrowMock', token.address, staking_escrow_contract.address
    )
    tx = staking_escrow_contract.functions.setPolicyManager(policy_manager.address).transact()
    testerchain.wait_for_receipt(tx)

    # Travel to the start of the next period to prevent problems with unexpected overflow first period
    testerchain.time_travel(hours=1)

    creator = testerchain.etherbase_account

    # Give Escrow tokens for reward and initialize contract
    tx = token.functions.approve(staking_escrow_contract.address, 10 ** 9).transact({'from': creator})
    testerchain.wait_for_receipt(tx)

    tx = staking_escrow_contract.functions.initialize(10 ** 9).transact({'from': creator})
    testerchain.wait_for_receipt(tx)

    stakers = testerchain.stakers_accounts
    amount = token.functions.balanceOf(creator).call() // len(stakers)

    # Airdrop
    for staker in stakers:
        tx = token.functions.transfer(staker, amount).transact({'from': creator})
        testerchain.wait_for_receipt(tx)

    all_locked_tokens = len(stakers) * amount
    for staker in stakers:
        balance = token.functions.balanceOf(staker).call()
        tx = token.functions.approve(staking_escrow_contract.address, balance).transact({'from': staker})
        testerchain.wait_for_receipt(tx)

        staking_agent.deposit_tokens(amount=balance, lock_periods=10, sender_address=staker, staker_address=staker)
        staking_agent.set_worker(staker_address=staker, worker_address=staker)
        staking_agent.confirm_activity(staker)

    # Wait next period and check all locked tokens
    testerchain.time_travel(hours=1)

    #
    # Test sampling distribution
    #

    ERROR_TOLERANCE = 0.05  # With this tolerance, all sampling ratios should between 5% and 15% (expected is 10%)
    SAMPLES = 1000
    quantity = 3
    counter = Counter()

    sampled, failed = 0, 0
    while sampled < SAMPLES:
        try:
            addresses = set(staking_agent.sample(quantity=quantity, additional_ursulas=1, duration=1))
            addresses.discard(NULL_ADDRESS)
        except staking_agent.NotEnoughStakers:
            failed += 1
            continue
        else:
            sampled += 1
            counter.update(addresses)

    total_times = sum(counter.values())

    expected = amount / all_locked_tokens
    for staker in stakers:
        times = counter[staker]
        sampled_ratio = times / total_times
        abs_error = abs(expected - sampled_ratio)
        assert abs_error < ERROR_TOLERANCE