nucypher/nkms_eth/escrow.py

192 lines
7.0 KiB
Python

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 Escrow:
"""
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: 'Escrow'):
"""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) -> 'Escrow':
"""
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))