mirror of https://github.com/nucypher/nucypher.git
[KMS-ETH]- Merge pull request #11 from KPrasch/python_api
Contract agent and deployer Interfacepull/195/head^2
commit
64a0558e0a
|
@ -0,0 +1,271 @@
|
|||
from abc import ABC
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from typing import Tuple, List, Union
|
||||
|
||||
from nkms_eth.agents import NuCypherKMSTokenAgent
|
||||
from nkms_eth.policies import BlockchainArrangement
|
||||
|
||||
|
||||
class TokenActor(ABC):
|
||||
|
||||
class ActorError(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, token_agent: NuCypherKMSTokenAgent, address: Union[bytes, str]):
|
||||
self.token_agent = token_agent
|
||||
|
||||
if isinstance(address, bytes):
|
||||
address = address.hex()
|
||||
self.address = address
|
||||
|
||||
self._transactions = OrderedDict() # Tracks
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(address='{}')"
|
||||
r = r.format(class_name, self.address)
|
||||
return r
|
||||
|
||||
def eth_balance(self):
|
||||
"""Return this actors's current ETH balance"""
|
||||
|
||||
balance = self.token_agent.blockchain._chain.web3.eth.getBalance(self.address)
|
||||
return balance
|
||||
|
||||
def token_balance(self):
|
||||
"""Return this actors's current token balance"""
|
||||
|
||||
balance = self.token_agent.get_balance(address=self.address)
|
||||
return balance
|
||||
|
||||
|
||||
class Miner(TokenActor):
|
||||
"""
|
||||
Ursula - practically carrying a pickaxe.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
class StakingError(TokenActor.ActorError):
|
||||
pass
|
||||
|
||||
def __init__(self, miner_agent, address):
|
||||
super().__init__(token_agent=miner_agent.token_agent, address=address)
|
||||
|
||||
self.miner_agent = miner_agent
|
||||
miner_agent.miners.append(self) # Track Miners
|
||||
|
||||
self.token_agent = miner_agent.token_agent
|
||||
self.blockchain = self.token_agent.blockchain
|
||||
|
||||
self._locked_tokens = None
|
||||
self._update_locked_tokens()
|
||||
|
||||
def _update_locked_tokens(self) -> None:
|
||||
"""Query the contract for the amount of locked tokens on this miner's eth address and cache it"""
|
||||
|
||||
self._locked_tokens = self.miner_agent.read().getLockedTokens(self.address)
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_staking(self, query=True):
|
||||
"""Checks if this Miner currently has locked tokens."""
|
||||
|
||||
if query:
|
||||
self._update_locked_tokens()
|
||||
return bool(self._locked_tokens > 0)
|
||||
|
||||
def _approve_escrow(self, amount: int) -> str:
|
||||
"""Approve the transfer of token from the miner's address to the escrow contract."""
|
||||
|
||||
txhash = self.token_agent.transact({'from': self.address}).approve(self.miner_agent.contract_address, amount)
|
||||
self.blockchain.wait_for_receipt(txhash)
|
||||
|
||||
self._transactions[datetime.utcnow()] = 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.wait_for_receipt(deposit_txhash)
|
||||
|
||||
self._transactions[datetime.utcnow()] = deposit_txhash
|
||||
|
||||
return deposit_txhash
|
||||
|
||||
def deposit(self, amount: int, locktime: int) -> Tuple[str, str]:
|
||||
"""Public facing method for token locking."""
|
||||
|
||||
approve_txhash = self._approve_escrow(amount=amount)
|
||||
deposit_txhash = self._send_tokens_to_escrow(amount=amount, locktime=locktime)
|
||||
|
||||
return approve_txhash, deposit_txhash
|
||||
|
||||
def switch_lock(self):
|
||||
lock_txhash = self.miner_agent.transact({'from': self.address}).switchLock()
|
||||
self.blockchain.wait_for_receipt(lock_txhash)
|
||||
|
||||
self._transactions[datetime.utcnow()] = lock_txhash
|
||||
return lock_txhash
|
||||
|
||||
def lock(self, amount: int, locktime: int) -> Tuple[str, str, str]:
|
||||
"""Public facing method for token locking."""
|
||||
|
||||
approve_txhash, deposit_txhash = self.deposit(amount=amount, locktime=locktime)
|
||||
lock_txhash = self.switch_lock()
|
||||
|
||||
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.wait_for_receipt(txhash)
|
||||
|
||||
self._transactions[datetime.utcnow()] = 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.wait_for_receipt(txhash)
|
||||
self._transactions[datetime.utcnow()] = txhash
|
||||
|
||||
return txhash
|
||||
|
||||
def collect_reward(self, policy_manager) -> str:
|
||||
"""Withdraw policy reward in ETH"""
|
||||
|
||||
token_reward_bytes = self.miner_agent().getMinerInfo(self.miner_agent.MinerInfoField.VALUE.value, self.address, 0).encode('latin-1')
|
||||
reward_amount = self.blockchain._chain.web3.toInt(token_reward_bytes)
|
||||
|
||||
txhash = policy_manager.transact({'from': self.address}).withdraw(reward_amount) # TODO: Calculate reward
|
||||
|
||||
self.blockchain.wait_for_receipt(txhash)
|
||||
self._transactions[datetime.utcnow()] = txhash
|
||||
|
||||
return txhash
|
||||
|
||||
def stake(self, amount, locktime, entire_balance=False, auto_switch_lock=False):
|
||||
"""
|
||||
High level staking method for Miners.
|
||||
"""
|
||||
staking_transactions = OrderedDict() # Time series of txhases
|
||||
|
||||
if entire_balance and amount:
|
||||
raise self.StakingError("Specify an amount or entire balance, not both")
|
||||
|
||||
if not locktime >= self.miner_agent._deployer._min_release_periods:
|
||||
raise self.StakingError('Locktime must be at least {}'.format(self.miner_agent._deployer._min_release_periods))
|
||||
|
||||
if entire_balance is True:
|
||||
balance_bytes = self.miner_agent.read().getMinerInfo(self.miner_agent._deployer.MinerInfoField.VALUE.value,
|
||||
self.address,
|
||||
0).encode('latin-1')
|
||||
|
||||
amount = self.blockchain._chain.web3.toInt(balance_bytes)
|
||||
else:
|
||||
if not amount > 0:
|
||||
raise self.StakingError('Staking amount must be greater than zero.')
|
||||
|
||||
approve_txhash, initial_deposit_txhash = self.deposit(amount=amount, locktime=locktime)
|
||||
staking_transactions[datetime.utcnow()] = initial_deposit_txhash
|
||||
|
||||
if auto_switch_lock is True:
|
||||
lock_txhash = self.switch_lock()
|
||||
staking_transactions[datetime.utcnow()] = lock_txhash
|
||||
|
||||
return staking_transactions
|
||||
|
||||
# TODO: Sidechain datastore
|
||||
# def publish_data(self, miner_id) -> str:
|
||||
# """Store a new Miner ID"""
|
||||
#
|
||||
# txhash = self.miner_agent.transact({'from': self.address}).setMinerId(miner_id)
|
||||
# self._blockchain.wait_for_receipt(txhash)
|
||||
# self._transactions[datetime.utcnow()] = txhash
|
||||
#
|
||||
# return txhash
|
||||
#
|
||||
# def fetch_miner_data(self) -> tuple:
|
||||
# """Retrieve all stored Miner IDs on this miner"""
|
||||
#
|
||||
# count = self.miner_agent.read().getMinerInfo(self.miner_agent._deployer.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.read().getMinerInfo(self.miner_agent._deployer.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)
|
||||
|
||||
|
||||
class PolicyAuthor(TokenActor):
|
||||
"""Alice"""
|
||||
|
||||
def __init__(self, address: bytes, policy_agent):
|
||||
super().__init__(token_agent=policy_agent._token, address=address)
|
||||
self.policy_agent = policy_agent
|
||||
|
||||
self._arrangements = OrderedDict() # Track authored policies by id
|
||||
|
||||
def make_arrangement(self, miner: Miner, periods: int, rate: int, arrangement_id: bytes=None) -> 'BlockchainArrangement':
|
||||
"""
|
||||
Create a new arrangement to carry out a blockchain policy for the specified rate and time.
|
||||
"""
|
||||
|
||||
value = rate * periods
|
||||
arrangement = BlockchainArrangement(author=self,
|
||||
miner=miner,
|
||||
value=value,
|
||||
periods=periods)
|
||||
|
||||
self._arrangements[arrangement.id] = {arrangement_id: arrangement}
|
||||
return arrangement
|
||||
|
||||
def get_arrangement(self, arrangement_id: bytes) -> BlockchainArrangement:
|
||||
"""Fetch a published arrangement from the blockchain"""
|
||||
|
||||
blockchain_record = self.policy_agent.read().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.miner_agent)
|
||||
arrangement = BlockchainArrangement(author=self, miner=miner, periods=duration)
|
||||
|
||||
arrangement.is_published = True
|
||||
return arrangement
|
||||
|
||||
def revoke_arrangement(self, arrangement_id):
|
||||
"""Get the arrangement from the cache and revoke it on the blockchain"""
|
||||
try:
|
||||
arrangement = self._arrangements[arrangement_id]
|
||||
except KeyError:
|
||||
raise Exception('No such arrangement') #TODO
|
||||
else:
|
||||
txhash = arrangement.revoke()
|
||||
return txhash
|
||||
|
||||
def recruit(self, quantity: int) -> List[str]:
|
||||
"""Uses sampling logic to gather miner address from the blockchain"""
|
||||
|
||||
miner_addresses = self.policy_agent.miner_agent.sample(quantity=quantity)
|
||||
return miner_addresses
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import random
|
||||
from abc import ABC
|
||||
from functools import partial
|
||||
from typing import Set, Generator, List
|
||||
|
||||
from nkms_eth.deployers import MinerEscrowDeployer, NuCypherKMSTokenDeployer, PolicyManagerDeployer, ContractDeployer
|
||||
|
||||
|
||||
class EthereumContractAgent(ABC):
|
||||
_principal_contract_name = NotImplemented
|
||||
|
||||
class ContractNotDeployed(ContractDeployer.ContractDeploymentError):
|
||||
pass
|
||||
|
||||
def __init__(self, blockchain, *args, **kwargs):
|
||||
|
||||
self.blockchain = blockchain
|
||||
self._contract = self.blockchain._chain.provider.get_contract(self._principal_contract_name)
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(blockchain={}, contract={})"
|
||||
return r.format(class_name, self.blockchain, self._contract)
|
||||
|
||||
def __eq__(self, other):
|
||||
return bool(self.contract_address == other.contract_address)
|
||||
|
||||
@property
|
||||
def contract_address(self):
|
||||
return self._contract.address
|
||||
|
||||
@property
|
||||
def contract_name(self) -> str:
|
||||
return self._principal_contract_name
|
||||
|
||||
@property
|
||||
def origin(self) -> str:
|
||||
return self.blockchain._chain.web3.eth.accounts[0] # TODO: make swappable
|
||||
|
||||
def read(self):
|
||||
return self._contract.call()
|
||||
|
||||
def transact(self, payload: dict):
|
||||
"""Packs kwargs into payload dictionary and transmits an eth contract transaction"""
|
||||
return self._contract.transact(payload)
|
||||
|
||||
def get_balance(self, address: str=None) -> int:
|
||||
"""Get the balance of a token address, or of this contract address"""
|
||||
if address is None:
|
||||
address = self.contract_address
|
||||
return self.read().balanceOf(address)
|
||||
|
||||
|
||||
class NuCypherKMSTokenAgent(EthereumContractAgent):
|
||||
|
||||
_deployer = NuCypherKMSTokenDeployer
|
||||
_principal_contract_name = NuCypherKMSTokenDeployer._contract_name
|
||||
|
||||
def registrar(self):
|
||||
"""Retrieve all known addresses for this contract"""
|
||||
all_known_address = self.blockchain._chain.registrar.get_contract_address(self._principal_contract_name)
|
||||
return all_known_address
|
||||
|
||||
|
||||
class MinerAgent(EthereumContractAgent):
|
||||
"""
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
_deployer = MinerEscrowDeployer
|
||||
_principal_contract_name = MinerEscrowDeployer._contract_name
|
||||
|
||||
class NotEnoughUrsulas(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, token_agent: NuCypherKMSTokenAgent):
|
||||
super().__init__(blockchain=token_agent.blockchain) # TODO: public
|
||||
self.token_agent = token_agent
|
||||
self.miners = list() # Tracks per client
|
||||
|
||||
def get_miner_ids(self) -> Set[str]:
|
||||
"""
|
||||
Fetch all miner IDs from the local cache and return them in a set
|
||||
"""
|
||||
|
||||
return {miner.get_id() for miner in self.miners}
|
||||
|
||||
def swarm(self) -> Generator[str, None, None]:
|
||||
"""
|
||||
Returns an iterator of all miner addresses via cumulative sum, on-network.
|
||||
|
||||
Miner addresses will be returned in the order in which they were added to the MinersEscrow's ledger
|
||||
"""
|
||||
|
||||
# TODO - Partial;
|
||||
info_reader = partial(self.read().getMinerInfo, self._deployer.MinerInfoField.MINERS_LENGTH.value, self._deployer._null_addr)
|
||||
count = info_reader(0).encode('latin-1')
|
||||
count = self.blockchain._chain.web3.toInt(count)
|
||||
|
||||
for index in range(count):
|
||||
addr = info_reader(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.read().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._deployer._null_addr, 0, 0
|
||||
for delta in deltas:
|
||||
addr, index, shift = self.read().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))
|
||||
|
||||
|
||||
class PolicyAgent(EthereumContractAgent):
|
||||
|
||||
_deployer = PolicyManagerDeployer
|
||||
_principal_contract_name = PolicyManagerDeployer._contract_name
|
||||
|
||||
def __init__(self, miner_agent):
|
||||
super().__init__(blockchain=miner_agent.blockchain)
|
||||
self.miner_agent = miner_agent
|
||||
|
||||
def fetch_arrangement_data(self, arrangement_id: bytes) -> list:
|
||||
blockchain_record = self.read().policies(arrangement_id)
|
||||
return blockchain_record
|
||||
|
||||
def revoke_arrangement(self, arrangement_id: bytes, author, 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.wait_for_receipt(txhash)
|
||||
return txhash
|
|
@ -1,7 +1,9 @@
|
|||
from abc import ABC
|
||||
|
||||
from nkms_eth.config import PopulusConfig
|
||||
|
||||
|
||||
class Blockchain:
|
||||
class TheBlockchain(ABC):
|
||||
"""
|
||||
http://populus.readthedocs.io/en/latest/config.html#chains
|
||||
|
||||
|
@ -12,13 +14,18 @@ class Blockchain:
|
|||
temp: Local private chain whos data directory is removed when the chain is shutdown. Runs via geth.
|
||||
"""
|
||||
|
||||
_network = ''
|
||||
_instance = False
|
||||
_network = NotImplemented
|
||||
_default_timeout = NotImplemented
|
||||
__instance = None
|
||||
|
||||
class AlreadyRunning(Exception):
|
||||
test_chains = ('tester', )
|
||||
transient_chains = test_chains + ('testrpc', 'temp')
|
||||
public_chains = ('mainnet', 'ropsten')
|
||||
|
||||
class IsAlreadyRunning(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, populus_config: PopulusConfig=None, timeout=60):
|
||||
def __init__(self, populus_config: PopulusConfig=None):
|
||||
"""
|
||||
Configures a populus project and connects to blockchain.network.
|
||||
Transaction timeouts specified measured in seconds.
|
||||
|
@ -28,21 +35,26 @@ class Blockchain:
|
|||
"""
|
||||
|
||||
# Singleton
|
||||
if Blockchain._instance is True:
|
||||
class_name = self.__class__.__name__
|
||||
raise Blockchain.AlreadyRunning('{} is already running. Use .get() to retrieve'.format(class_name))
|
||||
Blockchain._instance = True
|
||||
if TheBlockchain.__instance is not None:
|
||||
message = '{} is already running. Use .get() to retrieve'.format(self._network)
|
||||
raise TheBlockchain.IsAlreadyRunning(message)
|
||||
TheBlockchain.__instance = self
|
||||
|
||||
if populus_config is None:
|
||||
populus_config = PopulusConfig()
|
||||
|
||||
self._populus_config = populus_config
|
||||
self._timeout = timeout
|
||||
self._project = populus_config.project
|
||||
|
||||
# Opens and preserves connection to a running populus blockchain
|
||||
self._chain = self._project.get_chain(self._network).__enter__()
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
if cls.__instance is None:
|
||||
class_name = cls.__name__
|
||||
raise Exception('{} has not been created.'.format(class_name))
|
||||
return cls.__instance
|
||||
|
||||
def disconnect(self):
|
||||
self._chain.__exit__(None, None, None)
|
||||
|
||||
|
@ -51,8 +63,8 @@ class Blockchain:
|
|||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(network={}, timeout={})"
|
||||
return r.format(class_name, self._network, self._timeout)
|
||||
r = "{}(network={})"
|
||||
return r.format(class_name, self._network)
|
||||
|
||||
def get_contract(self, name):
|
||||
"""
|
||||
|
@ -62,19 +74,14 @@ class Blockchain:
|
|||
"""
|
||||
return self._chain.provider.get_contract(name)
|
||||
|
||||
def wait_time(self, wait_hours, step=50):
|
||||
"""Wait the specified number of wait_hours by comparing block timestamps."""
|
||||
def wait_for_receipt(self, txhash, timeout=None) -> None:
|
||||
if timeout is None:
|
||||
timeout = self._default_timeout
|
||||
|
||||
wait_seconds = wait_hours * 60 * 60
|
||||
current_block = self._chain.web3.eth.getBlock(self._chain.web3.eth.blockNumber)
|
||||
end_timestamp = current_block.timestamp + wait_seconds
|
||||
self._chain.wait.for_receipt(txhash, timeout=timeout)
|
||||
|
||||
not_time_yet = True
|
||||
while not_time_yet:
|
||||
self._chain.wait.for_block(self._chain.web3.eth.blockNumber+step)
|
||||
current_block = self._chain.web3.eth.getBlock(self._chain.web3.eth.blockNumber)
|
||||
not_time_yet = current_block.timestamp < end_timestamp
|
||||
# class TestRpcBlockchain:
|
||||
#
|
||||
# _network = 'testrpc'
|
||||
# _default_timeout = 60
|
||||
|
||||
|
||||
class TesterBlockchain(Blockchain):
|
||||
_network = 'tester'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from enum import Enum
|
||||
from os.path import dirname, join, abspath
|
||||
|
||||
import appdirs
|
||||
|
@ -6,13 +7,81 @@ import populus
|
|||
import nkms_eth
|
||||
|
||||
|
||||
class NuCypherTokenConfig:
|
||||
__subdigits = 18
|
||||
_M = 10 ** __subdigits
|
||||
__premine = int(1e9) * _M
|
||||
__saturation = int(1e10) * _M
|
||||
_reward = __saturation - __premine
|
||||
|
||||
@property
|
||||
def saturation(self):
|
||||
return self.__saturation
|
||||
|
||||
|
||||
class NuCypherMinerConfig:
|
||||
_hours_per_period = 24 # Hours
|
||||
_min_release_periods = 30 # 720 Hours
|
||||
__max_awarded_periods = 365
|
||||
|
||||
__min_allowed_locked = 10 ** 6
|
||||
__max_allowed_locked = 10 ** 7 * NuCypherTokenConfig._M
|
||||
|
||||
_null_addr = '0x' + '0' * 40
|
||||
__reward = NuCypherTokenConfig._reward
|
||||
|
||||
__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
|
||||
|
||||
@property
|
||||
def null_address(self):
|
||||
return self._null_addr
|
||||
|
||||
|
||||
@property
|
||||
def mining_coefficient(self):
|
||||
return self.__mining_coeff
|
||||
|
||||
@property
|
||||
def reward(self):
|
||||
return self.__reward
|
||||
|
||||
|
||||
class PopulusConfig:
|
||||
|
||||
def __init__(self):
|
||||
self._python_project_name = 'nucypher-kms'
|
||||
def __init__(self, project_name='nucypher-kms', registrar_path=None):
|
||||
self._python_project_name = project_name
|
||||
|
||||
# This config is persistent and is created in user's .local directory
|
||||
self._registrar_path = join(appdirs.user_data_dir(self._python_project_name), 'registrar.json')
|
||||
if registrar_path is None:
|
||||
registrar_path = join(appdirs.user_data_dir(self._python_project_name), 'registrar.json')
|
||||
self._registrar_path = registrar_path
|
||||
|
||||
# Populus project config
|
||||
self._project_dir = join(dirname(abspath(nkms_eth.__file__)), 'project')
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Tuple
|
||||
|
||||
from nkms_eth.config import NuCypherMinerConfig, NuCypherTokenConfig
|
||||
from .blockchain import TheBlockchain
|
||||
|
||||
|
||||
class ContractDeployer(ABC):
|
||||
|
||||
_contract_name = NotImplemented
|
||||
|
||||
class ContractDeploymentError(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, blockchain: TheBlockchain):
|
||||
self.__armed = False
|
||||
self._contract = None
|
||||
self.deployment_receipt = None
|
||||
|
||||
# Sanity check
|
||||
if not isinstance(blockchain, TheBlockchain):
|
||||
error = 'Only TheBlockchain can be used to create a deployer, got {}.'
|
||||
raise ValueError(error.format(type(blockchain)))
|
||||
self.blockchain = blockchain
|
||||
|
||||
@property
|
||||
def contract_address(self) -> str:
|
||||
try:
|
||||
address = self._contract.address
|
||||
except AttributeError:
|
||||
cls = self.__class__
|
||||
raise cls.ContractDeploymentError('Contract not deployed')
|
||||
else:
|
||||
return address
|
||||
|
||||
@property
|
||||
def is_deployed(self) -> bool:
|
||||
return bool(self._contract is not None)
|
||||
|
||||
@property
|
||||
def is_armed(self) -> bool:
|
||||
return bool(self.__armed is True)
|
||||
|
||||
def _ensure_contract_deployment(self) -> None:
|
||||
"""Raises ContractDeploymentError if the contract has not been armed and deployed."""
|
||||
|
||||
if self._contract is None:
|
||||
class_name = self.__class__.__name__
|
||||
message = '{} contract is not deployed. Arm, then deploy.'.format(class_name)
|
||||
raise self.ContractDeploymentError(message)
|
||||
|
||||
# http: // populus.readthedocs.io / en / latest / chain.contracts.html # checking-availability-of-contracts
|
||||
available = bool(self.blockchain._chain.provider.are_contract_dependencies_available(self._contract_name))
|
||||
if not available:
|
||||
raise self.ContractDeploymentError('Contract is not available')
|
||||
|
||||
def arm(self) -> None:
|
||||
"""Safety mechanism for ethereum contract deployment"""
|
||||
|
||||
if self.__armed is True:
|
||||
raise self.ContractDeploymentError("Deployer already armed, use .deploy() to deploy.")
|
||||
|
||||
# Check that the contract can be deployed
|
||||
is_ready = bool(self.blockchain._chain.provider.are_contract_dependencies_available(self._contract_name))
|
||||
|
||||
# If the blockchain network is public, prompt the user
|
||||
if self.blockchain._network not in self.blockchain.test_chains:
|
||||
message = """
|
||||
Are you sure you want to deploy {} on the {} network?
|
||||
|
||||
Type "I UNDERSTAND" to arm the deployer.
|
||||
"""
|
||||
|
||||
answer = input(message.format(self._contract_name, self.blockchain._network))
|
||||
if answer == "I UNDERSTAND":
|
||||
arm = True
|
||||
outcome_message = '{} is armed!'.format(self.__class__.__name__)
|
||||
else:
|
||||
arm = False
|
||||
outcome_message = '{} was not armed.'.format(self.__class__.__name__)
|
||||
|
||||
print(outcome_message)
|
||||
else:
|
||||
# If this is a private chain, just arm the deployer without interaction.
|
||||
arm = True
|
||||
|
||||
self.__armed = arm
|
||||
return
|
||||
|
||||
@abstractmethod
|
||||
def deploy(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NuCypherKMSTokenDeployer(ContractDeployer, NuCypherTokenConfig):
|
||||
|
||||
_contract_name = 'NuCypherKMSToken'
|
||||
|
||||
def __init__(self, blockchain):
|
||||
super().__init__(blockchain=blockchain)
|
||||
self._creator = self.blockchain._chain.web3.eth.accounts[0]
|
||||
|
||||
def deploy(self) -> 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!
|
||||
"""
|
||||
|
||||
if self.is_armed is False:
|
||||
raise self.ContractDeploymentError('use .arm() to arm the contract, then .deploy().')
|
||||
|
||||
if self.is_deployed is True:
|
||||
class_name = self.__class__.__name__
|
||||
message = '{} contract already deployed, use .get() to retrieve it.'.format(class_name)
|
||||
raise self.ContractDeploymentError(message)
|
||||
|
||||
the_nucypher_token_contract, deployment_txhash = self.blockchain._chain.provider.deploy_contract(
|
||||
self._contract_name,
|
||||
deploy_args=[self.saturation],
|
||||
deploy_transaction={'from': self._creator})
|
||||
|
||||
self.blockchain.wait_for_receipt(deployment_txhash)
|
||||
self._contract = the_nucypher_token_contract
|
||||
self.deployment_receipt = deployment_txhash
|
||||
|
||||
return self.deployment_receipt
|
||||
|
||||
|
||||
class MinerEscrowDeployer(ContractDeployer, NuCypherMinerConfig):
|
||||
"""
|
||||
Deploys the MinerEscrow ethereum contract to the blockchain. Depends on NuCypherTokenAgent
|
||||
"""
|
||||
|
||||
_contract_name = 'MinersEscrow'
|
||||
|
||||
def __init__(self, token_agent):
|
||||
super().__init__(blockchain=token_agent.blockchain)
|
||||
self.token_agent = token_agent
|
||||
|
||||
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.is_armed is False:
|
||||
raise self.ContractDeploymentError('use .arm() to arm the contract, then .deploy().')
|
||||
|
||||
if self.is_deployed is True:
|
||||
class_name = self.__class__.__name__
|
||||
message = '{} contract already deployed, use .get() to retrieve it.'.format(class_name)
|
||||
raise self.ContractDeploymentError(message)
|
||||
|
||||
deploy_args = [self.token_agent.contract_address] + self.mining_coefficient
|
||||
deploy_tx = {'from': self.token_agent.origin}
|
||||
|
||||
the_escrow_contract, deploy_txhash = self.blockchain._chain.provider.deploy_contract(self._contract_name,
|
||||
deploy_args=deploy_args,
|
||||
deploy_transaction=deploy_tx)
|
||||
|
||||
self.blockchain.wait_for_receipt(deploy_txhash)
|
||||
self._contract = the_escrow_contract
|
||||
|
||||
reward_txhash = self.token_agent.transact({'from': self.token_agent.origin}).transfer(self.contract_address,
|
||||
self.reward)
|
||||
self.blockchain.wait_for_receipt(reward_txhash)
|
||||
|
||||
init_txhash = self._contract.transact({'from': self.token_agent.origin}).initialize()
|
||||
self.blockchain.wait_for_receipt(init_txhash)
|
||||
|
||||
self.deployment_receipt = deploy_txhash
|
||||
|
||||
return deploy_txhash, reward_txhash, init_txhash
|
||||
|
||||
|
||||
class PolicyManagerDeployer(ContractDeployer):
|
||||
"""
|
||||
Depends on MinerAgent and NuCypherTokenAgent
|
||||
"""
|
||||
|
||||
_contract_name = 'PolicyManager'
|
||||
|
||||
def __init__(self, miner_agent):
|
||||
self.token_agent = miner_agent.token_agent
|
||||
self.miner_agent = miner_agent
|
||||
super().__init__(blockchain=self.token_agent.blockchain)
|
||||
|
||||
def deploy(self) -> Tuple[str, str]:
|
||||
if self.is_armed is False:
|
||||
raise self.ContractDeploymentError('PolicyManager contract not armed')
|
||||
if self.is_deployed is True:
|
||||
raise self.ContractDeploymentError('PolicyManager contract already deployed')
|
||||
|
||||
# Creator deploys the policy manager
|
||||
the_policy_manager_contract, deploy_txhash = self.blockchain._chain.provider.deploy_contract(
|
||||
self._contract_name,
|
||||
deploy_args=[self.miner_agent.contract_address],
|
||||
deploy_transaction={'from': self.token_agent.origin})
|
||||
|
||||
self._contract = the_policy_manager_contract
|
||||
|
||||
policy_setter_txhash = self.miner_agent.transact({'from': self.token_agent.origin}).setPolicyManager(the_policy_manager_contract.address)
|
||||
self.blockchain.wait_for_receipt(policy_setter_txhash)
|
||||
self.deployment_receipt = deploy_txhash
|
||||
|
||||
return deploy_txhash, policy_setter_txhash
|
|
@ -1,190 +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 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
|
||||
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))
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
from typing import Tuple
|
||||
|
||||
from .blockchain import Blockchain
|
||||
from .escrow import Escrow
|
||||
from .token import NuCypherKMSToken
|
||||
|
||||
|
||||
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, blockchain: Blockchain, token: NuCypherKMSToken, escrow: Escrow, address=None):
|
||||
self.blockchain = blockchain
|
||||
self.token = token
|
||||
self.address = address
|
||||
|
||||
self.escrow = escrow
|
||||
if not escrow.contract:
|
||||
raise Escrow.ContractDeploymentError('Escrow contract not deployed. Arm then deploy.')
|
||||
else:
|
||||
escrow.miners.append(self)
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(address='{}')"
|
||||
r.format(class_name, self.address)
|
||||
|
||||
def __del__(self):
|
||||
"""Removes this miner from the escrow's list of miners on delete."""
|
||||
self.escrow.miners.remove(self)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
return approve_txhash, deposit_txhash, lock_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)
|
||||
|
||||
return txhash
|
||||
|
||||
# TODO
|
||||
# def collect_reward(self):
|
||||
# tx = policy_manager.transact({'from': self.address}).withdraw()
|
||||
# chain.wait.for_receipt(tx)
|
||||
|
||||
def publish_dht_key(self, dht_id) -> str:
|
||||
"""Store a new DHT key"""
|
||||
|
||||
txhash = self.escrow.transact({'from': self.address}).setMinerId(dht_id)
|
||||
self.blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def get_dht_key(self) -> tuple:
|
||||
"""Retrieve all stored DHT keys for this miner"""
|
||||
|
||||
count = self.blockchain._chain.web3.toInt(
|
||||
self.escrow().getMinerInfo(self.escrow.MinerInfoField.MINER_IDS_LENGTH.value, self.address, 0)
|
||||
.encode('latin-1'))
|
||||
# TODO change when v4 web3.py will released
|
||||
dht_keys = tuple(self.escrow().getMinerInfo(self.escrow.MinerInfoField.MINER_ID.value, self.address, index)
|
||||
.encode('latin-1') for index in range(count))
|
||||
|
||||
return dht_keys
|
||||
|
||||
def confirm_activity(self) -> str:
|
||||
"""Miner rewarded for every confirmed period"""
|
||||
|
||||
txhash = self.escrow.contract.transact({'from': self.address}).confirmActivity()
|
||||
self.blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
return txhash
|
||||
|
||||
def balance(self) -> int:
|
||||
"""Check miner's current balance"""
|
||||
|
||||
self.token._check_contract_deployment()
|
||||
balance = self.token().balanceOf(self.address)
|
||||
|
||||
return balance
|
||||
|
||||
def withdraw(self) -> str:
|
||||
"""withdraw rewarded 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)
|
||||
|
||||
return txhash
|
|
@ -0,0 +1,62 @@
|
|||
class BlockchainArrangement:
|
||||
"""
|
||||
A relationship between Alice and a single Ursula as part of Blockchain Policy
|
||||
"""
|
||||
|
||||
def __init__(self, author, miner, value: int, periods: int, arrangement_id: bytes=None):
|
||||
|
||||
self.id = arrangement_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
|
||||
|
||||
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_agent.transact(payload).createPolicy(self.id,
|
||||
self.miner.address,
|
||||
self.periods)
|
||||
|
||||
self.policy_agent._blockchain._chain.wait.for_receipt(txhash)
|
||||
|
||||
self.publish_transaction = txhash
|
||||
self.is_published = True
|
||||
return txhash
|
||||
|
||||
def revoke(self, gas_price: int) -> str:
|
||||
"""Revoke this arrangement and return the transaction hash as hex."""
|
||||
txhash = self.policy_agent.revoke_arrangement(self.id, author=self.author, gas_price=gas_price)
|
||||
self.revoke_transaction = txhash
|
||||
return txhash
|
||||
|
||||
|
||||
class BlockchainPolicy:
|
||||
"""A collection of n BlockchainArrangements representing a single Policy"""
|
||||
|
||||
class NoSuchPolicy(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self._arrangements = list()
|
|
@ -226,6 +226,23 @@ contract MinersEscrow is Issuer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @notice Calculate locked periods for owner from start period
|
||||
* @param _owner Tokens owner
|
||||
* @param _lockedTokens Locked tokens in start period
|
||||
* @return Calculated locked periods
|
||||
**/
|
||||
function calculateLockedPeriods(
|
||||
address _owner,
|
||||
uint256 _lockedTokens
|
||||
)
|
||||
internal view returns (uint256)
|
||||
{
|
||||
MinerInfo storage info = minerInfo[_owner];
|
||||
return _lockedTokens.divCeil(info.releaseRate).sub(uint(1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
* @notice Pre-deposit tokens
|
||||
* @param _owners Tokens owners
|
||||
* @param _values Amount of token to deposit for each owner
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
from populus.contracts.contract import PopulusContract
|
||||
from .blockchain import Blockchain
|
||||
|
||||
|
||||
class NuCypherKMSToken:
|
||||
_contract_name = 'NuCypherKMSToken'
|
||||
subdigits = 18
|
||||
M = 10 ** subdigits
|
||||
premine = int(1e9) * M
|
||||
saturation = int(1e10) * M
|
||||
|
||||
class ContractDeploymentError(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, blockchain: Blockchain, token_contract: PopulusContract=None):
|
||||
self.creator = blockchain._chain.web3.eth.accounts[0]
|
||||
self.blockchain = blockchain
|
||||
self.contract = token_contract
|
||||
self.armed = False
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
r = "{}(blockchain={}, contract={})"
|
||||
return r.format(class_name, self.blockchain, self.contract)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Two token objects are equal if they have the same contract address"""
|
||||
return self.contract.address == other.contract.address
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Invoke contract -> No state change"""
|
||||
return self.contract.call(*args, **kwargs)
|
||||
|
||||
def _check_contract_deployment(self) -> None:
|
||||
"""Raises ContractDeploymentError if the contract has not been armed and deployed."""
|
||||
if not self.contract:
|
||||
class_name = self.__class__.__name__
|
||||
message = '{} contract is not deployed. Arm, then deploy.'.format(class_name)
|
||||
raise self.ContractDeploymentError(message)
|
||||
|
||||
def arm(self) -> None:
|
||||
"""Arm contract for deployment to blockchain."""
|
||||
self.armed = True
|
||||
|
||||
def deploy(self) -> 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!
|
||||
"""
|
||||
|
||||
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_nucypherKMS_token_contract, deployment_txhash = self.blockchain._chain.provider.deploy_contract(
|
||||
self._contract_name,
|
||||
deploy_args=[self.saturation],
|
||||
deploy_transaction={'from': self.creator})
|
||||
|
||||
self.blockchain._chain.wait.for_receipt(deployment_txhash, timeout=self.blockchain._timeout)
|
||||
|
||||
self.contract = the_nucypherKMS_token_contract
|
||||
return deployment_txhash
|
||||
|
||||
def transact(self, *args):
|
||||
"""Invoke contract -> State change"""
|
||||
self._check_contract_deployment()
|
||||
result = self.contract.transact(*args)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get(cls, blockchain):
|
||||
"""
|
||||
Returns the NuCypherKMSToken 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_contract=contract)
|
||||
|
||||
def registrar(self):
|
||||
"""Retrieve all known addresses for this contract"""
|
||||
self._check_contract_deployment()
|
||||
return self.blockchain._chain.registrar.get_contract_address(self._contract_name)
|
||||
|
||||
def balance(self, address: str):
|
||||
"""Get the balance of a token address"""
|
||||
self._check_contract_deployment()
|
||||
return self.__call__().balanceOf(address)
|
||||
|
||||
def _airdrop(self, amount: int):
|
||||
"""Airdrops from creator address to all other addresses!"""
|
||||
self._check_contract_deployment()
|
||||
_, *addresses = self.blockchain._chain.web3.eth.accounts
|
||||
|
||||
def txs():
|
||||
for address in addresses:
|
||||
yield self.transact({'from': self.creator}).transfer(address, amount*(10**6))
|
||||
|
||||
for tx in txs():
|
||||
self.blockchain._chain.wait.for_receipt(tx, timeout=10)
|
||||
|
||||
return self
|
|
@ -0,0 +1,65 @@
|
|||
import random
|
||||
|
||||
from nkms_eth.actors import Miner
|
||||
from nkms_eth.agents import MinerAgent, EthereumContractAgent
|
||||
from nkms_eth.blockchain import TheBlockchain
|
||||
from nkms_eth.config import NuCypherMinerConfig
|
||||
from nkms_eth.deployers import MinerEscrowDeployer, NuCypherKMSTokenDeployer
|
||||
|
||||
|
||||
class TesterBlockchain(TheBlockchain):
|
||||
"""Transient, in-memory, local, private chain"""
|
||||
_network = 'tester'
|
||||
|
||||
def wait_time(self, wait_hours, step=50):
|
||||
"""Wait the specified number of wait_hours by comparing block timestamps."""
|
||||
|
||||
end_timestamp = self._chain.web3.eth.getBlock(
|
||||
self._chain.web3.eth.blockNumber).timestamp + wait_hours * 60 * 60
|
||||
while self._chain.web3.eth.getBlock(self._chain.web3.eth.blockNumber).timestamp < end_timestamp:
|
||||
self._chain.wait.for_block(self._chain.web3.eth.blockNumber + step)
|
||||
|
||||
|
||||
class MockNuCypherKMSTokenDeployer(NuCypherKMSTokenDeployer):
|
||||
_M = 10 ** 6 #TODO
|
||||
|
||||
def _global_airdrop(self, amount: int):
|
||||
"""Airdrops from creator address to all other addresses!"""
|
||||
|
||||
_creator, *addresses = self.blockchain._chain.web3.eth.accounts
|
||||
|
||||
def txs():
|
||||
for address in addresses:
|
||||
yield self._contract.transact({'from': self._creator}).transfer(address, amount * (10 ** 6))
|
||||
|
||||
for tx in txs():
|
||||
self.blockchain._chain.wait.for_receipt(tx, timeout=10)
|
||||
|
||||
return self # for method chaining
|
||||
|
||||
|
||||
class MockNuCypherMinerConfig(NuCypherMinerConfig):
|
||||
"""Speed things up a bit"""
|
||||
_hours_per_period = 1 # Hours
|
||||
_min_release_periods = 1 # Minimum switchlock periods
|
||||
|
||||
|
||||
class MockMinerEscrowDeployer(MinerEscrowDeployer, MockNuCypherMinerConfig):
|
||||
"""Helper class for MockMinerAgent"""
|
||||
|
||||
|
||||
class MockMinerAgent(MinerAgent):
|
||||
"""MinerAgent with faked config subclass"""
|
||||
_deployer = MockMinerEscrowDeployer
|
||||
|
||||
|
||||
def spawn_miners(addresses: list, miner_agent: MinerAgent, m: int, locktime: int) -> None:
|
||||
"""
|
||||
Deposit and lock a random amount of tokens in the miner escrow
|
||||
from each address, "spawning" new Miners.
|
||||
"""
|
||||
# Create n Miners
|
||||
for address in addresses:
|
||||
miner = Miner(miner_agent=miner_agent, address=address)
|
||||
amount = (10+random.randrange(9000)) * m
|
||||
miner.lock(amount=amount, locktime=locktime)
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
"""
|
||||
Deploy contracts in tester.
|
||||
|
||||
A simple Python script to deploy contracts and then estimate gas for different methods.
|
||||
"""
|
||||
|
||||
from nkms_eth.blockchain import TesterBlockchain
|
||||
from nkms_eth.escrow import Escrow
|
||||
from nkms_eth.token import NuCypherKMSToken
|
||||
|
||||
from nkms_eth.agents import NuCypherKMSTokenAgent, MinerAgent
|
||||
import os
|
||||
|
||||
from tests.utilities import TesterBlockchain
|
||||
|
||||
|
||||
def main():
|
||||
testerchain = TesterBlockchain()
|
||||
|
@ -19,6 +19,7 @@ def main():
|
|||
|
||||
print("Web3 providers are", web3.providers)
|
||||
|
||||
# TODO: Updatae to agents and deployers
|
||||
# Create an ERC20 token
|
||||
token = NuCypherKMSToken(blockchain=testerchain)
|
||||
token.arm()
|
||||
|
@ -289,4 +290,4 @@ def main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
|
@ -1,8 +1,8 @@
|
|||
import pytest
|
||||
from nkms_eth.blockchain import TesterBlockchain, Blockchain
|
||||
from nkms_eth.token import NuCypherKMSToken
|
||||
from nkms_eth.escrow import Escrow
|
||||
from nkms_eth.miner import Miner
|
||||
|
||||
from nkms_eth.agents import NuCypherKMSTokenAgent, MinerAgent
|
||||
from nkms_eth.blockchain import TheBlockchain
|
||||
from nkms_eth.utilities import TesterBlockchain, MockNuCypherKMSTokenDeployer, MockMinerEscrowDeployer
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
|
@ -10,20 +10,34 @@ def testerchain():
|
|||
chain = TesterBlockchain()
|
||||
yield chain
|
||||
del chain
|
||||
Blockchain._instance = False
|
||||
TheBlockchain._TheBlockchain__instance = None
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def token(testerchain):
|
||||
token = NuCypherKMSToken(blockchain=testerchain)
|
||||
token.arm()
|
||||
token.deploy()
|
||||
def mock_token_deployer(testerchain):
|
||||
token_deployer = MockNuCypherKMSTokenDeployer(blockchain=testerchain)
|
||||
token_deployer.arm()
|
||||
token_deployer.deploy()
|
||||
yield token_deployer
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_miner_escrow_deployer(token_agent):
|
||||
escrow = MockMinerEscrowDeployer(token_agent)
|
||||
escrow.arm()
|
||||
escrow.deploy()
|
||||
yield escrow
|
||||
|
||||
|
||||
# Unused args preserve fixture dependency order #
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def token_agent(testerchain, mock_token_deployer):
|
||||
token = NuCypherKMSTokenAgent(blockchain=testerchain)
|
||||
yield token
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def escrow(testerchain, token):
|
||||
escrow = Escrow(blockchain=testerchain, token=token)
|
||||
escrow.arm()
|
||||
escrow.deploy()
|
||||
yield escrow
|
||||
def mock_miner_agent(token_agent, mock_token_deployer, mock_miner_escrow_deployer):
|
||||
miner_agent = MinerAgent(token_agent)
|
||||
yield miner_agent
|
||||
|
|
|
@ -620,7 +620,7 @@ def test_verifying_state(web3, chain):
|
|||
|
||||
# Can't upgrade to the previous version or to the bad version
|
||||
contract_library_bad, _ = chain.provider.deploy_contract(
|
||||
'PolicyManagerBad', deploy_args=[address2],
|
||||
'PolicyManagerBad', deploy_args=[address2],
|
||||
deploy_transaction={'from': creator})
|
||||
with pytest.raises(TransactionFailed):
|
||||
tx = dispatcher.transact({'from': creator}).upgrade(contract_library_v1.address)
|
||||
|
@ -641,4 +641,4 @@ def test_verifying_state(web3, chain):
|
|||
# Try to upgrade to the bad version
|
||||
with pytest.raises(TransactionFailed):
|
||||
tx = dispatcher.transact({'from': creator}).upgrade(contract_library_bad.address)
|
||||
chain.wait.for_receipt(tx)
|
||||
chain.wait.for_receipt(tx)
|
|
@ -0,0 +1,125 @@
|
|||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from nkms_eth.actors import Miner
|
||||
from nkms_eth.agents import MinerAgent
|
||||
from nkms_eth.utilities import spawn_miners
|
||||
|
||||
|
||||
def test_miner_locking_tokens(testerchain, mock_token_deployer, mock_miner_agent):
|
||||
|
||||
mock_token_deployer._global_airdrop(amount=10000) # weeee
|
||||
|
||||
miner = Miner(miner_agent=mock_miner_agent, address=testerchain._chain.web3.eth.accounts[1])
|
||||
|
||||
an_amount_of_tokens = 1000 * mock_token_deployer._M
|
||||
miner.stake(amount=an_amount_of_tokens, locktime=mock_miner_agent._deployer._min_release_periods, auto_switch_lock=False)
|
||||
|
||||
# Verify that the escrow is allowed to receive tokens
|
||||
# assert mock_miner_agent.token_agent.read().allowance(miner.address, mock_miner_agent.contract_address) == 0
|
||||
|
||||
# Stake starts after one period
|
||||
# assert miner.token_balance() == 0
|
||||
# assert mock_miner_agent.read().getLockedTokens(miner.address) == 0
|
||||
|
||||
# Wait for it...
|
||||
testerchain.wait_time(mock_miner_agent._deployer._hours_per_period)
|
||||
|
||||
assert mock_miner_agent.read().getLockedTokens(miner.address) == an_amount_of_tokens
|
||||
|
||||
|
||||
def test_mine_then_withdraw_tokens(testerchain, mock_token_deployer, token_agent, mock_miner_agent, mock_miner_escrow_deployer):
|
||||
"""
|
||||
- Airdrop tokens to everyone
|
||||
- Create a Miner (Ursula)
|
||||
- Spawn additional miners
|
||||
- All miners lock tokens
|
||||
- Wait (with time)
|
||||
- Miner (Ursula) mints new tokens
|
||||
"""
|
||||
|
||||
mock_token_deployer._global_airdrop(amount=10000)
|
||||
|
||||
_origin, *everybody = testerchain._chain.web3.eth.accounts
|
||||
ursula_address, *everyone_else = everybody
|
||||
|
||||
miner = Miner(miner_agent=mock_miner_agent, address=ursula_address)
|
||||
|
||||
initial_balance = miner.token_balance()
|
||||
assert token_agent.get_balance(miner.address) == miner.token_balance()
|
||||
|
||||
stake_amount = (10 + random.randrange(9000)) * mock_token_deployer._M
|
||||
miner.stake(amount=stake_amount, locktime=30, auto_switch_lock=False)
|
||||
|
||||
# Stake starts after one period
|
||||
assert miner.token_balance() == 0
|
||||
assert mock_miner_agent.read().getLockedTokens(ursula_address) == 0
|
||||
|
||||
# Wait for it...
|
||||
testerchain.wait_time(mock_miner_agent._deployer._hours_per_period)
|
||||
|
||||
# Have other address lock tokens, and wait...
|
||||
spawn_miners(miner_agent=mock_miner_agent, addresses=everyone_else, locktime=30, m=mock_token_deployer._M)
|
||||
testerchain.wait_time(mock_miner_agent._deployer._hours_per_period*2)
|
||||
|
||||
# miner.confirm_activity()
|
||||
miner.mint()
|
||||
miner.collect_reward()
|
||||
|
||||
final_balance = token_agent.get_balance(miner.address)
|
||||
assert final_balance > initial_balance
|
||||
|
||||
|
||||
def test_sample_miners(testerchain, mock_token_deployer, mock_miner_agent):
|
||||
mock_token_deployer._global_airdrop(amount=10000)
|
||||
|
||||
_origin, *everyone_else = testerchain._chain.web3.eth.accounts[1:]
|
||||
spawn_miners(addresses=everyone_else, locktime=100, miner_agent=mock_miner_agent, m=mock_token_deployer._M)
|
||||
|
||||
testerchain.wait_time(mock_miner_agent._deployer._hours_per_period)
|
||||
|
||||
with pytest.raises(MinerAgent.NotEnoughUrsulas):
|
||||
mock_miner_agent.sample(quantity=100) # Waay more than we have deployed
|
||||
|
||||
miners = mock_miner_agent.sample(quantity=3)
|
||||
assert len(miners) == 3
|
||||
assert len(set(miners)) == 3
|
||||
|
||||
|
||||
# def test_publish_miner_ids(testerchain, mock_token_deployer, mock_miner_agent):
|
||||
# mock_token_deployer._global_airdrop(amount=10000) # weeee
|
||||
#
|
||||
# miner_addr = testerchain._chain.web3.eth.accounts[1]
|
||||
# miner = Miner(miner_agent=mock_miner_agent, address=miner_addr)
|
||||
#
|
||||
# balance = miner.token_balance()
|
||||
# miner.lock(amount=balance, locktime=1)
|
||||
#
|
||||
# # Publish Miner IDs to the DHT
|
||||
# mock_miner_id = os.urandom(32)
|
||||
# _txhash = miner.publish_miner_id(mock_miner_id)
|
||||
#
|
||||
# # Fetch the miner Ids
|
||||
# stored_miner_ids = miner.fetch_miner_ids()
|
||||
#
|
||||
# assert len(stored_miner_ids) == 1
|
||||
# assert mock_miner_id == stored_miner_ids[0]
|
||||
#
|
||||
# # Repeat, with another miner ID
|
||||
# another_mock_miner_id = os.urandom(32)
|
||||
# _txhash = miner.publish_miner_id(another_mock_miner_id)
|
||||
#
|
||||
# stored_miner_ids = miner.fetch_miner_ids()
|
||||
#
|
||||
# assert len(stored_miner_ids) == 2
|
||||
# assert another_mock_miner_id == stored_miner_ids[1]
|
||||
#
|
||||
# # TODO change encoding when v4 of web3.py is released
|
||||
# supposedly_the_same_miner_id = mock_miner_agent.call() \
|
||||
# .getMinerInfo(mock_miner_agent._deployer.MinerInfoField.MINER_ID.value,
|
||||
# miner_addr,
|
||||
# 1).encode('latin-1')
|
||||
#
|
||||
# assert another_mock_miner_id == supposedly_the_same_miner_id
|
||||
#
|
|
@ -0,0 +1,31 @@
|
|||
import pytest
|
||||
|
||||
from nkms_eth.utilities import spawn_miners, MockNuCypherMinerConfig
|
||||
|
||||
M = 10 ** 6
|
||||
|
||||
|
||||
def test_get_swarm(testerchain, mock_token_deployer, mock_miner_agent):
|
||||
|
||||
mock_token_deployer._global_airdrop(amount=10000)
|
||||
|
||||
creator, *addresses = testerchain._chain.web3.eth.accounts
|
||||
spawn_miners(addresses=addresses, miner_agent=mock_miner_agent, locktime=1, m=M)
|
||||
|
||||
default_period_duration = MockNuCypherMinerConfig._hours_per_period
|
||||
testerchain.wait_time(default_period_duration)
|
||||
|
||||
swarm = mock_miner_agent.swarm()
|
||||
swarm_addresses = list(swarm)
|
||||
assert len(swarm_addresses) == 9
|
||||
|
||||
# Grab a miner address from the swarm
|
||||
miner_addr = swarm_addresses[0]
|
||||
assert isinstance(miner_addr, str)
|
||||
|
||||
# Verify the address is hex
|
||||
try:
|
||||
int(miner_addr, 16)
|
||||
except ValueError:
|
||||
pytest.fail()
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import random
|
||||
|
||||
import pytest
|
||||
from populus.contracts.exceptions import NoKnownAddress
|
||||
from pytest import raises
|
||||
|
||||
from nkms_eth.escrow import Escrow
|
||||
from nkms_eth.miner import Miner
|
||||
from nkms_eth.token import NuCypherKMSToken
|
||||
|
||||
M = 10 ** 6
|
||||
|
||||
def test_create_escrow(testerchain):
|
||||
with raises(NoKnownAddress):
|
||||
NuCypherKMSToken.get(blockchain=testerchain)
|
||||
|
||||
token = NuCypherKMSToken(blockchain=testerchain)
|
||||
token.arm()
|
||||
token.deploy()
|
||||
|
||||
same_token = NuCypherKMSToken.get(blockchain=testerchain)
|
||||
with raises(NuCypherKMSToken.ContractDeploymentError):
|
||||
same_token.arm()
|
||||
same_token.deploy()
|
||||
|
||||
assert len(token.contract.address) == 42
|
||||
assert token.contract.address == same_token.contract.address
|
||||
|
||||
with raises(NoKnownAddress):
|
||||
Escrow.get(blockchain=testerchain, token=token)
|
||||
|
||||
escrow = Escrow(blockchain=testerchain, token=token)
|
||||
escrow.arm()
|
||||
escrow.deploy()
|
||||
|
||||
same_escrow = Escrow.get(blockchain=testerchain, token=token)
|
||||
with raises(Escrow.ContractDeploymentError):
|
||||
same_escrow.arm()
|
||||
same_escrow.deploy()
|
||||
|
||||
assert len(escrow.contract.address) == 42
|
||||
assert escrow.contract.address == same_escrow.contract.address
|
||||
|
||||
|
||||
def test_get_swarm(testerchain, token, escrow):
|
||||
token._airdrop(amount=10000)
|
||||
|
||||
# Create 9 Miners
|
||||
for u in testerchain._chain.web3.eth.accounts[1:]:
|
||||
miner = Miner(blockchain=testerchain, token=token, escrow=escrow, address=u)
|
||||
amount = (10+random.randrange(9000)) * M
|
||||
miner.lock(amount=amount, locktime=1)
|
||||
|
||||
testerchain.wait_time(escrow.hours_per_period)
|
||||
|
||||
swarm = escrow.swarm()
|
||||
swarm_addresses = list(swarm)
|
||||
assert len(swarm_addresses) == 9
|
||||
|
||||
# Grab a miner address from the swarm
|
||||
miner_addr = swarm_addresses[0]
|
||||
assert isinstance(miner_addr, str)
|
||||
|
||||
# Verify the address is hex
|
||||
try:
|
||||
int(miner_addr, 16)
|
||||
except ValueError:
|
||||
pytest.fail()
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import random
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from nkms_eth.escrow import Escrow
|
||||
from nkms_eth.miner import Miner
|
||||
from nkms_eth.token import NuCypherKMSToken
|
||||
|
||||
|
||||
M = 10 ** 6
|
||||
|
||||
|
||||
def test_deposit(testerchain, token, escrow):
|
||||
token._airdrop(amount=10000) # weeee
|
||||
|
||||
ursula_address = testerchain._chain.web3.eth.accounts[1]
|
||||
miner = Miner(blockchain=testerchain, token=token, escrow=escrow, address=ursula_address)
|
||||
miner.lock(amount=1000*M, locktime=100)
|
||||
|
||||
|
||||
def test_mine_withdraw(testerchain, token, escrow):
|
||||
token._airdrop(amount=10000)
|
||||
|
||||
ursula_address = testerchain._chain.web3.eth.accounts[1]
|
||||
miner = Miner(blockchain=testerchain, token=token, escrow=escrow, address=ursula_address)
|
||||
|
||||
ursula = miner
|
||||
initial_balance = token.balance(address=ursula.address)
|
||||
|
||||
# Create a random set of miners (we have 9 in total)
|
||||
for address in testerchain._chain.web3.eth.accounts[1:]:
|
||||
miner = Miner(blockchain=testerchain, token=token,
|
||||
escrow=escrow, address=address)
|
||||
|
||||
amount = (10+random.randrange(9000)) * M
|
||||
miner.lock(amount=amount, locktime=1)
|
||||
|
||||
testerchain.wait_time(escrow.hours_per_period*2)
|
||||
|
||||
ursula.mint()
|
||||
ursula.withdraw()
|
||||
final_balance = token.balance(ursula.address)
|
||||
|
||||
assert final_balance > initial_balance
|
||||
|
||||
|
||||
def test_publish_dht_key(testerchain, token, escrow):
|
||||
token._airdrop(amount=10000) # weeee
|
||||
|
||||
miner_addr = testerchain._chain.web3.eth.accounts[1]
|
||||
miner = Miner(blockchain=testerchain, token=token,
|
||||
escrow=escrow, address=miner_addr)
|
||||
|
||||
balance = miner.balance()
|
||||
miner.lock(amount=balance, locktime=1)
|
||||
|
||||
# Publish DHT keys
|
||||
mock_dht_key = os.urandom(32)
|
||||
|
||||
txhash = miner.publish_dht_key(mock_dht_key)
|
||||
stored_miner_dht_keys = miner.get_dht_key()
|
||||
|
||||
assert len(stored_miner_dht_keys) == 1
|
||||
assert mock_dht_key == stored_miner_dht_keys[0]
|
||||
|
||||
another_mock_dht_key = os.urandom(32)
|
||||
txhash = miner.publish_dht_key(another_mock_dht_key)
|
||||
|
||||
stored_miner_dht_keys = miner.get_dht_key()
|
||||
|
||||
assert len(stored_miner_dht_keys) == 2
|
||||
assert another_mock_dht_key == stored_miner_dht_keys[1]
|
||||
# TODO change when v4 web3.py will released
|
||||
assert another_mock_dht_key == escrow().getMinerInfo(escrow.MinerInfoField.MINER_ID.value, miner_addr, 1)\
|
||||
.encode('latin-1')
|
||||
|
||||
|
||||
def test_select_ursulas(testerchain, token, escrow):
|
||||
token._airdrop(amount=10000)
|
||||
|
||||
# Create a random set of miners (we have 9 in total)
|
||||
for u in testerchain._chain.web3.eth.accounts[1:]:
|
||||
miner = Miner(blockchain=testerchain, token=token, escrow=escrow, address=u)
|
||||
amount = (10 + random.randrange(9000))*M
|
||||
miner.lock(amount=amount, locktime=100)
|
||||
|
||||
testerchain.wait_time(escrow.hours_per_period)
|
||||
|
||||
miners = escrow.sample(quantity=3)
|
||||
assert len(miners) == 3
|
||||
assert len(set(miners)) == 3
|
||||
|
||||
with pytest.raises(Escrow.NotEnoughUrsulas):
|
||||
escrow.sample(quantity=100) # Waay more than we have deployed
|
|
@ -1,25 +0,0 @@
|
|||
from pytest import raises
|
||||
from populus.contracts.exceptions import NoKnownAddress
|
||||
|
||||
from nkms_eth.blockchain import TesterBlockchain
|
||||
from nkms_eth.token import NuCypherKMSToken
|
||||
|
||||
|
||||
def test_create_and_get_nucypherkms_token(testerchain):
|
||||
with raises(NoKnownAddress):
|
||||
NuCypherKMSToken.get(blockchain=testerchain)
|
||||
|
||||
token = NuCypherKMSToken(blockchain=testerchain)
|
||||
token.arm()
|
||||
token.deploy()
|
||||
|
||||
assert len(token.contract.address) == 42
|
||||
assert token.contract.call().totalSupply() != 0
|
||||
# assert token.contract.call().totalSupply() == 10 ** 9 - 1
|
||||
|
||||
same_token = NuCypherKMSToken.get(blockchain=testerchain)
|
||||
|
||||
assert token.contract.address == same_token.contract.address
|
||||
assert token == same_token
|
||||
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
from populus.contracts.exceptions import NoKnownAddress
|
||||
from pytest import raises
|
||||
|
||||
from nkms_eth.agents import NuCypherKMSTokenAgent, MinerAgent
|
||||
from nkms_eth.deployers import NuCypherKMSTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer
|
||||
|
||||
|
||||
def test_token_deployer_and_agent(testerchain):
|
||||
|
||||
# Trying to get token from blockchain before it's been published fails
|
||||
with raises(NoKnownAddress):
|
||||
NuCypherKMSTokenAgent(blockchain=testerchain)
|
||||
|
||||
# The big day...
|
||||
deployer = NuCypherKMSTokenDeployer(blockchain=testerchain)
|
||||
|
||||
with raises(NuCypherKMSTokenDeployer.ContractDeploymentError):
|
||||
deployer.deploy()
|
||||
|
||||
# Token must be armed before deploying to the blockchain
|
||||
deployer.arm()
|
||||
deployer.deploy()
|
||||
|
||||
# Create a token instance
|
||||
token_agent = NuCypherKMSTokenAgent(blockchain=testerchain)
|
||||
|
||||
# Make sure we got the name right
|
||||
deployer_contract_identifier = NuCypherKMSTokenDeployer._contract_name
|
||||
assert'NuCypherKMSToken' == deployer_contract_identifier
|
||||
|
||||
# Ensure the contract is deployed and has a valid blockchain address
|
||||
assert len(token_agent.contract_address) == 42
|
||||
|
||||
# Check that the token contract has tokens
|
||||
assert token_agent.read().totalSupply() != 0
|
||||
# assert token().totalSupply() == 10 ** 9 - 1 # TODO
|
||||
|
||||
# Retrieve the token from the blockchain
|
||||
same_token_agent = NuCypherKMSTokenAgent(blockchain=testerchain)
|
||||
|
||||
# Compare the contract address for equality
|
||||
assert token_agent.contract_address == same_token_agent.contract_address
|
||||
assert token_agent == same_token_agent # __eq__
|
||||
|
||||
|
||||
def test_deploy_ethereum_contracts(testerchain):
|
||||
"""
|
||||
Launch all ethereum contracts:
|
||||
- NuCypherKMSToken
|
||||
- PolicyManager
|
||||
- MinersEscrow
|
||||
- UserEscrow
|
||||
- Issuer
|
||||
"""
|
||||
|
||||
token_deployer = NuCypherKMSTokenDeployer(blockchain=testerchain)
|
||||
token_deployer.arm()
|
||||
token_deployer.deploy()
|
||||
|
||||
token_agent = NuCypherKMSTokenAgent(blockchain=testerchain)
|
||||
|
||||
miner_escrow_deployer = MinerEscrowDeployer(token_agent=token_agent)
|
||||
miner_escrow_deployer.arm()
|
||||
miner_escrow_deployer.deploy()
|
||||
|
||||
miner_agent = MinerAgent(token_agent=token_agent)
|
||||
|
||||
policy_manager_contract = PolicyManagerDeployer(miner_agent=miner_agent)
|
||||
policy_manager_contract.arm()
|
||||
policy_manager_contract.deploy()
|
||||
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
from os.path import join, dirname, abspath
|
||||
|
||||
import nkms_eth
|
||||
from nkms_eth.token import NuCypherKMSToken
|
||||
from nkms_eth.deployers import NuCypherKMSTokenDeployer
|
||||
|
||||
|
||||
def test_testerchain_create(testerchain):
|
||||
def test_testerchain_creation(testerchain):
|
||||
# Ensure we are testing on the correct network...
|
||||
assert testerchain._network == 'tester'
|
||||
|
||||
|
@ -24,6 +24,6 @@ def test_nucypher_populus_project(testerchain):
|
|||
# ...and on the testerchain/blockchain class itself
|
||||
assert testerchain._project.project_dir == populus_project_dir
|
||||
|
||||
# Ensure that smart contacts are available, post solidity compile.
|
||||
token_contract_identifier = NuCypherKMSToken._contract_name
|
||||
# Ensure that solidity smart contacts are available, post-compile.
|
||||
token_contract_identifier = NuCypherKMSTokenDeployer(blockchain=testerchain)._contract_name
|
||||
assert token_contract_identifier in testerchain._project.compiled_contract_data
|
Loading…
Reference in New Issue