mirror of https://github.com/nucypher/nucypher.git
[KMS-ETH]- Actor and Contract subclasses, for client interaction and KMS integration via Mixin.
parent
1e2d277d59
commit
d538f5d745
|
@ -0,0 +1,281 @@
|
|||
from collections import OrderedDict
|
||||
from typing import Tuple, List
|
||||
|
||||
from nkms_eth.agents import MinerAgent, PolicyAgent
|
||||
from nkms_eth.base import Actor
|
||||
|
||||
|
||||
class PolicyArrangement:
|
||||
def __init__(self, author: 'PolicyAuthor', miner: 'Miner', value: int,
|
||||
periods: int, arrangement_id: bytes=None):
|
||||
|
||||
if arrangement_id is None:
|
||||
self.id = self.__class__._generate_arrangement_id() # TODO: Generate policy ID
|
||||
|
||||
# The relationship exists between two addresses
|
||||
self.author = author
|
||||
self.policy_agent = author.policy_agent
|
||||
|
||||
self.miner = miner
|
||||
|
||||
# Arrangement value, rate, and duration
|
||||
rate = value // periods
|
||||
self._rate = rate
|
||||
|
||||
self.value = value
|
||||
self.periods = periods # TODO: datetime -> duration in blocks
|
||||
|
||||
self.is_published = False
|
||||
|
||||
@staticmethod
|
||||
def _generate_arrangement_id(policy_hrac: bytes) -> bytes:
|
||||
pass # TODO
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(client={}, node={})"
|
||||
r = r.format(class_name, self.author, self.miner)
|
||||
return r
|
||||
|
||||
def publish(self, gas_price: int) -> str:
|
||||
|
||||
payload = {'from': self.author.address,
|
||||
'value': self.value,
|
||||
'gas_price': gas_price}
|
||||
|
||||
txhash = self.policy_manager.transact(payload).createPolicy(self.id,
|
||||
self.miner.address,
|
||||
self.periods)
|
||||
|
||||
self.policy_manager.blockchain._chain.wait.for_receipt(txhash)
|
||||
self.publish_transaction = txhash
|
||||
self.is_published = True
|
||||
return txhash
|
||||
|
||||
def __update_periods(self) -> None:
|
||||
blockchain_record = self.policy_manager.fetch_arrangement_data(self.id)
|
||||
client, delegate, rate, *periods = blockchain_record
|
||||
self._elapsed_periods = periods
|
||||
|
||||
def revoke(self, gas_price: int) -> str:
|
||||
"""Revoke this arrangement and return the transaction hash as hex."""
|
||||
txhash = self.policy_manager.revoke_arrangement(self.id, author=self.author, gas_price=gas_price)
|
||||
self.revoke_transaction = txhash
|
||||
return txhash
|
||||
|
||||
|
||||
class Miner(Actor):
|
||||
"""
|
||||
Practically carrying a pickaxe.
|
||||
Intended for use as an Ursula mixin.
|
||||
|
||||
Accepts a running blockchain, deployed token contract, and deployed escrow contract.
|
||||
If the provided token and escrow contracts are not deployed,
|
||||
ContractDeploymentError will be raised.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, miner_agent: MinerAgent, address):
|
||||
super().__init__(address)
|
||||
|
||||
self.miner_agent = miner_agent
|
||||
if not miner_agent._contract:
|
||||
raise MinerAgent.ContractDeploymentError('Escrow contract not deployed. Arm then deploy.')
|
||||
else:
|
||||
miner_agent.miners.append(self)
|
||||
|
||||
self._token = miner_agent._token
|
||||
self._blockchain = self._token._blockchain
|
||||
|
||||
self._transactions = list()
|
||||
self._locked_tokens = self._update_locked_tokens()
|
||||
|
||||
def _update_locked_tokens(self) -> None:
|
||||
self._locked_tokens = self.miner_agent.call().getLockedTokens(self.address)
|
||||
return None
|
||||
|
||||
def _approve_escrow(self, amount: int) -> str:
|
||||
"""Approve the transfer of token from the miner's address to the escrow contract."""
|
||||
|
||||
txhash = self._token.transact({'from': self.address}).approve(self.miner_agent._contract.address, amount)
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def _send_tokens_to_escrow(self, amount, locktime) -> str:
|
||||
"""Send tokes to the escrow from the miner's address"""
|
||||
|
||||
deposit_txhash = self.miner_agent.transact({'from': self.address}).deposit(amount, locktime)
|
||||
self._blockchain._chain.wait.for_receipt(deposit_txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.append(deposit_txhash)
|
||||
|
||||
return deposit_txhash
|
||||
|
||||
@property
|
||||
def is_staking(self):
|
||||
return bool(self._locked_tokens > 0)
|
||||
|
||||
def lock(self, amount: int, locktime: int) -> Tuple[str, str, str]:
|
||||
"""Deposit and lock tokens for mining."""
|
||||
|
||||
approve_txhash = self._approve_escrow(amount=amount)
|
||||
deposit_txhash = self._send_tokens_to_escrow(amount=amount, locktime=locktime)
|
||||
|
||||
lock_txhash = self.miner_agent.transact({'from': self.address}).switchLock()
|
||||
self._blockchain._chain.wait.for_receipt(lock_txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.extend([approve_txhash, deposit_txhash, lock_txhash])
|
||||
|
||||
return approve_txhash, deposit_txhash, lock_txhash
|
||||
|
||||
def confirm_activity(self) -> str:
|
||||
"""Miner rewarded for every confirmed period"""
|
||||
|
||||
txhash = self.miner_agent.transact({'from': self.address}).confirmActivity()
|
||||
self._blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def mint(self) -> str:
|
||||
"""Computes and transfers tokens to the miner's account"""
|
||||
|
||||
txhash = self.miner_agent.transact({'from': self.address}).mint()
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def collect_policy_reward(self, policy_manager):
|
||||
"""Collect policy reward in ETH"""
|
||||
|
||||
txhash = policy_manager.transact({'from': self.address}).withdraw()
|
||||
self._blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def publish_miner_id(self, miner_id) -> str:
|
||||
"""Store a new Miner ID"""
|
||||
|
||||
txhash = self.miner_agent.transact({'from': self.address}).setMinerId(miner_id)
|
||||
self._blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def fetch_miner_ids(self) -> tuple:
|
||||
"""Retrieve all stored Miner IDs on this miner"""
|
||||
|
||||
count = self.escrow().getMinerInfo(self.escrow.MinerInfoField.MINER_IDS_LENGTH.value,
|
||||
self.address,
|
||||
0).encode('latin-1')
|
||||
|
||||
count = self._blockchain._chain.web3.toInt(count)
|
||||
|
||||
miner_ids = list()
|
||||
for index in range(count):
|
||||
miner_id = self.miner_agent.call().getMinerInfo(self.escrow.MinerInfoField.MINER_ID.value, self.address, index)
|
||||
encoded_miner_id = miner_id.encode('latin-1') # TODO change when v4 of web3.py is released
|
||||
miner_ids.append(encoded_miner_id)
|
||||
|
||||
return tuple(miner_ids)
|
||||
|
||||
def eth_balance(self):
|
||||
return self._blockchain._chain.web3.eth.getBalance(self.address)
|
||||
|
||||
def token_balance(self) -> int:
|
||||
"""Check miner's current token balance"""
|
||||
|
||||
self._token._check_contract_deployment()
|
||||
balance = self._token().balanceOf(self.address)
|
||||
|
||||
return balance
|
||||
|
||||
def withdraw(self, amount: int=0, entire_balance=False) -> str:
|
||||
"""Withdraw tokens"""
|
||||
|
||||
tokens_amount = self._blockchain._chain.web3.toInt(
|
||||
self.escrow().getMinerInfo(self.escrow.MinerInfoField.VALUE.value, self.address, 0).encode('latin-1'))
|
||||
|
||||
txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount)
|
||||
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
if entire_balance and amount:
|
||||
raise Exception("Specify an amount or entire balance, not both")
|
||||
|
||||
if entire_balance:
|
||||
txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount)
|
||||
else:
|
||||
txhash = self.escrow.transact({'from': self.address}).withdraw(amount)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
return txhash
|
||||
|
||||
|
||||
class PolicyAuthor(Actor):
|
||||
"""Alice"""
|
||||
|
||||
def __init__(self, address: bytes, policy_agent: PolicyAgent):
|
||||
|
||||
if policy_agent.is_deployed is False:
|
||||
raise PolicyAgent.ContractDeploymentError('PolicyManager contract not deployed.')
|
||||
self.policy_agent = policy_agent
|
||||
super().__init__(address)
|
||||
self._arrangements = OrderedDict() # Track authored policies by id
|
||||
|
||||
def make_arrangement(self, miner: Miner, periods: int, rate: int, arrangement_id: bytes=None) -> PolicyArrangement:
|
||||
"""
|
||||
Create a new arrangement to carry out a blockchain policy for the specified rate and time.
|
||||
"""
|
||||
|
||||
value = rate * periods
|
||||
arrangement = PolicyArrangement(author=self,
|
||||
miner=miner,
|
||||
value=value,
|
||||
periods=periods)
|
||||
|
||||
self._arrangements[arrangement.id] = {arrangement_id: arrangement}
|
||||
return arrangement
|
||||
|
||||
def get_arrangement(self, arrangement_id: bytes) -> PolicyArrangement:
|
||||
"""Fetch a published arrangement from the blockchain"""
|
||||
|
||||
blockchain_record = self.policy_agent().policies(arrangement_id)
|
||||
author_address, miner_address, rate, start_block, end_block, downtime_index = blockchain_record
|
||||
|
||||
duration = end_block - start_block
|
||||
|
||||
miner = Miner(address=miner_address, miner_agent=self.policy_agent.escrow)
|
||||
arrangement = PolicyArrangement(author=self, miner=miner, periods=duration)
|
||||
|
||||
arrangement.is_published = True
|
||||
return arrangement
|
||||
|
||||
def revoke_arrangement(self, arrangement_id):
|
||||
"""Lookup the arrangement in the cache and revoke it on the blockchain"""
|
||||
try:
|
||||
arrangement = self._arrangements[arrangement_id]
|
||||
except KeyError:
|
||||
raise Exception('No such arrangement')
|
||||
else:
|
||||
txhash = arrangement.revoke()
|
||||
return txhash
|
||||
|
||||
def select_miners(self, quantity: int) -> List[str]:
|
||||
miner_addresses = self.policy_agent.escrow.sample(quantity=quantity)
|
||||
return miner_addresses
|
||||
|
||||
def balance(self):
|
||||
return self.policy_agent.token().balanceOf(self.address)
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
from typing import Generator, List
|
||||
|
||||
from nkms_eth.base import ContractAgent
|
||||
|
||||
|
||||
class MinerAgent(ContractAgent):
|
||||
"""
|
||||
Wraps NuCypher's Escrow solidity smart contract, and manages a PopulusContract.
|
||||
|
||||
In order to become a participant of the network,
|
||||
a miner locks tokens by depositing to the Escrow contract address
|
||||
for a duration measured in periods.
|
||||
|
||||
"""
|
||||
_contract_name = MinerEscrowDeployer.contract_name
|
||||
|
||||
_contract_name = 'MinersEscrow'
|
||||
hours_per_period = 1 # 24 Hours TODO
|
||||
min_release_periods = 1 # 30 Periods
|
||||
max_awarded_periods = 365 # Periods
|
||||
min_allowed_locked = 10 ** 6
|
||||
max_allowed_locked = 10 ** 7 * NuCypherKMSToken.M
|
||||
reward = NuCypherKMSToken.saturation - NuCypherKMSToken.premine
|
||||
null_addr = '0x' + '0' * 40
|
||||
|
||||
mining_coeff = [
|
||||
hours_per_period,
|
||||
2 * 10 ** 7,
|
||||
max_awarded_periods,
|
||||
max_awarded_periods,
|
||||
min_release_periods,
|
||||
min_allowed_locked,
|
||||
max_allowed_locked
|
||||
]
|
||||
|
||||
class MinerInfoField(Enum):
|
||||
MINERS_LENGTH = 0
|
||||
MINER = 1
|
||||
VALUE = 2
|
||||
DECIMALS = 3
|
||||
LOCKED_VALUE = 4
|
||||
RELEASE = 5
|
||||
MAX_RELEASE_PERIODS = 6
|
||||
RELEASE_RATE = 7
|
||||
CONFIRMED_PERIODS_LENGTH = 8
|
||||
CONFIRMED_PERIOD = 9
|
||||
CONFIRMED_PERIOD_LOCKED_VALUE = 10
|
||||
LAST_ACTIVE_PERIOD_F = 11
|
||||
DOWNTIME_LENGTH = 12
|
||||
DOWNTIME_START_PERIOD = 13
|
||||
DOWNTIME_END_PERIOD = 14
|
||||
MINER_IDS_LENGTH = 15
|
||||
MINER_ID = 16
|
||||
|
||||
class NotEnoughUrsulas(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, token: NuCypherKMSTokenAgent):
|
||||
super().__init__(agent=token)
|
||||
self._token = token
|
||||
self.miners = list()
|
||||
|
||||
def get_miner_ids(self) -> Set[str]:
|
||||
"""Fetch all miner IDs and return them in a set"""
|
||||
return {miner.get_id() for miner in self.miners}
|
||||
|
||||
def swarm(self) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generates all miner addresses via cumulative sum.
|
||||
"""
|
||||
count = self.call().getMinerInfo(self.MinerInfoField.MINERS_LENGTH.value, self.null_addr, 0).encode('latin-1')
|
||||
count = self.blockchain._chain.web3.toInt(count)
|
||||
|
||||
for index in range(count):
|
||||
addr = self.call().getMinerInfo(self.MinerInfoField.MINER.value, self.null_addr, index).encode('latin-1')
|
||||
yield self.blockchain._chain.web3.toChecksumAddress(addr)
|
||||
|
||||
def sample(self, quantity: int=10, additional_ursulas: float=1.7, attempts: int=5, duration: int=10) -> List[str]:
|
||||
"""
|
||||
Select n random staking Ursulas, according to their stake distribution.
|
||||
The returned addresses are shuffled, so one can request more than needed and
|
||||
throw away those which do not respond.
|
||||
|
||||
_startIndex
|
||||
v
|
||||
|-------->*--------------->*---->*------------->|
|
||||
| ^
|
||||
| stopIndex
|
||||
|
|
||||
| _delta
|
||||
|---------------------------->|
|
||||
|
|
||||
| shift
|
||||
| |----->|
|
||||
|
||||
|
||||
See full diagram here: https://github.com/nucypher/kms-whitepaper/blob/master/pdf/miners-ruler.pdf
|
||||
|
||||
"""
|
||||
|
||||
system_random = random.SystemRandom()
|
||||
n_select = round(quantity*additional_ursulas) # Select more Ursulas
|
||||
n_tokens = self.__call__().getAllLockedTokens()
|
||||
|
||||
if not n_tokens > 0:
|
||||
raise self.NotEnoughUrsulas('There are no locked tokens.')
|
||||
|
||||
for _ in range(attempts):
|
||||
points = [0] + sorted(system_random.randrange(n_tokens) for _ in range(n_select))
|
||||
deltas = [i-j for i, j in zip(points[1:], points[:-1])]
|
||||
|
||||
addrs, addr, shift = set(), MinerEscrowDeployer.null_address, 0
|
||||
for delta in deltas:
|
||||
addr, shift = self.__call__().findCumSum(addr, delta+shift, duration)
|
||||
addrs.add(addr)
|
||||
|
||||
if len(addrs) >= quantity:
|
||||
return system_random.sample(addrs, quantity)
|
||||
|
||||
raise self.NotEnoughUrsulas('Selection failed after {} attempts'.format(attempts))
|
||||
|
||||
|
||||
class PolicyAgent(ContractAgent):
|
||||
|
||||
def fetch_arrangement_data(self, arrangement_id: bytes) -> list:
|
||||
blockchain_record = self.__call__().policies(arrangement_id)
|
||||
return blockchain_record
|
||||
|
||||
def revoke_arrangement(self, arrangement_id: bytes, author: 'PolicyAuthor', gas_price: int):
|
||||
"""
|
||||
Revoke by arrangement ID; Only the policy author can revoke the policy
|
||||
"""
|
||||
txhash = self.transact({'from': author.address, 'gas_price': gas_price}).revokePolicy(arrangement_id)
|
||||
self.blockchain._chain.wait.for_receipt(txhash)
|
||||
return txhash
|
||||
|
||||
|
||||
class NuCypherKMSTokenAgent(ContractAgent):
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(blockchain={}, contract={})"
|
||||
return r.format(class_name, self._blockchain, self._contract)
|
||||
|
||||
def registrar(self):
|
||||
"""Retrieve all known addresses for this contract"""
|
||||
all_known_address = self._blockchain._chain.registrar.get_contract_address(NuCypherKMSTokenDeployer.contract_name())
|
||||
return all_known_address
|
||||
|
||||
def check_balance(self, address: str) -> int:
|
||||
"""Get the balance of a token address"""
|
||||
return self.__call__().balanceOf(address)
|
|
@ -1,191 +0,0 @@
|
|||
import random
|
||||
from typing import List, Tuple, Set, Generator
|
||||
from enum import Enum
|
||||
|
||||
from populus.contracts.contract import PopulusContract
|
||||
|
||||
from nkms_eth.token import NuCypherKMSToken
|
||||
from .blockchain import Blockchain
|
||||
|
||||
addr = str
|
||||
|
||||
|
||||
class MinerEscrow:
|
||||
"""
|
||||
Wraps NuCypher's Escrow solidity smart contract, and manages a PopulusContract.
|
||||
|
||||
In order to become a participant of the network,
|
||||
a miner locks tokens by depositing to the Escrow contract address
|
||||
for a duration measured in periods.
|
||||
|
||||
"""
|
||||
|
||||
_contract_name = 'MinersEscrow'
|
||||
hours_per_period = 1 # 24 Hours TODO
|
||||
min_release_periods = 1 # 30 Periods
|
||||
max_awarded_periods = 365 # Periods
|
||||
min_allowed_locked = 10 ** 6
|
||||
max_allowed_locked = 10 ** 7 * NuCypherKMSToken.M
|
||||
reward = NuCypherKMSToken.saturation - NuCypherKMSToken.premine
|
||||
null_addr = '0x' + '0' * 40
|
||||
|
||||
mining_coeff = [
|
||||
hours_per_period,
|
||||
2 * 10 ** 7,
|
||||
max_awarded_periods,
|
||||
max_awarded_periods,
|
||||
min_release_periods,
|
||||
min_allowed_locked,
|
||||
max_allowed_locked
|
||||
]
|
||||
|
||||
class MinerInfoField(Enum):
|
||||
MINERS_LENGTH = 0
|
||||
MINER = 1
|
||||
VALUE = 2
|
||||
DECIMALS = 3
|
||||
LOCKED_VALUE = 4
|
||||
RELEASE = 5
|
||||
MAX_RELEASE_PERIODS = 6
|
||||
RELEASE_RATE = 7
|
||||
CONFIRMED_PERIODS_LENGTH = 8
|
||||
CONFIRMED_PERIOD = 9
|
||||
CONFIRMED_PERIOD_LOCKED_VALUE = 10
|
||||
LAST_ACTIVE_PERIOD_F = 11
|
||||
DOWNTIME_LENGTH = 12
|
||||
DOWNTIME_START_PERIOD = 13
|
||||
DOWNTIME_END_PERIOD = 14
|
||||
MINER_IDS_LENGTH = 15
|
||||
MINER_ID = 16
|
||||
|
||||
class ContractDeploymentError(Exception):
|
||||
pass
|
||||
|
||||
class NotEnoughUrsulas(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, blockchain: Blockchain, token: NuCypherKMSToken, contract: PopulusContract=None):
|
||||
self.blockchain = blockchain
|
||||
self._contract = contract
|
||||
self.token = token
|
||||
self.armed = False
|
||||
self.miners = list()
|
||||
|
||||
def __call__(self):
|
||||
"""Gateway to contract function calls without state change."""
|
||||
return self._contract.call()
|
||||
|
||||
def __eq__(self, other: 'MinerEscrow'):
|
||||
"""If two deployed escrows have the same contract address, they are equal."""
|
||||
return self._contract.address == other._contract.address
|
||||
|
||||
def arm(self) -> None:
|
||||
self.armed = True
|
||||
|
||||
def deploy(self) -> Tuple[str, str, str]:
|
||||
"""
|
||||
Deploy and publish the NuCypherKMS Token contract
|
||||
to the blockchain network specified in self.blockchain.network.
|
||||
|
||||
The contract must be armed before it can be deployed.
|
||||
Deployment can only ever be executed exactly once!
|
||||
|
||||
Returns transaction hashes in a tuple: deploy, reward, and initialize.
|
||||
"""
|
||||
|
||||
if self.armed is False:
|
||||
raise self.ContractDeploymentError('use .arm() to arm the contract, then .deploy().')
|
||||
|
||||
if self._contract is not None:
|
||||
class_name = self.__class__.__name__
|
||||
message = '{} contract already deployed, use .get() to retrieve it.'.format(class_name)
|
||||
raise self.ContractDeploymentError(message)
|
||||
|
||||
the_escrow_contract, deploy_txhash = self.blockchain._chain.provider.deploy_contract(self._contract_name,
|
||||
deploy_args=[self.token._contract.address] + self.mining_coeff,
|
||||
deploy_transaction={'from': self.token.creator})
|
||||
|
||||
self.blockchain._chain.wait.for_receipt(deploy_txhash, timeout=self.blockchain._timeout)
|
||||
self._contract = the_escrow_contract
|
||||
|
||||
reward_txhash = self.token.transact({'from': self.token.creator}).transfer(self._contract.address, self.reward)
|
||||
self.blockchain._chain.wait.for_receipt(reward_txhash, timeout=self.blockchain._timeout)
|
||||
|
||||
init_txhash = self._contract.transact({'from': self.token.creator}).initialize()
|
||||
self.blockchain._chain.wait.for_receipt(init_txhash, timeout=self.blockchain._timeout)
|
||||
|
||||
return deploy_txhash, reward_txhash, init_txhash
|
||||
|
||||
@classmethod
|
||||
def get(cls, blockchain, token) -> 'MinerEscrow':
|
||||
"""
|
||||
Returns the Escrow object,
|
||||
or raises UnknownContract if the contract has not been deployed.
|
||||
"""
|
||||
contract = blockchain._chain.provider.get_contract(cls._contract_name)
|
||||
return cls(blockchain=blockchain, token=token, contract=contract)
|
||||
|
||||
def transact(self, *args, **kwargs):
|
||||
if self._contract is None:
|
||||
raise self.ContractDeploymentError('Contract must be deployed before executing transactions.')
|
||||
return self._contract.transact(*args, **kwargs)
|
||||
|
||||
def get_dht(self) -> Set[str]:
|
||||
"""Fetch all miner IDs and return them in a set"""
|
||||
return {miner.get_id() for miner in self.miners}
|
||||
|
||||
def swarm(self) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generates all miner addresses via cumulative sum.
|
||||
"""
|
||||
count = self.blockchain._chain.web3.toInt(
|
||||
self.__call__().getMinerInfo(self.MinerInfoField.MINERS_LENGTH.value, self.null_addr, 0)
|
||||
.encode('latin-1'))
|
||||
for index in range(count):
|
||||
yield self.blockchain._chain.web3.toChecksumAddress(
|
||||
self.__call__().getMinerInfo(self.MinerInfoField.MINER.value, self.null_addr, index).encode('latin-1'))
|
||||
|
||||
def sample(self, quantity: int=10, additional_ursulas: float=1.7, attempts: int=5, duration: int=10) -> List[addr]:
|
||||
"""
|
||||
Select n random staking Ursulas, according to their stake distribution.
|
||||
The returned addresses are shuffled, so one can request more than needed and
|
||||
throw away those which do not respond.
|
||||
|
||||
_startIndex
|
||||
v
|
||||
|-------->*--------------->*---->*------------->|
|
||||
| ^
|
||||
| stopIndex
|
||||
|
|
||||
| _delta
|
||||
|---------------------------->|
|
||||
|
|
||||
| shift
|
||||
| |----->|
|
||||
|
||||
|
||||
See full diagram here: https://github.com/nucypher/kms-whitepaper/blob/master/pdf/miners-ruler.pdf
|
||||
|
||||
"""
|
||||
|
||||
system_random = random.SystemRandom()
|
||||
n_select = round(quantity*additional_ursulas) # Select more Ursulas
|
||||
n_tokens = self.__call__().getAllLockedTokens()
|
||||
|
||||
if not n_tokens > 0:
|
||||
raise self.NotEnoughUrsulas('There are no locked tokens.')
|
||||
|
||||
for _ in range(attempts):
|
||||
points = [0] + sorted(system_random.randrange(n_tokens) for _ in range(n_select))
|
||||
deltas = [i-j for i, j in zip(points[1:], points[:-1])]
|
||||
|
||||
addrs, addr, index, shift = set(), self.null_addr, 0, 0
|
||||
for delta in deltas:
|
||||
addr, index, shift = self.__call__().findCumSum(index, delta+shift, duration)
|
||||
addrs.add(addr)
|
||||
|
||||
if len(addrs) >= quantity:
|
||||
return system_random.sample(addrs, quantity)
|
||||
|
||||
raise self.NotEnoughUrsulas('Selection failed after {} attempts'.format(attempts))
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
from typing import Tuple
|
||||
|
||||
from .escrow import MinerEscrow
|
||||
|
||||
|
||||
class Miner:
|
||||
"""
|
||||
Practically carrying a pickaxe.
|
||||
Intended for use as an Ursula mixin.
|
||||
|
||||
Accepts a running blockchain, deployed token contract, and deployed escrow contract.
|
||||
If the provided token and escrow contracts are not deployed,
|
||||
ContractDeploymentError will be raised.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, escrow: MinerEscrow, address=None):
|
||||
|
||||
self.escrow = escrow
|
||||
if not escrow._contract:
|
||||
raise MinerEscrow.ContractDeploymentError('Escrow contract not deployed. Arm then deploy.')
|
||||
else:
|
||||
escrow.miners.append(self)
|
||||
|
||||
self._token = escrow.token
|
||||
self._blockchain = self._token.blockchain
|
||||
|
||||
self.address = address
|
||||
self._transactions = []
|
||||
self._locked_tokens = self._update_locked_tokens()
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(address='{}')"
|
||||
r.format(class_name, self.address)
|
||||
return r
|
||||
|
||||
def __del__(self):
|
||||
"""Removes this miner from the escrow's list of miners on delete."""
|
||||
self.escrow.miners.remove(self)
|
||||
|
||||
def _update_locked_tokens(self) -> None:
|
||||
self._locked_tokens = self.escrow().getLockedTokens(self.address)
|
||||
|
||||
def _approve_escrow(self, amount: int) -> str:
|
||||
"""Approve the transfer of token from the miner's address to the escrow contract."""
|
||||
|
||||
txhash = self._token.transact({'from': self.address}).approve(self.escrow._contract.address, amount)
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def _send_tokens_to_escrow(self, amount, locktime) -> str:
|
||||
"""Send tokes to the escrow from the miner's address"""
|
||||
|
||||
deposit_txhash = self.escrow.transact({'from': self.address}).deposit(amount, locktime)
|
||||
self._blockchain._chain.wait.for_receipt(deposit_txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.append(deposit_txhash)
|
||||
|
||||
return deposit_txhash
|
||||
|
||||
def lock(self, amount: int, locktime: int) -> Tuple[str, str, str]:
|
||||
"""Deposit and lock tokens for mining."""
|
||||
|
||||
approve_txhash = self._approve_escrow(amount=amount)
|
||||
deposit_txhash = self._send_tokens_to_escrow(amount=amount, locktime=locktime)
|
||||
|
||||
lock_txhash = self.escrow.transact({'from': self.address}).switchLock()
|
||||
self._blockchain._chain.wait.for_receipt(lock_txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.extend([approve_txhash, deposit_txhash, lock_txhash])
|
||||
|
||||
return approve_txhash, deposit_txhash, lock_txhash
|
||||
|
||||
def confirm_activity(self) -> str:
|
||||
"""Miner rewarded for every confirmed period"""
|
||||
|
||||
txhash = self.escrow.transact({'from': self.address}).confirmActivity()
|
||||
self._blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def mint(self) -> str:
|
||||
"""Computes and transfers tokens to the miner's account"""
|
||||
|
||||
txhash = self.escrow.transact({'from': self.address}).mint()
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def collect_policy_reward(self, policy_manager):
|
||||
"""Collect policy reward in ETH"""
|
||||
|
||||
txhash = policy_manager.transact({'from': self.address}).withdraw()
|
||||
self._blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def publish_miner_id(self, miner_id) -> str:
|
||||
"""Store a new Miner ID"""
|
||||
|
||||
txhash = self.escrow.transact({'from': self.address}).setMinerId(miner_id)
|
||||
self._blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def fetch_miner_ids(self) -> tuple:
|
||||
"""Retrieve all stored Miner IDs on this miner"""
|
||||
|
||||
count = self.escrow().getMinerInfo(self.escrow.MinerInfoField.MINER_IDS_LENGTH.value,
|
||||
self.address,
|
||||
0).encode('latin-1')
|
||||
|
||||
count = self._blockchain._chain.web3.toInt(count)
|
||||
|
||||
# TODO change when v4 web3.py will released
|
||||
miner_ids = tuple(self.escrow().getMinerInfo(self.escrow.MinerInfoField.MINER_ID.value, self.address, index)
|
||||
.encode('latin-1') for index in range(count))
|
||||
|
||||
return tuple(miner_ids)
|
||||
|
||||
def eth_balance(self):
|
||||
return self._blockchain._chain.web3.eth.getBalance(self.address)
|
||||
|
||||
def token_balance(self) -> int:
|
||||
"""Check miner's current token balance"""
|
||||
|
||||
self._token._check_contract_deployment()
|
||||
balance = self._token().balanceOf(self.address)
|
||||
|
||||
return balance
|
||||
|
||||
def withdraw(self, amount: int=0, entire_balance=False) -> str:
|
||||
"""Withdraw tokens"""
|
||||
|
||||
tokens_amount = self._blockchain._chain.web3.toInt(
|
||||
self.escrow().getMinerInfo(self.escrow.MinerInfoField.VALUE.value, self.address, 0).encode('latin-1'))
|
||||
|
||||
txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount)
|
||||
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
if entire_balance and amount:
|
||||
raise Exception("Specify an amount or entire balance, not both")
|
||||
|
||||
if entire_balance:
|
||||
txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount)
|
||||
else:
|
||||
txhash = self.escrow.transact({'from': self.address}).withdraw(amount)
|
||||
|
||||
self._transactions.append(txhash)
|
||||
self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout)
|
||||
|
||||
return txhash
|
Loading…
Reference in New Issue