diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index f0b0c5bcc..c74371150 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -89,7 +89,7 @@ class NucypherTokenActor: self.checksum_public_address = checksum_address # type: str if blockchain is None: - blockchain = Blockchain.connect() + blockchain = Blockchain.connect() # Attempt to connect self.blockchain = blockchain self.token_agent = NucypherTokenAgent() diff --git a/nucypher/blockchain/eth/chains.py b/nucypher/blockchain/eth/chains.py index 355732d8b..62f0f3df7 100644 --- a/nucypher/blockchain/eth/chains.py +++ b/nucypher/blockchain/eth/chains.py @@ -39,9 +39,12 @@ class Blockchain: class ConnectionNotEstablished(RuntimeError): pass - def __init__(self, interface: Union[BlockchainInterface, BlockchainDeployerInterface] = None): + def __init__(self, + provider_process=None, + interface: Union[BlockchainInterface, BlockchainDeployerInterface] = None): self.log = Logger("blockchain") + self.__provider_process = provider_process # Default interface if interface is None: @@ -56,8 +59,8 @@ class Blockchain: def __repr__(self): class_name = self.__class__.__name__ - r = "{}(interface={})" - return r.format(class_name, self.__interface) + r = "{}(interface={}, process={})" + return r.format(class_name, self.__interface, self.__provider_process) @property def interface(self) -> Union[BlockchainInterface, BlockchainDeployerInterface]: @@ -65,6 +68,7 @@ class Blockchain: @classmethod def connect(cls, + provider_process=None, provider_uri: str = None, registry: EthereumContractRegistry = None, deployer: bool = False, @@ -74,6 +78,8 @@ class Blockchain: fetch_registry: bool = True ) -> 'Blockchain': + log = Logger('blockchain-init') + if cls._instance is NO_BLOCKCHAIN_AVAILABLE: if not registry and fetch_registry: from nucypher.config.node import NodeConfiguration @@ -85,15 +91,23 @@ class Blockchain: else: registry = registry or EthereumContractRegistry() + # Spawn child process + if provider_process: + provider_process.start() + else: + log.info(f"Using external Web3 Provider '{provider_uri}'") + 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) if poa is True: + log.debug('Injecting POA middleware at layer 0') interface.w3.middleware_onion.inject(geth_poa_middleware, layer=0) - cls._instance = cls(interface=interface) + cls._instance = cls(interface=interface, provider_process=provider_process) else: + if provider_uri is not None: existing_uri = cls._instance.interface.provider_uri if (existing_uri != provider_uri) and not force: @@ -107,6 +121,11 @@ class Blockchain: return cls._instance + @classmethod + def disconnect(cls): + if cls._instance.__provider_process: + cls._instance.__provider_process.stop() + def get_contract(self, name: str) -> Contract: """ Gets an existing contract from the registry, or raises UnknownContract diff --git a/nucypher/cli/characters/felix.py b/nucypher/cli/characters/felix.py index 82748bd1e..cbcdabdf3 100644 --- a/nucypher/cli/characters/felix.py +++ b/nucypher/cli/characters/felix.py @@ -9,6 +9,7 @@ from nucypher.cli.config import nucypher_click_config from nucypher.cli.types import NETWORK_PORT, EXISTING_READABLE_FILE, EIP55_CHECKSUM_ADDRESS from nucypher.config.characters import FelixConfiguration from nucypher.config.constants import DEFAULT_CONFIG_ROOT +from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION @click.command() @@ -54,14 +55,12 @@ def felix(click_config, force): click.clear() - if not click_config.quiet: click.secho(FELIX_BANNER.format(checksum_address or '')) - ETH_NODE = None # TODO: Make constant + ETH_NODE = NO_BLOCKCHAIN_CONNECTION.bool_value(False) if geth: ETH_NODE = NuCypherGethDevnetProcess(config_root=config_root) - ETH_NODE.start() provider_uri = ETH_NODE.provider_uri if action == "init": @@ -76,17 +75,26 @@ def felix(click_config, # Acquire Keyring Password new_password = click_config.get_password(confirm=True) - new_felix_config = FelixConfiguration.generate(password=new_password, - config_root=config_root, - rest_host=host, - rest_port=discovery_port, - db_filepath=db_filepath, - domains={network} if network else None, - checksum_public_address=checksum_address, - download_registry=not no_registry, - registry_filepath=registry_filepath, - provider_uri=provider_uri, - poa=poa) + + try: + new_felix_config = FelixConfiguration.generate(password=new_password, + config_root=config_root, + rest_host=host, + rest_port=discovery_port, + db_filepath=db_filepath, + domains={network} if network else None, + checksum_public_address=checksum_address, + download_registry=not no_registry, + registry_filepath=registry_filepath, + provider_uri=provider_uri, + provider_process=ETH_NODE, + poa=poa) + except Exception as e: + if click_config.debug: + raise + else: + click.secho(str(e), fg='red', bold=True) + raise click.Abort # Paint Help painting.paint_new_installation_help(new_configuration=new_felix_config, @@ -103,6 +111,7 @@ def felix(click_config, felix_config = FelixConfiguration.from_configuration_file(filepath=config_file, domains=domains, registry_filepath=registry_filepath, + provider_process=ETH_NODE, provider_uri=provider_uri, rest_host=host, rest_port=port, @@ -165,14 +174,15 @@ ETH ........ {str(eth_balance)} elif action == 'run': # Start web services - FELIX.start(host=host, - port=port, - web_services=not dry_run, - distribution=True, - crash_on_error=click_config.debug) + try: + FELIX.blockchain.connect() + FELIX.start(host=host, + port=port, + web_services=not dry_run, + distribution=True, + crash_on_error=click_config.debug) + finally: + FELIX.blockchain.disconnect() else: raise click.BadArgumentUsage("No such argument {}".format(action)) - - if ETH_NODE: - ETH_NODE.stop() diff --git a/nucypher/config/node.py b/nucypher/config/node.py index 85424726f..3de6008d1 100644 --- a/nucypher/config/node.py +++ b/nucypher/config/node.py @@ -148,7 +148,7 @@ class NodeConfiguration(ABC): # Blockchain poa: bool = False, provider_uri: str = None, - geth: bool = False, + provider_process = None, # Registry registry_source: str = None, @@ -249,7 +249,7 @@ class NodeConfiguration(ABC): # self.poa = poa self.provider_uri = provider_uri or self.DEFAULT_PROVIDER_URI - self.geth = geth + self.provider_process = provider_process or NO_BLOCKCHAIN_CONNECTION self.blockchain = NO_BLOCKCHAIN_CONNECTION self.accounts = NO_BLOCKCHAIN_CONNECTION @@ -286,8 +286,6 @@ class NodeConfiguration(ABC): return node_config def __write(self, password: str): - if not self.federated_only: - self.connect_to_blockchain() # Needed for access to ethereum node addresses and NC key signing _new_installation_path = self.initialize(password=password, download_registry=self.download_registry) _configuration_filepath = self.to_configuration_file(filepath=self.config_file_location) @@ -315,14 +313,16 @@ class NodeConfiguration(ABC): """ if self.federated_only: raise NodeConfiguration.ConfigurationError("Cannot connect to blockchain in federated mode") - self.blockchain = Blockchain.connect(provider_uri=self.provider_uri, compile=recompile_contracts, poa=self.poa, - fetch_registry=True) + fetch_registry=True, + provider_process=self.provider_process) + # Read Ethereum Node Keyring self.accounts = self.blockchain.interface.w3.eth.accounts + # Add Ethereum Peer if enode: if self.blockchain.interface.client_version == 'geth': self.blockchain.interface.w3.geth.admin.addPeer(enode) @@ -376,7 +376,11 @@ class NodeConfiguration(ABC): return payload @classmethod - def from_configuration_file(cls, filepath: str = None, **overrides) -> 'NodeConfiguration': + def from_configuration_file(cls, + filepath: str = None, + provider_process=None, + **overrides) -> 'NodeConfiguration': + """Initialize a NodeConfiguration from a JSON file.""" from nucypher.config.storages import NodeStorage @@ -414,7 +418,9 @@ class NodeConfiguration(ABC): overrides = {k: v for k, v in overrides.items() if v is not None} # Instantiate from merged params - node_configuration = cls(config_file_location=filepath, **{**payload, **overrides}) + node_configuration = cls(config_file_location=filepath, + provider_process=provider_process, + **{**payload, **overrides}) return node_configuration @@ -569,6 +575,13 @@ class NodeConfiguration(ABC): # Generate Installation Subdirectories self._cache_runtime_filepaths() + # + # Blockchain + # + if not self.federated_only: + if self.provider_process: + self.provider_process.initialize_blockchain() + # # Node Storage # @@ -621,27 +634,26 @@ class NodeConfiguration(ABC): *args, **kwargs) def write_keyring(self, password: str, **generation_kwargs) -> NucypherKeyring: - # Get or create wallet address - + # Note: It is assumed the blockchain is not yet available. if not self.federated_only and not self.checksum_public_address: - # TODO: Bury deeper in keying and powers - try: - checksum_address = self.blockchain.interface.w3.eth.accounts[0] # etherbase + # + # Integrated Provider Process + # - except IndexError: + if self.provider_process: - data_dir = os.path.join(self.config_root, '.ethereum', NuCypherGethDevnetProcess._CHAIN_NAME) - if not os.path.exists(data_dir): - os.mkdir(data_dir) + if not os.path.exists(self.provider_process.data_dir): + os.mkdir(self.provider_process.data_dir) - client_version = self.blockchain.interface.w3.clientVersion - if 'Geth' in client_version: - checksum_address = NuCypherGethDevnetProcess.ensure_account_exists(password=password, - data_dir=data_dir) + # Get or create wallet address (geth etherbase) + checksum_address = self.provider_process.ensure_account_exists(password=password) - else: - raise RuntimeError("THIS IS A TEMPORARY DEBUGGING EXCEPTION") # TODO + else: + # Manual Web3 Provider, We assume is already running and available + self.connect_to_blockchain() + # TODO: Create etherbase over RPC instead + raise self.ConfigurationError(f'Web3 provider "{self.provider_uri}" does not have any accounts') # Addresses read from some node keyrings are *not* returned in checksum format. checksum_address = to_checksum_address(checksum_address) diff --git a/nucypher/utilities/sandbox/blockchain.py b/nucypher/utilities/sandbox/blockchain.py index 5061041b6..3bd4b7d54 100644 --- a/nucypher/utilities/sandbox/blockchain.py +++ b/nucypher/utilities/sandbox/blockchain.py @@ -19,7 +19,7 @@ along with nucypher. If not, see . import os from typing import List, Tuple, Dict -from constant_sorrow.constants import NO_BLOCKCHAIN_AVAILABLE +from constant_sorrow.constants import NO_BLOCKCHAIN_AVAILABLE, TEST_PROVIDER_ON_MAIN_PROCESS from twisted.logger import Logger from web3 import Web3 from web3.middleware import geth_poa_middleware @@ -86,7 +86,7 @@ class TesterBlockchain(Blockchain): if test_accounts is None: test_accounts = self._default_test_accounts - super().__init__(*args, **kwargs) + super().__init__(provider_process=TEST_PROVIDER_ON_MAIN_PROCESS, *args, **kwargs) self.log = Logger("test-blockchain") self.attach_middleware(w3=self.interface.w3, poa=poa, free_transactions=free_transactions)