mirror of https://github.com/nucypher/nucypher.git
Merge pull request #465 from KPrasch/deploy-cli
CLI: The balance of two operating modespull/478/head
commit
3434938618
882
cli/main.py
882
cli/main.py
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,13 @@
|
|||
from collections import OrderedDict
|
||||
from logging import getLogger
|
||||
|
||||
import maya
|
||||
from constant_sorrow import constants
|
||||
from datetime import datetime
|
||||
from twisted.internet import task, reactor
|
||||
from typing import Tuple, List
|
||||
|
||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent
|
||||
from nucypher.blockchain.eth.interfaces import EthereumContractRegistry
|
||||
from nucypher.blockchain.eth.utils import (datetime_to_period,
|
||||
validate_stake_amount,
|
||||
validate_locktime,
|
||||
|
@ -30,8 +32,8 @@ class NucypherTokenActor:
|
|||
|
||||
def __init__(self,
|
||||
checksum_address: str = None,
|
||||
token_agent: NucypherTokenAgent = None,
|
||||
registry_filepath: str = None) -> None:
|
||||
token_agent: NucypherTokenAgent = None
|
||||
) -> None:
|
||||
"""
|
||||
:param checksum_address: If not passed, we assume this is an unknown actor
|
||||
|
||||
|
@ -47,10 +49,11 @@ class NucypherTokenActor:
|
|||
except AttributeError:
|
||||
self.checksum_public_address = checksum_address # type: str
|
||||
|
||||
if registry_filepath is not None:
|
||||
EthereumContractRegistry(registry_filepath=registry_filepath)
|
||||
if not token_agent:
|
||||
token_agent = NucypherTokenAgent()
|
||||
|
||||
self.token_agent = token_agent if token_agent is not None else NucypherTokenAgent()
|
||||
self.token_agent = token_agent
|
||||
self.blockchain = self.token_agent.blockchain
|
||||
self._transaction_cache = list() # type: list # track transactions transmitted
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -77,26 +80,121 @@ class Miner(NucypherTokenActor):
|
|||
Ursula baseclass for blockchain operations, practically carrying a pickaxe.
|
||||
"""
|
||||
|
||||
__current_period_sample_rate = 10
|
||||
|
||||
class MinerError(NucypherTokenActor.ActorError):
|
||||
pass
|
||||
|
||||
def __init__(self, miner_agent: MinerAgent, is_me=True, *args, **kwargs) -> None:
|
||||
if miner_agent is None:
|
||||
token_agent = NucypherTokenAgent()
|
||||
miner_agent = MinerAgent(token_agent=token_agent)
|
||||
super().__init__(token_agent=miner_agent.token_agent, *args, **kwargs)
|
||||
def __init__(self, is_me: bool, miner_agent: MinerAgent, *args, **kwargs) -> None:
|
||||
|
||||
# Extrapolate dependencies
|
||||
self.miner_agent = miner_agent
|
||||
self.token_agent = miner_agent.token_agent
|
||||
self.blockchain = self.token_agent.blockchain
|
||||
|
||||
# Establish initial state
|
||||
self.log = getLogger("miner")
|
||||
self.is_me = is_me
|
||||
if is_me:
|
||||
token_agent = miner_agent.token_agent
|
||||
blockchain = miner_agent.token_agent.blockchain
|
||||
else:
|
||||
token_agent = constants.STRANGER_MINER
|
||||
blockchain = constants.STRANGER_MINER
|
||||
|
||||
self.miner_agent = miner_agent
|
||||
self.token_agent = token_agent
|
||||
self.blockchain = blockchain
|
||||
|
||||
super().__init__(token_agent=self.token_agent, *args, **kwargs)
|
||||
|
||||
if is_me is True:
|
||||
self.__current_period = None # TODO: use constant
|
||||
self._abort_on_staking_error = True
|
||||
self._staking_task = task.LoopingCall(self._confirm_period)
|
||||
|
||||
#
|
||||
# Staking
|
||||
#
|
||||
|
||||
@only_me
|
||||
def stake(self,
|
||||
confirm_now=False,
|
||||
resume: bool = False,
|
||||
expiration: maya.MayaDT = None,
|
||||
lock_periods: int = None,
|
||||
*args, **kwargs) -> None:
|
||||
|
||||
"""High-level staking daemon loop"""
|
||||
|
||||
if lock_periods and expiration:
|
||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||
if expiration:
|
||||
lock_periods = datetime_to_period(expiration)
|
||||
|
||||
if resume is False:
|
||||
_staking_receipts = self.initialize_stake(expiration=expiration,
|
||||
lock_periods=lock_periods,
|
||||
*args, **kwargs)
|
||||
|
||||
# TODO: Check if this period has already been confirmed
|
||||
# TODO: Check if there is an active stake in the current period: Resume staking daemon
|
||||
# TODO: Validation and Sanity checks
|
||||
|
||||
if confirm_now:
|
||||
self.confirm_activity()
|
||||
|
||||
# record start time and periods
|
||||
self.__start_time = maya.now()
|
||||
self.__uptime_period = self.miner_agent.get_current_period()
|
||||
self.__terminal_period = self.__uptime_period + lock_periods
|
||||
self.__current_period = self.__uptime_period
|
||||
self.start_staking_loop()
|
||||
|
||||
#
|
||||
# Daemon
|
||||
#
|
||||
|
||||
@only_me
|
||||
def _confirm_period(self):
|
||||
|
||||
period = self.miner_agent.get_current_period()
|
||||
self.log.info("Checking for new period. Current period is {}".format(self.__current_period)) # TODO: set to debug?
|
||||
|
||||
if self.__current_period != period:
|
||||
|
||||
# check for stake expiration
|
||||
stake_expired = self.__current_period >= self.__terminal_period
|
||||
if stake_expired:
|
||||
self.log.info('Stake duration expired')
|
||||
return True
|
||||
|
||||
self.confirm_activity()
|
||||
self.__current_period = period
|
||||
self.log.info("Confirmed activity for period {}".format(self.__current_period))
|
||||
|
||||
@only_me
|
||||
def _crash_gracefully(self, failure=None):
|
||||
"""
|
||||
A facility for crashing more gracefully in the event that an exception
|
||||
is unhandled in a different thread, especially inside a loop like the learning loop.
|
||||
"""
|
||||
self._crashed = failure
|
||||
failure.raiseException()
|
||||
|
||||
@only_me
|
||||
def handle_staking_errors(self, *args, **kwargs):
|
||||
failure = args[0]
|
||||
if self._abort_on_staking_error:
|
||||
self.log.critical("Unhandled error during node staking. Attempting graceful crash.")
|
||||
reactor.callFromThread(self._crash_gracefully, failure=failure)
|
||||
else:
|
||||
self.log.warning("Unhandled error during node learning: {}".format(failure.getTraceback()))
|
||||
|
||||
@only_me
|
||||
def start_staking_loop(self, now=True):
|
||||
if self._staking_task.running:
|
||||
return False
|
||||
else:
|
||||
d = self._staking_task.start(interval=self.__current_period_sample_rate, now=now)
|
||||
d.addErrback(self.handle_staking_errors)
|
||||
self.log.info("Started staking loop")
|
||||
return d
|
||||
|
||||
@property
|
||||
def is_staking(self):
|
||||
"""Checks if this Miner currently has locked tokens."""
|
||||
|
@ -116,8 +214,6 @@ class Miner(NucypherTokenActor):
|
|||
@only_me
|
||||
def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]:
|
||||
"""Public facing method for token locking."""
|
||||
if not self.is_me:
|
||||
raise self.MinerError("Cannot execute miner staking functions with a non-self Miner instance.")
|
||||
|
||||
approve_txhash = self.token_agent.approve_transfer(amount=amount,
|
||||
target_address=self.miner_agent.contract_address,
|
||||
|
@ -176,7 +272,7 @@ class Miner(NucypherTokenActor):
|
|||
@only_me
|
||||
def __validate_stake(self, amount: int, lock_periods: int) -> bool:
|
||||
|
||||
assert validate_stake_amount(amount=amount)
|
||||
assert validate_stake_amount(amount=amount) # TODO: remove assertions..?
|
||||
assert validate_locktime(lock_periods=lock_periods)
|
||||
|
||||
if not self.token_balance >= amount:
|
||||
|
@ -219,6 +315,7 @@ class Miner(NucypherTokenActor):
|
|||
approve_txhash, initial_deposit_txhash = self.deposit(amount=amount, lock_periods=lock_periods)
|
||||
self._transaction_cache.append((datetime.utcnow(), initial_deposit_txhash))
|
||||
|
||||
self.log.info("{} Initialized new stake: {} tokens for {} periods".format(self.checksum_public_address, amount, lock_periods))
|
||||
return staking_transactions
|
||||
|
||||
#
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import random
|
||||
from abc import ABC
|
||||
from logging import getLogger
|
||||
|
||||
from constant_sorrow.constants import NO_CONTRACT_AVAILABLE
|
||||
from typing import Generator, List, Tuple, Union
|
||||
|
@ -31,15 +32,14 @@ class EthereumContractAgent(ABC):
|
|||
|
||||
def __init__(self,
|
||||
blockchain: Blockchain = None,
|
||||
registry_filepath: str = None,
|
||||
contract: Contract = None
|
||||
) -> None:
|
||||
|
||||
self.blockchain = blockchain or Blockchain.connect()
|
||||
self.log = getLogger('agency')
|
||||
|
||||
if registry_filepath is not None:
|
||||
# TODO: Warn on override/ do this elsewhere?
|
||||
self.blockchain.interface._registry._swap_registry(filepath=registry_filepath)
|
||||
if blockchain is None:
|
||||
blockchain = Blockchain.connect()
|
||||
self.blockchain = blockchain
|
||||
|
||||
if contract is None:
|
||||
# Fetch the contract by reading address and abi from the registry and blockchain
|
||||
|
@ -47,6 +47,10 @@ class EthereumContractAgent(ABC):
|
|||
upgradeable=self._upgradeable)
|
||||
self.__contract = contract
|
||||
super().__init__()
|
||||
self.log.info("Initialized new {} for {} with {} and {}".format(self.__class__.__name__,
|
||||
self.contract_address,
|
||||
self.blockchain.interface.provider_uri,
|
||||
self.blockchain.interface.registry.filepath))
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
|
@ -82,7 +86,18 @@ class NucypherTokenAgent(EthereumContractAgent):
|
|||
def approve_transfer(self, amount: int, target_address: str, sender_address: str) -> str:
|
||||
"""Approve the transfer of token from the sender address to the target address."""
|
||||
|
||||
txhash = self.contract.functions.approve(target_address, amount).transact({'from': sender_address})
|
||||
txhash = self.contract.functions.approve(target_address, amount)\
|
||||
.transact({'from': sender_address})#, 'gas': 40000}) # TODO: needed for use with geth.
|
||||
|
||||
self.blockchain.wait_for_receipt(txhash)
|
||||
return txhash
|
||||
|
||||
def transfer(self, amount: int, target_address: str, sender_address: str):
|
||||
"""
|
||||
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
|
||||
"""
|
||||
self.approve_transfer(amount=amount, target_address=target_address, sender_address=sender_address)
|
||||
txhash = self.contract.functions.transfer(target_address, amount).transact({'from': sender_address})
|
||||
self.blockchain.wait_for_receipt(txhash)
|
||||
return txhash
|
||||
|
||||
|
@ -103,8 +118,13 @@ class MinerAgent(EthereumContractAgent):
|
|||
class NotEnoughMiners(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, token_agent: NucypherTokenAgent, *args, **kwargs) -> None:
|
||||
super().__init__(blockchain=token_agent.blockchain, *args, **kwargs)
|
||||
def __init__(self,
|
||||
token_agent: NucypherTokenAgent,
|
||||
*args, **kwargs
|
||||
) -> None:
|
||||
super().__init__(blockchain=token_agent.blockchain,
|
||||
*args, **kwargs)
|
||||
|
||||
self.token_agent = token_agent
|
||||
|
||||
#
|
||||
|
@ -139,8 +159,8 @@ class MinerAgent(EthereumContractAgent):
|
|||
|
||||
def deposit_tokens(self, amount: int, lock_periods: int, sender_address: str) -> str:
|
||||
"""Send tokes to the escrow from the miner's address"""
|
||||
|
||||
deposit_txhash = self.contract.functions.deposit(amount, lock_periods).transact({'from': sender_address, 'gas': 2000000}) # TODO: what..?
|
||||
deposit_txhash = self.contract.functions.deposit(amount, lock_periods)\
|
||||
.transact({'from': sender_address, 'gas': 2000000}) # TODO: Causes tx to fail without high amount of gas
|
||||
self.blockchain.wait_for_receipt(deposit_txhash)
|
||||
return deposit_txhash
|
||||
|
||||
|
@ -235,8 +255,13 @@ class PolicyAgent(EthereumContractAgent):
|
|||
_upgradeable = True
|
||||
__instance = NO_CONTRACT_AVAILABLE
|
||||
|
||||
def __init__(self, miner_agent: MinerAgent, *args, **kwargs) -> None:
|
||||
super().__init__(blockchain=miner_agent.blockchain, *args, **kwargs)
|
||||
def __init__(self,
|
||||
miner_agent: MinerAgent,
|
||||
*args, **kwargs) -> None:
|
||||
|
||||
super().__init__(blockchain=miner_agent.blockchain,
|
||||
*args, **kwargs)
|
||||
|
||||
self.miner_agent = miner_agent
|
||||
self.token_agent = miner_agent.token_agent
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
from logging import getLogger
|
||||
|
||||
from constant_sorrow.constants import NO_BLOCKCHAIN_AVAILABLE
|
||||
from typing import Union
|
||||
from web3.contract import Contract
|
||||
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterface, BlockchainDeployerInterface
|
||||
from nucypher.blockchain.eth.registry import EthereumContractRegistry
|
||||
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
||||
|
||||
|
||||
class Blockchain:
|
||||
|
@ -16,6 +20,8 @@ class Blockchain:
|
|||
|
||||
def __init__(self, interface: Union[BlockchainInterface, BlockchainDeployerInterface] = None) -> None:
|
||||
|
||||
self.log = getLogger("blockchain") # type: Logger
|
||||
|
||||
# Default interface
|
||||
if interface is None:
|
||||
interface = self.__default_interface_class()
|
||||
|
@ -37,9 +43,19 @@ class Blockchain:
|
|||
return self.__interface
|
||||
|
||||
@classmethod
|
||||
def connect(cls, provider_uri: str = None) -> 'Blockchain':
|
||||
def connect(cls,
|
||||
provider_uri: str = None,
|
||||
registry_filepath: str = None,
|
||||
deployer: bool = False,
|
||||
compile: bool = False,
|
||||
) -> 'Blockchain':
|
||||
|
||||
if cls._instance is NO_BLOCKCHAIN_AVAILABLE:
|
||||
cls._instance = cls(interface=BlockchainInterface(provider_uri=provider_uri))
|
||||
registry = EthereumContractRegistry(registry_filepath=registry_filepath)
|
||||
compiler = SolidityCompiler() if compile is True else None
|
||||
InterfaceClass = BlockchainDeployerInterface if deployer is True else BlockchainInterface
|
||||
interface = InterfaceClass(provider_uri=provider_uri, registry=registry, compiler=compiler)
|
||||
cls._instance = cls(interface=interface)
|
||||
else:
|
||||
if provider_uri is not None:
|
||||
existing_uri = cls._instance.interface.provider_uri
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Nucypher Token and Miner constants."""
|
||||
|
||||
NUCYPHER_GAS_LIMIT = 5000000 # TODO: move elsewhere?
|
||||
|
||||
#
|
||||
# Dispatcher
|
||||
|
|
|
@ -18,33 +18,36 @@ class ContractDeployer:
|
|||
agency = NotImplemented
|
||||
_interface_class = BlockchainDeployerInterface
|
||||
_contract_name = NotImplemented
|
||||
_arming_word = "I UNDERSTAND"
|
||||
|
||||
class ContractDeploymentError(Exception):
|
||||
pass
|
||||
|
||||
class ContractNotDeployed(ContractDeploymentError):
|
||||
pass
|
||||
|
||||
def __init__(self,
|
||||
blockchain: Blockchain,
|
||||
deployer_address: str
|
||||
deployer_address: str,
|
||||
blockchain: Blockchain = None,
|
||||
) -> None:
|
||||
|
||||
self.__armed = False
|
||||
self._contract = CONTRACT_NOT_DEPLOYED
|
||||
self.deployment_receipt = CONTRACT_NOT_DEPLOYED
|
||||
self.__dispatcher = NotImplemented
|
||||
self.__deployer_address = deployer_address
|
||||
|
||||
# Sanity check
|
||||
if blockchain is not None:
|
||||
if not isinstance(blockchain, Blockchain):
|
||||
error = 'Only TheBlockchain can be used to create a deployer, got {}.'
|
||||
error = 'Only a Blockchain instance can be used to create a deployer; Got {}.'
|
||||
raise ValueError(error.format(type(blockchain)))
|
||||
self.blockchain = blockchain
|
||||
self.__deployer_address = deployer_address
|
||||
|
||||
self.blockchain = blockchain or Blockchain.connect()
|
||||
|
||||
@property
|
||||
def contract_address(self) -> str:
|
||||
if self._contract is CONTRACT_NOT_DEPLOYED:
|
||||
cls = self.__class__
|
||||
raise ContractDeployer.ContractDeploymentError('Contract not deployed')
|
||||
raise self.ContractNotDeployed
|
||||
address = self._contract.address # type: str
|
||||
return address
|
||||
|
||||
|
@ -68,7 +71,7 @@ class ContractDeployer:
|
|||
def is_armed(self) -> bool:
|
||||
return bool(self.__armed is True)
|
||||
|
||||
def check_ready_to_deploy(self, fail=False) -> Tuple[bool, list]:
|
||||
def check_ready_to_deploy(self, fail=False, check_arming=False) -> Tuple[bool, list]:
|
||||
"""
|
||||
Iterates through a set of rules required for an ethereum
|
||||
contract deployer to be eligible for deployment returning a
|
||||
|
@ -80,12 +83,14 @@ class ContractDeployer:
|
|||
If fail is set to True, raise a configuration error, instead of returning.
|
||||
"""
|
||||
|
||||
rules = (
|
||||
(self.is_armed is True, 'Contract not armed'),
|
||||
rules = [
|
||||
(self.is_deployed is not True, 'Contract already deployed'),
|
||||
(self.deployer_address is not NO_DEPLOYER_CONFIGURED, 'No deployer origin address set.'),
|
||||
(self.deployer_address is not None, 'No deployer address set.'),
|
||||
(self.deployer_address is not NO_DEPLOYER_CONFIGURED, 'No deployer address set.'),
|
||||
]
|
||||
|
||||
)
|
||||
if check_arming:
|
||||
rules.append((self.is_armed is True, 'Contract not armed'))
|
||||
|
||||
disqualifications = list()
|
||||
for failed_rule, failure_reason in rules:
|
||||
|
@ -93,7 +98,7 @@ class ContractDeployer:
|
|||
if fail is True:
|
||||
raise self.ContractDeploymentError(failure_reason)
|
||||
else:
|
||||
disqualifications.append(failure_reason) # ...and here's why
|
||||
disqualifications.append(failure_reason) # ... here's why
|
||||
continue
|
||||
|
||||
is_ready = True if len(disqualifications) == 0 else False
|
||||
|
@ -109,7 +114,7 @@ class ContractDeployer:
|
|||
|
||||
return True
|
||||
|
||||
def arm(self) -> None:
|
||||
def arm(self, abort=True) -> tuple:
|
||||
"""
|
||||
Safety mechanism for ethereum contract deployment
|
||||
|
||||
|
@ -120,9 +125,10 @@ class ContractDeployer:
|
|||
incorrectly types the arming_word.
|
||||
|
||||
"""
|
||||
if self.__armed is True:
|
||||
if self.__armed is True and abort is True:
|
||||
raise self.ContractDeploymentError('{} deployer is already armed.'.format(self._contract_name))
|
||||
self.__armed = True
|
||||
self.__armed, disqualifications = self.check_ready_to_deploy(fail=abort, check_arming=False)
|
||||
return self.__armed, disqualifications
|
||||
|
||||
def deploy(self) -> dict:
|
||||
"""
|
||||
|
@ -142,16 +148,8 @@ class NucypherTokenDeployer(ContractDeployer):
|
|||
agency = NucypherTokenAgent
|
||||
_contract_name = agency.principal_contract_name
|
||||
|
||||
def __init__(self,
|
||||
blockchain,
|
||||
deployer_address: str
|
||||
) -> None:
|
||||
|
||||
if not type(blockchain.interface) is self._interface_class:
|
||||
raise ValueError("{} must be used to create a {}".format(self._interface_class.__name__,
|
||||
self.__class__.__name__))
|
||||
|
||||
super().__init__(blockchain=blockchain, deployer_address=deployer_address)
|
||||
def __init__(self, deployer_address: str, *args, **kwargs) -> None:
|
||||
super().__init__(deployer_address=deployer_address, *args, **kwargs)
|
||||
self._creator = deployer_address
|
||||
|
||||
def deploy(self) -> dict:
|
||||
|
@ -162,16 +160,14 @@ class NucypherTokenDeployer(ContractDeployer):
|
|||
The contract must be armed before it can be deployed.
|
||||
Deployment can only ever be executed exactly once!
|
||||
"""
|
||||
|
||||
is_ready, _disqualifications = self.check_ready_to_deploy(fail=True)
|
||||
assert is_ready
|
||||
self.check_ready_to_deploy(fail=True, check_arming=True)
|
||||
|
||||
_contract, deployment_txhash = self.blockchain.interface.deploy_contract(
|
||||
self._contract_name,
|
||||
int(constants.TOKEN_SATURATION))
|
||||
constants.TOKEN_SATURATION)
|
||||
|
||||
self._contract = _contract
|
||||
return {'deployment_receipt': self.deployment_receipt}
|
||||
return {'txhash': deployment_txhash}
|
||||
|
||||
|
||||
class DispatcherDeployer(ContractDeployer):
|
||||
|
@ -182,14 +178,14 @@ class DispatcherDeployer(ContractDeployer):
|
|||
|
||||
_contract_name = 'Dispatcher'
|
||||
|
||||
def __init__(self, target_contract, secret_hash, *args, **kwargs):
|
||||
def __init__(self, target_contract, secret_hash: bytes, *args, **kwargs):
|
||||
self.target_contract = target_contract
|
||||
self.secret_hash = secret_hash
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deploy(self) -> dict:
|
||||
|
||||
dispatcher_contract, txhash = self.blockchain.interface.deploy_contract('Dispatcher',
|
||||
dispatcher_contract, txhash = self.blockchain.interface.deploy_contract(self._contract_name,
|
||||
self.target_contract.address,
|
||||
self.secret_hash)
|
||||
|
||||
|
@ -206,7 +202,7 @@ class MinerEscrowDeployer(ContractDeployer):
|
|||
_contract_name = agency.principal_contract_name
|
||||
|
||||
def __init__(self, token_agent, secret_hash, *args, **kwargs):
|
||||
super().__init__(blockchain=token_agent.blockchain, *args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.token_agent = token_agent
|
||||
self.secret_hash = secret_hash
|
||||
|
||||
|
@ -233,8 +229,7 @@ class MinerEscrowDeployer(ContractDeployer):
|
|||
"""
|
||||
|
||||
# Raise if not all-systems-go
|
||||
is_ready, _disqualifications = self.check_ready_to_deploy(fail=True)
|
||||
assert is_ready
|
||||
self.check_ready_to_deploy(fail=True, check_arming=True)
|
||||
|
||||
# Build deployment arguments
|
||||
origin_args = {'from': self.deployer_address}
|
||||
|
@ -246,7 +241,7 @@ class MinerEscrowDeployer(ContractDeployer):
|
|||
*map(int, constants.MINING_COEFFICIENT))
|
||||
|
||||
# 2 - Deploy the dispatcher used for updating this contract #
|
||||
dispatcher_deployer = DispatcherDeployer(blockchain=self.token_agent.blockchain,
|
||||
dispatcher_deployer = DispatcherDeployer(blockchain=self.blockchain,
|
||||
target_contract=the_escrow_contract,
|
||||
deployer_address=self.deployer_address,
|
||||
secret_hash=self.secret_hash)
|
||||
|
@ -267,7 +262,7 @@ class MinerEscrowDeployer(ContractDeployer):
|
|||
|
||||
# 3 - Transfer tokens to the miner escrow #
|
||||
reward_txhash = self.token_agent.contract.functions.transfer(the_escrow_contract.address,
|
||||
int(constants.TOKEN_SUPPLY)).transact(origin_args)
|
||||
constants.TOKEN_SUPPLY).transact(origin_args)
|
||||
|
||||
_reward_receipt = self.blockchain.wait_for_receipt(reward_txhash)
|
||||
|
||||
|
@ -308,17 +303,16 @@ class PolicyManagerDeployer(ContractDeployer):
|
|||
self.token_agent = miner_agent.token_agent
|
||||
self.miner_agent = miner_agent
|
||||
self.secret_hash = secret_hash
|
||||
super().__init__(blockchain=self.miner_agent.blockchain, *args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deploy(self) -> Dict[str, str]:
|
||||
is_ready, _disqualifications = self.check_ready_to_deploy(fail=True)
|
||||
assert is_ready
|
||||
self.check_ready_to_deploy(fail=True, check_arming=True)
|
||||
|
||||
# Creator deploys the policy manager
|
||||
the_policy_manager_contract, deploy_txhash = self.blockchain.interface.deploy_contract(
|
||||
self._contract_name, self.miner_agent.contract_address)
|
||||
|
||||
dispatcher_deployer = DispatcherDeployer(blockchain=self.token_agent.blockchain,
|
||||
dispatcher_deployer = DispatcherDeployer(blockchain=self.blockchain,
|
||||
target_contract=the_policy_manager_contract,
|
||||
deployer_address=self.deployer_address,
|
||||
secret_hash=self.secret_hash)
|
||||
|
@ -365,21 +359,20 @@ class UserEscrowDeployer(ContractDeployer):
|
|||
agency = UserEscrowAgent
|
||||
_contract_name = agency.principal_contract_name
|
||||
|
||||
def __init__(self, miner_escrow_deployer, policy_deployer, *args, **kwargs) -> None:
|
||||
self.miner_deployer = miner_escrow_deployer
|
||||
self.policy_deployer = policy_deployer
|
||||
self.token_deployer = miner_escrow_deployer.token_deployer
|
||||
super().__init__(blockchain=miner_escrow_deployer.blockchain, *args, **kwargs)
|
||||
def __init__(self, policy_agent, *args, **kwargs) -> None:
|
||||
self.policy_agent = policy_agent
|
||||
self.miner_agent = policy_agent.miner_agent
|
||||
self.token_agent = policy_agent.token_agent
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deploy(self) -> dict:
|
||||
is_ready, _disqualifications = self.check_ready_to_deploy(fail=True)
|
||||
assert is_ready
|
||||
self.check_ready_to_deploy(fail=True, check_arming=True)
|
||||
|
||||
deployment_args = [self.token_deployer.contract_address,
|
||||
self.miner_deployer.contract_address,
|
||||
self.policy_deployer.contract_address]
|
||||
deployment_args = [self.token_agent.contract_address,
|
||||
self.miner_agent.contract_address,
|
||||
self.policy_agent.contract_address]
|
||||
|
||||
deploy_transaction = {'from': self.token_deployer.contract_address} # TODO:.. eh?
|
||||
# deploy_transaction = {'from': self.token_agent.contract_address} # TODO:.. eh?
|
||||
|
||||
the_user_escrow_contract, deploy_txhash = self.blockchain.interface.deploy_contract(
|
||||
self._contract_name,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from logging import getLogger
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from constant_sorrow import constants
|
||||
|
@ -10,10 +11,9 @@ from web3 import Web3, WebsocketProvider, HTTPProvider, IPCProvider
|
|||
from web3.contract import Contract
|
||||
from web3.providers.eth_tester.main import EthereumTesterProvider
|
||||
|
||||
from nucypher.blockchain.eth.constants import NUCYPHER_GAS_LIMIT
|
||||
from nucypher.blockchain.eth.registry import EthereumContractRegistry
|
||||
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
from nucypher.config.parsers import parse_blockchain_config
|
||||
|
||||
|
||||
class BlockchainInterface:
|
||||
|
@ -22,8 +22,7 @@ class BlockchainInterface:
|
|||
ethereum contracts with the given web3 provider backend.
|
||||
"""
|
||||
__default_timeout = 10 # seconds
|
||||
__default_network = 'tester'
|
||||
__default_transaction_gas_limit = 500000 # TODO: determine sensible limit and validate transactions
|
||||
# __default_transaction_gas_limit = 500000 # TODO: determine sensible limit and validate transactions
|
||||
|
||||
class UnknownContract(Exception):
|
||||
pass
|
||||
|
@ -32,7 +31,6 @@ class BlockchainInterface:
|
|||
pass
|
||||
|
||||
def __init__(self,
|
||||
network_name: str = None,
|
||||
provider_uri: str = None,
|
||||
providers: list = None,
|
||||
autoconnect: bool = True,
|
||||
|
@ -44,33 +42,39 @@ class BlockchainInterface:
|
|||
A blockchain "network inerface"; The circumflex wraps entirely around the bounds of
|
||||
contract operations including compilation, deployment, and execution.
|
||||
|
||||
Filesystem Configuration Client Web3 Node
|
||||
================ ====================== =============== ===================== ===========================
|
||||
|
||||
Solidity Files -- SolidityCompiler --- --- HTTPProvider --
|
||||
| | |
|
||||
| | -- External EVM (geth, etc.)
|
||||
|
|
||||
*BlockchainInterface* -- IPCProvider --
|
||||
Solidity Files -- SolidityCompiler --- --- HTTPProvider ------ ...
|
||||
| |
|
||||
| |
|
||||
|
||||
*BlockchainInterface* -- IPCProvider ----- External EVM (geth, parity...)
|
||||
|
||||
| | |
|
||||
| | |
|
||||
Registry File -- ContractRegistry -- | ---- TestProvider -- EthereumTester
|
||||
Registry File -- ContractRegistry --- | ---- TestProvider ----- EthereumTester
|
||||
|
|
||||
| |
|
||||
|
|
||||
Pyevm (development chain)
|
||||
Blockchain
|
||||
PyEVM (Development Chain)
|
||||
Runtime Files --- -------- Blockchain
|
||||
| |
|
||||
| | |
|
||||
|
||||
Key Files ------ NodeConfiguration -------- Agent ... (Contract API)
|
||||
|
||||
| | |
|
||||
| |
|
||||
| ---------- Actor ... (Blockchain-Character API)
|
||||
|
|
||||
| |
|
||||
|
||||
Agent ... (Contract API)
|
||||
|
||||
|
|
||||
|
||||
Character / Actor
|
||||
Configuration File Character ... (Public API)
|
||||
|
||||
|
||||
The circumflex is the junction of the solidity compiler, a contract registry, and a collection of
|
||||
web3 network __providers as a means of interfacing with the ethereum blockchain to execute
|
||||
The BlockchainInterface is the junction of the solidity compiler, a contract registry, and a collection of
|
||||
web3 network providers as a means of interfacing with the ethereum blockchain to execute
|
||||
or deploy contract code on the network.
|
||||
|
||||
|
||||
|
@ -94,15 +98,16 @@ class BlockchainInterface:
|
|||
|
||||
"""
|
||||
|
||||
self.__network = network_name if network_name is not None else self.__default_network
|
||||
self.timeout = timeout if timeout is not None else self.__default_timeout
|
||||
self.log = getLogger("blockchain-interface") # type: Logger
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
self.w3 = constants.NO_BLOCKCHAIN_CONNECTION
|
||||
self.__providers = providers if providers is not None else constants.NO_BLOCKCHAIN_CONNECTION
|
||||
self.__providers = providers or constants.NO_BLOCKCHAIN_CONNECTION
|
||||
self.provider_uri = constants.NO_BLOCKCHAIN_CONNECTION
|
||||
self.timeout = timeout if timeout is not None else self.__default_timeout
|
||||
|
||||
if provider_uri and providers:
|
||||
raise self.InterfaceError("Pass a provider URI string, or a list of provider instances.")
|
||||
|
@ -114,8 +119,7 @@ class BlockchainInterface:
|
|||
for provider in providers:
|
||||
self.add_provider(provider)
|
||||
else:
|
||||
# TODO: Emit a warning / log: No provider supplied for blockchain interface
|
||||
pass
|
||||
self.log.warning("No provider supplied for new blockchain interface; Using defaults")
|
||||
|
||||
# if a SolidityCompiler class instance was passed, compile from solidity source code
|
||||
recompile = True if compiler is not None else False
|
||||
|
@ -123,13 +127,18 @@ class BlockchainInterface:
|
|||
self.__sol_compiler = compiler
|
||||
|
||||
# Setup the registry and base contract factory cache
|
||||
registry = registry if registry is not None else EthereumContractRegistry().from_config()
|
||||
self._registry = registry
|
||||
registry = registry if registry is not None else EthereumContractRegistry()
|
||||
self.registry = registry
|
||||
self.log.info("Using contract registry {}".format(self.registry.filepath))
|
||||
|
||||
if self.__recompile is True:
|
||||
# Execute the compilation if we're recompiling, otherwise read compiled contract data from the registry
|
||||
# Execute the compilation if we're recompiling
|
||||
# Otherwise read compiled contract data from the registry
|
||||
interfaces = self.__sol_compiler.compile()
|
||||
self.__raw_contract_cache = interfaces
|
||||
__raw_contract_cache = interfaces
|
||||
else:
|
||||
__raw_contract_cache = constants.NO_COMPILATION_PERFORMED
|
||||
self.__raw_contract_cache = __raw_contract_cache
|
||||
|
||||
# Auto-connect
|
||||
self.autoconnect = autoconnect
|
||||
|
@ -137,6 +146,7 @@ class BlockchainInterface:
|
|||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
self.log.info("Connecting to {}".format(self.provider_uri))
|
||||
|
||||
if self.__providers is constants.NO_BLOCKCHAIN_CONNECTION:
|
||||
raise self.InterfaceError("There are no configured blockchain providers")
|
||||
|
@ -149,34 +159,16 @@ class BlockchainInterface:
|
|||
if not self.is_connected:
|
||||
raise self.InterfaceError('Failed to connect to providers: {}'.format(self.__providers))
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def from_configuration_file(cls, config: NodeConfiguration) -> 'BlockchainInterface':
|
||||
# Parse
|
||||
payload = parse_blockchain_config(filepath=config.config_file_location)
|
||||
|
||||
# Init deps
|
||||
compiler = SolidityCompiler() if payload['compile'] else None
|
||||
registry = EthereumContractRegistry.from_config(config=config)
|
||||
interface_class = BlockchainInterface if not payload['deploy'] else BlockchainDeployerInterface
|
||||
|
||||
# init class
|
||||
interface = interface_class(timeout=payload['timeout'],
|
||||
provider_uri=payload['provider_uri'],
|
||||
compiler=compiler,
|
||||
registry=registry)
|
||||
|
||||
return interface
|
||||
if self.is_connected:
|
||||
self.log.info('Successfully Connected to {}'.format(self.provider_uri))
|
||||
return self.is_connected
|
||||
else:
|
||||
raise self.InterfaceError("Failed to connect to {}. Check your connection.".format(self.provider_uri))
|
||||
|
||||
@property
|
||||
def providers(self) -> Tuple[Union[IPCProvider, WebsocketProvider, HTTPProvider], ...]:
|
||||
return tuple(self.__providers)
|
||||
|
||||
@property
|
||||
def network(self) -> str:
|
||||
return self.__network
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""
|
||||
|
@ -185,9 +177,9 @@ class BlockchainInterface:
|
|||
return self.w3.isConnected()
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
def node_version(self) -> str:
|
||||
"""Return node version information"""
|
||||
return self.w3.version.node
|
||||
return self.w3.node_version.node
|
||||
|
||||
def add_provider(self,
|
||||
provider: Union[IPCProvider, WebsocketProvider, HTTPProvider] = None,
|
||||
|
@ -201,24 +193,27 @@ class BlockchainInterface:
|
|||
uri_breakdown = urlparse(provider_uri)
|
||||
|
||||
# PyEVM
|
||||
if uri_breakdown.scheme == 'pyevm':
|
||||
if uri_breakdown.scheme == 'tester':
|
||||
|
||||
if uri_breakdown.netloc == 'tester':
|
||||
|
||||
NUCYPHER_GAS_LIMIT = 5000000 # TODO: Move to constants
|
||||
genesis_parameter_overrides = {'gas_limit': NUCYPHER_GAS_LIMIT}
|
||||
if uri_breakdown.netloc == 'pyevm':
|
||||
|
||||
# TODO: Update to newest eth-tester after #123 is merged
|
||||
pyevm_backend = PyEVMBackend.from_genesis_overrides(parameter_overrides=genesis_parameter_overrides)
|
||||
|
||||
pyevm_backend = PyEVMBackend.from_genesis_overrides(parameter_overrides={'gas_limit': NUCYPHER_GAS_LIMIT})
|
||||
eth_tester = EthereumTester(backend=pyevm_backend, auto_mine_transactions=True)
|
||||
provider = EthereumTesterProvider(ethereum_tester=eth_tester)
|
||||
|
||||
elif uri_breakdown.netloc == 'trinity':
|
||||
raise NotImplemented
|
||||
elif uri_breakdown.netloc == 'geth':
|
||||
# TODO: Auto gethdev
|
||||
# https://web3py.readthedocs.io/en/stable/providers.html # geth-dev-proof-of-authority
|
||||
# from web3.auto.gethdev import w3
|
||||
|
||||
# Hardcoded gethdev IPC provider
|
||||
provider = IPCProvider(ipc_path='/tmp/geth.ipc', timeout=timeout)
|
||||
# w3 = Web3(providers=(provider))
|
||||
# w3.middleware_stack.inject(geth_poa_middleware, layer=0)
|
||||
|
||||
else:
|
||||
raise self.InterfaceError("{} is an ambiguous or unsupported blockchain provider URI".format(provider_uri))
|
||||
raise self.InterfaceError("{} is an invalid or unsupported blockchain provider URI".format(provider_uri))
|
||||
|
||||
# IPC
|
||||
elif uri_breakdown.scheme == 'ipc':
|
||||
|
@ -240,17 +235,20 @@ class BlockchainInterface:
|
|||
self.__providers = list()
|
||||
self.__providers.append(provider)
|
||||
|
||||
def get_contract_factory(self, contract_name) -> Contract:
|
||||
def get_contract_factory(self, contract_name: str) -> Contract:
|
||||
"""Retrieve compiled interface data from the cache and return web3 contract"""
|
||||
try:
|
||||
interface = self.__raw_contract_cache[contract_name]
|
||||
except KeyError:
|
||||
raise self.UnknownContract('{} is not a compiled contract.'.format(contract_name))
|
||||
|
||||
raise self.UnknownContract('{} is not a locally compiled contract.'.format(contract_name))
|
||||
except TypeError:
|
||||
if self.__raw_contract_cache is constants.NO_COMPILATION_PERFORMED:
|
||||
message = "The local contract compiler cache is empty because no compilation was performed."
|
||||
raise self.InterfaceError(message)
|
||||
else:
|
||||
contract = self.w3.eth.contract(abi=interface['abi'],
|
||||
bytecode=interface['bin'],
|
||||
ContractFactoryClass=Contract)
|
||||
|
||||
return contract
|
||||
|
||||
def _wrap_contract(self, dispatcher_contract: Contract,
|
||||
|
@ -266,7 +264,7 @@ class BlockchainInterface:
|
|||
def get_contract_by_address(self, address: str):
|
||||
"""Read a single contract's data from the registrar and return it."""
|
||||
try:
|
||||
contract_records = self._registry.search(contract_address=address)
|
||||
contract_records = self.registry.search(contract_address=address)
|
||||
except RuntimeError:
|
||||
raise self.InterfaceError('Corrupted Registrar') # TODO: Integrate with Registry
|
||||
else:
|
||||
|
@ -279,14 +277,14 @@ class BlockchainInterface:
|
|||
Instantiate a deployed contract from registrar data,
|
||||
and assemble it with it's dispatcher if it is upgradeable.
|
||||
"""
|
||||
target_contract_records = self._registry.search(contract_name=name)
|
||||
target_contract_records = self.registry.search(contract_name=name)
|
||||
|
||||
if not target_contract_records:
|
||||
raise self.InterfaceError("No such contract records with name {}".format(name))
|
||||
|
||||
if upgradeable:
|
||||
# Lookup dispatchers; Search fot a published dispatcher that targets this contract record
|
||||
dispatcher_records = self._registry.search(contract_name='Dispatcher')
|
||||
dispatcher_records = self.registry.search(contract_name='Dispatcher')
|
||||
|
||||
matching_pairs = list()
|
||||
for dispatcher_name, dispatcher_addr, dispatcher_abi in dispatcher_records:
|
||||
|
@ -339,7 +337,7 @@ class BlockchainInterface:
|
|||
signed_message = sig_key.sign_msg(message)
|
||||
return signed_message
|
||||
else:
|
||||
return self.w3.eth.sign(account, data=message) # Technically deprecated...
|
||||
return self.w3.eth.sign(account, data=message) # TODO: Technically deprecated...
|
||||
|
||||
def call_backend_verify(self, pubkey: PublicKey, signature: Signature, msg_hash: bytes):
|
||||
"""
|
||||
|
@ -352,15 +350,15 @@ class BlockchainInterface:
|
|||
return is_valid_sig and (sig_pubkey == pubkey)
|
||||
|
||||
def unlock_account(self, address, password, duration):
|
||||
if 'tester' in self.provider_uri:
|
||||
return True # Test accounts are unlocked by default.
|
||||
return self.w3.personal.unlockAccount(address, password, duration)
|
||||
|
||||
|
||||
class BlockchainDeployerInterface(BlockchainInterface):
|
||||
|
||||
def __init__(self, deployer_address: str=None, *args, **kwargs) -> None:
|
||||
|
||||
# Depends on web3 instance
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs) # Depends on web3 instance
|
||||
self.__deployer_address = deployer_address if deployer_address is not None else constants.NO_DEPLOYER_CONFIGURED
|
||||
|
||||
@property
|
||||
|
@ -368,43 +366,46 @@ class BlockchainDeployerInterface(BlockchainInterface):
|
|||
return self.__deployer_address
|
||||
|
||||
@deployer_address.setter
|
||||
def deployer_address(self, ether_address: str) -> None:
|
||||
def deployer_address(self, checksum_address: str) -> None:
|
||||
if self.deployer_address is not constants.NO_DEPLOYER_CONFIGURED:
|
||||
raise RuntimeError("{} already has a deployer address set.".format(self.__class__.__name__))
|
||||
self.__deployer_address = ether_address
|
||||
self.__deployer_address = checksum_address
|
||||
|
||||
def deploy_contract(self, contract_name: str, *args, **kwargs) -> Tuple[Contract, str]:
|
||||
"""
|
||||
Retrieve compiled interface data from the cache and
|
||||
return an instantiated deployed contract
|
||||
"""
|
||||
|
||||
if self.__deployer_address is constants.NO_DEPLOYER_CONFIGURED:
|
||||
raise self.InterfaceError('No deployer address is configured.')
|
||||
#
|
||||
# Build the deployment tx #
|
||||
#
|
||||
contract_factory = self.get_contract_factory(contract_name=contract_name)
|
||||
deploy_transaction = {'from': self.deployer_address, 'gasPrice': self.w3.eth.gasPrice} # TODO: price?
|
||||
deploy_bytecode = contract_factory.constructor(*args, **kwargs).buildTransaction(deploy_transaction)
|
||||
|
||||
# TODO: Logging
|
||||
contract_sizes = dict()
|
||||
if len(deploy_bytecode['data']) > 1000:
|
||||
contract_sizes[contract_name] = str(len(deploy_bytecode['data']))
|
||||
deploy_transaction = {'from': self.deployer_address, 'gasPrice': self.w3.eth.gasPrice}
|
||||
self.log.info("Deployer address is {}".format(deploy_transaction['from']))
|
||||
|
||||
contract_factory = self.get_contract_factory(contract_name=contract_name)
|
||||
deploy_bytecode = contract_factory.constructor(*args, **kwargs).buildTransaction(deploy_transaction)
|
||||
self.log.info("Deploying contract: {}: {} bytes".format(contract_name, len(deploy_bytecode['data'])))
|
||||
|
||||
#
|
||||
# Transmit the deployment tx #
|
||||
#
|
||||
txhash = contract_factory.constructor(*args, **kwargs).transact(transaction=deploy_transaction)
|
||||
self.log.info("{} Deployment TX sent : txhash {}".format(contract_name, txhash.hex()))
|
||||
|
||||
# Wait for receipt
|
||||
receipt = self.w3.eth.waitForTransactionReceipt(txhash)
|
||||
address = receipt['contractAddress']
|
||||
self.log.info("Confirmed {} deployment: address {}".format(contract_name, address))
|
||||
|
||||
#
|
||||
# Instantiate & enroll contract
|
||||
#
|
||||
contract = contract_factory(address=address)
|
||||
self._registry.enroll(contract_name=contract_name,
|
||||
|
||||
self.registry.enroll(contract_name=contract_name,
|
||||
contract_address=contract.address,
|
||||
contract_abi=contract_factory.abi)
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from json import JSONDecodeError
|
||||
from logging import getLogger
|
||||
|
||||
import shutil
|
||||
from constant_sorrow import constants
|
||||
|
@ -23,28 +25,25 @@ class EthereumContractRegistry:
|
|||
class RegistryError(Exception):
|
||||
pass
|
||||
|
||||
class EmptyRegistry(RegistryError):
|
||||
pass
|
||||
|
||||
class UnknownContract(RegistryError):
|
||||
pass
|
||||
|
||||
class IllegalRegistrar(RegistryError):
|
||||
class IllegalRegistry(RegistryError):
|
||||
"""Raised when invalid data is encountered in the registry"""
|
||||
|
||||
def __init__(self, registry_filepath: str=None) -> None:
|
||||
self.__registry_filepath = registry_filepath or self.__default_registry_path
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config) -> Union['EthereumContractRegistry', 'TemporaryEthereumContractRegistry']:
|
||||
if config.temp_registry is True: # In memory only
|
||||
return TemporaryEthereumContractRegistry()
|
||||
else:
|
||||
return EthereumContractRegistry()
|
||||
def __init__(self, registry_filepath: str = __default_registry_path) -> None:
|
||||
self.log = getLogger("registry")
|
||||
self.__filepath = registry_filepath
|
||||
|
||||
@property
|
||||
def registry_filepath(self):
|
||||
return self.__registry_filepath
|
||||
def filepath(self):
|
||||
return self.__filepath
|
||||
|
||||
def _swap_registry(self, filepath: str) -> bool:
|
||||
self.__registry_filepath = filepath
|
||||
self.__filepath = filepath
|
||||
return True
|
||||
|
||||
def __write(self, registry_data: list) -> None:
|
||||
|
@ -53,7 +52,7 @@ class EthereumContractRegistry:
|
|||
file exists, it will create it and write the data. If a file does exist
|
||||
it will _overwrite_ everything in it.
|
||||
"""
|
||||
with open(self.__registry_filepath, 'w+') as registry_file:
|
||||
with open(self.__filepath, 'w+') as registry_file:
|
||||
registry_file.seek(0)
|
||||
registry_file.write(json.dumps(registry_data))
|
||||
registry_file.truncate()
|
||||
|
@ -61,23 +60,27 @@ class EthereumContractRegistry:
|
|||
def read(self) -> list:
|
||||
"""
|
||||
Reads the registry file and parses the JSON and returns a list.
|
||||
If the file is empty or the JSON is corrupt, it will return an empty
|
||||
list.
|
||||
If the file is empty it will return an empty list.
|
||||
If you are modifying or updating the registry file, you _must_ call
|
||||
this function first to get the current state to append to the dict or
|
||||
modify it because _write_registry_file overwrites the file.
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(self.__registry_filepath, 'r') as registry_file:
|
||||
with open(self.__filepath, 'r') as registry_file:
|
||||
self.log.debug("Reading from registrar: filepath {}".format(self.__filepath))
|
||||
registry_file.seek(0)
|
||||
file_data = registry_file.read()
|
||||
if file_data:
|
||||
registry_data = json.loads(file_data)
|
||||
else:
|
||||
registry_data = list() # Existing, but empty registry
|
||||
registry_data = list()
|
||||
|
||||
except FileNotFoundError:
|
||||
raise self.RegistryError("No registy at filepath: {}".format(self.__registry_filepath))
|
||||
raise self.RegistryError("No registry at filepath: {}".format(self.__filepath))
|
||||
|
||||
except JSONDecodeError:
|
||||
raise
|
||||
|
||||
return registry_data
|
||||
|
||||
|
@ -90,9 +93,15 @@ class EthereumContractRegistry:
|
|||
need to use this.
|
||||
"""
|
||||
contract_data = [contract_name, contract_address, contract_abi]
|
||||
try:
|
||||
registry_data = self.read()
|
||||
except self.RegistryError:
|
||||
self.log.info("Blank registry encountered: enrolling {}:{}".format(contract_name, contract_address))
|
||||
registry_data = list() # empty registry
|
||||
|
||||
registry_data.append(contract_data)
|
||||
self.__write(registry_data)
|
||||
self.log.info("Enrolled {}:{} into registry {}".format(contract_name, contract_address, self.filepath))
|
||||
|
||||
def search(self, contract_name: str=None, contract_address: str=None):
|
||||
"""
|
||||
|
@ -105,15 +114,22 @@ class EthereumContractRegistry:
|
|||
contracts = list()
|
||||
registry_data = self.read()
|
||||
|
||||
try:
|
||||
for name, addr, abi in registry_data:
|
||||
if contract_name == name or contract_address == addr:
|
||||
contracts.append((name, addr, abi))
|
||||
except ValueError:
|
||||
message = "Missing or corrupted registry data".format(self.__filepath)
|
||||
self.log.critical(message)
|
||||
raise self.IllegalRegistry(message)
|
||||
|
||||
if not contracts:
|
||||
raise self.UnknownContract
|
||||
|
||||
if contract_address and len(contracts) > 1:
|
||||
m = "Multiple records returned for address {}"
|
||||
raise self.IllegalRegistrar(m.format(contract_address))
|
||||
self.log.critical(m)
|
||||
raise self.IllegalRegistry(m.format(contract_address))
|
||||
|
||||
return contracts if contract_name else contracts[0]
|
||||
|
||||
|
@ -125,19 +141,23 @@ class TemporaryEthereumContractRegistry(EthereumContractRegistry):
|
|||
super().__init__(registry_filepath=self.temp_filepath)
|
||||
|
||||
def clear(self):
|
||||
with open(self.registry_filepath, 'w') as registry_file:
|
||||
self.log.info("Cleared temporary registry at {}".format(self.filepath))
|
||||
with open(self.filepath, 'w') as registry_file:
|
||||
registry_file.write('')
|
||||
|
||||
def reset(self):
|
||||
def cleanup(self):
|
||||
os.remove(self.temp_filepath) # remove registrar tempfile
|
||||
|
||||
def commit(self, filepath) -> str:
|
||||
"""writes the current state of the registry to a file"""
|
||||
self.log.info("Committing temporary registry to {}".format(filepath))
|
||||
self._swap_registry(filepath) # I'll allow it
|
||||
|
||||
if os.path.exists(filepath):
|
||||
self.log.debug("Removing registry {}".format(filepath))
|
||||
self.clear() # clear prior sim runs
|
||||
|
||||
_ = shutil.copy(self.temp_filepath, filepath)
|
||||
self.temp_filepath = constants.REGISTRY_COMMITED # just in case
|
||||
self.log.info("Wrote temporary registry to filesystem {}".format(filepath))
|
||||
return filepath
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
from logging import getLogger
|
||||
from os.path import abspath, dirname
|
||||
|
||||
import itertools
|
||||
|
@ -26,13 +27,14 @@ class SolidityCompiler:
|
|||
solc_binary_path: str = None,
|
||||
configuration_path: str = None,
|
||||
chain_name: str = None,
|
||||
contract_dir: str = None,
|
||||
source_dir: str = None,
|
||||
test_contract_dir: str= None
|
||||
) -> None:
|
||||
|
||||
self.log = getLogger('solidity-compiler')
|
||||
# Compiler binary and root solidity source code directory
|
||||
self.__sol_binary_path = solc_binary_path if solc_binary_path is not None else self.__default_sol_binary_path
|
||||
self._solidity_source_dir = contract_dir if contract_dir is not None else self.__default_contract_dir
|
||||
self.source_dir = source_dir if source_dir is not None else self.__default_contract_dir
|
||||
self._test_solidity_source_dir = test_contract_dir
|
||||
|
||||
# JSON config
|
||||
|
@ -53,8 +55,11 @@ class SolidityCompiler:
|
|||
def compile(self) -> dict:
|
||||
"""Executes the compiler with parameters specified in the json config"""
|
||||
|
||||
self.log.info("Using solidity compiler binary at {}".format(self.__sol_binary_path))
|
||||
self.log.info("Compiling solidity source files at {}".format(self.source_dir))
|
||||
|
||||
source_paths = set()
|
||||
source_walker = os.walk(top=self._solidity_source_dir, topdown=True)
|
||||
source_walker = os.walk(top=self.source_dir, topdown=True)
|
||||
if self._test_solidity_source_dir:
|
||||
test_source_walker = os.walk(top=self._test_solidity_source_dir, topdown=True)
|
||||
source_walker = itertools.chain(source_walker, test_source_walker)
|
||||
|
@ -62,19 +67,29 @@ class SolidityCompiler:
|
|||
for root, dirs, files in source_walker:
|
||||
for filename in files:
|
||||
if filename.endswith('.sol'):
|
||||
source_paths.add(os.path.join(root, filename))
|
||||
path = os.path.join(root, filename)
|
||||
source_paths.add(path)
|
||||
self.log.debug("Collecting solidity source {}".format(path))
|
||||
|
||||
# Compile with remappings: https://github.com/ethereum/py-solc
|
||||
project_root = dirname(self._solidity_source_dir)
|
||||
project_root = dirname(self.source_dir)
|
||||
|
||||
remappings = ("contracts={}".format(self._solidity_source_dir),
|
||||
remappings = ("contracts={}".format(self.source_dir),
|
||||
"zeppelin={}".format(os.path.join(project_root, 'zeppelin')),
|
||||
)
|
||||
|
||||
self.log.info("Compiling with import remappings {}".format(", ".join(remappings)))
|
||||
|
||||
optimization_runs = 10 # TODO: Move..?
|
||||
try:
|
||||
compiled_sol = compile_files(source_files=source_paths,
|
||||
import_remappings=remappings,
|
||||
allow_paths=project_root,
|
||||
optimize=10)
|
||||
optimize=optimization_runs)
|
||||
|
||||
self.log.info("Successfully compiled {} contracts with {} optimization runs".format(len(compiled_sol),
|
||||
optimization_runs))
|
||||
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError("The solidity compiler is not at the specified path. "
|
||||
"Check that the file exists and is executable.")
|
||||
|
|
|
@ -6,11 +6,6 @@ from nucypher.blockchain.eth.constants import (MIN_ALLOWED_LOCKED,
|
|||
MAX_MINTING_PERIODS,
|
||||
SECONDS_PER_PERIOD)
|
||||
|
||||
|
||||
class PolicyConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def __validate(rulebook) -> bool:
|
||||
for rule, failure_message in rulebook:
|
||||
if not rule:
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import os
|
||||
import random
|
||||
from abc import abstractmethod, ABC
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from logging import Logger
|
||||
from logging import getLogger
|
||||
from typing import Dict, ClassVar, Set
|
||||
from typing import Tuple
|
||||
from typing import Union, List
|
||||
|
||||
import maya
|
||||
import requests
|
||||
import time
|
||||
from constant_sorrow import constants, default_constant_splitter
|
||||
from eth_keys import KeyAPI as EthKeyAPI
|
||||
from eth_utils import to_checksum_address, to_canonical_address
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet import task
|
||||
from typing import Dict, ClassVar, Set
|
||||
from typing import Tuple
|
||||
from typing import Union, List
|
||||
from umbral.keys import UmbralPublicKey
|
||||
from umbral.signing import Signature
|
||||
|
||||
|
@ -30,7 +29,7 @@ from nucypher.network.nodes import VerifiableNode
|
|||
from nucypher.network.server import TLSHostingPower
|
||||
|
||||
|
||||
class Learner(ABC):
|
||||
class Learner:
|
||||
"""
|
||||
Any participant in the "learning loop" - a class inheriting from
|
||||
this one has the ability, synchronously or asynchronously,
|
||||
|
@ -45,16 +44,24 @@ class Learner(ABC):
|
|||
class NotEnoughTeachers(RuntimeError):
|
||||
pass
|
||||
|
||||
class UnresponsiveTeacher(ConnectionError):
|
||||
pass
|
||||
|
||||
def __init__(self,
|
||||
common_name: str,
|
||||
network_middleware: RestMiddleware = RestMiddleware(),
|
||||
start_learning_now: bool = False,
|
||||
learn_on_same_thread: bool = False,
|
||||
known_nodes: tuple = None,
|
||||
known_certificates_dir: str = None,
|
||||
known_metadata_dir: str = None,
|
||||
save_metadata: bool = False,
|
||||
abort_on_learning_error: bool = False) -> None:
|
||||
|
||||
self.log = getLogger("characters") # type: Logger
|
||||
|
||||
self.__common_name = common_name
|
||||
self.network_middleware = network_middleware
|
||||
self.save_metadata = save_metadata
|
||||
self.start_learning_now = start_learning_now
|
||||
self.learn_on_same_thread = learn_on_same_thread
|
||||
|
@ -63,6 +70,7 @@ class Learner(ABC):
|
|||
self._learning_listeners = defaultdict(list)
|
||||
self._node_ids_to_learn_about_immediately = set()
|
||||
|
||||
self.known_certificates_dir = known_certificates_dir
|
||||
self.__known_nodes = dict()
|
||||
|
||||
# Read
|
||||
|
@ -71,8 +79,12 @@ class Learner(ABC):
|
|||
raise ValueError("Cannot save nodes without a known_metadata_dir")
|
||||
|
||||
known_nodes = known_nodes or tuple()
|
||||
self.unresponsive_nodes = list() # TODO: Attempt to use these again later
|
||||
for node in known_nodes:
|
||||
try:
|
||||
self.remember_node(node)
|
||||
except self.UnresponsiveTeacher:
|
||||
self.unresponsive_nodes.append(node)
|
||||
|
||||
self.teacher_nodes = deque()
|
||||
self._current_teacher_node = None # type: Teacher
|
||||
|
@ -93,14 +105,21 @@ class Learner(ABC):
|
|||
with suppress(KeyError):
|
||||
already_known_node = self.known_nodes[node.checksum_public_address]
|
||||
if not node.timestamp > already_known_node.timestamp:
|
||||
self.log.debug("Skipping already known node {}".format(already_known_node))
|
||||
# This node is already known. We can safely return.
|
||||
return
|
||||
|
||||
node.verify_node(self.network_middleware, # TODO: Take middleware directly in this class?
|
||||
certificate_filepath = node.save_certificate_to_disk(directory=self.known_certificates_dir)
|
||||
try:
|
||||
node.verify_node(self.network_middleware,
|
||||
force=force_verification_check,
|
||||
accept_federated_only=self.federated_only) # TODO: 466
|
||||
accept_federated_only=self.federated_only, # TODO: 466
|
||||
certificate_filepath=certificate_filepath)
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.log.info("No Response from known node {}|{}".format(node.rest_interface, node.checksum_public_address))
|
||||
raise self.UnresponsiveTeacher
|
||||
|
||||
listeners = self._learning_listeners.pop(node.checksum_public_address, ())
|
||||
listeners = self._learning_listeners.pop(node.checksum_public_address, tuple())
|
||||
address = node.checksum_public_address
|
||||
|
||||
self.__known_nodes[address] = node
|
||||
|
@ -136,10 +155,12 @@ class Learner(ABC):
|
|||
"""
|
||||
self._crashed = failure
|
||||
failure.raiseException()
|
||||
self.log.critical("{} crashed with {}".format(self.__common_name, failure))
|
||||
|
||||
def shuffled_known_nodes(self):
|
||||
nodes_we_know_about = list(self.__known_nodes.values())
|
||||
random.shuffle(nodes_we_know_about)
|
||||
self.log.info("Shuffled {} known nodes".format(len(nodes_we_know_about)))
|
||||
return nodes_we_know_about
|
||||
|
||||
def select_teacher_nodes(self):
|
||||
|
@ -158,6 +179,7 @@ class Learner(ABC):
|
|||
except IndexError:
|
||||
error = "Not enough nodes to select a good teacher, Check your network connection then node configuration"
|
||||
raise self.NotEnoughTeachers(error)
|
||||
self.log.info("Cycled teachers; New teacher is {}".format(self._current_teacher_node.checksum_public_address))
|
||||
|
||||
def current_teacher_node(self, cycle=False):
|
||||
if not self._current_teacher_node:
|
||||
|
@ -176,7 +198,7 @@ class Learner(ABC):
|
|||
self._learning_task()
|
||||
elif not force:
|
||||
self.log.warning(
|
||||
"Learning loop isn't started; can't learn about nodes now. You can ovverride this with force=True.")
|
||||
"Learning loop isn't started; can't learn about nodes now. You can override this with force=True.")
|
||||
elif force:
|
||||
self.log.info("Learning loop wasn't started; forcing start now.")
|
||||
self._learning_task.start(self._SHORT_LEARNING_DELAY, now=True)
|
||||
|
@ -214,8 +236,7 @@ class Learner(ABC):
|
|||
|
||||
if (maya.now() - start).seconds > timeout:
|
||||
if not self._learning_task.running:
|
||||
raise self.NotEnoughTeachers(
|
||||
"We didn't discover any nodes because the learning loop isn't running. Start it with start_learning().")
|
||||
raise self.NotEnoughTeachers("Learning loop is not running. Start it with start_learning().")
|
||||
else:
|
||||
raise self.NotEnoughTeachers("After {} seconds and {} rounds, didn't find {} nodes".format(
|
||||
timeout, rounds_undertaken, number_of_nodes_to_know))
|
||||
|
@ -305,19 +326,104 @@ class Learner(ABC):
|
|||
def write_node_metadata(self, node, serializer=bytes) -> str:
|
||||
|
||||
try:
|
||||
filename = "{}.node".format(node.checksum_public_address) # TODO: Use common name
|
||||
filename = "{}.node".format(node.checksum_public_address)
|
||||
except AttributeError:
|
||||
raise AttributeError("{} does not have a rest_interface attached".format(self))
|
||||
|
||||
metadata_filepath = os.path.join(self.known_metadata_dir, filename)
|
||||
|
||||
with open(metadata_filepath, "w") as f:
|
||||
f.write(serializer(node).hex())
|
||||
self.log.info("Wrote new node metadata {}".format(metadata_filepath))
|
||||
return metadata_filepath
|
||||
|
||||
@abstractmethod
|
||||
def learn_from_teacher_node(self, eager: bool = True):
|
||||
raise NotImplementedError
|
||||
def learn_from_teacher_node(self, eager=True):
|
||||
"""
|
||||
Sends a request to node_url to find out about known nodes.
|
||||
"""
|
||||
self._learning_round += 1
|
||||
|
||||
try:
|
||||
current_teacher = self.current_teacher_node()
|
||||
except self.NotEnoughTeachers as e:
|
||||
self.log.warning("Can't learn right now: {}".format(e.args[0]))
|
||||
return
|
||||
|
||||
rest_url = current_teacher.rest_interface # TODO: Name this..?
|
||||
|
||||
# TODO: Do we really want to try to learn about all these nodes instantly?
|
||||
# Hearing this traffic might give insight to an attacker.
|
||||
if VerifiableNode in self.__class__.__bases__:
|
||||
announce_nodes = [self]
|
||||
else:
|
||||
announce_nodes = None
|
||||
|
||||
unresponsive_nodes = set()
|
||||
try:
|
||||
|
||||
# TODO: Streamline path generation
|
||||
certificate_filepath = current_teacher.get_certificate_filepath(certificates_dir=self.known_certificates_dir)
|
||||
response = self.network_middleware.get_nodes_via_rest(url=rest_url,
|
||||
nodes_i_need=self._node_ids_to_learn_about_immediately,
|
||||
announce_nodes=announce_nodes,
|
||||
certificate_filepath=certificate_filepath)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
unresponsive_nodes.add(current_teacher)
|
||||
teacher_rest_info = current_teacher.rest_information()[0]
|
||||
|
||||
# TODO: This error isn't necessarily "no repsonse" - let's maybe pass on the text of the exception here.
|
||||
self.log.info("No Response from teacher: {}:{}.".format(teacher_rest_info.host, teacher_rest_info.port))
|
||||
self.cycle_teacher_node()
|
||||
return
|
||||
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError("Bad response from teacher: {} - {}".format(response, response.content))
|
||||
|
||||
signature, nodes = signature_splitter(response.content, return_remainder=True)
|
||||
|
||||
# TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node.
|
||||
from nucypher.characters.lawful import Ursula
|
||||
node_list = Ursula.batch_from_bytes(nodes, federated_only=self.federated_only) # TODO: 466
|
||||
|
||||
new_nodes = []
|
||||
for node in node_list:
|
||||
|
||||
if node.checksum_public_address in self.known_nodes or node.checksum_public_address == self.__common_name:
|
||||
continue # TODO: 168 Check version and update if required.
|
||||
|
||||
try:
|
||||
if eager:
|
||||
certificate_filepath = current_teacher.get_certificate_filepath(certificates_dir=certificate_filepath)
|
||||
node.verify_node(self.network_middleware,
|
||||
accept_federated_only=self.federated_only, # TODO: 466
|
||||
certificate_filepath=certificate_filepath)
|
||||
self.log.debug("Verified node: {}".format(node.checksum_public_address))
|
||||
|
||||
else:
|
||||
node.validate_metadata(accept_federated_only=self.federated_only) # TODO: 466
|
||||
|
||||
except node.SuspiciousActivity:
|
||||
# TODO: Account for possibility that stamp, rather than interface, was bad.
|
||||
message = "Suspicious Activity: Discovered node with bad signature: {}. " \
|
||||
"Propagated by: {}".format(current_teacher.checksum_public_address, rest_url)
|
||||
self.log.warning(message)
|
||||
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
|
||||
|
||||
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
|
||||
self.remember_node(node)
|
||||
new_nodes.append(node)
|
||||
|
||||
self._adjust_learning(new_nodes)
|
||||
|
||||
learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new."
|
||||
self.log.info(learning_round_log_message.format(self._learning_round,
|
||||
current_teacher.checksum_public_address,
|
||||
len(node_list),
|
||||
len(new_nodes)), )
|
||||
if new_nodes and self.known_certificates_dir:
|
||||
for node in new_nodes:
|
||||
node.save_certificate_to_disk(self.known_certificates_dir)
|
||||
|
||||
return new_nodes
|
||||
|
||||
|
||||
class Character(Learner):
|
||||
|
@ -339,7 +445,6 @@ class Character(Learner):
|
|||
def __init__(self,
|
||||
is_me: bool = True,
|
||||
network_middleware: RestMiddleware = None,
|
||||
known_certificates_dir: str = None,
|
||||
crypto_power: CryptoPower = None,
|
||||
crypto_power_ups: List[CryptoPowerUp] = None,
|
||||
federated_only: bool = False,
|
||||
|
@ -371,7 +476,6 @@ class Character(Learner):
|
|||
|
||||
"""
|
||||
self.federated_only = federated_only # type: bool
|
||||
self.known_certificates_dir = known_certificates_dir
|
||||
|
||||
#
|
||||
# Power-ups and Powers
|
||||
|
@ -403,15 +507,17 @@ class Character(Learner):
|
|||
except NoSigningPower:
|
||||
self._stamp = constants.NO_SIGNING_POWER
|
||||
|
||||
Learner.__init__(self,
|
||||
common_name=checksum_address,
|
||||
network_middleware=network_middleware,
|
||||
*args, **kwargs)
|
||||
|
||||
else: # Feel like a stranger
|
||||
if network_middleware is not None:
|
||||
raise TypeError(
|
||||
"Can't attach network middleware to a Character who isn't me. What are you even trying to do?")
|
||||
self._stamp = StrangerStamp(self.public_keys(SigningPower))
|
||||
|
||||
# Init the Learner superclass.
|
||||
Learner.__init__(self, *args, **kwargs)
|
||||
|
||||
# Decentralized
|
||||
if not federated_only:
|
||||
if not checksum_address:
|
||||
|
@ -467,10 +573,6 @@ class Character(Learner):
|
|||
def canonical_public_address(self, address_bytes):
|
||||
self._checksum_address = to_checksum_address(address_bytes)
|
||||
|
||||
@property
|
||||
def ether_address(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def checksum_public_address(self):
|
||||
if self._checksum_address is constants.NO_BLOCKCHAIN_CONNECTION:
|
||||
|
@ -483,7 +585,6 @@ class Character(Learner):
|
|||
|
||||
@classmethod
|
||||
def from_public_keys(cls, powers_and_material: Dict, federated_only=True, *args, **kwargs) -> 'Character':
|
||||
# TODO: Need to be federated only until we figure out the best way to get the checksum_address in here.
|
||||
"""
|
||||
Sometimes we discover a Character and, at the same moment,
|
||||
learn the public parts of more of their powers. Here, we take a Dict
|
||||
|
@ -493,7 +594,11 @@ class Character(Learner):
|
|||
Each item in the collection will have the CryptoPowerUp instantiated
|
||||
with the public_material_bytes, and the resulting CryptoPowerUp instance
|
||||
consumed by the Character.
|
||||
|
||||
# TODO: Need to be federated only until we figure out the best way to get the checksum_address in here.
|
||||
|
||||
"""
|
||||
|
||||
crypto_power = CryptoPower()
|
||||
|
||||
for power_up, public_key in powers_and_material.items():
|
||||
|
@ -506,89 +611,6 @@ class Character(Learner):
|
|||
|
||||
return cls(is_me=False, federated_only=federated_only, crypto_power=crypto_power, *args, **kwargs)
|
||||
|
||||
def learn_from_teacher_node(self, eager=True):
|
||||
"""
|
||||
Sends a request to node_url to find out about known nodes.
|
||||
"""
|
||||
self._learning_round += 1
|
||||
|
||||
try:
|
||||
current_teacher = self.current_teacher_node()
|
||||
except self.NotEnoughTeachers as e:
|
||||
self.log.warning("Can't learn right now: {}".format(e.args[0]))
|
||||
return
|
||||
|
||||
rest_url = current_teacher.rest_interface # TODO: Name this..?
|
||||
|
||||
# TODO: Do we really want to try to learn about all these nodes instantly?
|
||||
# Hearing this traffic might give insight to an attacker.
|
||||
if VerifiableNode in self.__class__.__bases__:
|
||||
announce_nodes = [self]
|
||||
else:
|
||||
announce_nodes = None
|
||||
|
||||
unresponsive_nodes = set()
|
||||
try:
|
||||
|
||||
# TODO: Streamline path generation
|
||||
certificate_filepath = os.path.join(self.known_certificates_dir, current_teacher.certificate_filename)
|
||||
response = self.network_middleware.get_nodes_via_rest(url=rest_url,
|
||||
nodes_i_need=self._node_ids_to_learn_about_immediately,
|
||||
announce_nodes=announce_nodes,
|
||||
certificate_filepath=certificate_filepath)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
unresponsive_nodes.add(current_teacher)
|
||||
teacher_rest_info = current_teacher.rest_information()[0]
|
||||
|
||||
# TODO: This error isn't necessarily "no repsonse" - let's maybe pass on the text of the exception here.
|
||||
self.log.info("No Response from teacher: {}:{}.".format(teacher_rest_info.host, teacher_rest_info.port))
|
||||
self.cycle_teacher_node()
|
||||
return
|
||||
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError("Bad response from teacher: {} - {}".format(response, response.content))
|
||||
|
||||
signature, nodes = signature_splitter(response.content, return_remainder=True)
|
||||
|
||||
# TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node.
|
||||
from nucypher.characters.lawful import Ursula
|
||||
node_list = Ursula.batch_from_bytes(nodes, federated_only=self.federated_only) # TODO: 466
|
||||
|
||||
new_nodes = []
|
||||
for node in node_list:
|
||||
|
||||
if node.checksum_public_address in self.known_nodes or node.checksum_public_address == self.checksum_public_address:
|
||||
continue # TODO: 168 Check version and update if required.
|
||||
|
||||
try:
|
||||
if eager:
|
||||
node.verify_node(self.network_middleware, accept_federated_only=self.federated_only)
|
||||
else:
|
||||
node.validate_metadata(accept_federated_only=self.federated_only) # TODO: 466
|
||||
except node.SuspiciousActivity:
|
||||
# TODO: Account for possibility that stamp, rather than interface, was bad.
|
||||
message = "Suspicious Activity: Discovered node with bad signature: {}. " \
|
||||
"Propagated by: {}".format(current_teacher.checksum_public_address, rest_url)
|
||||
self.log.warning(message)
|
||||
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
|
||||
|
||||
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
|
||||
self.remember_node(node)
|
||||
new_nodes.append(node)
|
||||
|
||||
self._adjust_learning(new_nodes)
|
||||
|
||||
learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new."
|
||||
self.log.info(learning_round_log_message.format(self._learning_round,
|
||||
current_teacher.checksum_public_address,
|
||||
len(node_list),
|
||||
len(new_nodes)), )
|
||||
if new_nodes and self.known_certificates_dir:
|
||||
for node in new_nodes:
|
||||
node.save_certificate_to_disk(self.known_certificates_dir)
|
||||
|
||||
return new_nodes
|
||||
|
||||
def encrypt_for(self,
|
||||
recipient: 'Character',
|
||||
plaintext: bytes,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import binascii
|
||||
import random
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
|
||||
import maya
|
||||
import time
|
||||
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
||||
from constant_sorrow import constants
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
@ -11,16 +13,12 @@ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
|
|||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.x509 import load_pem_x509_certificate, Certificate
|
||||
from eth_utils import to_checksum_address
|
||||
from functools import partial
|
||||
from twisted.internet import threads
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from umbral.keys import UmbralPublicKey
|
||||
from umbral.signing import Signature
|
||||
|
||||
from nucypher.blockchain.eth.actors import PolicyAuthor, Miner, only_me
|
||||
from nucypher.blockchain.eth.actors import PolicyAuthor, Miner
|
||||
from nucypher.blockchain.eth.agents import MinerAgent
|
||||
from nucypher.blockchain.eth.utils import datetime_to_period
|
||||
from nucypher.characters.base import Character, Learner
|
||||
from nucypher.crypto.api import keccak_digest
|
||||
from nucypher.crypto.constants import PUBLIC_ADDRESS_LENGTH, PUBLIC_KEY_LENGTH
|
||||
|
@ -408,9 +406,10 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
# Blockchain
|
||||
miner_agent=None,
|
||||
checksum_address: str = None,
|
||||
registry_filepath: str = None,
|
||||
# registry_filepath: str = None,
|
||||
|
||||
# Character
|
||||
passphrase: str = None,
|
||||
abort_on_learning_error: bool = False,
|
||||
federated_only: bool = False,
|
||||
start_learning_now: bool = None,
|
||||
|
@ -434,12 +433,11 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
known_nodes=known_nodes,
|
||||
**character_kwargs)
|
||||
|
||||
if not federated_only:
|
||||
if is_me is True and not federated_only:
|
||||
Miner.__init__(self,
|
||||
is_me=is_me,
|
||||
miner_agent=miner_agent,
|
||||
checksum_address=checksum_address,
|
||||
registry_filepath=registry_filepath)
|
||||
checksum_address=checksum_address)
|
||||
|
||||
blockchain_power = BlockchainPower(blockchain=self.blockchain, account=self.checksum_public_address)
|
||||
self._crypto_power.consume_power_up(blockchain_power)
|
||||
|
@ -448,7 +446,9 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
# TODO: 340
|
||||
self._stored_treasure_maps = {}
|
||||
if not federated_only:
|
||||
self.substantiate_stamp()
|
||||
# if passphrase is None:
|
||||
# raise self.ActorError("No passphrase supplied to unlock account")
|
||||
self.substantiate_stamp(passphrase=passphrase)
|
||||
|
||||
if not crypto_power or (TLSHostingPower not in crypto_power._power_ups):
|
||||
# TODO: Maybe we want _power_ups to be public after all?
|
||||
|
@ -482,12 +482,10 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
self.datastore = rest_routes.datastore # TODO: Maybe organize this better?
|
||||
|
||||
tls_hosting_keypair = HostingKeypair(
|
||||
common_name=self.checksum_public_address,
|
||||
private_key=tls_private_key,
|
||||
curve=tls_curve,
|
||||
host=rest_host,
|
||||
certificate=certificate,
|
||||
certificate_dir=self.known_certificates_dir)
|
||||
certificate=certificate)
|
||||
|
||||
tls_hosting_power = TLSHostingPower(rest_server=rest_server,
|
||||
keypair=tls_hosting_keypair)
|
||||
|
@ -498,19 +496,15 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
rest_host=rest_host,
|
||||
rest_port=rest_port
|
||||
)
|
||||
if certificate or certificate_filepath:
|
||||
if certificate or certificate_filepath: # existing certificate
|
||||
tls_hosting_power = TLSHostingPower(rest_server=rest_server,
|
||||
certificate_filepath=certificate_filepath,
|
||||
certificate=certificate,
|
||||
certificate_dir=self.known_certificates_dir,
|
||||
common_name=self.checksum_public_address,)
|
||||
certificate=certificate)
|
||||
else:
|
||||
tls_hosting_keypair = HostingKeypair(
|
||||
common_name=self.checksum_public_address,
|
||||
curve=tls_curve,
|
||||
host=rest_host,
|
||||
certificate_filepath=certificate_filepath,
|
||||
certificate_dir=self.known_certificates_dir)
|
||||
certificate_filepath=certificate_filepath)
|
||||
|
||||
tls_hosting_power = TLSHostingPower(rest_server=rest_server,
|
||||
keypair=tls_hosting_keypair)
|
||||
|
@ -583,7 +577,7 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
port: int,
|
||||
federated_only: bool = False) -> 'Ursula':
|
||||
|
||||
response = network_middleware.node_information(host, port)
|
||||
response = network_middleware.node_information(host, port) # TODO
|
||||
if not response.status_code == 200:
|
||||
raise RuntimeError("Got a bad response: {}".format(response))
|
||||
|
||||
|
@ -658,11 +652,11 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
return stranger_ursulas
|
||||
|
||||
@classmethod
|
||||
def from_metadata_file(cls, filepath: str, federated_only: bool) -> 'Ursula':
|
||||
def from_metadata_file(cls, filepath: str, federated_only: bool, *args, **kwargs) -> 'Ursula':
|
||||
with open(filepath, "r") as seed_file:
|
||||
seed_file.seek(0)
|
||||
node_bytes = binascii.unhexlify(seed_file.read())
|
||||
node = Ursula.from_bytes(node_bytes, federated_only=federated_only)
|
||||
node = Ursula.from_bytes(node_bytes, federated_only=federated_only, *args, **kwargs)
|
||||
return node
|
||||
|
||||
#
|
||||
|
@ -699,74 +693,3 @@ class Ursula(Character, VerifiableNode, Miner):
|
|||
if work_order.bob == bob:
|
||||
work_orders_from_bob.append(work_order)
|
||||
return work_orders_from_bob
|
||||
|
||||
@only_me
|
||||
def stake(self,
|
||||
sample_rate: int = 10,
|
||||
refresh_rate: int = 60,
|
||||
confirm_now=True,
|
||||
resume: bool = False,
|
||||
expiration: maya.MayaDT = None,
|
||||
lock_periods: int = None,
|
||||
*args, **kwargs) -> None:
|
||||
|
||||
"""High-level staking daemon loop"""
|
||||
|
||||
if lock_periods and expiration:
|
||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||
if expiration:
|
||||
lock_periods = datetime_to_period(expiration)
|
||||
|
||||
if resume is False:
|
||||
_staking_receipts = super().initialize_stake(expiration=expiration,
|
||||
lock_periods=lock_periods,
|
||||
*args, **kwargs)
|
||||
|
||||
# TODO: Check if this period has already been confirmed
|
||||
# TODO: Check if there is an active stake in the current period: Resume staking daemon
|
||||
# TODO: Validation and Sanity checks
|
||||
|
||||
if confirm_now:
|
||||
self.confirm_activity()
|
||||
|
||||
# record start time and periods
|
||||
start_time = maya.now()
|
||||
uptime_period = self.miner_agent.get_current_period()
|
||||
terminal_period = uptime_period + lock_periods
|
||||
current_period = uptime_period
|
||||
|
||||
#
|
||||
# Daemon
|
||||
#
|
||||
|
||||
try:
|
||||
while True:
|
||||
|
||||
# calculate timedeltas
|
||||
now = maya.now()
|
||||
initialization_delta = now - start_time
|
||||
|
||||
# check if iteration re-samples
|
||||
sample_stale = initialization_delta.seconds > (refresh_rate - 1)
|
||||
if sample_stale:
|
||||
|
||||
period = self.miner_agent.get_current_period()
|
||||
# check for stale sample data
|
||||
if current_period != period:
|
||||
|
||||
# check for stake expiration
|
||||
stake_expired = current_period >= terminal_period
|
||||
if stake_expired:
|
||||
break
|
||||
|
||||
self.confirm_activity()
|
||||
current_period = period
|
||||
# wait before resampling
|
||||
time.sleep(sample_rate)
|
||||
continue
|
||||
|
||||
finally:
|
||||
|
||||
# TODO: Cleanup #
|
||||
|
||||
pass
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import maya
|
||||
from eth_tester.exceptions import ValidationError
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
@ -17,23 +16,32 @@ class Vladimir(Ursula):
|
|||
fraud_key = 'a75d701cc4199f7646909d15f22e2e0ef6094b3e2aa47a188f35f47e8932a7b9'
|
||||
|
||||
@classmethod
|
||||
def from_target_ursula(cls, target_ursula, claim_signing_key=False):
|
||||
def from_target_ursula(cls,
|
||||
target_ursula: Ursula,
|
||||
claim_signing_key: bool = False,
|
||||
attach_transacting_key: bool = True
|
||||
) -> 'Vladimir':
|
||||
"""
|
||||
Sometimes Vladimir seeks to attack or imitate a *specific* target Ursula.
|
||||
|
||||
TODO: This is probably a more instructive method if it takes a bytes representation instead of the entire Ursula.
|
||||
"""
|
||||
crypto_power = CryptoPower(power_ups=Ursula._default_crypto_powerups)
|
||||
crypto_power = CryptoPower(power_ups=target_ursula._default_crypto_powerups)
|
||||
|
||||
if claim_signing_key:
|
||||
crypto_power.consume_power_up(SigningPower(pubkey=target_ursula.stamp.as_umbral_pubkey()))
|
||||
|
||||
vladimir = cls(crypto_power=crypto_power,
|
||||
if attach_transacting_key:
|
||||
cls.attach_transacting_key(blockchain=target_ursula.blockchain)
|
||||
|
||||
vladimir = cls(is_me=True,
|
||||
crypto_power=crypto_power,
|
||||
rest_host=target_ursula.rest_information()[0].host,
|
||||
rest_port=target_ursula.rest_information()[0].port,
|
||||
checksum_address=cls.fraud_address,
|
||||
certificate=target_ursula.rest_server_certificate(),
|
||||
is_me=False)
|
||||
network_middleware=cls.network_middleware,
|
||||
checksum_address = cls.fraud_address,
|
||||
miner_agent=target_ursula.miner_agent)
|
||||
|
||||
# Asshole.
|
||||
vladimir._interface_signature_object = target_ursula._interface_signature_object
|
||||
|
|
|
@ -4,8 +4,10 @@ from constant_sorrow import constants
|
|||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
|
||||
from cryptography.x509 import Certificate
|
||||
from web3.middleware import geth_poa_middleware
|
||||
|
||||
from nucypher.blockchain.eth.agents import EthereumContractAgent
|
||||
from nucypher.blockchain.eth.agents import EthereumContractAgent, NucypherTokenAgent, MinerAgent
|
||||
from nucypher.blockchain.eth.chains import Blockchain
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_FILE_LOCATION
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
from nucypher.crypto.powers import CryptoPower
|
||||
|
@ -13,7 +15,7 @@ from nucypher.crypto.powers import CryptoPower
|
|||
|
||||
class UrsulaConfiguration(NodeConfiguration):
|
||||
|
||||
DEFAULT_REST_HOST = 'localhost'
|
||||
DEFAULT_REST_HOST = '127.0.0.1'
|
||||
DEFAULT_REST_PORT = 9151
|
||||
__DB_TEMPLATE = "ursula.{port}.db"
|
||||
DEFAULT_DB_NAME = __DB_TEMPLATE.format(port=DEFAULT_REST_PORT)
|
||||
|
@ -28,6 +30,7 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
tls_curve: EllipticCurve = None,
|
||||
tls_private_key: bytes = None,
|
||||
certificate: Certificate = None,
|
||||
certificate_filepath: str = None,
|
||||
|
||||
# Ursula
|
||||
db_name: str = None,
|
||||
|
@ -36,8 +39,9 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
crypto_power: CryptoPower = None,
|
||||
|
||||
# Blockchain
|
||||
poa: bool = False,
|
||||
provider_uri: str = None,
|
||||
miner_agent: EthereumContractAgent = None,
|
||||
checksum_address: str = None,
|
||||
|
||||
*args, **kwargs
|
||||
) -> None:
|
||||
|
@ -54,6 +58,7 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
|
||||
self.tls_private_key = tls_private_key
|
||||
self.certificate = certificate
|
||||
self.certificate_filepath = certificate_filepath
|
||||
|
||||
# Ursula
|
||||
self.interface_signature = interface_signature
|
||||
|
@ -62,8 +67,9 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
#
|
||||
# Blockchain
|
||||
#
|
||||
self.poa = poa
|
||||
self.blockchain_uri = provider_uri
|
||||
self.miner_agent = miner_agent
|
||||
self.checksum_address = checksum_address
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
@ -86,7 +92,6 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
def payload(self) -> dict:
|
||||
|
||||
ursula_payload = dict(
|
||||
|
||||
# REST
|
||||
rest_host=self.rest_host,
|
||||
rest_port=self.rest_port,
|
||||
|
@ -97,27 +102,38 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
tls_curve=self.tls_curve,
|
||||
tls_private_key=self.tls_private_key,
|
||||
certificate=self.certificate,
|
||||
# certificate_filepath=self.certificate_filepath, # TODO: Handle existing certificates, injecting the path
|
||||
certificate_filepath=self.certificate_filepath,
|
||||
|
||||
# Ursula
|
||||
interface_signature=self.interface_signature,
|
||||
crypto_power=self.crypto_power,
|
||||
|
||||
# Blockchain
|
||||
miner_agent=self.miner_agent,
|
||||
checksum_address=self.checksum_address,
|
||||
registry_filepath=self.registry_filepath
|
||||
miner_agent=self.miner_agent
|
||||
)
|
||||
|
||||
base_payload = super().payload
|
||||
ursula_payload.update(base_payload)
|
||||
return ursula_payload
|
||||
base_payload.update(ursula_payload)
|
||||
return base_payload
|
||||
|
||||
def produce(self, **overrides):
|
||||
merged_parameters = {**self.payload, **overrides}
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
||||
if self.federated_only is False:
|
||||
|
||||
if not self.miner_agent: # TODO: move this..?
|
||||
blockchain = Blockchain.connect(provider_uri=self.blockchain_uri, registry_filepath=self.registry_filepath)
|
||||
token_agent = NucypherTokenAgent(blockchain=blockchain)
|
||||
miner_agent = MinerAgent(token_agent=token_agent)
|
||||
merged_parameters.update(miner_agent=miner_agent)
|
||||
|
||||
ursula = Ursula(**merged_parameters)
|
||||
|
||||
if self.poa:
|
||||
w3 = ursula.blockchain.interface.w3
|
||||
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
|
||||
|
||||
# if self.save_metadata: # TODO: Does this belong here..?
|
||||
ursula.write_node_metadata(node=ursula)
|
||||
ursula.save_certificate_to_disk(directory=ursula.known_certificates_dir) # TODO: Move this..?
|
||||
|
|
|
@ -10,21 +10,33 @@ from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|||
from eth_account import Account
|
||||
from nacl.exceptions import CryptoError
|
||||
from nacl.secret import SecretBox
|
||||
from typing import ClassVar
|
||||
from typing import ClassVar, Tuple
|
||||
from umbral.keys import UmbralPrivateKey
|
||||
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
from nucypher.config.utils import validate_passphrase
|
||||
from nucypher.crypto.powers import SigningPower, EncryptingPower, CryptoPower
|
||||
|
||||
|
||||
def validate_passphrase(passphrase) -> bool:
|
||||
"""Validate a passphrase and return True or raise an error with a failure reason"""
|
||||
|
||||
rules = (
|
||||
(len(passphrase) >= 16, 'Passphrase is too short, must be >= 16 chars.'),
|
||||
)
|
||||
|
||||
for rule, failure_message in rules:
|
||||
if not rule:
|
||||
raise NodeConfiguration.ConfigurationError(failure_message)
|
||||
return True
|
||||
|
||||
|
||||
def _parse_keyfile(keypath: str):
|
||||
"""Parses a keyfile and returns key metadata as a dict."""
|
||||
|
||||
with open(keypath, 'r') as keyfile:
|
||||
with open(keypath, 'rb') as keyfile:
|
||||
try:
|
||||
key_metadata = json.loads(keyfile)
|
||||
key_metadata = json.loads(keyfile.read())
|
||||
except json.JSONDecodeError:
|
||||
raise NodeConfiguration.ConfigurationError("Invalid data in keyfile {}".format(keypath))
|
||||
else:
|
||||
|
@ -54,18 +66,16 @@ def _save_private_keyfile(keypath: str, key_data: dict) -> str:
|
|||
mode = stat.S_IRUSR | stat.S_IWUSR # 0o600
|
||||
|
||||
try:
|
||||
keyfile_descriptor = os.open(file=keypath, flags=flags, mode=mode)
|
||||
keyfile_descriptor = os.open(keypath, flags=flags, mode=mode)
|
||||
finally:
|
||||
os.umask(0) # Set the umask to 0 after opening
|
||||
|
||||
# Write and destroy file descriptor reference
|
||||
with os.fdopen(keyfile_descriptor, 'wb') as keyfile:
|
||||
keyfile.write(json.dumps(key_data))
|
||||
output_path = keyfile.name
|
||||
keyfile.write(bytes(json.dumps(key_data), encoding='utf-8'))
|
||||
|
||||
# TODO: output_path is an integer, who knows why?
|
||||
del keyfile_descriptor
|
||||
return output_path
|
||||
return keypath
|
||||
|
||||
|
||||
def _save_public_keyfile(keypath: str, key_data: bytes) -> str:
|
||||
|
@ -91,7 +101,7 @@ def _save_public_keyfile(keypath: str, key_data: bytes) -> str:
|
|||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH # 0o644
|
||||
|
||||
try:
|
||||
keyfile_descriptor = os.open(file=keypath, flags=flags, mode=mode)
|
||||
keyfile_descriptor = os.open(keypath, flags=flags, mode=mode)
|
||||
finally:
|
||||
os.umask(0) # Set the umask to 0 after opening
|
||||
|
||||
|
@ -189,16 +199,18 @@ def _generate_signing_keys() -> tuple:
|
|||
return privkey, pubkey
|
||||
|
||||
|
||||
def _generate_transacting_keys(passphrase: str) -> dict:
|
||||
def _generate_wallet(passphrase: str) -> Tuple[str, dict]:
|
||||
"""Create a new wallet address and private "transacting" key from the provided passphrase"""
|
||||
entropy = os.urandom(32) # max out entropy for keccak256
|
||||
account = Account.create(extra_entropy=entropy)
|
||||
encrypted_wallet_data = Account.encrypt(private_key=account.privateKey, password=passphrase)
|
||||
return encrypted_wallet_data
|
||||
return account.address, encrypted_wallet_data
|
||||
|
||||
|
||||
class NucypherKeyring:
|
||||
"""
|
||||
Handles keys for a single __common_name.
|
||||
|
||||
Warning: This class handles private keys!
|
||||
|
||||
OS configuration and interface for ethereum and umbral keys
|
||||
|
@ -213,20 +225,10 @@ class NucypherKeyring:
|
|||
|
||||
"""
|
||||
|
||||
# TODO: Make lazy for better integration with config classes
|
||||
__default_keyring_root = os.path.join(DEFAULT_CONFIG_ROOT, "keyring")
|
||||
|
||||
__default_keyring_root = os.path.join(DEFAULT_CONFIG_ROOT, 'keyring')
|
||||
__default_public_key_dir = os.path.join(__default_keyring_root, 'public')
|
||||
__default_private_key_dir = os.path.join(__default_keyring_root, 'private')
|
||||
|
||||
__default_key_filepaths = {
|
||||
'root': os.path.join(__default_private_key_dir, 'root_key.priv'),
|
||||
'root_pub': os.path.join(__default_public_key_dir, 'root_key.pub'),
|
||||
'signing': os.path.join(__default_private_key_dir, 'signing_key.priv'),
|
||||
'signing_pub': os.path.join(__default_public_key_dir, 'signing_key.pub'),
|
||||
'transacting': os.path.join(__default_private_key_dir, 'wallet.json'),
|
||||
}
|
||||
|
||||
class KeyringError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -234,40 +236,76 @@ class NucypherKeyring:
|
|||
pass
|
||||
|
||||
def __init__(self,
|
||||
common_name: str,
|
||||
keyring_root: str = None,
|
||||
public_key_dir: str = None,
|
||||
private_key_dir: str = None,
|
||||
root_key_path: str = None,
|
||||
pub_root_key_path: str = None,
|
||||
signing_key_path: str = None,
|
||||
pub_signing_key_path: str = None,
|
||||
transacting_key_path: str=None) -> None:
|
||||
wallet_path: str = None,
|
||||
tls_key_path: str = None) -> None:
|
||||
"""
|
||||
Generates a NuCypherKeyring instance with the provided key paths,
|
||||
falling back to default keyring paths.
|
||||
"""
|
||||
|
||||
self.__common_name = common_name
|
||||
|
||||
# Check for a custom private key or keyring root directory to use when locating keys
|
||||
self.__keyring_root = self.__default_keyring_root
|
||||
self.__private_key_dir = self.__default_private_key_dir
|
||||
self.__keyring_root = keyring_root or self.__default_keyring_root
|
||||
self.__public_key_dir = public_key_dir or self.__default_public_key_dir
|
||||
self.__private_key_dir = private_key_dir or self.__default_private_key_dir
|
||||
|
||||
__key_filepaths = self.generate_filepaths(common_name=self.__common_name,
|
||||
public_key_dir=self.__public_key_dir,
|
||||
private_key_dir=self.__private_key_dir)
|
||||
|
||||
# Check for any custom individual key paths
|
||||
self.__root_keypath = root_key_path or self.__default_key_filepaths['root']
|
||||
self.__signing_keypath = signing_key_path or self.__default_key_filepaths['signing']
|
||||
self.__transacting_keypath = transacting_key_path or self.__default_key_filepaths['transacting']
|
||||
self.__root_keypath = root_key_path or __key_filepaths['root']
|
||||
self.__signing_keypath = signing_key_path or __key_filepaths['signing']
|
||||
self.__wallet_path = wallet_path or __key_filepaths['wallet']
|
||||
self.__tls_keypath = tls_key_path or __key_filepaths['tls']
|
||||
|
||||
# Check for any custom individual public key paths
|
||||
self.__root_pub_keypath = pub_root_key_path or self.__default_key_filepaths['root_pub']
|
||||
self.__signing_pub_keypath = pub_signing_key_path or self.__default_key_filepaths['signing_pub']
|
||||
self.__root_pub_keypath = pub_root_key_path or __key_filepaths['root_pub']
|
||||
self.__signing_pub_keypath = pub_signing_key_path or __key_filepaths['signing_pub']
|
||||
|
||||
# Setup key cache
|
||||
self.__derived_key_material = None
|
||||
self.__transacting_private_key = None
|
||||
|
||||
# Check that the keyring is reflected on the filesystem
|
||||
for private_path in (self.__root_keypath, self.__signing_keypath, self.__transacting_keypath):
|
||||
pass
|
||||
|
||||
def __del__(self):
|
||||
self.lock()
|
||||
|
||||
@property
|
||||
def transacting_public_key(self):
|
||||
wallet = _parse_keyfile(keypath=self.__wallet_path)
|
||||
return wallet['address']
|
||||
|
||||
@staticmethod
|
||||
def generate_filepaths(public_key_dir: str,
|
||||
private_key_dir: str,
|
||||
common_name: str) -> dict:
|
||||
|
||||
__key_filepaths = {
|
||||
'root': os.path.join(private_key_dir, 'root-{}.priv'.format(common_name)),
|
||||
'root_pub': os.path.join(public_key_dir, 'root-{}.pub'.format(common_name)),
|
||||
'signing': os.path.join(private_key_dir, 'signing-{}.priv'.format(common_name)),
|
||||
'signing_pub': os.path.join(public_key_dir, 'signing-{}.pub'.format(common_name)),
|
||||
'wallet': os.path.join(private_key_dir, 'wallet-{}.json'.format(common_name)),
|
||||
'tls': os.path.join(private_key_dir, '{}.pem'.format(common_name))
|
||||
}
|
||||
|
||||
return __key_filepaths
|
||||
|
||||
def _export(self, blockchain, passphrase):
|
||||
with open(self.__wallet_path, 'rb') as wallet:
|
||||
data = wallet.read().decode('utf-8')
|
||||
account = Account.decrypt(keyfile_json=data, password=passphrase)
|
||||
blockchain.interface.w3.personal.importRawKey(private_key=account, passphrase=passphrase)
|
||||
|
||||
def __decrypt_keyfile(self, key_path: str) -> UmbralPrivateKey:
|
||||
"""Returns plaintext version of decrypting key."""
|
||||
|
||||
|
@ -284,7 +322,7 @@ class NucypherKeyring:
|
|||
|
||||
def unlock(self, passphrase: bytes) -> None:
|
||||
if self.__derived_key_material is not None:
|
||||
raise Exception('Keyring already unlocked')
|
||||
raise Exception('Keyring already unlocked') # TODO better exception
|
||||
|
||||
# TODO: missing salt parameter below
|
||||
derived_key = _derive_key_material_from_passphrase(passphrase=passphrase)
|
||||
|
@ -322,38 +360,58 @@ class NucypherKeyring:
|
|||
@classmethod
|
||||
def generate(cls,
|
||||
passphrase: str,
|
||||
encryption: bool = True,
|
||||
transacting: bool = True,
|
||||
output_path: str = None
|
||||
encrypting: bool = True,
|
||||
wallet: bool = True,
|
||||
public_key_dir: str = None,
|
||||
private_key_dir: str = None,
|
||||
keyring_root: str = None,
|
||||
exists_ok: bool = True
|
||||
) -> 'NucypherKeyring':
|
||||
"""
|
||||
Generates new encryption, signing, and transacting keys encrypted with the passphrase,
|
||||
Generates new encrypting, signing, and wallet keys encrypted with the passphrase,
|
||||
respectively saving keyfiles on the local filesystem from *default* paths,
|
||||
returning the corresponding Keyring instance.
|
||||
"""
|
||||
|
||||
# Prepare and validate user input
|
||||
_private_key_dir = output_path if output_path else cls.__default_private_key_dir
|
||||
_public_key_dir = public_key_dir if public_key_dir else cls.__default_public_key_dir
|
||||
_private_key_dir = private_key_dir if private_key_dir else cls.__default_private_key_dir
|
||||
|
||||
if not encryption and not transacting:
|
||||
raise ValueError('Either "encryption" or "transacting" must be True to generate new keys.')
|
||||
if not encrypting and not wallet:
|
||||
raise ValueError('Either "encrypting" or "wallet" must be True to generate new keys.')
|
||||
|
||||
assert validate_passphrase(passphrase)
|
||||
|
||||
# TODO
|
||||
# Ensure the configuration base directory exists
|
||||
# utils.generate_confg_dir()
|
||||
validate_passphrase(passphrase)
|
||||
|
||||
# Create the key directories with default paths. Raises OSError if dirs exist
|
||||
os.mkdir(cls.__default_public_key_dir, mode=0o744) # public
|
||||
if exists_ok and not os.path.isdir(_public_key_dir):
|
||||
os.mkdir(_public_key_dir, mode=0o744) # public()
|
||||
|
||||
if exists_ok and not os.path.isdir(_private_key_dir):
|
||||
os.mkdir(_private_key_dir, mode=0o700) # private
|
||||
|
||||
# Generate keys
|
||||
keyring_args = dict() # type: dict
|
||||
if encryption is True:
|
||||
keyring_args = dict()
|
||||
|
||||
if wallet is True:
|
||||
new_address, new_wallet = _generate_wallet(passphrase)
|
||||
new_wallet_path = os.path.join(_private_key_dir, 'wallet-{}.json'.format(new_address))
|
||||
saved_wallet_path = _save_private_keyfile(new_wallet_path, new_wallet)
|
||||
keyring_args.update(wallet_path=saved_wallet_path)
|
||||
|
||||
if encrypting is True:
|
||||
enc_privkey, enc_pubkey = _generate_encryption_keys()
|
||||
sig_privkey, sig_pubkey = _generate_signing_keys()
|
||||
|
||||
if wallet: # common name router, prefer checksum address
|
||||
common_name = new_address
|
||||
elif encrypting:
|
||||
common_name = sig_pubkey
|
||||
|
||||
__key_filepaths = cls.generate_filepaths(public_key_dir=_public_key_dir,
|
||||
private_key_dir=_private_key_dir,
|
||||
common_name=common_name)
|
||||
|
||||
if encrypting is True:
|
||||
passphrase_salt = os.urandom(32)
|
||||
enc_salt = os.urandom(32)
|
||||
sig_salt = os.urandom(32)
|
||||
|
@ -372,35 +430,30 @@ class NucypherKeyring:
|
|||
sig_json['wrap_salt'] = urlsafe_b64encode(sig_salt).decode()
|
||||
|
||||
# Write private keys to files
|
||||
rootkey_path = _save_private_keyfile(cls.__default_key_filepaths['root'], enc_json)
|
||||
sigkey_path = _save_private_keyfile(cls.__default_key_filepaths['signing'], sig_json)
|
||||
rootkey_path = _save_private_keyfile(__key_filepaths['root'], enc_json)
|
||||
sigkey_path = _save_private_keyfile(__key_filepaths['signing'], sig_json)
|
||||
|
||||
bytes_enc_pubkey = enc_pubkey.to_bytes(encoder=urlsafe_b64encode)
|
||||
bytes_sig_pubkey = sig_pubkey.to_bytes(encoder=urlsafe_b64encode)
|
||||
|
||||
# Write public keys to files
|
||||
rootkey_pub_path = _save_public_keyfile(
|
||||
cls.__default_key_filepaths['root_pub'],
|
||||
__key_filepaths['root_pub'],
|
||||
bytes_enc_pubkey
|
||||
)
|
||||
sigkey_pub_path = _save_public_keyfile(
|
||||
cls.__default_key_filepaths['signing_pub'],
|
||||
__key_filepaths['signing_pub'],
|
||||
bytes_sig_pubkey
|
||||
)
|
||||
|
||||
keyring_args.update(
|
||||
keyring_root=keyring_root or cls.__default_keyring_root,
|
||||
root_key_path=rootkey_path,
|
||||
pub_root_key_path=rootkey_pub_path,
|
||||
signing_key_path=sigkey_path,
|
||||
pub_signing_key_path=sigkey_pub_path
|
||||
)
|
||||
|
||||
if transacting is True:
|
||||
wallet = _generate_transacting_keys(passphrase)
|
||||
_wallet_path = _save_private_keyfile(cls.__default_key_filepaths['transacting'], wallet)
|
||||
|
||||
keyring_args.update(transacting_key_path=_wallet_path)
|
||||
|
||||
# return an instance using the generated key paths
|
||||
keyring_instance = cls(**keyring_args)
|
||||
keyring_instance = cls(common_name=common_name, **keyring_args)
|
||||
return keyring_instance
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import contextlib
|
||||
import json
|
||||
import os
|
||||
from json import JSONDecodeError
|
||||
|
||||
import shutil
|
||||
from glob import glob
|
||||
from logging import getLogger
|
||||
from os.path import abspath
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
|
@ -8,7 +13,8 @@ from constant_sorrow import constants
|
|||
from itertools import islice
|
||||
|
||||
from nucypher.characters.base import Character
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, DEFAULT_CONFIG_FILE_LOCATION, TEMPLATE_CONFIG_FILE_LOCATION
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, DEFAULT_CONFIG_FILE_LOCATION, TEMPLATE_CONFIG_FILE_LOCATION, \
|
||||
BASE_DIR
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
|
||||
|
||||
|
@ -18,10 +24,13 @@ class NodeConfiguration:
|
|||
_parser = NotImplemented
|
||||
|
||||
DEFAULT_OPERATING_MODE = 'decentralized'
|
||||
|
||||
__TEMP_CONFIGURATION_DIR_PREFIX = "nucypher-tmp-"
|
||||
__REGISTRY_NAME = 'contract_registry.json'
|
||||
__DEFAULT_NETWORK_MIDDLEWARE_CLASS = RestMiddleware
|
||||
|
||||
__REGISTRY_NAME = 'contract_registry.json'
|
||||
REGISTRY_SOURCE = os.path.join(BASE_DIR, __REGISTRY_NAME) # TODO: Where will this be hosted?
|
||||
|
||||
class ConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
@ -41,7 +50,10 @@ class NodeConfiguration:
|
|||
is_me: bool = True,
|
||||
federated_only: bool = None,
|
||||
network_middleware: RestMiddleware = None,
|
||||
|
||||
registry_source: str = REGISTRY_SOURCE,
|
||||
registry_filepath: str = None,
|
||||
no_seed_registry: bool = False,
|
||||
|
||||
# Learner
|
||||
learn_on_same_thread: bool = False,
|
||||
|
@ -56,25 +68,30 @@ class NodeConfiguration:
|
|||
|
||||
) -> None:
|
||||
|
||||
self.log = getLogger(self.__class__.__name__)
|
||||
|
||||
#
|
||||
# Configuration Filepaths
|
||||
#
|
||||
|
||||
self.keyring_dir = keyring_dir or constants.UNINITIALIZED_CONFIGURATION
|
||||
self.known_nodes_dir = constants.UNINITIALIZED_CONFIGURATION
|
||||
self.known_certificates_dir = known_metadata_dir or constants.UNINITIALIZED_CONFIGURATION
|
||||
self.known_metadata_dir = known_metadata_dir or constants.UNINITIALIZED_CONFIGURATION
|
||||
|
||||
self.__registry_source = registry_source
|
||||
self.registry_filepath = registry_filepath or constants.UNINITIALIZED_CONFIGURATION
|
||||
|
||||
self.temp = temp
|
||||
self.__temp_dir = constants.LIVE_CONFIGURATION
|
||||
if temp:
|
||||
# Create a temp dir and set it as the config root if no config root was specified
|
||||
self.__temp_dir = constants.UNINITIALIZED_CONFIGURATION
|
||||
config_root = constants.UNINITIALIZED_CONFIGURATION
|
||||
else:
|
||||
self.__cache_runtime_filepaths(config_root=config_root)
|
||||
self.config_root = constants.UNINITIALIZED_CONFIGURATION
|
||||
self.__temp = temp
|
||||
|
||||
if self.__temp:
|
||||
self.__temp_dir = constants.UNINITIALIZED_CONFIGURATION
|
||||
else:
|
||||
self.config_root = config_root
|
||||
self.__temp_dir = constants.LIVE_CONFIGURATION
|
||||
self.__cache_runtime_filepaths()
|
||||
|
||||
self.config_file_location = config_file_location
|
||||
|
||||
#
|
||||
|
@ -106,10 +123,14 @@ class NodeConfiguration:
|
|||
#
|
||||
|
||||
if auto_initialize:
|
||||
self.write_defaults() # <<< Write runtime files and dirs
|
||||
self.write_defaults(no_registry=no_seed_registry) # <<< Write runtime files and dirs
|
||||
if load_metadata:
|
||||
self.load_known_nodes(known_metadata_dir=known_metadata_dir)
|
||||
|
||||
@property
|
||||
def temp(self):
|
||||
return self.__temp
|
||||
|
||||
@classmethod
|
||||
def from_configuration_file(cls, filepath=None) -> 'NodeConfiguration':
|
||||
filepath = filepath if filepath is None else DEFAULT_CONFIG_FILE_LOCATION
|
||||
|
@ -153,13 +174,15 @@ class NodeConfiguration:
|
|||
return filepaths
|
||||
|
||||
@staticmethod
|
||||
def check_config_tree_exists(config_root: str) -> bool:
|
||||
def check_config_tree_exists(config_root: str, no_registry=False) -> bool:
|
||||
# Top-level
|
||||
if not os.path.exists(config_root):
|
||||
raise NodeConfiguration.ConfigurationError('No configuration directory found at {}.'.format(config_root))
|
||||
|
||||
# Sub-paths
|
||||
filepaths = NodeConfiguration.generate_runtime_filepaths(config_root=config_root)
|
||||
if no_registry:
|
||||
del filepaths['registry_filepath']
|
||||
for field, path in filepaths.items():
|
||||
if not os.path.exists(path):
|
||||
message = 'Missing configuration directory {}.'
|
||||
|
@ -167,19 +190,20 @@ class NodeConfiguration:
|
|||
|
||||
return True
|
||||
|
||||
def __cache_runtime_filepaths(self, config_root: str) -> None:
|
||||
def __cache_runtime_filepaths(self) -> None:
|
||||
"""Generate runtime filepaths and cache them on the config object"""
|
||||
filepaths = self.generate_runtime_filepaths(config_root=config_root)
|
||||
filepaths = self.generate_runtime_filepaths(config_root=self.config_root)
|
||||
for field, filepath in filepaths.items():
|
||||
if getattr(self, field) is constants.UNINITIALIZED_CONFIGURATION:
|
||||
setattr(self, field, filepath)
|
||||
|
||||
def write_defaults(self) -> str:
|
||||
def write_defaults(self, no_registry=False) -> str:
|
||||
"""Writes the configuration and runtime directory tree starting with the config root directory."""
|
||||
|
||||
#
|
||||
# Create Config Root
|
||||
#
|
||||
if self.temp:
|
||||
if self.__temp:
|
||||
self.__temp_dir = TemporaryDirectory(prefix=self.__TEMP_CONFIGURATION_DIR_PREFIX)
|
||||
self.config_root = self.__temp_dir.name
|
||||
else:
|
||||
|
@ -195,7 +219,7 @@ class NodeConfiguration:
|
|||
#
|
||||
# Create Config Subdirectories
|
||||
#
|
||||
self.__cache_runtime_filepaths(config_root=self.config_root)
|
||||
self.__cache_runtime_filepaths()
|
||||
try:
|
||||
|
||||
# Directories
|
||||
|
@ -205,20 +229,22 @@ class NodeConfiguration:
|
|||
os.mkdir(self.known_metadata_dir, mode=0o755) # known_metadata
|
||||
|
||||
# Files
|
||||
with open(self.registry_filepath, 'w') as registry_file:
|
||||
registry_file.write('MOCK REGISTRY') # TODO: write the default registry
|
||||
if not no_registry:
|
||||
self.import_registry(output_filepath=self.registry_filepath,
|
||||
source=self.__registry_source,
|
||||
blank=no_registry)
|
||||
|
||||
except FileExistsError:
|
||||
# TODO: beef up the error message
|
||||
# existing_paths = [os.path.join(self.config_root, f) for f in os.listdir(self.config_root)]
|
||||
# NodeConfiguration.ConfigurationError("There are existing files at {}".format())
|
||||
message = "There are pre-existing nucypher installation files at {}".format(self.config_root)
|
||||
existing_paths = [os.path.join(self.config_root, f) for f in os.listdir(self.config_root)]
|
||||
message = "There are pre-existing nucypher installation files at {}: {}".format(self.config_root, existing_paths)
|
||||
self.log.critical(message)
|
||||
raise NodeConfiguration.ConfigurationError(message)
|
||||
|
||||
self.check_config_tree_exists(config_root=self.config_root)
|
||||
# self.check_config_tree_exists(config_root=self.config_root)
|
||||
return self.config_root
|
||||
|
||||
def load_known_nodes(self, known_metadata_dir=None) -> None:
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
||||
if known_metadata_dir is None:
|
||||
known_metadata_dir = self.known_metadata_dir
|
||||
|
@ -226,11 +252,42 @@ class NodeConfiguration:
|
|||
glob_pattern = os.path.join(known_metadata_dir, '*.node')
|
||||
metadata_paths = sorted(glob(glob_pattern), key=os.path.getctime)
|
||||
|
||||
self.log.info("Found {} known node metadata files at {}".format(len(metadata_paths), known_metadata_dir))
|
||||
for metadata_path in metadata_paths:
|
||||
from nucypher.characters.lawful import Ursula
|
||||
node = Ursula.from_metadata_file(filepath=abspath(metadata_path), federated_only=self.federated_only) # TODO: 466
|
||||
self.known_nodes.add(node)
|
||||
|
||||
def import_registry(self,
|
||||
output_filepath: str = None,
|
||||
source: str = None,
|
||||
force: bool = False,
|
||||
blank=False) -> str:
|
||||
|
||||
if force and os.path.isfile(output_filepath):
|
||||
raise self.ConfigurationError('There is an existing file at the registry output_filepath {}'.format(output_filepath))
|
||||
|
||||
output_filepath = output_filepath or self.registry_filepath
|
||||
source = source or self.REGISTRY_SOURCE
|
||||
|
||||
if not blank and not self.temp:
|
||||
# Validate Registry
|
||||
with open(source, 'r') as registry_file:
|
||||
try:
|
||||
json.loads(registry_file.read())
|
||||
except JSONDecodeError:
|
||||
message = "The registry source {} is not valid JSON".format(source)
|
||||
self.log.critical(message)
|
||||
raise self.ConfigurationError(message)
|
||||
else:
|
||||
self.log.debug("Source registry {} is valid JSON".format(source))
|
||||
|
||||
else:
|
||||
self.log.warning("Writing blank registry")
|
||||
open(output_filepath, 'w').close() # blank
|
||||
|
||||
self.log.info("Successfully wrote registry to {}".format(output_filepath))
|
||||
return output_filepath
|
||||
|
||||
def write_default_configuration_file(self, filepath: str = DEFAULT_CONFIG_FILE_LOCATION):
|
||||
with contextlib.ExitStack() as stack:
|
||||
template_file = stack.enter_context(open(TEMPLATE_CONFIG_FILE_LOCATION, 'r'))
|
||||
|
@ -243,10 +300,12 @@ class NodeConfiguration:
|
|||
new_file.writelines(line.lstrip(';')) # TODO Copy Default Sections, Perhaps interactively
|
||||
|
||||
def cleanup(self) -> None:
|
||||
if self.temp:
|
||||
if self.__temp:
|
||||
self.__temp_dir.cleanup()
|
||||
|
||||
def produce(self, **overrides) -> Character:
|
||||
"""Initialize a new character instance and return it"""
|
||||
if overrides:
|
||||
self.log.debug("Overrides supplied to {}".format(self.__class__.__name__))
|
||||
merged_parameters = {**self.payload, **overrides}
|
||||
return self._Character(**merged_parameters)
|
||||
|
|
|
@ -4,20 +4,21 @@ import os
|
|||
from typing import Union, Tuple
|
||||
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_FILE_LOCATION
|
||||
from nucypher.config.keyring import NucypherKeyring
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
|
||||
|
||||
def validate_passphrase(passphrase) -> bool:
|
||||
"""Validate a passphrase and return True or raise an error with a failure reason"""
|
||||
def generate_local_wallet(keyring_root:str, passphrase: str) -> NucypherKeyring:
|
||||
keyring = NucypherKeyring.generate(passphrase=passphrase,
|
||||
keyring_root=keyring_root,
|
||||
encrypting=False,
|
||||
wallet=True)
|
||||
return keyring
|
||||
|
||||
rules = (
|
||||
(len(passphrase) >= 16, 'Passphrase is too short, must be >= 16 chars.'),
|
||||
)
|
||||
|
||||
for rule, failure_message in rules:
|
||||
if not rule:
|
||||
raise NodeConfiguration.ConfigurationError(failure_message)
|
||||
return True
|
||||
def generate_account(w3, passphrase: str) -> NucypherKeyring:
|
||||
address = w3.personal.newAccount(passphrase)
|
||||
return address
|
||||
|
||||
|
||||
def check_config_permissions() -> bool:
|
||||
|
@ -52,7 +53,7 @@ def validate_configuration_file(config=None,
|
|||
except KeyError:
|
||||
raise NodeConfiguration.ConfigurationError("No operating mode configured")
|
||||
else:
|
||||
modes = ('federated', 'testing', 'decentralized', 'centralized')
|
||||
modes = ('federated', 'tester', 'decentralized', 'centralized')
|
||||
if operating_mode not in modes:
|
||||
missing_sections.append("mode")
|
||||
if raise_on_failure is True:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
from ipaddress import IPv4Address
|
||||
from random import SystemRandom
|
||||
|
||||
import datetime
|
||||
|
@ -116,9 +117,12 @@ def ecdsa_verify(message: bytes,
|
|||
|
||||
def _save_tls_certificate(certificate: Certificate,
|
||||
full_filepath: str,
|
||||
# save_private: bool = False,
|
||||
force: bool = True, # TODO: Make configurable, or set to False by default.
|
||||
) -> str:
|
||||
if force is False and os.path.isfile(full_filepath):
|
||||
|
||||
cert_already_exists = os.path.isfile(full_filepath)
|
||||
if force is False and cert_already_exists:
|
||||
raise FileExistsError('A TLS certificate already exists at {}.'.format(full_filepath))
|
||||
|
||||
with open(full_filepath, 'wb') as certificate_file:
|
||||
|
@ -139,9 +143,8 @@ def load_tls_certificate(filepath: str) -> Certificate:
|
|||
raise # TODO: Better error message here
|
||||
|
||||
|
||||
def generate_self_signed_certificate(common_name: str,
|
||||
def generate_self_signed_certificate(host: str,
|
||||
curve: EllipticCurve,
|
||||
host: str,
|
||||
private_key: _EllipticCurvePrivateKey = None,
|
||||
days_valid: int = 365
|
||||
) -> Tuple[Certificate, _EllipticCurvePrivateKey]:
|
||||
|
@ -153,7 +156,7 @@ def generate_self_signed_certificate(common_name: str,
|
|||
|
||||
now = datetime.datetime.utcnow()
|
||||
subject = issuer = x509.Name([
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, host),
|
||||
])
|
||||
cert = x509.CertificateBuilder().subject_name(subject)
|
||||
cert = cert.issuer_name(issuer)
|
||||
|
@ -162,7 +165,7 @@ def generate_self_signed_certificate(common_name: str,
|
|||
cert = cert.not_valid_before(now)
|
||||
cert = cert.not_valid_after(now + datetime.timedelta(days=days_valid))
|
||||
# TODO: What are we going to do about domain name here? 179
|
||||
cert = cert.add_extension(x509.SubjectAlternativeName([x509.DNSName(host)]), critical=False)
|
||||
cert = cert.add_extension(x509.SubjectAlternativeName([x509.IPAddress(IPv4Address(host))]), critical=False)
|
||||
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
|
||||
|
||||
return cert, private_key
|
||||
|
|
|
@ -87,7 +87,7 @@ class BlockchainPower(CryptoPowerUp):
|
|||
Unlocks the account for the specified duration. If no duration is
|
||||
provided, it will remain unlocked indefinitely.
|
||||
"""
|
||||
self.is_unlocked = self.blockchain.unlock_account(self.account, password, duration=duration)
|
||||
self.is_unlocked = self.blockchain.interface.unlock_account(self.account, password, duration=duration)
|
||||
|
||||
if not self.is_unlocked:
|
||||
raise PowerUpError("Failed to unlock account {}".format(self.account))
|
||||
|
|
|
@ -128,13 +128,11 @@ class HostingKeypair(Keypair):
|
|||
_DEFAULT_CURVE = ec.SECP384R1
|
||||
|
||||
def __init__(self,
|
||||
common_name=None,
|
||||
host=None,
|
||||
private_key: Union[UmbralPrivateKey, UmbralPublicKey] = None,
|
||||
curve=None,
|
||||
certificate=None,
|
||||
certificate_filepath: str = None,
|
||||
certificate_dir=None,
|
||||
generate_certificate=True,
|
||||
) -> None:
|
||||
|
||||
|
@ -151,15 +149,14 @@ class HostingKeypair(Keypair):
|
|||
|
||||
elif generate_certificate:
|
||||
|
||||
if not all((common_name, host)):
|
||||
if not host:
|
||||
message = "If you don't supply the certificate, one will be generated for you." \
|
||||
"But for that, you need to pass both host and common_name.."
|
||||
"But for that, you need to pass a hostname."
|
||||
raise TypeError(message)
|
||||
|
||||
certificate, private_key = generate_self_signed_certificate(common_name=common_name,
|
||||
certificate, private_key = generate_self_signed_certificate(host=host,
|
||||
private_key=private_key,
|
||||
curve=self.curve,
|
||||
host=host)
|
||||
curve=self.curve)
|
||||
|
||||
super().__init__(private_key=private_key)
|
||||
else:
|
||||
|
@ -170,7 +167,6 @@ class HostingKeypair(Keypair):
|
|||
|
||||
self.certificate = certificate
|
||||
self.certificate_filepath = certificate_filepath
|
||||
self.certificate_dir = certificate_dir
|
||||
|
||||
def generate_self_signed_cert(self, common_name):
|
||||
cryptography_key = self._privkey.to_cryptography_privkey()
|
||||
|
|
|
@ -49,9 +49,9 @@ class RestMiddleware:
|
|||
endpoint = 'https://{}/kFrag/{}/reencrypt'.format(work_order.ursula.rest_interface, id_as_hex)
|
||||
return requests.post(endpoint, payload, verify=work_order.ursula.certificate_filepath)
|
||||
|
||||
def node_information(self, host, port, certificate_filepath=None):
|
||||
def node_information(self, host, port, certificate_filepath):
|
||||
endpoint = "https://{}:{}/public_information".format(host, port)
|
||||
return requests.get(endpoint, verify=False)
|
||||
return requests.get(endpoint, verify=certificate_filepath)
|
||||
|
||||
def get_nodes_via_rest(self,
|
||||
url,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import os
|
||||
from logging import getLogger
|
||||
|
||||
import OpenSSL
|
||||
import maya
|
||||
from constant_sorrow import constants
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.x509 import Certificate
|
||||
from eth_keys.datatypes import Signature as EthSignature
|
||||
|
||||
|
@ -28,6 +30,8 @@ class VerifiableNode:
|
|||
timestamp=constants.NOT_SIGNED,
|
||||
) -> None:
|
||||
|
||||
self.log = getLogger(self.__class__.__name__)
|
||||
|
||||
self.certificate = certificate
|
||||
self.certificate_filepath = certificate_filepath
|
||||
self._interface_signature_object = interface_signature
|
||||
|
@ -44,7 +48,7 @@ class VerifiableNode:
|
|||
"""
|
||||
|
||||
@classmethod
|
||||
def from_tls_hosting_power(cls, tls_hosting_power: TLSHostingPower, *args, **kwargs):
|
||||
def from_tls_hosting_power(cls, tls_hosting_power: TLSHostingPower, *args, **kwargs) -> 'VerifiableNode':
|
||||
certificate_filepath = tls_hosting_power.keypair.certificate_filepath
|
||||
certificate = tls_hosting_power.keypair.certificate
|
||||
return cls(certificate=certificate, certificate_filepath=certificate_filepath, *args, **kwargs)
|
||||
|
@ -103,7 +107,12 @@ class VerifiableNode:
|
|||
if not accept_federated_only:
|
||||
raise
|
||||
|
||||
def verify_node(self, network_middleware, accept_federated_only=False, force=False):
|
||||
def verify_node(self,
|
||||
network_middleware,
|
||||
certificate_filepath: str = None,
|
||||
accept_federated_only: bool = False,
|
||||
force: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Three things happening here:
|
||||
|
||||
|
@ -119,10 +128,13 @@ class VerifiableNode:
|
|||
|
||||
# The node's metadata is valid; let's be sure the interface is in order.
|
||||
response = network_middleware.node_information(host=self.rest_information()[0].host,
|
||||
port=self.rest_information()[0].port)
|
||||
port=self.rest_information()[0].port,
|
||||
certificate_filepath=certificate_filepath)
|
||||
if not response.status_code == 200:
|
||||
raise RuntimeError("Or something.") # TODO: Raise an error here? Or return False? Or something?
|
||||
timestamp, signature, identity_evidence, verifying_key, encrypting_key, public_address, certificate_vbytes, rest_info = self._internal_splitter(response.content)
|
||||
timestamp, signature, identity_evidence, \
|
||||
verifying_key, encrypting_key, \
|
||||
public_address, certificate_vbytes, rest_info = self._internal_splitter(response.content)
|
||||
|
||||
verifying_keys_match = verifying_key == self.public_keys(SigningPower)
|
||||
encrypting_keys_match = encrypting_key == self.public_keys(EncryptingPower)
|
||||
|
@ -139,9 +151,9 @@ class VerifiableNode:
|
|||
else:
|
||||
self._verified_node = True
|
||||
|
||||
def substantiate_stamp(self):
|
||||
def substantiate_stamp(self, passphrase: str):
|
||||
blockchain_power = self._crypto_power.power_ups(BlockchainPower)
|
||||
blockchain_power.unlock_account(password=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD) # TODO: 349
|
||||
blockchain_power.unlock_account(password=passphrase) # TODO: 349
|
||||
signature = blockchain_power.sign_message(bytes(self.stamp))
|
||||
self._evidence_of_decentralized_identity = signature
|
||||
|
||||
|
@ -160,7 +172,7 @@ class VerifiableNode:
|
|||
try:
|
||||
self._sign_and_date_interface_info()
|
||||
except NoSigningPower:
|
||||
raise NoSigningPower("This Node is a Stranger; you didn't init with an interface signature, so you can't verify.")
|
||||
raise NoSigningPower("This Ursula is a stranger and cannot be used to verify.")
|
||||
return self._interface_signature_object
|
||||
|
||||
@property
|
||||
|
@ -185,7 +197,10 @@ class VerifiableNode:
|
|||
|
||||
@property
|
||||
def certificate_filename(self):
|
||||
return self.common_name + '.pem' # TODO: use cert encoding..?
|
||||
return '{}.{}'.format(self.common_name, Encoding.PEM.name.lower()) # TODO: use cert's encoding..?
|
||||
|
||||
def get_certificate_filepath(self, certificates_dir: str) -> str:
|
||||
return os.path.join(certificates_dir, self.certificate_filename)
|
||||
|
||||
def save_certificate_to_disk(self, directory):
|
||||
x509 = OpenSSL.crypto.X509.from_cryptography(self.certificate)
|
||||
|
@ -193,10 +208,14 @@ class VerifiableNode:
|
|||
common_name_as_bytes = subject_components[0][1]
|
||||
common_name_from_cert = common_name_as_bytes.decode()
|
||||
|
||||
if not self.checksum_public_address == common_name_from_cert:
|
||||
if not self.rest_information()[0].host == common_name_from_cert:
|
||||
# TODO: It's better for us to have checked this a while ago so that this situation is impossible. #443
|
||||
raise ValueError("You passed a common_name that is not the same one as the cert. Why? FWIW, You don't even need to pass a common name here; the cert will be saved according to the name on the cert itself.")
|
||||
raise ValueError("You passed a __common_name that is not the same one as the cert. "
|
||||
"Common name is optional; the cert will be saved according to "
|
||||
"the name on the cert itself.")
|
||||
|
||||
certificate_filepath = os.path.join(directory, self.certificate_filename)
|
||||
certificate_filepath = self.get_certificate_filepath(certificates_dir=directory)
|
||||
_save_tls_certificate(self.certificate, full_filepath=certificate_filepath)
|
||||
self.certificate_filepath = certificate_filepath
|
||||
self.log.info("Saved new TLS certificate {}".format(certificate_filepath))
|
||||
return self.certificate_filepath
|
||||
|
|
|
@ -1,146 +1,16 @@
|
|||
import asyncio
|
||||
|
||||
import kademlia
|
||||
from bytestring_splitter import VariableLengthBytestring
|
||||
from constant_sorrow import default_constant_splitter, constants
|
||||
from kademlia.node import Node
|
||||
from kademlia.protocol import KademliaProtocol
|
||||
from kademlia.utils import digest
|
||||
|
||||
from nucypher.network.routing import NucypherRoutingTable
|
||||
|
||||
|
||||
class SuspiciousActivity(RuntimeError):
|
||||
"""raised when an action appears to amount to malicious conduct."""
|
||||
|
||||
|
||||
class NucypherHashProtocol(KademliaProtocol):
|
||||
def __init__(self,
|
||||
sourceNode,
|
||||
storage,
|
||||
ksize,
|
||||
*args, **kwargs) -> None:
|
||||
|
||||
super().__init__(sourceNode, storage, ksize, *args, **kwargs)
|
||||
|
||||
self.router = NucypherRoutingTable(self, ksize, sourceNode)
|
||||
self.illegal_keys_seen = [] # type: list # TODO: 340
|
||||
|
||||
@property
|
||||
def ursulas(self):
|
||||
raise NotImplementedError("This approach is deprecated. Find a way to use _known_nodes instead. See #227.")
|
||||
|
||||
@property
|
||||
def storage(self):
|
||||
raise NotImplementedError("This approach is deprecated. Find a way to use _known_nodes instead. See #227.")
|
||||
|
||||
@storage.setter
|
||||
def storage(self, not_gonna_use_this):
|
||||
# TODO: 331
|
||||
pass
|
||||
|
||||
def check_node_for_storage(self, node):
|
||||
try:
|
||||
return node.can_store()
|
||||
except AttributeError:
|
||||
return True
|
||||
|
||||
async def callStore(self, nodeToAsk, key, value):
|
||||
# nodeToAsk = NucypherNode
|
||||
if self.check_node_for_storage(nodeToAsk):
|
||||
address = (nodeToAsk.ip, nodeToAsk.port)
|
||||
# TODO: encrypt `value` with public key of nodeToAsk
|
||||
store_future = self.store(address, self.sourceNode.id, key, value)
|
||||
result = await store_future
|
||||
success, data = self.handleCallResponse(result, nodeToAsk)
|
||||
return success, data
|
||||
else:
|
||||
return constants.NODE_HAS_NO_STORAGE, False
|
||||
|
||||
def rpc_store(self, sender, nodeid, key, value):
|
||||
source = kademlia.node.Node(nodeid, sender[0], sender[1])
|
||||
self.welcomeIfNewNode(source)
|
||||
self.log.debug("got a store request from %s" % str(sender))
|
||||
|
||||
header, payload = default_constant_splitter(value, return_remainder=True)
|
||||
|
||||
if header == constants.BYTESTRING_IS_URSULA_IFACE_INFO:
|
||||
from nucypher.characters.lawful import Ursula
|
||||
stranger_ursula = Ursula.from_bytes(payload,
|
||||
federated_only=self.sourceNode.federated_only) # TODO: Is federated_only the right thing here?
|
||||
|
||||
if stranger_ursula.interface_is_valid() and key == digest(stranger_ursula.canonical_public_address):
|
||||
self.sourceNode._node_storage[stranger_ursula.checksum_public_address] = stranger_ursula # TODO: 340
|
||||
return True
|
||||
else:
|
||||
self.log.warning("Got request to store invalid node: {} / {}".format(key, value))
|
||||
self.illegal_keys_seen.append(key)
|
||||
return False
|
||||
elif header == constants.BYTESTRING_IS_TREASURE_MAP:
|
||||
from nucypher.policy.models import TreasureMap
|
||||
try:
|
||||
treasure_map = TreasureMap.from_bytes(payload)
|
||||
self.log.info("Storing TreasureMap: {} / {}".format(key, value))
|
||||
self.sourceNode._treasure_maps[treasure_map.public_id()] = value
|
||||
return True
|
||||
except TreasureMap.InvalidSignature:
|
||||
self.log.warning("Got request to store invalid TreasureMap: {} / {}".format(key, value))
|
||||
self.illegal_keys_seen.append(key)
|
||||
return False
|
||||
else:
|
||||
self.log.info(
|
||||
"Got request to store bad k/v: {} / {}".format(key, value))
|
||||
return False
|
||||
|
||||
def welcomeIfNewNode(self, node):
|
||||
"""
|
||||
Given a new node, send it all the keys/values it should be storing,
|
||||
then add it to the routing table.
|
||||
|
||||
@param node: A new node that just joined (or that we just found out
|
||||
about).
|
||||
|
||||
Process:
|
||||
For each key in storage, get k closest nodes. If newnode is closer
|
||||
than the furtherst in that list, and the node for this server
|
||||
is closer than the closest in that list, then store the key/value
|
||||
on the new node (per section 2.5 of the paper)
|
||||
"""
|
||||
if not self.router.isNewNode(node):
|
||||
return
|
||||
|
||||
self.log.info("never seen %s before, adding to router and setting nearby " % node)
|
||||
# TODO: 331 and 340 next two lines
|
||||
ursulas = [(id, bytes(node)) for id, node in self.sourceNode._node_storage.items()]
|
||||
for key, value in tuple(self.sourceNode._treasure_maps.items()) + tuple(ursulas):
|
||||
keynode = Node(digest(key))
|
||||
neighbors = self.router.findNeighbors(keynode)
|
||||
if len(neighbors) > 0:
|
||||
newNodeClose = node.distanceTo(keynode) < neighbors[-1].distanceTo(keynode)
|
||||
thisNodeClosest = self.sourceNode.distanceTo(keynode) < neighbors[0].distanceTo(keynode)
|
||||
if len(neighbors) == 0 or (newNodeClose and thisNodeClosest):
|
||||
asyncio.ensure_future(self.callStore(node, key, value))
|
||||
self.router.addContact(node)
|
||||
|
||||
|
||||
class NucypherSeedOnlyProtocol(NucypherHashProtocol):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def rpc_store(self, sender, nodeid, key, value):
|
||||
source = Node(nodeid, sender[0], sender[1])
|
||||
self.welcomeIfNewNode(source)
|
||||
self.log.debug(
|
||||
"got a store request from %s, but THIS VALUE WILL NOT BE STORED as this is a seed-only node." % str(
|
||||
sender))
|
||||
return True
|
||||
|
||||
|
||||
class InterfaceInfo:
|
||||
expected_bytes_length = lambda: VariableLengthBytestring
|
||||
|
||||
def __init__(self, host, port) -> None:
|
||||
self.host = host
|
||||
loopback, localhost = '127.0.0.1', 'localhost'
|
||||
self.host = loopback if host == localhost else host
|
||||
self.port = int(port)
|
||||
|
||||
@classmethod
|
||||
|
@ -150,6 +20,14 @@ class InterfaceInfo:
|
|||
host = host_bytes.decode("utf-8")
|
||||
return cls(host=host, port=port)
|
||||
|
||||
@property
|
||||
def uri(self):
|
||||
return u"{}:{}".format(self.host, self.port)
|
||||
|
||||
@property
|
||||
def formal_uri(self):
|
||||
return u"{}://{}".format('https', self.uri)
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes(self.host, encoding="utf-8") + b":" + self.port.to_bytes(4, "big")
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import binascii
|
||||
import os
|
||||
from logging import getLogger
|
||||
|
||||
from apistar import Route, App
|
||||
|
@ -139,10 +140,14 @@ class ProxyRESTRoutes:
|
|||
if node.checksum_public_address in self._node_tracker:
|
||||
continue # TODO: 168 Check version and update if required.
|
||||
|
||||
certificate_filepath = node.get_certificate_filepath(certificates_dir=self._certificate_dir)
|
||||
|
||||
@crosstown_traffic()
|
||||
def learn_about_announced_nodes():
|
||||
try:
|
||||
node.verify_node(self.network_middleware, accept_federated_only=self.federated_only) # TODO: 466
|
||||
node.verify_node(self.network_middleware,
|
||||
accept_federated_only=self.federated_only, # TODO: 466
|
||||
certificate_filepath=certificate_filepath)
|
||||
except node.SuspiciousActivity:
|
||||
# TODO: Account for possibility that stamp, rather than interface, was bad.
|
||||
message = "Suspicious Activity: Discovered node with bad signature: {}. " \
|
||||
|
@ -286,20 +291,16 @@ class TLSHostingPower(KeyPairBasedPower):
|
|||
|
||||
def __init__(self,
|
||||
rest_server,
|
||||
certificate_filepath=None,
|
||||
certificate=None,
|
||||
certificate_dir=None,
|
||||
common_name=None, # TODO: Is this actually optional?
|
||||
certificate_filepath=None,
|
||||
*args, **kwargs) -> None:
|
||||
|
||||
if certificate and certificate_filepath:
|
||||
# TODO: Design decision here: if they do pass both, and they're identical, do we let that slide?
|
||||
raise ValueError("Pass either a certificate or a certificate_filepath - what do you even expect from passing both?")
|
||||
raise ValueError("Pass either a certificate or a certificate_filepath, not both.")
|
||||
|
||||
if certificate:
|
||||
kwargs['keypair'] = HostingKeypair(certificate=certificate,
|
||||
certificate_dir=certificate_dir,
|
||||
common_name=common_name)
|
||||
kwargs['keypair'] = HostingKeypair(certificate=certificate, host=rest_server.rest_interface.host)
|
||||
elif certificate_filepath:
|
||||
kwargs['keypair'] = HostingKeypair(certificate_filepath=certificate_filepath)
|
||||
self.rest_server = rest_server
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import binascii
|
||||
import os
|
||||
from abc import abstractmethod
|
||||
from collections import OrderedDict
|
||||
|
||||
|
@ -235,9 +236,11 @@ class Policy:
|
|||
return self.publish(network_middleware)
|
||||
|
||||
def consider_arrangement(self, network_middleware, ursula, arrangement):
|
||||
|
||||
certificate_filepath = ursula.get_certificate_filepath(certificates_dir=self.alice.known_certificates_dir)
|
||||
try:
|
||||
ursula.verify_node(network_middleware, accept_federated_only=arrangement.federated)
|
||||
ursula.verify_node(network_middleware,
|
||||
accept_federated_only=arrangement.federated,
|
||||
certificate_filepath=certificate_filepath)
|
||||
except ursula.InvalidNode:
|
||||
# TODO: What do we actually do here? Report this at least (355)?
|
||||
# Maybe also have another bucket for invalid nodes?
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from logging import getLogger
|
||||
|
||||
from constant_sorrow.constants import NO_BLOCKCHAIN_AVAILABLE
|
||||
from typing import List
|
||||
from umbral.keys import UmbralPrivateKey
|
||||
|
@ -31,21 +33,20 @@ class TesterBlockchain(Blockchain):
|
|||
"""
|
||||
|
||||
_instance = NO_BLOCKCHAIN_AVAILABLE
|
||||
_default_network = 'tester'
|
||||
_test_account_cache = list()
|
||||
|
||||
def __init__(self, test_accounts=None, poa=True, airdrop=True, *args, **kwargs):
|
||||
|
||||
# Depends on circumflex
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.log = getLogger("test-blockchain") # type: Logger
|
||||
|
||||
# For use with Proof-Of-Authority test-blockchains
|
||||
if poa is True:
|
||||
w3 = self.interface.w3
|
||||
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
|
||||
|
||||
# Generate additional ethereum accounts for testing
|
||||
enough_accounts = len(self.interface.w3.eth.accounts) > DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
enough_accounts = len(self.interface.w3.eth.accounts) >= DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
if test_accounts is not None and not enough_accounts:
|
||||
|
||||
accounts_to_make = DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK - len(self.interface.w3.eth.accounts)
|
||||
|
@ -62,28 +63,22 @@ class TesterBlockchain(Blockchain):
|
|||
def sever_connection(cls) -> None:
|
||||
cls._instance = NO_BLOCKCHAIN_AVAILABLE
|
||||
|
||||
def unlock_account(self, address, password, duration):
|
||||
self.interface.w3.personal.unlockAccount(address, passphrase=password)
|
||||
return True # Test accounts are always unlocked
|
||||
|
||||
def __generate_insecure_unlocked_accounts(self, quantity: int) -> List[str]:
|
||||
"""
|
||||
Generate additional unlocked accounts transferring wei_balance to each account on creation.
|
||||
Generate additional unlocked accounts transferring a balance to each account on creation.
|
||||
"""
|
||||
|
||||
addresses = list()
|
||||
insecure_passphrase = TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
for _ in range(quantity):
|
||||
umbral_priv_key = UmbralPrivateKey.gen_key()
|
||||
|
||||
umbral_priv_key = UmbralPrivateKey.gen_key()
|
||||
address = self.interface.w3.personal.importRawKey(private_key=umbral_priv_key.to_bytes(),
|
||||
passphrase=insecure_passphrase)
|
||||
|
||||
assert self.unlock_account(address, password=insecure_passphrase, duration=None), \
|
||||
'Failed to unlock {}'.format(address)
|
||||
|
||||
assert self.interface.unlock_account(address, password=insecure_passphrase, duration=None), 'Failed to unlock {}'.format(address)
|
||||
addresses.append(address)
|
||||
self._test_account_cache.append(addresses)
|
||||
self._test_account_cache.append(address)
|
||||
self.log.info('Generated new insecure account {}'.format(address))
|
||||
|
||||
return addresses
|
||||
|
||||
|
@ -100,6 +95,7 @@ class TesterBlockchain(Blockchain):
|
|||
|
||||
_receipt = self.wait_for_receipt(txhash)
|
||||
tx_hashes.append(txhash)
|
||||
self.log.info("Airdropped {} ETH {} -> {}".format(amount, tx['from'], tx['to']))
|
||||
|
||||
return tx_hashes
|
||||
|
||||
|
@ -130,3 +126,4 @@ class TesterBlockchain(Blockchain):
|
|||
|
||||
self.interface.w3.eth.web3.testing.timeTravel(timestamp=end_timestamp)
|
||||
self.interface.w3.eth.web3.testing.mine(1)
|
||||
self.log.info("Time traveled to {}".format(end_timestamp))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
|
||||
from nucypher.blockchain.eth import constants
|
||||
from nucypher.blockchain.eth.constants import DISPATCHER_SECRET_LENGTH, M
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
|
||||
TEST_KNOWN_URSULAS_CACHE = {}
|
||||
|
||||
|
@ -8,12 +9,14 @@ TEST_URSULA_STARTING_PORT = 7468
|
|||
|
||||
DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK = 10
|
||||
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT = 1000000 * int(constants.M)
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT = 1000000 * int(M)
|
||||
|
||||
DEVELOPMENT_ETH_AIRDROP_AMOUNT = 10 ** 6 * 10 ** 18 # wei -> ether
|
||||
|
||||
MINERS_ESCROW_DEPLOYMENT_SECRET = os.urandom(constants.DISPATCHER_SECRET_LENGTH)
|
||||
MINERS_ESCROW_DEPLOYMENT_SECRET = os.urandom(DISPATCHER_SECRET_LENGTH)
|
||||
|
||||
POLICY_MANAGER_DEPLOYMENT_SECRET = os.urandom(constants.DISPATCHER_SECRET_LENGTH)
|
||||
POLICY_MANAGER_DEPLOYMENT_SECRET = os.urandom(DISPATCHER_SECRET_LENGTH)
|
||||
|
||||
TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD = 'this-is-not-a-secure-password'
|
||||
|
||||
DEFAULT_SIMULATION_REGISTRY_FILEPATH = os.path.join(DEFAULT_CONFIG_ROOT, 'simulated_registry.json')
|
||||
|
|
|
@ -51,7 +51,7 @@ class MockRestMiddleware(RestMiddleware):
|
|||
mock_client = self._get_mock_client_by_ursula(node)
|
||||
return mock_client.get("http://localhost/treasure_map/{}".format(map_id))
|
||||
|
||||
def node_information(self, host, port, certificate_filepath=None):
|
||||
def node_information(self, host, port, certificate_filepath):
|
||||
mock_client = self._get_mock_client_by_port(port)
|
||||
response = mock_client.get("http://localhost/public_information")
|
||||
return response
|
||||
|
|
|
@ -2,7 +2,7 @@ import random
|
|||
|
||||
from eth_utils import to_checksum_address
|
||||
from twisted.internet import protocol
|
||||
from typing import Set
|
||||
from typing import Set, Union
|
||||
|
||||
from nucypher.blockchain.eth import constants
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
@ -10,7 +10,7 @@ from nucypher.config.characters import UrsulaConfiguration
|
|||
from nucypher.crypto.api import secure_random
|
||||
from nucypher.utilities.sandbox.constants import (DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
TEST_URSULA_STARTING_PORT,
|
||||
TEST_KNOWN_URSULAS_CACHE)
|
||||
TEST_KNOWN_URSULAS_CACHE, TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
|
||||
def make_federated_ursulas(ursula_config: UrsulaConfiguration,
|
||||
|
@ -48,11 +48,12 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration,
|
|||
|
||||
|
||||
def make_decentralized_ursulas(ursula_config: UrsulaConfiguration,
|
||||
ether_addresses: list,
|
||||
ether_addresses: Union[list, int],
|
||||
stake: bool = False,
|
||||
know_each_other: bool = True,
|
||||
**ursula_overrides) -> Set[Ursula]:
|
||||
|
||||
# Alternately accepts an int of the quantity of ursulas to make
|
||||
if isinstance(ether_addresses, int):
|
||||
ether_addresses = [to_checksum_address(secure_random(20)) for _ in range(ether_addresses)]
|
||||
|
||||
|
@ -121,28 +122,30 @@ def spawn_random_staking_ursulas(miner_agent, addresses: list) -> list:
|
|||
|
||||
class UrsulaProcessProtocol(protocol.ProcessProtocol):
|
||||
|
||||
def __init__(self, command):
|
||||
def __init__(self, command, checksum_address):
|
||||
self.command = command
|
||||
self.checksum_address = checksum_address
|
||||
|
||||
def connectionMade(self):
|
||||
print("connectionMade!")
|
||||
self.transport.closeStdin() # tell them we're done
|
||||
print("Started simulated Ursula {}".format(self.checksum_address))
|
||||
|
||||
def outReceived(self, data):
|
||||
print(data)
|
||||
if b'passphrase' in data:
|
||||
self.transport.write(bytes(TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD, encoding='ascii'))
|
||||
self.transport.closeStdin() # tell them we're done
|
||||
|
||||
def errReceived(self, data):
|
||||
print(data)
|
||||
|
||||
def inConnectionLost(self):
|
||||
print("inConnectionLost! stdin is closed! (we probably did it)")
|
||||
print("Lost connection to simulated Ursula {}".format(self.checksum_address))
|
||||
|
||||
def outConnectionLost(self):
|
||||
print("outConnectionLost! The child closed their stdout!")
|
||||
print("Lost connection to simulated Ursula {}".format(self.checksum_address))
|
||||
|
||||
def errConnectionLost(self):
|
||||
print("errConnectionLost! The child closed their stderr.")
|
||||
print("Simulated Ursula {} raised an Exception".format(self.checksum_address))
|
||||
|
||||
def processEnded(self, status_object):
|
||||
print("processEnded, status %d" % status_object.value.exitCode)
|
||||
print("quitting")
|
||||
print("Ursula {} stopped".format(self.checksum_address))
|
||||
|
|
|
@ -14,12 +14,13 @@ class TestMiner:
|
|||
token_agent, miner_agent, policy_agent = three_agents
|
||||
origin, *everybody_else = testerchain.interface.w3.eth.accounts
|
||||
token_airdrop(token_agent, origin=origin, addresses=everybody_else, amount=DEVELOPMENT_TOKEN_AIRDROP_AMOUNT)
|
||||
miner = Miner(miner_agent=miner_agent, checksum_address=everybody_else[0])
|
||||
miner = Miner(miner_agent=miner_agent, checksum_address=everybody_else[0], is_me=True)
|
||||
return miner
|
||||
|
||||
def test_miner_locking_tokens(self, testerchain, three_agents, miner):
|
||||
token_agent, miner_agent, policy_agent = three_agents
|
||||
testerchain.ether_airdrop(amount=10000)
|
||||
# testerchain.ether_airdrop(amount=10000)
|
||||
|
||||
assert constants.MIN_ALLOWED_LOCKED < miner.token_balance, "Insufficient miner balance"
|
||||
|
||||
expiration = maya.now().add(days=constants.MIN_LOCKED_PERIODS)
|
||||
|
@ -35,6 +36,9 @@ class TestMiner:
|
|||
# Staking starts after one period
|
||||
locked_tokens = miner_agent.contract.functions.getLockedTokens(miner.checksum_public_address).call()
|
||||
assert 0 == locked_tokens
|
||||
|
||||
# testerchain.time_travel(periods=1)
|
||||
|
||||
locked_tokens = miner_agent.contract.functions.getLockedTokens(miner.checksum_public_address, 1).call()
|
||||
assert constants.MIN_ALLOWED_LOCKED == locked_tokens
|
||||
|
||||
|
@ -101,8 +105,8 @@ class TestPolicyAuthor:
|
|||
token_agent, miner_agent, policy_agent = three_agents
|
||||
token_agent.ether_airdrop(amount=100000 * constants.M)
|
||||
_origin, ursula, alice, *everybody_else = testerchain.interface.w3.eth.accounts
|
||||
miner = PolicyAuthor(checksum_address=alice, policy_agent=policy_agent)
|
||||
return miner
|
||||
author = PolicyAuthor(checksum_address=alice, policy_agent=policy_agent)
|
||||
return author
|
||||
|
||||
def test_create_policy_author(self, testerchain, three_agents):
|
||||
token_agent, miner_agent, policy_agent = three_agents
|
||||
|
|
|
@ -5,7 +5,8 @@ from constant_sorrow import constants
|
|||
|
||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent
|
||||
from nucypher.blockchain.eth.constants import DISPATCHER_SECRET_LENGTH
|
||||
from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer
|
||||
from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer, \
|
||||
ContractDeployer
|
||||
from nucypher.blockchain.eth.interfaces import EthereumContractRegistry
|
||||
|
||||
|
||||
|
@ -41,40 +42,97 @@ def test_token_deployer_and_agent(testerchain):
|
|||
assert token_agent.contract_address == same_token_agent.contract_address
|
||||
assert token_agent == same_token_agent # __eq__
|
||||
|
||||
testerchain.interface._registry.clear()
|
||||
testerchain.interface.registry.clear()
|
||||
|
||||
|
||||
@pytest.mark.slow()
|
||||
def test_deploy_ethereum_contracts(testerchain):
|
||||
"""
|
||||
A bare minimum nucypher deployment fixture.
|
||||
"""
|
||||
|
||||
origin, *everybody_else = testerchain.interface.w3.eth.accounts
|
||||
|
||||
#
|
||||
# Nucypher Token
|
||||
#
|
||||
token_deployer = NucypherTokenDeployer(blockchain=testerchain, deployer_address=origin)
|
||||
assert token_deployer.deployer_address == origin
|
||||
|
||||
with pytest.raises(ContractDeployer.ContractDeploymentError):
|
||||
assert token_deployer.contract_address is constants.CONTRACT_NOT_DEPLOYED
|
||||
assert not token_deployer.is_armed
|
||||
assert not token_deployer.is_deployed
|
||||
|
||||
token_deployer.arm()
|
||||
assert token_deployer.is_armed
|
||||
|
||||
token_deployer.deploy()
|
||||
assert token_deployer.is_deployed
|
||||
assert len(token_deployer.contract_address) == 42
|
||||
|
||||
token_agent = NucypherTokenAgent(blockchain=testerchain)
|
||||
assert len(token_agent.contract_address) == 42
|
||||
assert token_agent.contract_address == token_deployer.contract_address
|
||||
|
||||
another_token_agent = token_deployer.make_agent()
|
||||
assert len(another_token_agent.contract_address) == 42
|
||||
assert another_token_agent.contract_address == token_deployer.contract_address == token_agent.contract_address
|
||||
|
||||
#
|
||||
# Miner Escrow
|
||||
#
|
||||
miners_escrow_secret = os.urandom(DISPATCHER_SECRET_LENGTH)
|
||||
miner_escrow_deployer = MinerEscrowDeployer(
|
||||
token_agent=token_agent,
|
||||
deployer_address=origin,
|
||||
secret_hash=testerchain.interface.w3.sha3(miners_escrow_secret))
|
||||
assert miner_escrow_deployer.deployer_address == origin
|
||||
|
||||
with pytest.raises(ContractDeployer.ContractDeploymentError):
|
||||
assert miner_escrow_deployer.contract_address is constants.CONTRACT_NOT_DEPLOYED
|
||||
assert not miner_escrow_deployer.is_armed
|
||||
assert not miner_escrow_deployer.is_deployed
|
||||
|
||||
miner_escrow_deployer.arm()
|
||||
assert miner_escrow_deployer.is_armed
|
||||
|
||||
miner_escrow_deployer.deploy()
|
||||
assert miner_escrow_deployer.is_deployed
|
||||
assert len(miner_escrow_deployer.contract_address) == 42
|
||||
|
||||
miner_agent = MinerAgent(token_agent=token_agent)
|
||||
assert len(miner_agent.contract_address) == 42
|
||||
assert miner_agent.contract_address == miner_escrow_deployer.contract_address
|
||||
|
||||
another_miner_agent = miner_escrow_deployer.make_agent()
|
||||
assert len(another_miner_agent.contract_address) == 42
|
||||
assert another_miner_agent.contract_address == miner_escrow_deployer.contract_address == miner_agent.contract_address
|
||||
|
||||
|
||||
#
|
||||
# Policy Manager
|
||||
#
|
||||
policy_manager_secret = os.urandom(DISPATCHER_SECRET_LENGTH)
|
||||
policy_manager_deployer = PolicyManagerDeployer(
|
||||
miner_agent=miner_agent,
|
||||
deployer_address=origin,
|
||||
secret_hash=testerchain.interface.w3.sha3(policy_manager_secret))
|
||||
assert policy_manager_deployer.deployer_address == origin
|
||||
|
||||
with pytest.raises(ContractDeployer.ContractDeploymentError):
|
||||
assert policy_manager_deployer.contract_address is constants.CONTRACT_NOT_DEPLOYED
|
||||
assert not policy_manager_deployer.is_armed
|
||||
assert not policy_manager_deployer.is_deployed
|
||||
|
||||
policy_manager_deployer.arm()
|
||||
assert policy_manager_deployer.is_armed
|
||||
|
||||
policy_manager_deployer.deploy()
|
||||
assert policy_manager_deployer.is_deployed
|
||||
assert len(policy_manager_deployer.contract_address) == 42
|
||||
|
||||
policy_agent = policy_manager_deployer.make_agent()
|
||||
assert len(policy_agent.contract_address) == 42
|
||||
assert policy_agent.contract_address == policy_manager_deployer.contract_address
|
||||
|
||||
# TODO: assert
|
||||
another_policy_agent = policy_manager_deployer.make_agent()
|
||||
assert len(another_policy_agent.contract_address) == 42
|
||||
assert another_policy_agent.contract_address == policy_manager_deployer.contract_address == policy_agent.contract_address
|
||||
|
|
|
@ -12,8 +12,7 @@ def test_contract_registry(tempfile_path):
|
|||
# Tests everything is as it should be when initially created
|
||||
test_registry = EthereumContractRegistry(registry_filepath=tempfile_path)
|
||||
|
||||
should_be_empty = test_registry.read()
|
||||
assert should_be_empty == []
|
||||
assert test_registry.read() == list()
|
||||
|
||||
# Test contract enrollment and dump_chain
|
||||
test_name = 'TestContract'
|
||||
|
@ -52,5 +51,5 @@ def test_contract_registry(tempfile_path):
|
|||
test_registry._EthereumContractRegistry__write(current_dataset)
|
||||
|
||||
# Check that searching for an unknown contract raises
|
||||
with pytest.raises(EthereumContractRegistry.IllegalRegistrar):
|
||||
with pytest.raises(EthereumContractRegistry.IllegalRegistry):
|
||||
test_registry.search(contract_address=test_addr)
|
||||
|
|
|
@ -3,7 +3,7 @@ from nucypher.blockchain.eth.deployers import NucypherTokenDeployer
|
|||
|
||||
def test_chain_creation(testerchain):
|
||||
# Ensure we are testing on the correct network...
|
||||
assert testerchain.interface.network == 'tester'
|
||||
assert 'tester' in testerchain.interface.provider_uri
|
||||
|
||||
# ... and that there are already some blocks mined
|
||||
assert testerchain.interface.w3.eth.blockNumber >= 0
|
||||
|
|
|
@ -114,7 +114,7 @@ def test_character_blockchain_power(testerchain):
|
|||
power.sign_message(b'test')
|
||||
|
||||
# Test lockAccount call
|
||||
del (power)
|
||||
del power
|
||||
|
||||
|
||||
"""
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
from eth_keys.datatypes import Signature as EthSignature
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.characters.unlawful import Vladimir
|
||||
from nucypher.crypto.powers import SigningPower, CryptoPower
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
from nucypher.utilities.sandbox.ursula import make_federated_ursulas
|
||||
|
||||
|
@ -71,7 +74,7 @@ def test_vladimir_cannot_verify_interface_with_ursulas_signing_key(blockchain_ur
|
|||
vladimir = Vladimir.from_target_ursula(his_target, claim_signing_key=True)
|
||||
|
||||
# Vladimir can substantiate the stamp using his own ether address...
|
||||
vladimir.substantiate_stamp()
|
||||
vladimir.substantiate_stamp(passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir.stamp_is_valid()
|
||||
|
||||
# Now, even though his public signing key matches Ursulas...
|
||||
|
@ -102,7 +105,7 @@ def test_vladimir_uses_his_own_signing_key(blockchain_alice, blockchain_ursulas)
|
|||
signature = vladimir._crypto_power.power_ups(SigningPower).sign(vladimir.timestamp_bytes() + message)
|
||||
vladimir._interface_signature_object = signature
|
||||
|
||||
vladimir.substantiate_stamp()
|
||||
vladimir.substantiate_stamp(passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
# With this slightly more sophisticated attack, his metadata does appear valid.
|
||||
vladimir.validate_metadata()
|
||||
|
|
|
@ -4,15 +4,41 @@ from click.testing import CliRunner
|
|||
from cli.main import cli
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_list():
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_list(testerchain):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['accounts', 'list'], catch_exceptions=False)
|
||||
account = testerchain.interface.w3.eth.accounts[0]
|
||||
args = '--dev --provider-uri tester://pyevm accounts list'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert account in result.output
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_balance(testerchain):
|
||||
runner = CliRunner()
|
||||
account = testerchain.interface.w3.eth.accounts[0]
|
||||
args = '--dev --provider-uri tester://pyevm accounts balance'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert 'Tokens:' in result.output
|
||||
assert 'ETH:' in result.output
|
||||
assert account in result.output
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_transfer_eth(testerchain):
|
||||
runner = CliRunner()
|
||||
account = testerchain.interface.w3.eth.accounts[1]
|
||||
args = '--dev --provider-uri tester://pyevm accounts transfer-eth'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False, input=account+'\n100\nY\n')
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_balance():
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_transfer_tokens(testerchain):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['accounts', 'balance'], catch_exceptions=False)
|
||||
account = testerchain.interface.w3.eth.accounts[2]
|
||||
args = '--provider-uri tester://pyevm accounts transfer-tokens'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False, input=account+'\n100\nY\n')
|
||||
assert result.exit_code == 0
|
||||
|
|
|
@ -6,7 +6,6 @@ import shutil
|
|||
from click.testing import CliRunner
|
||||
|
||||
from cli.main import cli
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, DEFAULT_CONFIG_FILE_LOCATION
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
|
||||
|
||||
|
@ -22,12 +21,12 @@ def test_initialize_configuration_directory(custom_filepath):
|
|||
runner = CliRunner()
|
||||
|
||||
# Use the system temporary storage area
|
||||
result = runner.invoke(cli, ['configure', 'init', '--temp'], input='Y', catch_exceptions=False)
|
||||
result = runner.invoke(cli, ['--dev', 'configure', 'install', '--no-registry'], input='Y', catch_exceptions=False)
|
||||
assert '/tmp' in result.output, "Configuration not in system temporary directory"
|
||||
assert NodeConfiguration._NodeConfiguration__TEMP_CONFIGURATION_DIR_PREFIX in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
args = ['configure', 'init', '--config-root', custom_filepath]
|
||||
args = [ '--config-root', custom_filepath, 'configure', 'install', '--no-registry']
|
||||
result = runner.invoke(cli, args, input='Y', catch_exceptions=False)
|
||||
assert '[y/N]' in result.output, "'configure init' did not prompt the user before attempting to write files"
|
||||
assert '/tmp' in result.output, "Configuration not in system temporary directory"
|
||||
|
@ -40,7 +39,7 @@ def test_initialize_configuration_directory(custom_filepath):
|
|||
with pytest.raises(NodeConfiguration.ConfigurationError):
|
||||
_result = runner.invoke(cli, args, input='Y', catch_exceptions=False)
|
||||
|
||||
args = ['configure', 'destroy', '--config-root', custom_filepath]
|
||||
args = ['--config-root', custom_filepath, 'configure', 'destroy']
|
||||
result = runner.invoke(cli, args, input='Y', catch_exceptions=False)
|
||||
assert '[y/N]' in result.output
|
||||
assert '/tmp' in result.output, "Configuration not in system temporary directory"
|
||||
|
@ -55,49 +54,22 @@ def test_initialize_configuration_directory(custom_filepath):
|
|||
def test_validate_runtime_filepaths(custom_filepath):
|
||||
runner = CliRunner()
|
||||
|
||||
args = ['configure', 'init', '--config-root', custom_filepath]
|
||||
args = ['--config-root', custom_filepath, 'configure', 'install', '--no-registry']
|
||||
result = runner.invoke(cli, args, input='Y', catch_exceptions=False)
|
||||
result = runner.invoke(cli, ['configure', 'validate',
|
||||
'--config-root', custom_filepath,
|
||||
'--filesystem'], catch_exceptions=False)
|
||||
result = runner.invoke(cli, ['--config-root', custom_filepath,
|
||||
'configure', 'validate',
|
||||
'--filesystem',
|
||||
'--no-registry'], catch_exceptions=False)
|
||||
assert custom_filepath in result.output
|
||||
assert 'Valid' in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Remove the known nodes dir to "corrupt" the tree
|
||||
shutil.rmtree(os.path.join(custom_filepath, 'known_nodes'))
|
||||
result = runner.invoke(cli, ['configure', 'validate',
|
||||
'--config-root', custom_filepath,
|
||||
'--filesystem'], catch_exceptions=False)
|
||||
result = runner.invoke(cli, ['--config-root', custom_filepath,
|
||||
'configure', 'validate',
|
||||
'--filesystem',
|
||||
'--no-registry'], catch_exceptions=False)
|
||||
assert custom_filepath in result.output
|
||||
assert 'Invalid' in result.output
|
||||
assert result.exit_code == 0 # TODO: exit differently for invalidity?
|
||||
|
||||
|
||||
@pytest.mark.skip("To be implemented")
|
||||
def test_write_default_configuration_file():
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(cli, ['configure', 'init', '--temp'], input='Y', catch_exceptions=False)
|
||||
assert DEFAULT_CONFIG_ROOT in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
assert os.path.isfile(DEFAULT_CONFIG_FILE_LOCATION)
|
||||
with open(DEFAULT_CONFIG_FILE_LOCATION, 'r') as ini_file:
|
||||
assert ini_file.read()
|
||||
config_payload = ini_file.read()
|
||||
assert '[nucypher]' in config_payload
|
||||
|
||||
result = runner.invoke(cli, ['configure', 'destroy'], input='Y', catch_exceptions=False)
|
||||
assert DEFAULT_CONFIG_ROOT in result.output
|
||||
assert result.exit_code == 0
|
||||
assert not os.path.isfile(DEFAULT_CONFIG_FILE_LOCATION)
|
||||
|
||||
|
||||
@pytest.mark.skip("To be implemented")
|
||||
def test_validate_configuration_file():
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(cli, ['configure', 'validate'], catch_exceptions=False)
|
||||
assert 'Valid'.casefold() in result.output
|
||||
assert result.exit_code == 0
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from cli.main import cli
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_finnegans_wake_demo():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['simulate', 'demo', '--federated-only'], catch_exceptions=False)
|
||||
|
||||
assert result.exit_code == 0
|
|
@ -1,28 +1,30 @@
|
|||
import time
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
import time
|
||||
from click.testing import CliRunner
|
||||
from twisted.internet import threads
|
||||
from twisted.internet.error import CannotListenError
|
||||
|
||||
from cli.main import cli
|
||||
from nucypher.characters.base import Learner
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Handle second call to reactor.run, or multiproc")
|
||||
@pytest.mark.skip()
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_run_lone_federated_default_ursula():
|
||||
|
||||
args = ['run_ursula',
|
||||
'--dev',
|
||||
args = ['--dev',
|
||||
'--federated-only',
|
||||
'ursula', 'run',
|
||||
'--rest-port', '9999', # TODO: use different port to avoid premature ConnectionError with many test runs?
|
||||
'--no-reactor'
|
||||
]
|
||||
|
||||
runner = CliRunner()
|
||||
result = yield threads.deferToThread(runner.invoke(cli, args, catch_exceptions=False))
|
||||
# result = runner.invoke(cli, args, catch_exceptions=False) # TODO: Handle second call to reactor.run
|
||||
result = yield threads.deferToThread(runner.invoke, cli, args, catch_exceptions=False, input=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD+'\n')
|
||||
|
||||
alone = "WARNING - Can't learn right now: Need some nodes to start learning from."
|
||||
time.sleep(Learner._SHORT_LEARNING_DELAY)
|
||||
assert alone in result.output
|
||||
|
@ -30,18 +32,4 @@ def test_run_lone_federated_default_ursula():
|
|||
|
||||
# Cannot start another Ursula on the same REST port
|
||||
with pytest.raises(CannotListenError):
|
||||
_result = runner.invoke(cli, args, catch_exceptions=False)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Handle second call to reactor.run, or multiproc")
|
||||
def test_federated_ursula_with_manual_teacher_uri():
|
||||
args = ['run_ursula',
|
||||
'--dev',
|
||||
'--federated-only',
|
||||
'--rest-port', '9091', # TODO: Test Constant?
|
||||
'--teacher-uri', 'localhost:{}'.format(UrsulaConfiguration.DEFAULT_REST_PORT)]
|
||||
|
||||
# TODO: Handle second call to reactor.run
|
||||
runner = CliRunner()
|
||||
result_with_teacher = runner.invoke(cli, args, catch_exceptions=False)
|
||||
assert result_with_teacher.exit_code == 0
|
||||
_result = runner.invoke(cli, args, catch_exceptions=False, input=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from cli.main import cli
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_init():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['simulate', 'init'], catch_exceptions=False)
|
||||
|
||||
assert result.exit_code == 0
|
||||
# assert 'Debug mode is on' in result.output
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_deploy():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['simulate', 'deploy'], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_swarm():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['simulate', 'swarm'], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_status():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['simulate', 'status'], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_stop():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['simulate', 'stop'], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
|
@ -23,7 +23,7 @@ from nucypher.keystore.db import Base
|
|||
from nucypher.keystore.keypairs import SigningKeypair
|
||||
from nucypher.utilities.sandbox.blockchain import TesterBlockchain, token_airdrop
|
||||
from nucypher.utilities.sandbox.constants import (DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT)
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT, DEVELOPMENT_ETH_AIRDROP_AMOUNT)
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
from nucypher.utilities.sandbox.ursula import make_federated_ursulas, make_decentralized_ursulas
|
||||
|
||||
|
@ -54,7 +54,8 @@ def temp_config_root(temp_dir_path):
|
|||
"""
|
||||
default_node_config = NodeConfiguration(temp=True,
|
||||
auto_initialize=False,
|
||||
config_root=temp_dir_path)
|
||||
config_root=temp_dir_path,
|
||||
no_seed_registry=True)
|
||||
yield default_node_config.config_root
|
||||
default_node_config.cleanup()
|
||||
|
||||
|
@ -80,7 +81,8 @@ def ursula_federated_test_config():
|
|||
start_learning_now=False,
|
||||
abort_on_learning_error=True,
|
||||
federated_only=True,
|
||||
network_middleware=MockRestMiddleware())
|
||||
network_middleware=MockRestMiddleware(),
|
||||
no_seed_registry=True)
|
||||
yield ursula_config
|
||||
ursula_config.cleanup()
|
||||
|
||||
|
@ -96,7 +98,8 @@ def ursula_decentralized_test_config(three_agents):
|
|||
abort_on_learning_error=True,
|
||||
miner_agent=miner_agent,
|
||||
federated_only=False,
|
||||
network_middleware=MockRestMiddleware())
|
||||
network_middleware=MockRestMiddleware(),
|
||||
no_seed_registry=True)
|
||||
yield ursula_config
|
||||
ursula_config.cleanup()
|
||||
|
||||
|
@ -109,7 +112,8 @@ def alice_federated_test_config(federated_ursulas):
|
|||
network_middleware=MockRestMiddleware(),
|
||||
known_nodes=federated_ursulas,
|
||||
federated_only=True,
|
||||
abort_on_learning_error=True)
|
||||
abort_on_learning_error=True,
|
||||
no_seed_registry=True)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
@ -126,7 +130,8 @@ def alice_blockchain_test_config(blockchain_ursulas, three_agents):
|
|||
policy_agent=policy_agent,
|
||||
known_nodes=blockchain_ursulas,
|
||||
abort_on_learning_error=True,
|
||||
checksum_address=alice_address)
|
||||
checksum_address=alice_address,
|
||||
no_seed_registry=True)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
@ -138,7 +143,8 @@ def bob_federated_test_config():
|
|||
network_middleware=MockRestMiddleware(),
|
||||
start_learning_now=False,
|
||||
abort_on_learning_error=True,
|
||||
federated_only=True)
|
||||
federated_only=True,
|
||||
no_seed_registry=True)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
@ -155,7 +161,8 @@ def bob_blockchain_test_config(blockchain_ursulas, three_agents):
|
|||
known_nodes=blockchain_ursulas,
|
||||
start_learning_now=False,
|
||||
abort_on_learning_error=True,
|
||||
federated_only=False)
|
||||
federated_only=False,
|
||||
no_seed_registry=True)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
@ -299,14 +306,13 @@ def testerchain(solidity_compiler):
|
|||
"""
|
||||
https: // github.com / ethereum / eth - tester # available-backends
|
||||
"""
|
||||
|
||||
temp_registrar = TemporaryEthereumContractRegistry()
|
||||
_temp_registry = TemporaryEthereumContractRegistry()
|
||||
|
||||
# Use the the custom provider and registrar to init an interface
|
||||
|
||||
deployer_interface = BlockchainDeployerInterface(compiler=solidity_compiler, # freshly recompile if not None
|
||||
registry=temp_registrar,
|
||||
provider_uri='pyevm://tester')
|
||||
registry=_temp_registry,
|
||||
provider_uri='tester://pyevm')
|
||||
|
||||
# Create the blockchain
|
||||
testerchain = TesterBlockchain(interface=deployer_interface,
|
||||
|
@ -315,6 +321,7 @@ def testerchain(solidity_compiler):
|
|||
|
||||
origin, *everyone = testerchain.interface.w3.eth.accounts
|
||||
deployer_interface.deployer_address = origin # Set the deployer address from a freshly created test account
|
||||
testerchain.ether_airdrop(amount=1000000000) # TODO: Use test constant
|
||||
|
||||
yield testerchain
|
||||
testerchain.sever_connection()
|
||||
|
@ -335,7 +342,7 @@ def three_agents(testerchain):
|
|||
token_deployer.arm()
|
||||
token_deployer.deploy()
|
||||
|
||||
token_agent = token_deployer.make_agent()
|
||||
token_agent = token_deployer.make_agent() # 1
|
||||
|
||||
miners_escrow_secret = os.urandom(DISPATCHER_SECRET_LENGTH)
|
||||
miner_escrow_deployer = MinerEscrowDeployer(
|
||||
|
@ -345,7 +352,7 @@ def three_agents(testerchain):
|
|||
miner_escrow_deployer.arm()
|
||||
miner_escrow_deployer.deploy()
|
||||
|
||||
miner_agent = miner_escrow_deployer.make_agent()
|
||||
miner_agent = miner_escrow_deployer.make_agent() # 2
|
||||
|
||||
policy_manager_secret = os.urandom(DISPATCHER_SECRET_LENGTH)
|
||||
policy_manager_deployer = PolicyManagerDeployer(
|
||||
|
@ -355,6 +362,6 @@ def three_agents(testerchain):
|
|||
policy_manager_deployer.arm()
|
||||
policy_manager_deployer.deploy()
|
||||
|
||||
policy_agent = policy_manager_deployer.make_agent()
|
||||
policy_agent = policy_manager_deployer.make_agent() # 3
|
||||
|
||||
return token_agent, miner_agent, policy_agent
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
from hendrix.experience import crosstown_traffic
|
||||
from hendrix.utils.test_utils import crosstownTaskListDecoratorFactory
|
||||
|
@ -7,6 +9,7 @@ from nucypher.characters.lawful import Ursula
|
|||
from nucypher.characters.unlawful import Vladimir
|
||||
from nucypher.crypto.api import keccak_digest
|
||||
from nucypher.crypto.powers import SigningPower
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
|
||||
|
||||
|
@ -122,13 +125,12 @@ def test_vladimir_illegal_interface_key_does_not_propagate(blockchain_ursulas):
|
|||
ursulas = list(blockchain_ursulas)
|
||||
ursula_whom_vladimir_will_imitate, other_ursula = ursulas[0], ursulas[1]
|
||||
|
||||
# This Ursula is totally legit...
|
||||
ursula_whom_vladimir_will_imitate.verify_node(MockRestMiddleware(),
|
||||
accept_federated_only=True)
|
||||
|
||||
# ...until Vladimir sees her on the network and tries to use her public information.
|
||||
# Vladimir sees Ursula on the network and tries to use her public information.
|
||||
vladimir = Vladimir.from_target_ursula(ursula_whom_vladimir_will_imitate)
|
||||
|
||||
# This Ursula is totally legit...
|
||||
ursula_whom_vladimir_will_imitate.verify_node(MockRestMiddleware(), accept_federated_only=True)
|
||||
|
||||
learning_callers = []
|
||||
crosstown_traffic.decorator = crosstownTaskListDecoratorFactory(learning_callers)
|
||||
|
||||
|
@ -167,7 +169,7 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali
|
|||
message = vladimir._signable_interface_info_message()
|
||||
signature = vladimir._crypto_power.power_ups(SigningPower).sign(message)
|
||||
|
||||
vladimir.substantiate_stamp()
|
||||
vladimir.substantiate_stamp(passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir._interface_signature_object = signature
|
||||
|
||||
class FakeArrangement:
|
||||
|
|
Loading…
Reference in New Issue