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))
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