Merge pull request #465 from KPrasch/deploy-cli

CLI: The balance of two operating modes
pull/478/head
K Prasch 2018-10-09 11:24:36 -07:00 committed by GitHub
commit 3434938618
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1708 additions and 1210 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
"""Nucypher Token and Miner constants."""
NUCYPHER_GAS_LIMIT = 5000000 # TODO: move elsewhere?
#
# Dispatcher

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -114,7 +114,7 @@ def test_character_blockchain_power(testerchain):
power.sign_message(b'test')
# Test lockAccount call
del (power)
del power
"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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