The Staking Loop; Add POA ans staking node configuration options

pull/465/head
Kieran Prasch 2018-09-27 01:11:43 -07:00
parent 74d52bba80
commit bd64521992
4 changed files with 132 additions and 89 deletions

View File

@ -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:

View File

@ -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:
def __init__(self, is_me: bool, miner_agent: MinerAgent = None, *args, **kwargs) -> None:
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)
super().__init__(token_agent=miner_agent.token_agent, *args, **kwargs)
else:
token_agent = miner_agent.token_agent
blockchain = miner_agent.token_agent.blockchain
else:
token_agent = constants.STRANGER_MINER
blockchain = constants.STRANGER_MINER
# Extrapolate dependencies
self.miner_agent = miner_agent
self.token_agent = miner_agent.token_agent
self.blockchain = self.token_agent.blockchain
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)
# Establish initial state
self.is_me = is_me
#
# 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:

View File

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

View File

@ -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..?