From 8bc795e630bb0cd1f82c060fa6de2c776cf2c840 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Sun, 10 Jun 2018 12:58:49 -0700 Subject: [PATCH] Agent validation and self-awareness, and documentation. --- nucypher/blockchain/eth/actors.py | 83 +++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 70ffcd30d..dce1a5cab 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -7,22 +7,27 @@ from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, Polic from constant_sorrow import constants - class NucypherTokenActor: """ - Concrete base class for any actor that will interface with NuCypher's ethereum smart contracts + Concrete base class for any actor that will interface with NuCypher's ethereum smart contracts. """ class ActorError(Exception): pass - def __init__(self, ether_address: Union[str, bytes, None]=None, - token_agent: NucypherTokenAgent=None, *args, **kwargs): + def __init__(self, ether_address: str=None, token_agent: NucypherTokenAgent=None, *args, **kwargs): + """ + :param ether_address: If not passed, we assume this is an unknown actor + + :param token_agent: The token agent with the blockchain attached; If not passed, A default + token agent and blockchain connection will be created from default values. + + """ # Auto-connect, if needed self.token_agent = token_agent if token_agent is not None else NucypherTokenAgent() - self.__ether_address = ether_address if ether_address is not None else constants.UNKNOWN_ACTOR + self.ether_address = ether_address if ether_address is not None else constants.UNKNOWN_ACTOR self._transaction_cache = list() # track transactions transmitted def __repr__(self): @@ -33,15 +38,9 @@ class NucypherTokenActor: @classmethod def from_config(cls, config): + """Read actor data from a configuration file, and create an actor instance.""" raise NotImplementedError - # - # Crypto-asset balances - # - @property - def ether_address(self): - return self.__ether_address - def eth_balance(self): """Return this actors's current ETH balance""" balance = self.token_agent.blockchain.interface.w3.eth.getBalance(self.ether_address) @@ -55,13 +54,13 @@ class NucypherTokenActor: class Miner(NucypherTokenActor): """ - Ursula, practically carrying a pickaxe. + Ursula baseclass for blockchain operations, practically carrying a pickaxe. """ class MinerError(NucypherTokenActor.ActorError): pass - def __init__(self, miner_agent: MinerAgent=None, *args, **kwargs): + def __init__(self, is_me=True, miner_agent: MinerAgent=None, *args, **kwargs): miner_agent = miner_agent if miner_agent is not None else MinerAgent() super().__init__(token_agent=miner_agent.token_agent, *args, **kwargs) @@ -71,6 +70,7 @@ class Miner(NucypherTokenActor): self.blockchain = self.token_agent.blockchain # Establish initial state + self.is_me = is_me self.__locked_tokens = constants.LOCKED_TOKENS_UNAVAILIBLE self.__datastore_entries = constants.CONTRACT_DATASTORE_UNAVAILIBLE self.__node_datastore = constants.CONTRACT_DATASTORE_UNAVAILIBLE @@ -81,6 +81,7 @@ class Miner(NucypherTokenActor): @classmethod def from_config(cls, blockchain_config) -> 'Miner': + """Read miner data from a configuration file, and create an miner instance.""" # Use BlockchainConfig to default to the first wallet address wallet_address = blockchain_config.wallet_addresses[0] @@ -93,6 +94,8 @@ class Miner(NucypherTokenActor): # def _approve_escrow(self, amount: int) -> str: """Approve the transfer of token from the miner's address to the escrow contract.""" + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") txhash = self.token_agent.contract.functions.approve(self.miner_agent.contract_address, amount).transact({'from': self.ether_address}) self.blockchain.wait_for_receipt(txhash) @@ -103,6 +106,8 @@ class Miner(NucypherTokenActor): def _send_tokens_to_escrow(self, amount, lock_periods) -> str: """Send tokes to the escrow from the miner's address""" + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") deposit_txhash = self.miner_agent.contract.functions.deposit(amount, lock_periods).transact({'from': self.ether_address}) @@ -113,6 +118,9 @@ class Miner(NucypherTokenActor): 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 contract staking functions with a non-self Miner instance.") + approve_txhash = self._approve_escrow(amount=amount) deposit_txhash = self._send_tokens_to_escrow(amount=amount, lock_periods=lock_periods) @@ -145,6 +153,9 @@ class Miner(NucypherTokenActor): # # TODO add divide_stake method def switch_lock(self): + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") + lock_txhash = self.miner_agent.contract.functions.switchLock().transact({'from': self.ether_address}) self.blockchain.wait_for_receipt(lock_txhash) @@ -152,6 +163,8 @@ class Miner(NucypherTokenActor): return lock_txhash def __validate_stake(self, amount: int, lock_periods: int) -> bool: + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") from .constants import validate_locktime, validate_stake_amount assert validate_stake_amount(amount=amount) @@ -165,9 +178,16 @@ class Miner(NucypherTokenActor): def stake(self, amount, lock_periods, entire_balance=False): """ High level staking method for Miners. - """ - # manual type checking below this point; force an int to allow use of constants + :param amount: Amount of tokens to stake denominated in the smallest unit. + :param lock_periods: Duration of stake in periods. + :param entire_balance: If True, stake the entire balance of this node, or the maximum possible. + + """ + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") + + # manual type checking below this point; force an int to allow use of constants amount, lock_periods = int(amount), int(lock_periods) staking_transactions = OrderedDict() # Time series of txhases @@ -193,6 +213,9 @@ class Miner(NucypherTokenActor): def confirm_activity(self) -> str: """Miner rewarded for every confirmed period""" + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") + txhash = self.miner_agent.contract.functions.confirmActivity().transact({'from': self.ether_address}) self.blockchain.wait_for_receipt(txhash) @@ -203,6 +226,9 @@ class Miner(NucypherTokenActor): def mint(self) -> Tuple[str, str]: """Computes and transfers tokens to the miner's account""" + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") + mint_txhash = self.miner_agent.contract.functions.mint().transact({'from': self.ether_address}) self.blockchain.wait_for_receipt(mint_txhash) @@ -213,6 +239,9 @@ class Miner(NucypherTokenActor): def collect_policy_reward(self, policy_manager): """Collect rewarded ETH""" + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") + policy_reward_txhash = policy_manager.contract.functions.withdraw().transact({'from': self.ether_address}) self.blockchain.wait_for_receipt(policy_reward_txhash) @@ -223,6 +252,9 @@ class Miner(NucypherTokenActor): def collect_staking_reward(self) -> str: """Withdraw tokens rewarded for staking.""" + if not self.is_me: + raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.") + token_amount = self.miner_agent.contract.functions.minerInfo(self.ether_address).call()[0] staked_amount = max(self.miner_agent.contract.functions.getLockedTokens(self.ether_address).call(), self.miner_agent.contract.functions.getLockedTokens(self.ether_address, 1).call()) @@ -238,9 +270,12 @@ class Miner(NucypherTokenActor): # Miner Datastore # - def publish_datastore(self, data) -> str: + def _publish_datastore(self, data) -> str: """Publish new data to the MinerEscrow contract as a public record associated with this miner.""" + if not self.is_me: + raise self.MinerError("Cannot write to contract datastore with a non-self Miner instance.") + txhash = self.miner_agent.contract.functions.setMinerId(data).transact({'from': self.ether_address}) self.blockchain.wait_for_receipt(txhash) @@ -260,13 +295,14 @@ class Miner(NucypherTokenActor): yield value self.__node_datastore = node_datastore_reader() - def read_datastore(self, index: int=None, refresh=False): + def _read_datastore(self, index: int=None, refresh=False): """ Read a value from the nodes datastore, within the MinersEscrow ethereum contract. since there may be multiple values, select one, and return it. The most recently pushed entry is returned by default, and can be specified with the index parameter. If refresh it True, read the node's data from the blockchain before returning. + """ if refresh is True: self.__fetch_node_datastore() @@ -285,13 +321,18 @@ class Miner(NucypherTokenActor): class PolicyAuthor(NucypherTokenActor): - """Alice, mocking up new policies!""" + """Alice base class for blockchain operations, mocking up new policies!""" def __init__(self, policy_agent: PolicyAgent=None, *args, **kwargs): + """ + + :param policy_agent: A policy agent with the blockchain attached; If not passed, A default policy + agent and blockchain connection will be created from default values. + + """ - # From defaults if policy_agent is None: - # all defaults + # From defaults self.token_agent = NucypherTokenAgent() self.miner_agent = MinerAgent(token_agent=self.token_agent) self.policy_agent = PolicyAgent(miner_agent=self.miner_agent)