mirror of https://github.com/nucypher/nucypher.git
Relocate staking methods to the Stake class, away from Actors; Staking API bug fixes and consistency check.
parent
07aa603d30
commit
13d4b5c448
|
@ -16,28 +16,38 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict
|
from datetime import datetime
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
from typing import Tuple, List, Dict, Union
|
||||||
|
|
||||||
import maya
|
import maya
|
||||||
from constant_sorrow import constants
|
from constant_sorrow import constants
|
||||||
from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_ADDRESS
|
from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_ADDRESS, EMPTY_STAKING_SLOT
|
||||||
from datetime import datetime
|
|
||||||
from twisted.internet import task, reactor
|
from twisted.internet import task, reactor
|
||||||
from twisted.logger import Logger
|
from twisted.logger import Logger
|
||||||
from typing import Tuple, List, Dict, Union
|
|
||||||
|
|
||||||
from nucypher.blockchain.economics import TokenEconomics
|
from nucypher.blockchain.economics import TokenEconomics
|
||||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent, MiningAdjudicatorAgent, \
|
from nucypher.blockchain.eth.agents import (
|
||||||
|
NucypherTokenAgent,
|
||||||
|
MinerAgent,
|
||||||
|
PolicyAgent,
|
||||||
|
MiningAdjudicatorAgent,
|
||||||
EthereumContractAgent
|
EthereumContractAgent
|
||||||
|
)
|
||||||
from nucypher.blockchain.eth.chains import Blockchain
|
from nucypher.blockchain.eth.chains import Blockchain
|
||||||
from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer, \
|
from nucypher.blockchain.eth.deployers import (
|
||||||
UserEscrowProxyDeployer, UserEscrowDeployer, MiningAdjudicatorDeployer
|
NucypherTokenDeployer,
|
||||||
|
MinerEscrowDeployer,
|
||||||
|
PolicyManagerDeployer,
|
||||||
|
UserEscrowProxyDeployer,
|
||||||
|
UserEscrowDeployer,
|
||||||
|
MiningAdjudicatorDeployer
|
||||||
|
)
|
||||||
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
||||||
from nucypher.blockchain.eth.registry import AllocationRegistry
|
from nucypher.blockchain.eth.registry import AllocationRegistry
|
||||||
|
from nucypher.blockchain.eth.token import NU, Stake
|
||||||
from nucypher.blockchain.eth.utils import (datetime_to_period,
|
from nucypher.blockchain.eth.utils import (datetime_to_period,
|
||||||
calculate_period_duration)
|
calculate_period_duration)
|
||||||
from nucypher.blockchain.eth.token import NU, Stake
|
|
||||||
|
|
||||||
|
|
||||||
def only_me(func):
|
def only_me(func):
|
||||||
|
@ -56,10 +66,7 @@ class NucypherTokenActor:
|
||||||
class ActorError(Exception):
|
class ActorError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, checksum_address: str = None, blockchain: Blockchain = None):
|
||||||
checksum_address: str = None,
|
|
||||||
blockchain: Blockchain = None
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
:param checksum_address: If not passed, we assume this is an unknown actor
|
:param checksum_address: If not passed, we assume this is an unknown actor
|
||||||
"""
|
"""
|
||||||
|
@ -322,6 +329,7 @@ class Miner(NucypherTokenActor):
|
||||||
self.__uptime_period = constants.NO_STAKES
|
self.__uptime_period = constants.NO_STAKES
|
||||||
self.__terminal_period = constants.NO_STAKES
|
self.__terminal_period = constants.NO_STAKES
|
||||||
|
|
||||||
|
# Read on-chain stakes
|
||||||
self.__read_stakes()
|
self.__read_stakes()
|
||||||
if self.stakes and start_staking_loop:
|
if self.stakes and start_staking_loop:
|
||||||
self.stake()
|
self.stake()
|
||||||
|
@ -329,11 +337,14 @@ class Miner(NucypherTokenActor):
|
||||||
#
|
#
|
||||||
# Staking
|
# Staking
|
||||||
#
|
#
|
||||||
|
|
||||||
@only_me
|
@only_me
|
||||||
def stake(self, confirm_now: bool = True) -> None:
|
def stake(self, confirm_now: bool = True) -> None:
|
||||||
"""High-level staking looping call initialization"""
|
"""
|
||||||
# TODO #841: Check if there is an active stake in the current period: Resume staking daemon
|
High-level staking looping call initialization, this function aims
|
||||||
|
to be safely called at any time - For example, it is okay to call
|
||||||
|
this function multiple time within the same period.
|
||||||
|
"""
|
||||||
# Get the last stake end period of all stakes
|
# Get the last stake end period of all stakes
|
||||||
terminal_period = max(stake.end_period for stake in self.stakes.values())
|
terminal_period = max(stake.end_period for stake in self.stakes.values())
|
||||||
|
|
||||||
|
@ -343,10 +354,15 @@ class Miner(NucypherTokenActor):
|
||||||
# record start time and periods
|
# record start time and periods
|
||||||
self.__start_time = maya.now()
|
self.__start_time = maya.now()
|
||||||
self.__uptime_period = self.miner_agent.get_current_period()
|
self.__uptime_period = self.miner_agent.get_current_period()
|
||||||
self.__terminal_period = self.__uptime_period + terminal_period
|
self.__terminal_period = terminal_period
|
||||||
self.__current_period = self.__uptime_period
|
self.__current_period = self.__uptime_period
|
||||||
self.start_staking_loop()
|
self.start_staking_loop()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_active_period(self) -> int:
|
||||||
|
period = self.miner_agent.get_last_active_period(address=self.checksum_public_address)
|
||||||
|
return period
|
||||||
|
|
||||||
@only_me
|
@only_me
|
||||||
def _confirm_period(self):
|
def _confirm_period(self):
|
||||||
|
|
||||||
|
@ -355,10 +371,16 @@ class Miner(NucypherTokenActor):
|
||||||
|
|
||||||
if self.__current_period != period:
|
if self.__current_period != period:
|
||||||
|
|
||||||
# check for stake expiration
|
# Let's see how much time has passed
|
||||||
|
delta = self.__current_period - self.last_active_period
|
||||||
|
if delta > 1:
|
||||||
|
self.log.warn(f"{delta} Missed staking confirmation(s) detected")
|
||||||
|
self.__read_stakes() # Invalidate the stake cache
|
||||||
|
|
||||||
|
# Check for stake expiration
|
||||||
stake_expired = self.__current_period >= self.__terminal_period
|
stake_expired = self.__current_period >= self.__terminal_period
|
||||||
if stake_expired:
|
if stake_expired:
|
||||||
self.log.info('Stake duration expired')
|
self.log.info('STOPPED STAKING - Final stake ended.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.confirm_activity()
|
self.confirm_activity()
|
||||||
|
@ -394,14 +416,14 @@ class Miner(NucypherTokenActor):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_staking(self):
|
def is_staking(self) -> bool:
|
||||||
"""Checks if this Miner currently has locked tokens."""
|
"""Checks if this Miner currently has locked tokens."""
|
||||||
return bool(self.locked_tokens > 0)
|
return bool(self.locked_tokens > 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locked_tokens(self):
|
def locked_tokens(self) -> NU:
|
||||||
"""Returns the amount of tokens this miner has locked."""
|
"""Returns the amount of tokens this miner has locked."""
|
||||||
return self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address)
|
return NU(self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address), 'NU')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_staked(self) -> NU:
|
def total_staked(self) -> NU:
|
||||||
|
@ -410,15 +432,93 @@ class Miner(NucypherTokenActor):
|
||||||
else:
|
else:
|
||||||
return NU.ZERO()
|
return NU.ZERO()
|
||||||
|
|
||||||
|
@only_me
|
||||||
|
def divide_stake(self,
|
||||||
|
stake_index: int,
|
||||||
|
target_value: NU,
|
||||||
|
additional_periods: int = None,
|
||||||
|
expiration: maya.MayaDT = None) -> tuple:
|
||||||
|
|
||||||
|
# Validate function input
|
||||||
|
if additional_periods and expiration:
|
||||||
|
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||||
|
|
||||||
|
# Select stake to divide
|
||||||
|
try:
|
||||||
|
current_stake = self.stakes[stake_index]
|
||||||
|
except KeyError:
|
||||||
|
if len(self.stakes):
|
||||||
|
message = f"Cannot divide stake - No stake exists with index {stake_index}."
|
||||||
|
else:
|
||||||
|
message = "Cannot divide stake - There are no active stakes."
|
||||||
|
raise Stake.StakingError(message)
|
||||||
|
|
||||||
|
# Calculate stake duration in periods
|
||||||
|
if expiration:
|
||||||
|
additional_periods = datetime_to_period(datetime=expiration) - current_stake.end_period
|
||||||
|
if additional_periods <= 0:
|
||||||
|
raise Stake.StakingError("Expiration {} must be at least 1 period from now.".format(expiration))
|
||||||
|
|
||||||
|
# Check the new stake will not exceed the staking limit
|
||||||
|
if (self.total_staked + target_value) > self.economics.maximum_allowed_locked:
|
||||||
|
raise Stake.StakingError(f"Cannot divide stake - Maximum stake value exceeded with a target value of {target_value}.")
|
||||||
|
|
||||||
|
# Do it already!
|
||||||
|
modified_stake, new_stake = current_stake.divide(target_value=target_value,
|
||||||
|
additional_periods=additional_periods)
|
||||||
|
|
||||||
|
# Update stake cache
|
||||||
|
self.__read_stakes()
|
||||||
|
|
||||||
|
return modified_stake, new_stake
|
||||||
|
|
||||||
|
@only_me
|
||||||
|
def initialize_stake(self,
|
||||||
|
amount: NU,
|
||||||
|
lock_periods: int = None,
|
||||||
|
expiration: maya.MayaDT = None,
|
||||||
|
entire_balance: bool = False) -> Stake:
|
||||||
|
|
||||||
|
if lock_periods and expiration:
|
||||||
|
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||||
|
if expiration:
|
||||||
|
lock_periods = calculate_period_duration(future_time=expiration)
|
||||||
|
|
||||||
|
if entire_balance and amount:
|
||||||
|
raise ValueError("Specify an amount or entire balance, not both")
|
||||||
|
if entire_balance:
|
||||||
|
amount = self.token_balance
|
||||||
|
if not self.token_balance >= amount:
|
||||||
|
raise self.MinerError(f"Insufficient token balance ({self.token_agent}) for new stake initialization of {amount}")
|
||||||
|
|
||||||
|
# Write to blockchain
|
||||||
|
stake = Stake.initialize_stake(miner=self, amount=amount, lock_periods=lock_periods)
|
||||||
|
|
||||||
|
# Update local on-chain stake cache
|
||||||
|
self.__read_stakes()
|
||||||
|
return stake
|
||||||
|
|
||||||
|
#
|
||||||
|
# Staking Cache
|
||||||
|
#
|
||||||
|
|
||||||
def __read_stakes(self) -> None:
|
def __read_stakes(self) -> None:
|
||||||
|
|
||||||
stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address)
|
stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address)
|
||||||
|
|
||||||
stakes = dict()
|
stakes = dict()
|
||||||
|
terminal_period = 0
|
||||||
for index, stake_info in enumerate(stakes_reader):
|
for index, stake_info in enumerate(stakes_reader):
|
||||||
stake = Stake.from_stake_info(owner_address=self.checksum_public_address,
|
if not stake_info:
|
||||||
stake_info=stake_info,
|
stake = EMPTY_STAKING_SLOT
|
||||||
index=index,
|
else:
|
||||||
economics=self.economics)
|
stake = Stake.from_stake_info(miner=self, stake_info=stake_info, index=index)
|
||||||
|
if stake.end_period > terminal_period:
|
||||||
|
terminal_period = stake.end_period
|
||||||
|
|
||||||
stakes[index] = stake
|
stakes[index] = stake
|
||||||
|
|
||||||
|
self.__terminal_period = terminal_period
|
||||||
self.__stakes = stakes
|
self.__stakes = stakes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -426,160 +526,8 @@ class Miner(NucypherTokenActor):
|
||||||
"""Return all cached stakes from the blockchain."""
|
"""Return all cached stakes from the blockchain."""
|
||||||
return self.__stakes
|
return self.__stakes
|
||||||
|
|
||||||
@only_me
|
def refresh_staking_cache(self):
|
||||||
def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]:
|
return self.__read_stakes()
|
||||||
"""Public facing method for token locking."""
|
|
||||||
|
|
||||||
approve_txhash = self.token_agent.approve_transfer(amount=amount,
|
|
||||||
target_address=self.miner_agent.contract_address,
|
|
||||||
sender_address=self.checksum_public_address)
|
|
||||||
|
|
||||||
deposit_txhash = self.miner_agent.deposit_tokens(amount=amount,
|
|
||||||
lock_periods=lock_periods,
|
|
||||||
sender_address=self.checksum_public_address)
|
|
||||||
|
|
||||||
return approve_txhash, deposit_txhash
|
|
||||||
|
|
||||||
@only_me
|
|
||||||
def divide_stake(self,
|
|
||||||
stake_index: int,
|
|
||||||
target_value: NU,
|
|
||||||
additional_periods: int = None,
|
|
||||||
expiration: maya.MayaDT = None) -> dict:
|
|
||||||
"""
|
|
||||||
Modifies the unlocking schedule and value of already locked tokens.
|
|
||||||
|
|
||||||
This actor requires that is_me is True, and that the expiration datetime is after the existing
|
|
||||||
locking schedule of this miner, or an exception will be raised.
|
|
||||||
|
|
||||||
:param stake_index: The miner's stake index of the stake to divide
|
|
||||||
:param additional_periods: The number of periods to extend the stake by
|
|
||||||
:param target_value: The quantity of tokens in the smallest denomination to divide.
|
|
||||||
:param expiration: The new expiration date to set as an end period for stake division.
|
|
||||||
:return: Returns the blockchain transaction hash
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Validate function input
|
|
||||||
if additional_periods and expiration:
|
|
||||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
|
||||||
|
|
||||||
# Re-read stakes from blockchain and select stake to divide
|
|
||||||
self.__read_stakes()
|
|
||||||
current_stake = self.stakes[stake_index]
|
|
||||||
|
|
||||||
# Ensure selected stake is active
|
|
||||||
if current_stake.is_expired:
|
|
||||||
raise self.MinerError(f'Cannot dive an expired stake')
|
|
||||||
|
|
||||||
# Validate stake division parameters
|
|
||||||
if expiration:
|
|
||||||
additional_periods = datetime_to_period(datetime=expiration) - current_stake.end_period
|
|
||||||
if additional_periods <= 0:
|
|
||||||
raise self.MinerError("Expiration {} must be at least 1 period from now.".format(expiration))
|
|
||||||
|
|
||||||
if target_value >= current_stake.value:
|
|
||||||
raise self.MinerError(f"Cannot divide stake; Value ({target_value}) must be less "
|
|
||||||
f"than the existing stake value {current_stake.value}.")
|
|
||||||
|
|
||||||
#
|
|
||||||
# Generate Stakes
|
|
||||||
#
|
|
||||||
|
|
||||||
# Modified Original Stake
|
|
||||||
new_stake_1 = Stake(owner_address=self.checksum_public_address,
|
|
||||||
index=len(self.stakes)+1,
|
|
||||||
start_period=current_stake.start_period,
|
|
||||||
end_period=current_stake.end_period,
|
|
||||||
value=target_value,
|
|
||||||
economics=self.economics)
|
|
||||||
|
|
||||||
# New Derived Stake
|
|
||||||
new_stake_2 = Stake(owner_address=self.checksum_public_address,
|
|
||||||
index=len(self.stakes)+1,
|
|
||||||
start_period=current_stake.start_period,
|
|
||||||
end_period=current_stake.end_period + additional_periods,
|
|
||||||
value=current_stake.value - target_value,
|
|
||||||
economics=self.economics)
|
|
||||||
|
|
||||||
# Ensure both halves are for valid amounts
|
|
||||||
new_stake_1.validate_value()
|
|
||||||
new_stake_2.validate_value()
|
|
||||||
|
|
||||||
# Transmit the stake division transaction
|
|
||||||
tx = self.miner_agent.divide_stake(miner_address=self.checksum_public_address,
|
|
||||||
stake_index=stake_index,
|
|
||||||
target_value=int(target_value),
|
|
||||||
periods=additional_periods)
|
|
||||||
self.blockchain.wait_for_receipt(tx)
|
|
||||||
|
|
||||||
# Update stake cache
|
|
||||||
self.__read_stakes()
|
|
||||||
|
|
||||||
return tx
|
|
||||||
|
|
||||||
@only_me
|
|
||||||
def __validate_stake(self, stake: Stake) -> bool:
|
|
||||||
|
|
||||||
stake.validate_value()
|
|
||||||
stake.validate_duration()
|
|
||||||
|
|
||||||
if not self.token_balance >= stake.value:
|
|
||||||
raise self.MinerError(f"Insufficient token balance ({self.token_agent}) for new stake of {stake.value}")
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@only_me
|
|
||||||
def initialize_stake(self,
|
|
||||||
amount: NU,
|
|
||||||
lock_periods: int = None,
|
|
||||||
expiration: maya.MayaDT = None,
|
|
||||||
entire_balance: bool = False) -> dict:
|
|
||||||
"""
|
|
||||||
High level staking method for Miners.
|
|
||||||
|
|
||||||
:param amount: Amount of tokens to stake denominated in the smallest unit.
|
|
||||||
:param lock_periods: Duration of stake in periods.
|
|
||||||
:param expiration: A MayaDT object representing the time the stake expires; used to calculate lock_periods.
|
|
||||||
:param entire_balance: If True, stake the entire balance of this node, or the maximum possible.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if lock_periods and expiration:
|
|
||||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
|
||||||
if entire_balance and amount:
|
|
||||||
raise self.MinerError("Specify an amount or entire balance, not both")
|
|
||||||
|
|
||||||
if expiration:
|
|
||||||
lock_periods = calculate_period_duration(future_time=expiration)
|
|
||||||
|
|
||||||
if entire_balance is True:
|
|
||||||
amount = self.token_balance
|
|
||||||
|
|
||||||
amount = NU(int(amount), 'NuNit')
|
|
||||||
|
|
||||||
current_period = self.miner_agent.get_current_period()
|
|
||||||
stake = Stake(owner_address=self.checksum_public_address,
|
|
||||||
start_period=current_period+1,
|
|
||||||
end_period=current_period + lock_periods,
|
|
||||||
value=amount,
|
|
||||||
economics=self.economics)
|
|
||||||
|
|
||||||
staking_transactions = OrderedDict() # type: OrderedDict # Time series of txhases
|
|
||||||
|
|
||||||
# Validate
|
|
||||||
assert self.__validate_stake(stake=stake)
|
|
||||||
|
|
||||||
# Transact
|
|
||||||
approve_txhash, initial_deposit_txhash = self.deposit(amount=int(amount), lock_periods=lock_periods)
|
|
||||||
self._transaction_cache.append((datetime.utcnow(), initial_deposit_txhash))
|
|
||||||
|
|
||||||
staking_transactions['approve'] = approve_txhash
|
|
||||||
staking_transactions['deposit'] = initial_deposit_txhash
|
|
||||||
self.__read_stakes() # update local on-chain stake cache
|
|
||||||
|
|
||||||
self.log.info("{} Initialized new stake: {} tokens for {} periods".format(self.checksum_public_address, amount, lock_periods))
|
|
||||||
return staking_transactions
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Reward and Collection
|
# Reward and Collection
|
||||||
|
|
|
@ -174,6 +174,10 @@ class MinerAgent(EthereumContractAgent):
|
||||||
self.blockchain.wait_for_receipt(tx)
|
self.blockchain.wait_for_receipt(tx)
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
|
def get_last_active_period(self, address: str) -> int:
|
||||||
|
period = self.contract.functions.getLastActivePeriod(address).call()
|
||||||
|
return int(period)
|
||||||
|
|
||||||
def confirm_activity(self, node_address: str) -> str:
|
def confirm_activity(self, node_address: str) -> str:
|
||||||
"""Miner rewarded for every confirmed period"""
|
"""Miner rewarded for every confirmed period"""
|
||||||
|
|
||||||
|
@ -496,4 +500,4 @@ class MiningAdjudicatorAgent(EthereumContractAgent):
|
||||||
:param precomputed_data:
|
:param precomputed_data:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# TODO: #931 - Challenge Agent and Actor
|
# TODO: #931 - Challenge Agent and Actor - "Investigator"
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
from _pydecimal import Decimal
|
from _pydecimal import Decimal
|
||||||
|
|
||||||
import eth_utils
|
|
||||||
import maya
|
|
||||||
from eth_utils import currency
|
|
||||||
from nacl.hash import sha256
|
|
||||||
from typing import Union, Tuple
|
from typing import Union, Tuple
|
||||||
|
|
||||||
from nucypher.blockchain.economics import TokenEconomics
|
import maya
|
||||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent
|
from constant_sorrow.constants import NEW_STAKE, NO_STAKING_RECEIPT
|
||||||
|
from eth_utils import currency
|
||||||
|
from twisted.logger import Logger
|
||||||
|
|
||||||
|
from nucypher.blockchain.eth.agents import MinerAgent
|
||||||
from nucypher.blockchain.eth.utils import datetime_at_period, datetime_to_period
|
from nucypher.blockchain.eth.utils import datetime_at_period, datetime_to_period
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,17 +131,23 @@ class Stake:
|
||||||
A quantity of tokens and staking duration for one stake for one miner.
|
A quantity of tokens and staking duration for one stake for one miner.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class StakingError(Exception):
|
||||||
|
"""Raised when a staking operation cannot be executed due to failure."""
|
||||||
|
|
||||||
__ID_LENGTH = 16
|
__ID_LENGTH = 16
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
owner_address: str,
|
miner,
|
||||||
value: NU,
|
value: NU,
|
||||||
start_period: int,
|
start_period: int,
|
||||||
end_period: int,
|
end_period: int,
|
||||||
index: int = None,
|
index: int,
|
||||||
economics: TokenEconomics = None,
|
|
||||||
validate_now: bool = True):
|
validate_now: bool = True):
|
||||||
|
|
||||||
|
self.miner = miner
|
||||||
|
owner_address = miner.checksum_public_address
|
||||||
|
self.log = Logger(f'stake-{owner_address}-{index}')
|
||||||
|
|
||||||
# Stake Metadata
|
# Stake Metadata
|
||||||
self.owner_address = owner_address
|
self.owner_address = owner_address
|
||||||
self.index = index
|
self.index = index
|
||||||
|
@ -156,16 +161,23 @@ class Stake:
|
||||||
self.end_datetime = datetime_at_period(period=end_period)
|
self.end_datetime = datetime_at_period(period=end_period)
|
||||||
self.duration_delta = self.end_datetime - self.start_datetime
|
self.duration_delta = self.end_datetime - self.start_datetime
|
||||||
|
|
||||||
|
self.blockchain = miner.blockchain
|
||||||
|
|
||||||
|
# Agency
|
||||||
|
self.miner_agent = miner.miner_agent
|
||||||
|
self.token_agent = miner.token_agent
|
||||||
|
|
||||||
# Economics
|
# Economics
|
||||||
if not economics:
|
self.economics = miner.economics
|
||||||
economics = TokenEconomics()
|
self.minimum_nu = NU(int(self.economics.minimum_allowed_locked), 'NuNit')
|
||||||
self.economics = economics
|
self.maximum_nu = NU(int(self.economics.maximum_allowed_locked), 'NuNit')
|
||||||
self.minimum_nu = NU(int(economics.minimum_allowed_locked), 'NuNit')
|
|
||||||
self.maximum_nu = NU(int(economics.maximum_allowed_locked), 'NuNit')
|
|
||||||
|
|
||||||
if validate_now:
|
if validate_now:
|
||||||
self.validate_duration()
|
self.validate_duration()
|
||||||
|
|
||||||
|
self.transactions = NO_STAKING_RECEIPT
|
||||||
|
self.receipt = NO_STAKING_RECEIPT
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
r = f'Stake(index={self.index}, value={self.value}, end_period={self.end_period})'
|
r = f'Stake(index={self.index}, value={self.value}, end_period={self.end_period})'
|
||||||
return r
|
return r
|
||||||
|
@ -173,55 +185,46 @@ class Stake:
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other) -> bool:
|
||||||
return bool(self.value == other.value)
|
return bool(self.value == other.value)
|
||||||
|
|
||||||
@property
|
#
|
||||||
def is_active(self):
|
# Metadata
|
||||||
now = maya.now()
|
#
|
||||||
if now >= self.end_datetime:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self) -> bool:
|
||||||
now = maya.now()
|
current_period = self.miner_agent.get_current_period()
|
||||||
if now >= self.end_datetime:
|
if current_period >= self.end_period:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
return not self.is_expired
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_stake_info(cls,
|
def from_stake_info(cls,
|
||||||
owner_address: str,
|
miner,
|
||||||
index: int,
|
index: int,
|
||||||
stake_info: Tuple[int, int, int],
|
stake_info: Tuple[int, int, int]
|
||||||
economics: TokenEconomics) -> 'Stake':
|
) -> 'Stake':
|
||||||
|
|
||||||
"""Reads staking values as they exist on the blockchain"""
|
"""Reads staking values as they exist on the blockchain"""
|
||||||
start_period, end_period, value = stake_info
|
start_period, end_period, value = stake_info
|
||||||
instance = cls(owner_address=owner_address,
|
|
||||||
|
instance = cls(miner=miner,
|
||||||
index=index,
|
index=index,
|
||||||
start_period=start_period,
|
start_period=start_period,
|
||||||
end_period=end_period,
|
end_period=end_period,
|
||||||
value=NU(value, 'NuNit'),
|
value=NU(value, 'NuNit'))
|
||||||
economics=economics)
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def to_stake_info(self) -> Tuple[int, int, int]:
|
def to_stake_info(self) -> Tuple[int, int, int]:
|
||||||
"""Returns a tuple representing the blockchain record of a stake"""
|
"""Returns a tuple representing the blockchain record of a stake"""
|
||||||
return self.start_period, self.end_period, int(self.value)
|
return self.start_period, self.end_period, int(self.value)
|
||||||
|
|
||||||
@property
|
#
|
||||||
def id(self) -> str:
|
# Duration
|
||||||
"""TODO: Unique staking ID, currently unused"""
|
#
|
||||||
digest_elements = list()
|
|
||||||
digest_elements.append(eth_utils.to_canonical_address(address=self.owner_address))
|
|
||||||
digest_elements.append(str(self.index).encode())
|
|
||||||
digest_elements.append(str(self.start_period).encode())
|
|
||||||
digest_elements.append(str(self.end_period).encode())
|
|
||||||
digest_elements.append(str(self.value).encode())
|
|
||||||
digest = b'|'.join(digest_elements)
|
|
||||||
stake_id = sha256(digest).hex()[:16] # type: str
|
|
||||||
return stake_id[:self.__ID_LENGTH]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def periods_remaining(self) -> int:
|
def periods_remaining(self) -> int:
|
||||||
|
@ -240,6 +243,10 @@ class Stake:
|
||||||
result = delta.seconds
|
result = delta.seconds
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
#
|
||||||
|
# Validation
|
||||||
|
#
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __handle_validation_failure(rulebook: Tuple[Tuple[bool, str], ...]) -> bool:
|
def __handle_validation_failure(rulebook: Tuple[Tuple[bool, str], ...]) -> bool:
|
||||||
"""Validate a staking rulebook"""
|
"""Validate a staking rulebook"""
|
||||||
|
@ -275,14 +282,134 @@ class Stake:
|
||||||
rulebook = (
|
rulebook = (
|
||||||
|
|
||||||
(self.economics.minimum_locked_periods <= self.duration,
|
(self.economics.minimum_locked_periods <= self.duration,
|
||||||
'Locktime ({duration}) too short; must be at least {minimum}'
|
'Stake duration of ({duration}) is too short; must be at least {minimum} periods.'
|
||||||
.format(minimum=self.economics.minimum_locked_periods, duration=self.duration)),
|
.format(minimum=self.economics.minimum_locked_periods, duration=self.duration)),
|
||||||
|
|
||||||
(self.economics.maximum_locked_periods >= self.duration,
|
(self.economics.maximum_locked_periods >= self.duration,
|
||||||
'Locktime ({duration}) too long; must be no more than {maximum}'
|
'Stake duration of ({duration}) is too long; must be no more than {maximum} periods.'
|
||||||
.format(maximum=self.economics.maximum_locked_periods, duration=self.duration)),
|
.format(maximum=self.economics.maximum_locked_periods, duration=self.duration)),
|
||||||
)
|
)
|
||||||
|
|
||||||
if raise_on_fail is True:
|
if raise_on_fail is True:
|
||||||
self.__handle_validation_failure(rulebook=rulebook)
|
self.__handle_validation_failure(rulebook=rulebook)
|
||||||
return all(rulebook)
|
return all(rulebook)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Blockchain
|
||||||
|
#
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __deposit(cls, miner, amount: int, lock_periods: int) -> Tuple[str, str]:
|
||||||
|
"""Public facing method for token locking."""
|
||||||
|
|
||||||
|
approve_txhash = miner.token_agent.approve_transfer(amount=amount,
|
||||||
|
target_address=miner.miner_agent.contract_address,
|
||||||
|
sender_address=miner.checksum_public_address)
|
||||||
|
|
||||||
|
deposit_txhash = miner.miner_agent.deposit_tokens(amount=amount,
|
||||||
|
lock_periods=lock_periods,
|
||||||
|
sender_address=miner.checksum_public_address)
|
||||||
|
|
||||||
|
return approve_txhash, deposit_txhash
|
||||||
|
|
||||||
|
def divide(self, target_value: NU, additional_periods: int = None) -> tuple:
|
||||||
|
"""
|
||||||
|
Modifies the unlocking schedule and value of already locked tokens.
|
||||||
|
|
||||||
|
This actor requires that is_me is True, and that the expiration datetime is after the existing
|
||||||
|
locking schedule of this miner, or an exception will be raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Re-read stakes from blockchain and select stake to divide
|
||||||
|
current_stake = self
|
||||||
|
|
||||||
|
# Ensure selected stake is active
|
||||||
|
if current_stake.is_expired:
|
||||||
|
raise self.StakingError(f'Cannot divide an expired stake')
|
||||||
|
|
||||||
|
if target_value >= current_stake.value:
|
||||||
|
raise self.StakingError(f"Cannot divide stake; Target value ({target_value}) must be less "
|
||||||
|
f"than the existing stake value {current_stake.value}.")
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate SubStakes
|
||||||
|
#
|
||||||
|
|
||||||
|
# Modified Original Stake
|
||||||
|
modified_stake = Stake(miner=self.miner,
|
||||||
|
index=self.index,
|
||||||
|
start_period=current_stake.start_period,
|
||||||
|
end_period=current_stake.end_period,
|
||||||
|
value=target_value)
|
||||||
|
|
||||||
|
# New Derived Stake
|
||||||
|
end_period = current_stake.end_period + additional_periods
|
||||||
|
new_stake = Stake(miner=self.miner,
|
||||||
|
start_period=current_stake.start_period,
|
||||||
|
end_period=end_period,
|
||||||
|
value=current_stake.value - target_value,
|
||||||
|
index=NEW_STAKE)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Validate
|
||||||
|
#
|
||||||
|
|
||||||
|
# Ensure both halves are for valid amounts
|
||||||
|
modified_stake.validate_value()
|
||||||
|
new_stake.validate_value()
|
||||||
|
|
||||||
|
# Detect recycled or inconsistent slot then check start period or fail
|
||||||
|
stake_info = self.miner_agent.get_stake_info(miner_address=self.owner_address, stake_index=self.index)
|
||||||
|
first_period, last_period, locked_value = stake_info
|
||||||
|
if not modified_stake.start_period == first_period:
|
||||||
|
raise self.StakingError("Inconsistent staking cache, aborting stake division. ")
|
||||||
|
|
||||||
|
#
|
||||||
|
# Transact
|
||||||
|
#
|
||||||
|
|
||||||
|
# Transmit the stake division transaction
|
||||||
|
tx = self.miner_agent.divide_stake(miner_address=self.owner_address,
|
||||||
|
stake_index=self.index,
|
||||||
|
target_value=int(target_value),
|
||||||
|
periods=additional_periods)
|
||||||
|
receipt = self.blockchain.wait_for_receipt(tx)
|
||||||
|
new_stake.receipt = receipt
|
||||||
|
|
||||||
|
return modified_stake, new_stake
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def initialize_stake(cls, miner, amount: NU, lock_periods: int = None) -> 'Stake':
|
||||||
|
|
||||||
|
# Value
|
||||||
|
amount = NU(int(amount), 'NuNit')
|
||||||
|
|
||||||
|
# Duration
|
||||||
|
current_period = miner.miner_agent.get_current_period()
|
||||||
|
end_period = current_period + lock_periods
|
||||||
|
|
||||||
|
stake = Stake(miner=miner,
|
||||||
|
start_period=current_period+1,
|
||||||
|
end_period=end_period,
|
||||||
|
value=amount,
|
||||||
|
index=NEW_STAKE)
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
stake.validate_value()
|
||||||
|
stake.validate_duration()
|
||||||
|
|
||||||
|
# Transact
|
||||||
|
approve_txhash, initial_deposit_txhash = stake.__deposit(amount=int(amount),
|
||||||
|
lock_periods=lock_periods,
|
||||||
|
miner=miner)
|
||||||
|
|
||||||
|
# Store the staking transactions on the instance
|
||||||
|
staking_transactions = dict(approve=approve_txhash, deposit=initial_deposit_txhash)
|
||||||
|
stake.transactions = staking_transactions
|
||||||
|
|
||||||
|
# Log and return Stake instance
|
||||||
|
log = Logger(f'stake-{miner.checksum_public_address}-creation')
|
||||||
|
log.info("{} Initialized new stake: {} tokens for {} periods".format(miner.checksum_public_address,
|
||||||
|
amount,
|
||||||
|
lock_periods))
|
||||||
|
return stake
|
||||||
|
|
|
@ -364,13 +364,13 @@ def ursula(click_config,
|
||||||
|
|
||||||
click.confirm("Is this correct?", abort=True)
|
click.confirm("Is this correct?", abort=True)
|
||||||
|
|
||||||
txhash_bytes = URSULA.divide_stake(stake_index=index,
|
modified_stake, new_stake = URSULA.divide_stake(stake_index=index,
|
||||||
target_value=value,
|
target_value=value,
|
||||||
additional_periods=extension)
|
additional_periods=extension)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.secho('Successfully divided stake', fg='green')
|
click.secho('Successfully divided stake', fg='green')
|
||||||
click.secho(f'Transaction Hash ........... {txhash_bytes.hex()}')
|
click.secho(f'Transaction Hash ........... {new_stake.receipt}')
|
||||||
|
|
||||||
# Show the resulting stake list
|
# Show the resulting stake list
|
||||||
painting.paint_stakes(stakes=URSULA.stakes)
|
painting.paint_stakes(stakes=URSULA.stakes)
|
||||||
|
@ -421,8 +421,8 @@ def ursula(click_config,
|
||||||
if not force:
|
if not force:
|
||||||
click.confirm("Publish staged stake to the blockchain?", abort=True)
|
click.confirm("Publish staged stake to the blockchain?", abort=True)
|
||||||
|
|
||||||
staking_transactions = URSULA.initialize_stake(amount=int(value), lock_periods=duration)
|
stake = URSULA.initialize_stake(amount=int(value), lock_periods=duration)
|
||||||
painting.paint_staking_confirmation(ursula=URSULA, transactions=staking_transactions)
|
painting.paint_staking_confirmation(ursula=URSULA, transactions=stake.transactions)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif action == 'confirm-activity':
|
elif action == 'confirm-activity':
|
||||||
|
|
|
@ -82,9 +82,8 @@ def test_miner_divides_stake(miner, token_economics):
|
||||||
expected_yet_another_stake = Stake(start_period=current_period + 1,
|
expected_yet_another_stake = Stake(start_period=current_period + 1,
|
||||||
end_period=current_period + 34,
|
end_period=current_period + 34,
|
||||||
value=yet_another_stake_value,
|
value=yet_another_stake_value,
|
||||||
owner_address=miner.checksum_public_address,
|
miner=miner,
|
||||||
index=3,
|
index=3)
|
||||||
economics=token_economics)
|
|
||||||
|
|
||||||
assert 4 == len(miner.stakes), 'A new stake was not added after two stake divisions'
|
assert 4 == len(miner.stakes), 'A new stake was not added after two stake divisions'
|
||||||
assert expected_old_stake == miner.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'
|
assert expected_old_stake == miner.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'
|
||||||
|
|
|
@ -3,6 +3,7 @@ from decimal import InvalidOperation, Decimal
|
||||||
import pytest
|
import pytest
|
||||||
from web3 import Web3
|
from web3 import Web3
|
||||||
|
|
||||||
|
from nucypher.blockchain.economics import TokenEconomics
|
||||||
from nucypher.blockchain.eth.token import NU, Stake
|
from nucypher.blockchain.eth.token import NU, Stake
|
||||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
|
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
|
||||||
|
|
||||||
|
@ -98,21 +99,25 @@ def test_NU(token_economics):
|
||||||
_nan = NU(float('NaN'), 'NU')
|
_nan = NU(float('NaN'), 'NU')
|
||||||
|
|
||||||
|
|
||||||
def test_stake():
|
def test_stake(testerchain, three_agents):
|
||||||
|
|
||||||
class FakeUrsula:
|
class FakeUrsula:
|
||||||
|
token_agent, miner_agent, _policy_agent = three_agents
|
||||||
|
|
||||||
burner_wallet = Web3().eth.account.create(INSECURE_DEVELOPMENT_PASSWORD)
|
burner_wallet = Web3().eth.account.create(INSECURE_DEVELOPMENT_PASSWORD)
|
||||||
checksum_public_address = burner_wallet.address
|
checksum_public_address = burner_wallet.address
|
||||||
miner_agent = None
|
miner_agent = miner_agent
|
||||||
|
token_agent = token_agent
|
||||||
|
blockchain = testerchain
|
||||||
|
economics = TokenEconomics()
|
||||||
|
|
||||||
ursula = FakeUrsula()
|
ursula = FakeUrsula()
|
||||||
stake = Stake(owner_address=ursula.checksum_public_address,
|
stake = Stake(miner=ursula,
|
||||||
start_period=1,
|
start_period=1,
|
||||||
end_period=100,
|
end_period=100,
|
||||||
value=NU(100, 'NU'),
|
value=NU(100, 'NU'),
|
||||||
index=0)
|
index=0)
|
||||||
|
|
||||||
assert len(stake.id) == 16
|
|
||||||
assert stake.value, 'NU' == NU(100, 'NU')
|
assert stake.value, 'NU' == NU(100, 'NU')
|
||||||
|
|
||||||
assert isinstance(stake.time_remaining(), int) # seconds
|
assert isinstance(stake.time_remaining(), int) # seconds
|
||||||
|
|
Loading…
Reference in New Issue