diff --git a/cli/main.py b/cli/main.py index 42888f133..091879cb7 100755 --- a/cli/main.py +++ b/cli/main.py @@ -758,12 +758,15 @@ def status(config, provider, contracts, network): @cli.command() @click.option('--dev', is_flag=True, default=False) @click.option('--federated-only', is_flag=True) +@click.option('--poa', is_flag=True) @click.option('--rest-host', type=str) @click.option('--rest-port', type=int) @click.option('--db-name', type=str) @click.option('--provider-uri', type=str) @click.option('--registry-filepath', type=click.Path()) @click.option('--checksum-address', type=str) +@click.option('--stake-amount', type=int) +@click.option('--stake-periods', type=int) @click.option('--metadata-dir', type=click.Path()) @click.option('--config-file', type=click.Path()) def run_ursula(rest_port, @@ -772,9 +775,12 @@ def run_ursula(rest_port, provider_uri, registry_filepath, checksum_address, + stake_amount, + stake_periods, federated_only, metadata_dir, config_file, + poa, dev ) -> None: """ @@ -802,6 +808,7 @@ def run_ursula(rest_port, else: ursula_config = UrsulaConfiguration(temp=temp, auto_initialize=temp, + poa=poa, rest_host=rest_host, rest_port=rest_port, db_name=db_name, @@ -820,9 +827,12 @@ def run_ursula(rest_port, try: URSULA = ursula_config.produce(passphrase=passphrase) + + if not federated_only: + + URSULA.stake(amount=stake_amount, lock_periods=stake_periods) + URSULA.get_deployer().run() # Run TLS Deploy (Reactor) - if not URSULA.federated_only: # TODO: Resume / Init - URSULA.stake() # Start Staking Daemon finally: diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index b15475c97..f1ad1c59d 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -1,12 +1,13 @@ from collections import OrderedDict +from logging import getLogger import maya +from constant_sorrow import constants from datetime import datetime +from twisted.internet import task, reactor from typing import Tuple, List from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent -from nucypher.blockchain.eth.chains import Blockchain -from nucypher.blockchain.eth.interfaces import EthereumContractRegistry from nucypher.blockchain.eth.utils import (datetime_to_period, validate_stake_amount, validate_locktime, @@ -79,26 +80,123 @@ class Miner(NucypherTokenActor): Ursula baseclass for blockchain operations, practically carrying a pickaxe. """ + __current_period_sample_rate = 10 + class MinerError(NucypherTokenActor.ActorError): pass - def __init__(self, miner_agent: MinerAgent = None, is_me=True, *args, **kwargs) -> None: - if miner_agent is None: - token_agent = NucypherTokenAgent() - miner_agent = MinerAgent(token_agent=token_agent) - super().__init__(token_agent=miner_agent.token_agent, *args, **kwargs) + def __init__(self, is_me: bool, miner_agent: MinerAgent = None, *args, **kwargs) -> None: - # Extrapolate dependencies - self.miner_agent = miner_agent - self.token_agent = miner_agent.token_agent - self.blockchain = self.token_agent.blockchain - - # Establish initial state + self.log = getLogger("miner") self.is_me = is_me + if is_me: + if miner_agent is None: + token_agent = NucypherTokenAgent() + miner_agent = MinerAgent(token_agent=token_agent) + else: + token_agent = miner_agent.token_agent + blockchain = miner_agent.token_agent.blockchain + else: + token_agent = constants.STRANGER_MINER + blockchain = constants.STRANGER_MINER + + self.miner_agent = miner_agent + self.token_agent = token_agent + self.blockchain = blockchain + + super().__init__(token_agent=self.token_agent, *args, **kwargs) + + if is_me is True: + self.__current_period = None # TODO: use constant + self._abort_on_staking_error = True + self._staking_task = task.LoopingCall(self._confirm_period) + # # Staking # + @only_me + def stake(self, + confirm_now=False, + resume: bool = False, + expiration: maya.MayaDT = None, + lock_periods: int = None, + *args, **kwargs) -> None: + + """High-level staking daemon loop""" + + if lock_periods and expiration: + raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.") + if expiration: + lock_periods = datetime_to_period(expiration) + + if resume is False: + _staking_receipts = self.initialize_stake(expiration=expiration, + lock_periods=lock_periods, + *args, **kwargs) + + # TODO: Check if this period has already been confirmed + # TODO: Check if there is an active stake in the current period: Resume staking daemon + # TODO: Validation and Sanity checks + + if confirm_now: + self.confirm_activity() + + # record start time and periods + self.__start_time = maya.now() + self.__uptime_period = self.miner_agent.get_current_period() + self.__terminal_period = self.__uptime_period + lock_periods + self.__current_period = self.__uptime_period + self.start_staking_loop() + + # + # Daemon + # + + def _confirm_period(self): + period = self.miner_agent.get_current_period() + # check for stale sample data + self.log.info("Checking for new period. Current period is {}".format(self.__current_period)) # TODO: set to debug? + if self.__current_period != period: + + # check for stake expiration + stake_expired = self.__current_period >= self.__terminal_period + if stake_expired: + self.log.info('Stake duration expired') + return True + + self.confirm_activity() + self.__current_period = period + self.log.info("Confirmed activity for period {}".format(self.__current_period)) + + @only_me + def _crash_gracefully(self, failure=None): + """ + A facility for crashing more gracefully in the event that an exception + is unhandled in a different thread, especially inside a loop like the learning loop. + """ + self._crashed = failure + failure.raiseException() + + @only_me + def handle_staking_errors(self, *args, **kwargs): + failure = args[0] + if self._abort_on_staking_error: + self.log.critical("Unhandled error during node staking. Attempting graceful crash.") + reactor.callFromThread(self._crash_gracefully, failure=failure) + else: + self.log.warning("Unhandled error during node learning: {}".format(failure.getTraceback())) + + @only_me + def start_staking_loop(self, now=True): + if self._staking_task.running: + return False + else: + d = self._staking_task.start(interval=self.__current_period_sample_rate, now=now) + d.addErrback(self.handle_staking_errors) + self.log.info("Started staking loop") + return d + @property def is_staking(self): """Checks if this Miner currently has locked tokens.""" @@ -118,8 +216,6 @@ class Miner(NucypherTokenActor): @only_me def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]: """Public facing method for token locking.""" - if not self.is_me: - raise self.MinerError("Cannot execute miner staking functions with a non-self Miner instance.") approve_txhash = self.token_agent.approve_transfer(amount=amount, target_address=self.miner_agent.contract_address, @@ -178,7 +274,7 @@ class Miner(NucypherTokenActor): @only_me def __validate_stake(self, amount: int, lock_periods: int) -> bool: - assert validate_stake_amount(amount=amount) + assert validate_stake_amount(amount=amount) # TODO: remove assertions..? assert validate_locktime(lock_periods=lock_periods) if not self.token_balance >= amount: diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index ba7358b11..be6489208 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -701,74 +701,3 @@ class Ursula(Character, VerifiableNode, Miner): if work_order.bob == bob: work_orders_from_bob.append(work_order) return work_orders_from_bob - - @only_me - def stake(self, - sample_rate: int = 10, - refresh_rate: int = 60, - confirm_now=True, - resume: bool = False, - expiration: maya.MayaDT = None, - lock_periods: int = None, - *args, **kwargs) -> None: - - """High-level staking daemon loop""" - - if lock_periods and expiration: - raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.") - if expiration: - lock_periods = datetime_to_period(expiration) - - if resume is False: - _staking_receipts = super().initialize_stake(expiration=expiration, - lock_periods=lock_periods, - *args, **kwargs) - - # TODO: Check if this period has already been confirmed - # TODO: Check if there is an active stake in the current period: Resume staking daemon - # TODO: Validation and Sanity checks - - if confirm_now: - self.confirm_activity() - - # record start time and periods - start_time = maya.now() - uptime_period = self.miner_agent.get_current_period() - terminal_period = uptime_period + lock_periods - current_period = uptime_period - - # - # Daemon - # - - try: - while True: - - # calculate timedeltas - now = maya.now() - initialization_delta = now - start_time - - # check if iteration re-samples - sample_stale = initialization_delta.seconds > (refresh_rate - 1) - if sample_stale: - - period = self.miner_agent.get_current_period() - # check for stale sample data - if current_period != period: - - # check for stake expiration - stake_expired = current_period >= terminal_period - if stake_expired: - break - - self.confirm_activity() - current_period = period - # wait before resampling - time.sleep(sample_rate) - continue - - finally: - - # TODO: Cleanup # - - pass diff --git a/nucypher/config/characters.py b/nucypher/config/characters.py index 2d1137334..4efd41f8a 100644 --- a/nucypher/config/characters.py +++ b/nucypher/config/characters.py @@ -4,6 +4,7 @@ from constant_sorrow import constants from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve from cryptography.x509 import Certificate +from web3.middleware import geth_poa_middleware from nucypher.blockchain.eth.agents import EthereumContractAgent, NucypherTokenAgent, MinerAgent from nucypher.blockchain.eth.chains import Blockchain @@ -37,6 +38,7 @@ class UrsulaConfiguration(NodeConfiguration): crypto_power: CryptoPower = None, # Blockchain + poa: bool = False, provider_uri: str = None, miner_agent: EthereumContractAgent = None, @@ -63,6 +65,7 @@ class UrsulaConfiguration(NodeConfiguration): # # Blockchain # + self.poa = poa self.blockchain_uri = provider_uri self.miner_agent = miner_agent @@ -116,6 +119,7 @@ class UrsulaConfiguration(NodeConfiguration): from nucypher.characters.lawful import Ursula if self.federated_only is False: + blockchain = Blockchain.connect(provider_uri=self.blockchain_uri) # TODO: move this..? token_agent = NucypherTokenAgent(blockchain=blockchain, registry_filepath=self.registry_filepath) miner_agent = MinerAgent(token_agent=token_agent) @@ -123,6 +127,10 @@ class UrsulaConfiguration(NodeConfiguration): ursula = Ursula(**merged_parameters) + if self.poa: + w3 = ursula.blockchain.interface.w3 + w3.middleware_stack.inject(geth_poa_middleware, layer=0) + # if self.save_metadata: # TODO: Does this belong here..? ursula.write_node_metadata(node=ursula) ursula.save_certificate_to_disk(directory=ursula.known_certificates_dir) # TODO: Move this..?