[KMS-ETH]- Merge pull request #11 from KPrasch/python_api

Contract agent and deployer Interface
pull/195/head^2
K Prasch 2018-04-04 20:44:01 -07:00 committed by GitHub
commit 64a0558e0a
23 changed files with 1177 additions and 664 deletions

271
nkms_eth/actors.py Normal file
View File

@ -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

174
nkms_eth/agents.py Normal file
View File

@ -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

View File

@ -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'

View File

@ -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')

214
nkms_eth/deployers.py Normal file
View File

@ -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

View File

@ -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))

View File

@ -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

62
nkms_eth/policies.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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

65
nkms_eth/utilities.py Normal file
View File

@ -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)

View File

@ -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()

0
tests/__init__.py Normal file
View File

View File

@ -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

View File

@ -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)

View File

@ -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
#

View File

@ -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()

View File

View File

@ -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()

View File

@ -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

View File

@ -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

73
tests/test_api.py Normal file
View File

@ -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()

View File

@ -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