def __init__(self, domains: Set = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = NO_BLOCKCHAIN_CONNECTION.bool_value( False), network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, keyring_root: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, *args, **kwargs) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Operating Mode if hasattr(self, '_interface_class' ): # TODO: have argument about meaning of 'lawful' # and whether maybe only Lawful characters have an interface self.interface = self._interface_class(character=self) if is_me: if not known_node_class: # Once in a while, in tests or demos, we init a plain Character who doesn't already know about its node class. from nucypher.characters.lawful import Ursula known_node_class = Ursula # If we're federated only, we assume that all other nodes in our domain are as well. known_node_class.set_federated_mode(federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or provider_uri: raise ValueError( f"Cannot init federated-only character with {registry or provider_uri}." ) self.federated_only = bool(federated_only) # type: bool # # Powers # # Derive powers from keyring if keyring_root and keyring: if keyring_root != keyring.keyring_root: raise ValueError("Inconsistent keyring root directory path") if keyring: keyring_root, checksum_address = keyring.keyring_root, keyring.checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring_root = keyring_root self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) self._checksum_address = checksum_address # Fleet and Blockchain Connection (Everyone) if not domains: domains = {CharacterConfiguration.DEFAULT_DOMAIN} # # Self-Character # if is_me: self.treasure_maps = {} # type: dict # # Signing Power # self.signer = signer try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Blockchain # self.provider_uri = provider_uri if not self.federated_only: self.registry = registry or InMemoryContractRegistry.from_latest_publication( network=list(domains)[0]) #TODO: #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware( registry=self.registry) # # Learner # Learner.__init__(self, domains=domains, network_middleware=self.network_middleware, node_class=known_node_class, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stranger-Character." ) if registry is not None: raise TypeError( "Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keyring_root = STRANGER self.network_middleware = STRANGER # # Decentralized # if not federated_only: self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower # # Federated # elif federated_only: try: self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION if checksum_address: # We'll take a checksum address, as long as it matches their singing key if not checksum_address == self.checksum_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity( error.format(checksum_address)) # # Nicknames # if self._checksum_address is NO_BLOCKCHAIN_CONNECTION and not self.federated_only and not is_me: # Sometimes we don't care about the nickname. For example, if Alice is granting to Bob, she usually # doesn't know or care about his wallet. Maybe this needs to change? # Currently, if this is a stranger and there's no blockchain connection, we assign NO_NICKNAME: self.nickname = self.nickname_metadata = NO_NICKNAME else: try: self.nickname, self.nickname_metadata = nickname_from_seed( self.checksum_address) except SigningPower.not_found_error: # TODO: Handle NO_BLOCKCHAIN_CONNECTION more coherently - #1547 if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL
def __init__(self, domain: str = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = None, network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, include_self_in_the_state: bool = False, *args, **kwargs) -> None: """ A participant in the cryptological drama (a screenplay, if you like) of NuCypher. Characters can represent users, nodes, wallets, offline devices, or other objects of varying levels of abstraction. The Named Characters use this class as a Base, and achieve their individuality from additional methods and PowerUps. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Prologue of the federation # # FIXME: excuse me... can I speak to the manager? if is_me: # If this is a federated-is_me-character, assume everyone else is too. self._set_known_node_class(known_node_class, federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or provider_uri: raise ValueError( f"Cannot init federated-only character with {registry or provider_uri}." ) self.federated_only: bool = federated_only ########################################## # # Keys & Powers # if keyring: keyring_root, keyring_checksum_address = keyring.keyring_root, keyring.checksum_address if checksum_address and (keyring_checksum_address != checksum_address): raise ValueError( f"Provided checksum address {checksum_address} " f"does not match character's keyring checksum address {keyring_checksum_address}" ) checksum_address = keyring_checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) # # Self # if is_me: # Signing Power self.signer = signer try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # Blockchainy if not self.federated_only: self.provider_uri = provider_uri # TODO: Implicit / lazy blockchain connection here? # if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri): # BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri) self.registry = registry or InMemoryContractRegistry.from_latest_publication( network=domain) # See #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware( registry=self.registry) # Learner Learner.__init__( self, domain=domain, network_middleware=self.network_middleware, node_class=known_node_class, include_self_in_the_state=include_self_in_the_state, *args, **kwargs) if self.federated_only: try: derived_federated_address = self.derive_federated_address() except NoSigningPower: derived_federated_address = NO_SIGNING_POWER.bool_value( False) if checksum_address and (checksum_address != derived_federated_address): raise ValueError( f"Provided checksum address {checksum_address} " f"does not match federated character's verifying key {derived_federated_address}" ) checksum_address = derived_federated_address self.checksum_address = checksum_address # # Stranger # else: if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stranger-Character." ) if registry is not None: raise TypeError( "Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keyring_root = STRANGER self.network_middleware = STRANGER self.checksum_address = checksum_address self.__setup_nickname(is_me=is_me) # Character Control # TODO: have argument about meaning of 'lawful' and whether maybe only Lawful characters have an interface if hasattr(self, '_interface_class'): # Controller Interface self.interface = self._interface_class(character=self) self.controller = NO_CONTROL_PROTOCOL
class CharacterConfiguration(BaseConfiguration): """ 'Sideways Engagement' of Character classes; a reflection of input parameters. """ VERSION = 1 # bump when static payload scheme changes CHARACTER_CLASS = NotImplemented DEFAULT_CONTROLLER_PORT = NotImplemented DEFAULT_DOMAIN = NetworksInventory.DEFAULT DEFAULT_NETWORK_MIDDLEWARE = RestMiddleware TEMP_CONFIGURATION_DIR_PREFIX = 'tmp-nucypher' # Gas DEFAULT_GAS_STRATEGY = 'fast' def __init__(self, # Base config_root: str = None, filepath: str = None, # Mode dev_mode: bool = False, federated_only: bool = False, # Identity checksum_address: str = None, crypto_power: CryptoPower = None, # Keyring keyring: NucypherKeyring = None, keyring_root: str = None, # Learner learn_on_same_thread: bool = False, abort_on_learning_error: bool = False, start_learning_now: bool = True, # Network controller_port: int = None, domains: Set[str] = None, # TODO: Mapping between learning domains and "registry" domains - #1580 interface_signature: Signature = None, network_middleware: RestMiddleware = None, # Node Storage known_nodes: set = None, node_storage: NodeStorage = None, reload_metadata: bool = True, save_metadata: bool = True, # Blockchain poa: bool = False, light: bool = False, sync: bool = False, provider_uri: str = None, provider_process=None, gas_strategy: Union[Callable, str] = DEFAULT_GAS_STRATEGY, # Registry registry: BaseContractRegistry = None, registry_filepath: str = None, emitter=None, ): self.log = Logger(self.__class__.__name__) UNINITIALIZED_CONFIGURATION.bool_value(False) # Identity # NOTE: NodeConfigurations can only be used with Self-Characters self.is_me = True self.checksum_address = checksum_address # Network self.controller_port = controller_port or self.DEFAULT_CONTROLLER_PORT self.network_middleware = network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE() self.interface_signature = interface_signature # Keyring self.crypto_power = crypto_power self.keyring = keyring or NO_KEYRING_ATTACHED self.keyring_root = keyring_root or UNINITIALIZED_CONFIGURATION # Contract Registry if registry and registry_filepath: if registry.filepath != registry_filepath: error = f"Inconsistent registry filepaths for '{registry.filepath}' and '{registry_filepath}'." raise ValueError(error) else: self.log.warn(f"Registry and registry filepath were both passed.") self.registry = registry or NO_BLOCKCHAIN_CONNECTION.bool_value(False) self.registry_filepath = registry_filepath or UNINITIALIZED_CONFIGURATION # Blockchain self.poa = poa self.is_light = light self.provider_uri = provider_uri or NO_BLOCKCHAIN_CONNECTION self.provider_process = provider_process or NO_BLOCKCHAIN_CONNECTION # Learner self.federated_only = federated_only self.domains = domains or {self.DEFAULT_DOMAIN} self.learn_on_same_thread = learn_on_same_thread self.abort_on_learning_error = abort_on_learning_error self.start_learning_now = start_learning_now self.save_metadata = save_metadata self.reload_metadata = reload_metadata self.known_nodes = known_nodes or set() # handpicked # Configuration self.__dev_mode = dev_mode self.config_file_location = filepath or UNINITIALIZED_CONFIGURATION self.config_root = UNINITIALIZED_CONFIGURATION # # Federated vs. Blockchain arguments consistency # # # Federated # if self.federated_only: # Check for incompatible values blockchain_args = {'filepath': registry_filepath, 'poa': poa, 'provider_process': provider_process, 'provider_uri': provider_uri, 'gas_strategy': gas_strategy} if any(blockchain_args.values()): bad_args = (f"{arg}={val}" for arg, val in blockchain_args.items() if val) self.log.warn(f"Arguments {bad_args} are incompatible with federated_only. " f"Overridden with a sane default.") # Clear decentralized attributes to ensure consistency with a # federated configuration. self.poa = False self.is_light = False self.provider_uri = None self.provider_process = None self.registry_filepath = None self.gas_strategy = None # # Decentralized # else: self.gas_strategy = gas_strategy is_initialized = BlockchainInterfaceFactory.is_interface_initialized(provider_uri=self.provider_uri) if not is_initialized and provider_uri: BlockchainInterfaceFactory.initialize_interface(provider_uri=self.provider_uri, poa=self.poa, light=self.is_light, provider_process=self.provider_process, sync=sync, emitter=emitter, gas_strategy=gas_strategy) else: self.log.warn(f"Using existing blockchain interface connection ({self.provider_uri}).") if not self.registry: # TODO: These two code blocks are untested. if not self.registry_filepath: # TODO: Registry URI (goerli://speedynet.json) :-) self.log.info(f"Fetching latest registry from source.") self.registry = InMemoryContractRegistry.from_latest_publication(network=list(self.domains)[0]) # TODO: #1580 else: self.registry = LocalContractRegistry(filepath=self.registry_filepath) self.log.info(f"Using local registry ({self.registry}).") if dev_mode: self.__temp_dir = UNINITIALIZED_CONFIGURATION self.__setup_node_storage() self.initialize(password=DEVELOPMENT_CONFIGURATION) else: self.__temp_dir = LIVE_CONFIGURATION self.config_root = config_root or self.DEFAULT_CONFIG_ROOT self._cache_runtime_filepaths() self.__setup_node_storage(node_storage=node_storage) super().__init__(filepath=self.config_file_location, config_root=self.config_root) def __call__(self, **character_kwargs): return self.produce(**character_kwargs) def update(self, **kwargs) -> None: """ A facility for updating existing attributes on existing configuration instances. Warning: This method allows mutation and may result in an inconsistent configuration. """ return super().update(modifier=self.checksum_address, filepath=self.config_file_location, **kwargs) @classmethod def generate(cls, password: str, *args, **kwargs): """Shortcut: Hook-up a new initial installation and write configuration file to the disk""" node_config = cls(dev_mode=False, *args, **kwargs) node_config.initialize(password=password) node_config.to_configuration_file() return node_config def cleanup(self) -> None: if self.__dev_mode: self.__temp_dir.cleanup() @property def dev_mode(self) -> bool: return self.__dev_mode def __setup_node_storage(self, node_storage=None) -> None: if self.dev_mode: node_storage = ForgetfulNodeStorage(registry=self.registry, federated_only=self.federated_only) elif not node_storage: node_storage = LocalFileBasedNodeStorage(registry=self.registry, config_root=self.config_root, federated_only=self.federated_only) self.node_storage = node_storage def forget_nodes(self) -> None: self.node_storage.clear() message = "Removed all stored node node metadata and certificates" self.log.debug(message) def destroy(self) -> None: """Parse a node configuration and remove all associated files from the filesystem""" self.attach_keyring() self.keyring.destroy() os.remove(self.config_file_location) def generate_parameters(self, **overrides) -> dict: """ Warning: This method allows mutation and may result in an inconsistent configuration. """ merged_parameters = {**self.static_payload(), **self.dynamic_payload, **overrides} non_init_params = ('config_root', 'poa', 'light', 'provider_uri', 'registry_filepath', 'gas_strategy') character_init_params = filter(lambda t: t[0] not in non_init_params, merged_parameters.items()) return dict(character_init_params) def produce(self, **overrides) -> CHARACTER_CLASS: """Initialize a new character instance and return it.""" merged_parameters = self.generate_parameters(**overrides) character = self.CHARACTER_CLASS(**merged_parameters) return character @classmethod def assemble(cls, filepath: str = None, **overrides) -> dict: """ Warning: This method allows mutation and may result in an inconsistent configuration. """ payload = cls._read_configuration_file(filepath=filepath) node_storage = cls.load_node_storage(storage_payload=payload['node_storage'], federated_only=payload['federated_only']) domains = set(payload['domains']) # Assemble payload.update(dict(node_storage=node_storage, domains=domains)) # Filter out None values from **overrides to detect, well, overrides... # Acts as a shim for optional CLI flags. overrides = {k: v for k, v in overrides.items() if v is not None} payload = {**payload, **overrides} return payload @classmethod def from_configuration_file(cls, filepath: str = None, provider_process=None, **overrides # < ---- Inlet for CLI Flags ) -> 'CharacterConfiguration': """Initialize a CharacterConfiguration from a JSON file.""" filepath = filepath or cls.default_filepath() assembled_params = cls.assemble(filepath=filepath, **overrides) try: node_configuration = cls(filepath=filepath, provider_process=provider_process, **assembled_params) except TypeError as e: raise cls.ConfigurationError(e) return node_configuration def validate(self) -> bool: # Top-level if not os.path.exists(self.config_root): raise self.ConfigurationError(f'No configuration directory found at {self.config_root}.') # Sub-paths filepaths = self.runtime_filepaths for field, path in filepaths.items(): if path and not os.path.exists(path): message = 'Missing configuration file or directory: {}.' if 'registry' in path: message += ' Did you mean to pass --federated-only?' raise CharacterConfiguration.InvalidConfiguration(message.format(path)) return True def static_payload(self) -> dict: """Exported static configuration values for initializing Ursula""" payload = dict( # Identity federated_only=self.federated_only, checksum_address=self.checksum_address, keyring_root=self.keyring_root, # Behavior domains=list(self.domains), # From Set learn_on_same_thread=self.learn_on_same_thread, abort_on_learning_error=self.abort_on_learning_error, start_learning_now=self.start_learning_now, save_metadata=self.save_metadata, node_storage=self.node_storage.payload(), ) # Optional values (mode) if not self.federated_only: if self.provider_uri: payload.update(dict(provider_uri=self.provider_uri, poa=self.poa, light=self.is_light)) if self.registry_filepath: payload.update(dict(registry_filepath=self.registry_filepath)) # Gas Price payload.update(dict(gas_strategy=self.gas_strategy)) # Merge with base payload base_payload = super().static_payload() base_payload.update(payload) return payload @property # TODO: Graduate to a method and "derive" dynamic from static payload. def dynamic_payload(self) -> dict: """Exported dynamic configuration values for initializing Ursula""" payload = dict() if not self.federated_only: payload.update(dict(registry=self.registry)) payload.update(dict(network_middleware=self.network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE(), known_nodes=self.known_nodes, node_storage=self.node_storage, crypto_power_ups=self.derive_node_power_ups())) return payload def generate_filepath(self, filepath: str = None, modifier: str = None, override: bool = False) -> str: modifier = modifier or self.checksum_address filepath = super().generate_filepath(filepath=filepath, modifier=modifier, override=override) return filepath @property def runtime_filepaths(self) -> dict: filepaths = dict(config_root=self.config_root, keyring_root=self.keyring_root, registry_filepath=self.registry_filepath) return filepaths @classmethod def generate_runtime_filepaths(cls, config_root: str) -> dict: """Dynamically generate paths based on configuration root directory""" filepaths = dict(config_root=config_root, config_file_location=os.path.join(config_root, cls.generate_filename()), keyring_root=os.path.join(config_root, 'keyring')) return filepaths def _cache_runtime_filepaths(self) -> None: """Generate runtime filepaths and cache them on the config object""" filepaths = self.generate_runtime_filepaths(config_root=self.config_root) for field, filepath in filepaths.items(): if getattr(self, field) is UNINITIALIZED_CONFIGURATION: setattr(self, field, filepath) def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None: account = checksum_address or self.checksum_address if not account: raise self.ConfigurationError("No account specified to unlock keyring") if self.keyring is not NO_KEYRING_ATTACHED: if self.keyring.checksum_address != account: raise self.ConfigurationError("There is already a keyring attached to this configuration.") return self.keyring = NucypherKeyring(keyring_root=self.keyring_root, account=account, *args, **kwargs) def derive_node_power_ups(self) -> List[CryptoPowerUp]: power_ups = list() if self.is_me and not self.dev_mode: for power_class in self.CHARACTER_CLASS._default_crypto_powerups: power_up = self.keyring.derive_crypto_power(power_class) power_ups.append(power_up) return power_ups def initialize(self, password: str) -> str: """Initialize a new configuration and write installation files to disk.""" # Development if self.dev_mode: self.__temp_dir = TemporaryDirectory(prefix=self.TEMP_CONFIGURATION_DIR_PREFIX) self.config_root = self.__temp_dir.name # Persistent else: self._ensure_config_root_exists() self.write_keyring(password=password) self._cache_runtime_filepaths() self.node_storage.initialize() # Validate if not self.__dev_mode: self.validate() # Success message = "Created nucypher installation files at {}".format(self.config_root) self.log.debug(message) return self.config_root def write_keyring(self, password: str, checksum_address: str = None, **generation_kwargs) -> NucypherKeyring: if self.federated_only: checksum_address = FEDERATED_ADDRESS elif not checksum_address: # Note: It is assumed the blockchain interface is not yet connected. if self.provider_process: # Generate Geth's "datadir" if not os.path.exists(self.provider_process.data_dir): os.mkdir(self.provider_process.data_dir) # Get or create wallet address if not self.checksum_address: self.checksum_address = self.provider_process.ensure_account_exists(password=password) elif self.checksum_address not in self.provider_process.accounts(): raise self.ConfigurationError(f'Unknown Account {self.checksum_address}') elif not self.checksum_address: raise self.ConfigurationError(f'No checksum address provided for decentralized configuration.') checksum_address = self.checksum_address self.keyring = NucypherKeyring.generate(password=password, keyring_root=self.keyring_root, checksum_address=checksum_address, **generation_kwargs) if self.federated_only: self.checksum_address = self.keyring.checksum_address return self.keyring @classmethod def load_node_storage(cls, storage_payload: dict, federated_only: bool): from nucypher.config.storages import NodeStorage node_storage_subclasses = {storage._name: storage for storage in NodeStorage.__subclasses__()} storage_type = storage_payload[NodeStorage._TYPE_LABEL] storage_class = node_storage_subclasses[storage_type] node_storage = storage_class.from_payload(payload=storage_payload, federated_only=federated_only) return node_storage
def __init__(self, domains: Set = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = NO_BLOCKCHAIN_CONNECTION.bool_value(False), network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, keyring_root: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, registry: BaseContractRegistry = None, *args, **kwargs ) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Operating Mode # if federated_only: if registry or provider_uri: raise ValueError(f"Cannot init federated-only character with {registry or provider_uri}.") self.federated_only = federated_only # type: bool # # Powers # # Derive powers from keyring if keyring_root and keyring: if keyring_root != keyring.keyring_root: raise ValueError("Inconsistent keyring root directory path") if keyring: keyring_root, checksum_address = keyring.keyring_root, keyring.checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring_root = keyring_root self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.") crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups) self._checksum_address = checksum_address # Fleet and Blockchain Connection (Everyone) if not domains: domains = (CharacterConfiguration.DEFAULT_DOMAIN,) # # Self-Character # if is_me: if not bool(federated_only) ^ bool(registry): raise ValueError(f"Pass either federated only or registry for is_me Characters. \ Got '{federated_only}' and '{registry}'.") self.treasure_maps = {} # type: dict self.network_middleware = network_middleware or RestMiddleware() # # Signing Power # try: signing_power = self._crypto_power.power_ups(SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp() # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Blockchain # self.provider_uri = provider_uri if not self.federated_only: self.registry = registry or InMemoryContractRegistry.from_latest_publication() else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # # Learner # Learner.__init__(self, domains=domains, network_middleware=self.network_middleware, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError("Network middleware cannot be attached to a Stranger-Character.") if registry is not None: raise TypeError("Registry cannot be attached to stranger-Characters.") self._stamp = StrangerStamp(self.public_keys(SigningPower)) self.network_middleware = STRANGER # # Decentralized # if not federated_only: self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower # # Federated # elif federated_only: try: self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION if checksum_address: # We'll take a checksum address, as long as it matches their singing key if not checksum_address == self.checksum_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity(error.format(checksum_address)) # # Nicknames # try: self.nickname, self.nickname_metadata = nickname_from_seed(self.checksum_address) except SigningPower.not_found_error: if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL
class CharacterConfiguration(BaseConfiguration): """ 'Sideways Engagement' of Character classes; a reflection of input parameters. """ VERSION = 2 # bump when static payload scheme changes CHARACTER_CLASS = NotImplemented DEFAULT_CONTROLLER_PORT = NotImplemented DEFAULT_DOMAIN = NetworksInventory.DEFAULT DEFAULT_NETWORK_MIDDLEWARE = RestMiddleware TEMP_CONFIGURATION_DIR_PREFIX = 'tmp-nucypher' # When we begin to support other threshold schemes, this will be one of the concepts that makes us want a factory. #571 known_node_class = Ursula # Gas DEFAULT_GAS_STRATEGY = 'fast' # Fields specified here are *not* passed into the Character's constructor # and can be understood as configuration fields only. _CONFIG_FIELDS = ('config_root', 'poa', 'light', 'registry_filepath', 'gas_strategy', 'max_gas_price', # gwei 'signer_uri', 'keyring_root' ) def __init__(self, # Base emitter=None, config_root: str = None, filepath: str = None, # Mode dev_mode: bool = False, federated_only: bool = False, # Identity checksum_address: str = None, crypto_power: CryptoPower = None, # Keyring keyring: NucypherKeyring = None, keyring_root: str = None, # Learner learn_on_same_thread: bool = False, abort_on_learning_error: bool = False, start_learning_now: bool = True, # Network controller_port: int = None, domain: str = DEFAULT_DOMAIN, interface_signature: Signature = None, network_middleware: RestMiddleware = None, lonely: bool = False, # Node Storage known_nodes: set = None, node_storage: NodeStorage = None, reload_metadata: bool = True, save_metadata: bool = True, # Blockchain poa: bool = None, light: bool = False, provider_uri: str = None, gas_strategy: Union[Callable, str] = DEFAULT_GAS_STRATEGY, max_gas_price: Optional[int] = None, signer_uri: str = None, # Registry registry: BaseContractRegistry = None, registry_filepath: str = None, # Deployed Workers worker_data: dict = None ): self.log = Logger(self.__class__.__name__) # This constant is used to signal that a path can be generated if one is not provided. UNINITIALIZED_CONFIGURATION.bool_value(False) # Identity # NOTE: NodeConfigurations can only be used with Self-Characters self.is_me = True self.checksum_address = checksum_address # Keyring self.crypto_power = crypto_power self.keyring = keyring or NO_KEYRING_ATTACHED self.keyring_root = keyring_root or UNINITIALIZED_CONFIGURATION # Contract Registry if registry and registry_filepath: if registry.filepath != registry_filepath: error = f"Inconsistent registry filepaths for '{registry.filepath}' and '{registry_filepath}'." raise ValueError(error) else: self.log.warn(f"Registry and registry filepath were both passed.") self.registry = registry or NO_BLOCKCHAIN_CONNECTION.bool_value(False) self.registry_filepath = registry_filepath or UNINITIALIZED_CONFIGURATION # Blockchain self.poa = poa self.is_light = light self.provider_uri = provider_uri or NO_BLOCKCHAIN_CONNECTION self.signer_uri = signer_uri or None # Learner self.federated_only = federated_only self.domain = domain self.learn_on_same_thread = learn_on_same_thread self.abort_on_learning_error = abort_on_learning_error self.start_learning_now = start_learning_now self.save_metadata = save_metadata self.reload_metadata = reload_metadata self.known_nodes = known_nodes or set() # handpicked self.lonely = lonely # Configuration self.__dev_mode = dev_mode self.config_file_location = filepath or UNINITIALIZED_CONFIGURATION self.config_root = UNINITIALIZED_CONFIGURATION # Deployed Workers self.worker_data = worker_data # # Federated vs. Blockchain arguments consistency # # # Federated # if self.federated_only: # Check for incompatible values blockchain_args = {'filepath': registry_filepath, 'poa': poa, 'provider_uri': provider_uri, 'gas_strategy': gas_strategy, 'max_gas_price': max_gas_price} if any(blockchain_args.values()): bad_args = ", ".join(f"{arg}={val}" for arg, val in blockchain_args.items() if val) self.log.warn(f"Arguments {bad_args} are incompatible with federated_only. " f"Overridden with a sane default.") # Clear decentralized attributes to ensure consistency with a # federated configuration. self.poa = False self.is_light = False self.provider_uri = None self.registry_filepath = None self.gas_strategy = None self.max_gas_price = None # # Decentralized # else: self.gas_strategy = gas_strategy self.max_gas_price = max_gas_price # gwei is_initialized = BlockchainInterfaceFactory.is_interface_initialized(provider_uri=self.provider_uri) if not is_initialized and provider_uri: BlockchainInterfaceFactory.initialize_interface(provider_uri=self.provider_uri, poa=self.poa, light=self.is_light, emitter=emitter, gas_strategy=self.gas_strategy, max_gas_price=self.max_gas_price) else: self.log.warn(f"Using existing blockchain interface connection ({self.provider_uri}).") if not self.registry: # TODO: These two code blocks are untested. if not self.registry_filepath: # TODO: Registry URI (goerli://speedynet.json) :-) self.log.info(f"Fetching latest registry from source.") self.registry = InMemoryContractRegistry.from_latest_publication(network=self.domain) else: self.registry = LocalContractRegistry(filepath=self.registry_filepath) self.log.info(f"Using local registry ({self.registry}).") if dev_mode: self.__temp_dir = UNINITIALIZED_CONFIGURATION self.__setup_node_storage() self.initialize(password=DEVELOPMENT_CONFIGURATION) else: self.__temp_dir = LIVE_CONFIGURATION self.config_root = config_root or self.DEFAULT_CONFIG_ROOT self._cache_runtime_filepaths() self.__setup_node_storage(node_storage=node_storage) # Network self.controller_port = controller_port or self.DEFAULT_CONTROLLER_PORT self.network_middleware = network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE(registry=self.registry) self.interface_signature = interface_signature super().__init__(filepath=self.config_file_location, config_root=self.config_root) def __call__(self, **character_kwargs): return self.produce(**character_kwargs) @classmethod def checksum_address_from_filepath(cls, filepath: str) -> str: pattern = re.compile(r''' (^\w+)- (0x{1} # Then, 0x the start of the string, exactly once [0-9a-fA-F]{40}) # Followed by exactly 40 hex chars ''', re.VERBOSE) filename = os.path.basename(filepath) match = pattern.match(filename) if match: character_name, checksum_address = match.groups() else: # Extract from default by "peeking" inside the configuration file. default_name = cls.generate_filename() if filename == default_name: checksum_address = cls.peek(filepath=filepath, field='checksum_address') ########### # TODO: Cleanup and deprecate worker_address in config files, leaving only checksum_address from nucypher.config.characters import UrsulaConfiguration if isinstance(cls, UrsulaConfiguration): federated = bool(cls.peek(filepath=filepath, field='federated_only')) if not federated: checksum_address = cls.peek(filepath=cls.filepath, field='worker_address') ########### else: raise ValueError(f"Cannot extract checksum from filepath '{filepath}'") if not is_checksum_address(checksum_address): raise RuntimeError(f"Invalid checksum address detected in configuration file at '{filepath}'.") return checksum_address def update(self, **kwargs) -> None: """ A facility for updating existing attributes on existing configuration instances. Warning: This method allows mutation and may result in an inconsistent configuration. """ return super().update(modifier=self.checksum_address, filepath=self.config_file_location, **kwargs) @classmethod def generate(cls, password: str, *args, **kwargs): """Shortcut: Hook-up a new initial installation and write configuration file to the disk""" node_config = cls(dev_mode=False, *args, **kwargs) node_config.initialize(password=password) node_config.to_configuration_file() return node_config def cleanup(self) -> None: if self.__dev_mode: self.__temp_dir.cleanup() @property def dev_mode(self) -> bool: return self.__dev_mode def __setup_node_storage(self, node_storage=None) -> None: if self.dev_mode: node_storage = ForgetfulNodeStorage(registry=self.registry, federated_only=self.federated_only) elif not node_storage: node_storage = LocalFileBasedNodeStorage(registry=self.registry, config_root=self.config_root, federated_only=self.federated_only) self.node_storage = node_storage def forget_nodes(self) -> None: self.node_storage.clear() message = "Removed all stored node node metadata and certificates" self.log.debug(message) def destroy(self) -> None: """Parse a node configuration and remove all associated files from the filesystem""" self.attach_keyring() self.keyring.destroy() os.remove(self.config_file_location) def generate_parameters(self, **overrides) -> dict: """ Warning: This method allows mutation and may result in an inconsistent configuration. """ merged_parameters = {**self.static_payload(), **self.dynamic_payload, **overrides} character_init_params = filter(lambda t: t[0] not in self._CONFIG_FIELDS, merged_parameters.items()) return dict(character_init_params) def produce(self, **overrides) -> CHARACTER_CLASS: """Initialize a new character instance and return it.""" merged_parameters = self.generate_parameters(**overrides) character = self.CHARACTER_CLASS(**merged_parameters) return character @classmethod def assemble(cls, filepath: str = None, **overrides) -> dict: """ Warning: This method allows mutation and may result in an inconsistent configuration. """ payload = cls._read_configuration_file(filepath=filepath) node_storage = cls.load_node_storage(storage_payload=payload['node_storage'], federated_only=payload['federated_only']) domain = payload['domain'] max_gas_price = payload.get('max_gas_price') # gwei if max_gas_price: max_gas_price = Decimal(max_gas_price) # Assemble payload.update(dict(node_storage=node_storage, domain=domain, max_gas_price=max_gas_price)) # Filter out None values from **overrides to detect, well, overrides... # Acts as a shim for optional CLI flags. overrides = {k: v for k, v in overrides.items() if v is not None} payload = {**payload, **overrides} return payload @classmethod def from_configuration_file(cls, filepath: str = None, **overrides # < ---- Inlet for CLI Flags ) -> 'CharacterConfiguration': """Initialize a CharacterConfiguration from a JSON file.""" filepath = filepath or cls.default_filepath() assembled_params = cls.assemble(filepath=filepath, **overrides) node_configuration = cls(filepath=filepath, **assembled_params) return node_configuration def validate(self) -> bool: # Top-level if not os.path.exists(self.config_root): raise self.ConfigurationError(f'No configuration directory found at {self.config_root}.') # Sub-paths filepaths = self.runtime_filepaths for field, path in filepaths.items(): if path and not os.path.exists(path): message = 'Missing configuration file or directory: {}.' if 'registry' in path: message += ' Did you mean to pass --federated-only?' raise CharacterConfiguration.InvalidConfiguration(message.format(path)) return True def static_payload(self) -> dict: """Exported static configuration values for initializing Ursula""" payload = dict( # Identity federated_only=self.federated_only, checksum_address=self.checksum_address, keyring_root=self.keyring_root, # Behavior domain=self.domain, learn_on_same_thread=self.learn_on_same_thread, abort_on_learning_error=self.abort_on_learning_error, start_learning_now=self.start_learning_now, save_metadata=self.save_metadata, node_storage=self.node_storage.payload(), lonely=self.lonely, ) # Optional values (mode) if not self.federated_only: if self.provider_uri: if not self.signer_uri: self.signer_uri = self.provider_uri payload.update(dict(provider_uri=self.provider_uri, poa=self.poa, light=self.is_light, signer_uri=self.signer_uri)) if self.registry_filepath: payload.update(dict(registry_filepath=self.registry_filepath)) # Gas Price __max_price = str(self.max_gas_price) if self.max_gas_price else None payload.update(dict(gas_strategy=self.gas_strategy, max_gas_price=__max_price)) # Merge with base payload base_payload = super().static_payload() base_payload.update(payload) return payload @property # TODO: Graduate to a method and "derive" dynamic from static payload. def dynamic_payload(self) -> dict: """Exported dynamic configuration values for initializing Ursula""" payload = dict() if not self.federated_only: testnet = self.domain != NetworksInventory.MAINNET signer = Signer.from_signer_uri(self.signer_uri, testnet=testnet) payload.update(dict(registry=self.registry, signer=signer)) payload.update(dict(network_middleware=self.network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE(), known_nodes=self.known_nodes, node_storage=self.node_storage, keyring=self.keyring, crypto_power_ups=self.derive_node_power_ups())) return payload def generate_filepath(self, filepath: str = None, modifier: str = None, override: bool = False) -> str: modifier = modifier or self.checksum_address filepath = super().generate_filepath(filepath=filepath, modifier=modifier, override=override) return filepath @property def runtime_filepaths(self) -> dict: filepaths = dict(config_root=self.config_root, keyring_root=self.keyring_root, registry_filepath=self.registry_filepath) return filepaths @classmethod def generate_runtime_filepaths(cls, config_root: str) -> dict: """Dynamically generate paths based on configuration root directory""" filepaths = dict(config_root=config_root, config_file_location=os.path.join(config_root, cls.generate_filename()), keyring_root=os.path.join(config_root, 'keyring')) return filepaths def _cache_runtime_filepaths(self) -> None: """Generate runtime filepaths and cache them on the config object""" filepaths = self.generate_runtime_filepaths(config_root=self.config_root) for field, filepath in filepaths.items(): if getattr(self, field) is UNINITIALIZED_CONFIGURATION: setattr(self, field, filepath) def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None: account = checksum_address or self.checksum_address if not account: raise self.ConfigurationError("No account specified to unlock keyring") if self.keyring is not NO_KEYRING_ATTACHED: if self.keyring.checksum_address != account: raise self.ConfigurationError("There is already a keyring attached to this configuration.") return self.keyring = NucypherKeyring(keyring_root=self.keyring_root, account=account, *args, **kwargs) def derive_node_power_ups(self) -> List[CryptoPowerUp]: power_ups = list() if self.is_me and not self.dev_mode: for power_class in self.CHARACTER_CLASS._default_crypto_powerups: power_up = self.keyring.derive_crypto_power(power_class) power_ups.append(power_up) return power_ups def initialize(self, password: str) -> str: """Initialize a new configuration and write installation files to disk.""" # Development if self.dev_mode: self.__temp_dir = TemporaryDirectory(prefix=self.TEMP_CONFIGURATION_DIR_PREFIX) self.config_root = self.__temp_dir.name # Persistent else: self._ensure_config_root_exists() self.write_keyring(password=password) self._cache_runtime_filepaths() self.node_storage.initialize() # Validate if not self.__dev_mode: self.validate() # Success message = "Created nucypher installation files at {}".format(self.config_root) self.log.debug(message) return self.config_root def write_keyring(self, password: str, checksum_address: str = None, **generation_kwargs) -> NucypherKeyring: # Configure checksum address checksum_address = checksum_address or self.checksum_address if self.federated_only: checksum_address = FEDERATED_ADDRESS elif not checksum_address: raise self.ConfigurationError(f'No checksum address provided for decentralized configuration.') # Generate new keys self.keyring = NucypherKeyring.generate(password=password, keyring_root=self.keyring_root, checksum_address=checksum_address, **generation_kwargs) # In the case of a federated keyring generation, # the generated federated address must be set here. if self.federated_only: self.checksum_address = self.keyring.checksum_address return self.keyring @classmethod def load_node_storage(cls, storage_payload: dict, federated_only: bool): from nucypher.config.storages import NodeStorage node_storage_subclasses = {storage._name: storage for storage in NodeStorage.__subclasses__()} storage_type = storage_payload[NodeStorage._TYPE_LABEL] storage_class = node_storage_subclasses[storage_type] node_storage = storage_class.from_payload(payload=storage_payload, federated_only=federated_only) return node_storage
def __init__(self, domain: str = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = NO_BLOCKCHAIN_CONNECTION.bool_value(False), network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, keyring_root: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, *args, **kwargs ) -> None: """ A participant in the cryptological drama (a screenplay, if you like) of NuCypher. Characters can represent users, nodes, wallets, offline devices, or other objects of varying levels of abstraction. The Named Characters use this class as a Base, and achieve their individuality from additional methods and PowerUps. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ if provider_uri: if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri): BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri) # # Operating Mode # if hasattr(self, '_interface_class'): # TODO: have argument about meaning of 'lawful' # and whether maybe only Lawful characters have an interface self.interface = self._interface_class(character=self) if is_me: self._set_known_node_class(known_node_class, federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or provider_uri: raise ValueError(f"Cannot init federated-only character with {registry or provider_uri}.") self.federated_only: bool = federated_only # # Powers # # Derive powers from keyring if keyring_root and keyring: if keyring_root != keyring.keyring_root: raise ValueError("Inconsistent keyring root directory path") if keyring: keyring_root, checksum_address = keyring.keyring_root, keyring.checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring_root = keyring_root self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.") crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups) # # Self-Character # if is_me: # # Signing Power # self.signer = signer try: signing_power = self._crypto_power.power_ups(SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp() # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Blockchain # self.provider_uri = provider_uri if not self.federated_only: self.registry = registry or InMemoryContractRegistry.from_latest_publication(network=domain) # See #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware(registry=self.registry) # # Learner # Learner.__init__(self, domain=domain, network_middleware=self.network_middleware, node_class=known_node_class, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError("Network middleware cannot be attached to a Stranger-Character.") if registry is not None: raise TypeError("Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keyring_root = STRANGER self.network_middleware = STRANGER # TODO: Figure out when to do this. try: _transacting_power = self._crypto_power.power_ups(TransactingPower) except NoTransactingPower: self._checksum_address = checksum_address else: self._set_checksum_address(checksum_address) # # Nicknames # if self._checksum_address is NO_BLOCKCHAIN_CONNECTION and not self.federated_only and not is_me: # Sometimes we don't care about the nickname. For example, if Alice is granting to Bob, she usually # doesn't know or care about his wallet. Maybe this needs to change? # Currently, if this is a stranger and there's no blockchain connection, we assign NO_NICKNAME: self.nickname = NO_NICKNAME else: try: # TODO: It's possible that this is NO_BLOCKCHAIN_CONNECTION. if self.checksum_address is NO_BLOCKCHAIN_CONNECTION: self.nickname = NO_NICKNAME else: # This can call _set_checksum_address. self.nickname = Nickname.from_seed(self.checksum_address) except SigningPower.not_found_error: # TODO: Handle NO_BLOCKCHAIN_CONNECTION more coherently - #1547 if self.federated_only: self.nickname = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL
class CharacterConfiguration(BaseConfiguration): """ 'Sideways Engagement' of Character classes; a reflection of input parameters. """ CHARACTER_CLASS = NotImplemented DEFAULT_CONTROLLER_PORT = NotImplemented DEFAULT_PROVIDER_URI = 'http://localhost:8545' DEFAULT_DOMAIN = 'goerli' DEFAULT_NETWORK_MIDDLEWARE = RestMiddleware TEMP_CONFIGURATION_DIR_PREFIX = 'tmp-nucypher' def __init__( self, # Base config_root: str = None, filepath: str = None, # Mode dev_mode: bool = False, federated_only: bool = False, # Identity checksum_address: str = None, crypto_power: CryptoPower = None, # Keyring keyring: NucypherKeyring = None, keyring_root: str = None, # Learner learn_on_same_thread: bool = False, abort_on_learning_error: bool = False, start_learning_now: bool = True, # Network controller_port: int = None, domains: Set[str] = None, interface_signature: Signature = None, network_middleware: RestMiddleware = None, # Node Storage known_nodes: set = None, node_storage: NodeStorage = None, reload_metadata: bool = True, save_metadata: bool = True, # Blockchain poa: bool = False, provider_uri: str = None, provider_process=None, # Registry registry_filepath: str = None, download_registry: bool = True) -> None: self.log = Logger(self.__class__.__name__) # Identity # NOTE: NodeConfigurations can only be used with Self-Characters self.is_me = True self.checksum_address = checksum_address # Network self.controller_port = controller_port or self.DEFAULT_CONTROLLER_PORT self.network_middleware = network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE( ) self.interface_signature = interface_signature # Keyring self.crypto_power = crypto_power self.keyring = keyring or NO_KEYRING_ATTACHED self.keyring_root = keyring_root or UNINITIALIZED_CONFIGURATION # Contract Registry self.download_registry = download_registry self.registry_filepath = registry_filepath or UNINITIALIZED_CONFIGURATION # Blockchain self.poa = poa self.provider_uri = provider_uri or self.DEFAULT_PROVIDER_URI self.provider_process = provider_process or NO_BLOCKCHAIN_CONNECTION self.blockchain = NO_BLOCKCHAIN_CONNECTION.bool_value(False) self.token_agent = NO_BLOCKCHAIN_CONNECTION self.staking_agent = NO_BLOCKCHAIN_CONNECTION self.policy_agent = NO_BLOCKCHAIN_CONNECTION # Learner self.federated_only = federated_only self.domains = domains or {self.DEFAULT_DOMAIN} self.learn_on_same_thread = learn_on_same_thread self.abort_on_learning_error = abort_on_learning_error self.start_learning_now = start_learning_now self.save_metadata = save_metadata self.reload_metadata = reload_metadata self.__known_nodes = known_nodes or set() # handpicked self.__fleet_state = FleetStateTracker() # Configuration self.__dev_mode = dev_mode self.config_file_location = filepath or UNINITIALIZED_CONFIGURATION self.config_root = UNINITIALIZED_CONFIGURATION if dev_mode: self.__temp_dir = UNINITIALIZED_CONFIGURATION self.__setup_node_storage() self.initialize(password=DEVELOPMENT_CONFIGURATION) else: self.__temp_dir = LIVE_CONFIGURATION self.config_root = config_root or self.DEFAULT_CONFIG_ROOT self._cache_runtime_filepaths() self.__setup_node_storage(node_storage=node_storage) super().__init__(filepath=self.config_file_location, config_root=self.config_root) def __call__(self, **character_kwargs): return self.produce(**character_kwargs) @classmethod def generate(cls, password: str, *args, **kwargs): """Shortcut: Hook-up a new initial installation and write configuration file to the disk""" node_config = cls(dev_mode=False, *args, **kwargs) node_config.initialize(password=password) node_config.to_configuration_file() return node_config def cleanup(self) -> None: if self.__dev_mode: self.__temp_dir.cleanup() @property def dev_mode(self) -> bool: return self.__dev_mode def get_blockchain_interface(self) -> None: if self.federated_only: raise CharacterConfiguration.ConfigurationError( "Cannot connect to blockchain in federated mode") registry = None if self.registry_filepath: registry = EthereumContractRegistry( registry_filepath=self.registry_filepath) self.blockchain = BlockchainInterface( provider_uri=self.provider_uri, poa=self.poa, registry=registry, provider_process=self.provider_process) def acquire_agency(self) -> None: self.token_agent = NucypherTokenAgent(blockchain=self.blockchain) self.staking_agent = StakingEscrowAgent(blockchain=self.blockchain) self.policy_agent = PolicyManagerAgent(blockchain=self.blockchain) self.log.debug("Established connection to nucypher contracts") @property def known_nodes(self) -> FleetStateTracker: return self.__fleet_state def __setup_node_storage(self, node_storage=None) -> None: if self.dev_mode: node_storage = ForgetfulNodeStorage( federated_only=self.federated_only) elif not node_storage: node_storage = LocalFileBasedNodeStorage( federated_only=self.federated_only, config_root=self.config_root) self.node_storage = node_storage def read_known_nodes(self, additional_nodes=None) -> None: known_nodes = self.node_storage.all(federated_only=self.federated_only) known_nodes = {node.checksum_address: node for node in known_nodes} if additional_nodes: known_nodes.update( {node.checksum_address: node for node in additional_nodes}) if self.__known_nodes: known_nodes.update( {node.checksum_address: node for node in self.__known_nodes}) self.__fleet_state._nodes.update(known_nodes) self.__fleet_state.record_fleet_state( additional_nodes_to_track=self.__known_nodes) def forget_nodes(self) -> None: self.node_storage.clear() message = "Removed all stored node node metadata and certificates" self.log.debug(message) def destroy(self) -> None: """Parse a node configuration and remove all associated files from the filesystem""" self.attach_keyring() self.keyring.destroy() os.remove(self.config_file_location) def generate_parameters(self, **overrides) -> dict: merged_parameters = { **self.static_payload(), **self.dynamic_payload, **overrides } non_init_params = ('config_root', 'poa', 'provider_uri') character_init_params = filter(lambda t: t[0] not in non_init_params, merged_parameters.items()) return dict(character_init_params) def produce(self, **overrides) -> CHARACTER_CLASS: """Initialize a new character instance and return it.""" merged_parameters = self.generate_parameters(**overrides) character = self.CHARACTER_CLASS(**merged_parameters) return character @classmethod def assemble(cls, filepath: str = None, **overrides) -> dict: payload = cls._read_configuration_file(filepath=filepath) node_storage = cls.load_node_storage( storage_payload=payload['node_storage'], federated_only=payload['federated_only']) domains = set(payload['domains']) # Assemble payload.update(dict(node_storage=node_storage, domains=domains)) # Filter out None values from **overrides to detect, well, overrides... # Acts as a shim for optional CLI flags. overrides = {k: v for k, v in overrides.items() if v is not None} payload = {**payload, **overrides} return payload @classmethod def from_configuration_file(cls, filepath: str = None, provider_process=None, **overrides) -> 'CharacterConfiguration': """Initialize a CharacterConfiguration from a JSON file.""" filepath = filepath or cls.default_filepath() assembled_params = cls.assemble(filepath=filepath, **overrides) node_configuration = cls(filepath=filepath, provider_process=provider_process, **assembled_params) return node_configuration def validate(self, no_registry: bool = False) -> bool: # Top-level if not os.path.exists(self.config_root): raise self.ConfigurationError( f'No configuration directory found at {self.config_root}.') # Sub-paths filepaths = self.runtime_filepaths if no_registry: del filepaths['registry_filepath'] for field, path in filepaths.items(): if not os.path.exists(path): message = 'Missing configuration file or directory: {}.' if 'registry' in path: message += ' Did you mean to pass --federated-only?' raise CharacterConfiguration.InvalidConfiguration( message.format(path)) return True def static_payload(self) -> dict: """Exported static configuration values for initializing Ursula""" payload = dict( # Identity federated_only=self.federated_only, checksum_address=self.checksum_address, keyring_root=self.keyring_root, # Behavior domains=list(self.domains), # From Set provider_uri=self.provider_uri, learn_on_same_thread=self.learn_on_same_thread, abort_on_learning_error=self.abort_on_learning_error, start_learning_now=self.start_learning_now, save_metadata=self.save_metadata, node_storage=self.node_storage.payload(), ) # Optional values (mode) if not self.federated_only: payload.update(dict(provider_uri=self.provider_uri, poa=self.poa)) # Merge with base payload base_payload = super().static_payload() base_payload.update(payload) return payload @property def dynamic_payload(self) -> dict: """Exported dynamic configuration values for initializing Ursula""" self.read_known_nodes() payload = dict(network_middleware=self.network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE(), known_nodes=self.known_nodes, node_storage=self.node_storage, crypto_power_ups=self.derive_node_power_ups()) if not self.federated_only: self.get_blockchain_interface() self.blockchain.connect( ) # TODO: This makes blockchain connection more eager than transacting power acivation payload.update(blockchain=self.blockchain) return payload def generate_filepath(self, filepath: str = None, modifier: str = None, override: bool = False) -> str: modifier = modifier or self.checksum_address filepath = super().generate_filepath(filepath=filepath, modifier=modifier, override=override) return filepath @property def runtime_filepaths(self) -> dict: filepaths = dict(config_root=self.config_root, keyring_root=self.keyring_root, registry_filepath=self.registry_filepath) return filepaths @classmethod def generate_runtime_filepaths(cls, config_root: str) -> dict: """Dynamically generate paths based on configuration root directory""" filepaths = dict(config_root=config_root, config_file_location=os.path.join( config_root, cls.generate_filename()), keyring_root=os.path.join(config_root, 'keyring'), registry_filepath=os.path.join( config_root, EthereumContractRegistry.REGISTRY_NAME)) return filepaths def _cache_runtime_filepaths(self) -> None: """Generate runtime filepaths and cache them on the config object""" filepaths = self.generate_runtime_filepaths( config_root=self.config_root) for field, filepath in filepaths.items(): if getattr(self, field) is UNINITIALIZED_CONFIGURATION: setattr(self, field, filepath) def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None: account = checksum_address or self.checksum_address if not account: raise self.ConfigurationError( "No account specified to unlock keyring") if self.keyring is not NO_KEYRING_ATTACHED: if self.keyring.checksum_address != account: raise self.ConfigurationError( "There is already a keyring attached to this configuration." ) return self.keyring = NucypherKeyring(keyring_root=self.keyring_root, account=account, *args, **kwargs) def derive_node_power_ups(self) -> List[CryptoPowerUp]: power_ups = list() if self.is_me and not self.dev_mode: for power_class in self.CHARACTER_CLASS._default_crypto_powerups: power_up = self.keyring.derive_crypto_power(power_class) power_ups.append(power_up) return power_ups def initialize(self, password: str) -> str: """Initialize a new configuration and write installation files to disk.""" # Development if self.dev_mode: self.__temp_dir = TemporaryDirectory( prefix=self.TEMP_CONFIGURATION_DIR_PREFIX) self.config_root = self.__temp_dir.name # Persistent else: self._ensure_config_root_exists() self.write_keyring(password=password) self._cache_runtime_filepaths() self.node_storage.initialize() if self.download_registry: self.registry_filepath = EthereumContractRegistry.download_latest_publication( ) # Validate if not self.__dev_mode: self.validate(no_registry=( not self.download_registry) or self.federated_only) # Success message = "Created nucypher installation files at {}".format( self.config_root) self.log.debug(message) return self.config_root def write_keyring(self, password: str, checksum_address: str = None, **generation_kwargs) -> NucypherKeyring: if self.federated_only: checksum_address = FEDERATED_ADDRESS elif not checksum_address: # Note: It is assumed the blockchain interface is not yet connected. if self.provider_process: # Generate Geth's "datadir" if not os.path.exists(self.provider_process.data_dir): os.mkdir(self.provider_process.data_dir) # Get or create wallet address if not self.checksum_address: self.checksum_address = self.provider_process.ensure_account_exists( password=password) elif self.checksum_address not in self.provider_process.accounts( ): raise self.ConfigurationError( f'Unknown Account {self.checksum_address}') elif not self.checksum_address: raise self.ConfigurationError( f'No checksum address provided for decentralized configuration.' ) checksum_address = self.checksum_address self.keyring = NucypherKeyring.generate( password=password, keyring_root=self.keyring_root, checksum_address=checksum_address, **generation_kwargs) if self.federated_only: self.checksum_address = self.keyring.checksum_address return self.keyring @classmethod def load_node_storage(cls, storage_payload: dict, federated_only: bool): from nucypher.config.storages import NodeStorage node_storage_subclasses = { storage._name: storage for storage in NodeStorage.__subclasses__() } storage_type = storage_payload[NodeStorage._TYPE_LABEL] storage_class = node_storage_subclasses[storage_type] node_storage = storage_class.from_payload( payload=storage_payload, federated_only=federated_only) return node_storage
def test_keyring_restoration(tmpdir): keyring = _generate_keyring(tmpdir) keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) account = keyring.account checksum_address = keyring.checksum_address certificate_filepath = keyring.certificate_filepath encrypting_public_key_hex = keyring.encrypting_public_key.hex() signing_public_key_hex = keyring.signing_public_key.hex() # tls power tls_hosting_power = keyring.derive_crypto_power( power_class=TLSHostingPower, host=LOOPBACK_ADDRESS) tls_hosting_power_public_key_numbers = tls_hosting_power.public_key( ).public_numbers() tls_hosting_power_certificate_public_bytes = \ tls_hosting_power.keypair.certificate.public_bytes(encoding=Encoding.PEM) tls_hosting_power_certificate_filepath = tls_hosting_power.keypair.certificate_filepath # decrypting power decrypting_power = keyring.derive_crypto_power(power_class=DecryptingPower) decrypting_power_public_key_hex = decrypting_power.public_key().hex() decrypting_power_fingerprint = decrypting_power.keypair.fingerprint() # signing power signing_power = keyring.derive_crypto_power(power_class=SigningPower) signing_power_public_key_hex = signing_power.public_key().hex() signing_power_fingerprint = signing_power.keypair.fingerprint() # get rid of object, but not persistent data del keyring restored_keyring = NucypherKeyring(keyring_root=tmpdir, account=account) restored_keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) assert restored_keyring.account == account assert restored_keyring.checksum_address == checksum_address assert restored_keyring.certificate_filepath == certificate_filepath assert restored_keyring.encrypting_public_key.hex( ) == encrypting_public_key_hex assert restored_keyring.signing_public_key.hex() == signing_public_key_hex # tls power restored_tls_hosting_power = restored_keyring.derive_crypto_power( power_class=TLSHostingPower, host=LOOPBACK_ADDRESS) assert restored_tls_hosting_power.public_key().public_numbers( ) == tls_hosting_power_public_key_numbers assert restored_tls_hosting_power.keypair.certificate.public_bytes(encoding=Encoding.PEM) == \ tls_hosting_power_certificate_public_bytes assert restored_tls_hosting_power.keypair.certificate_filepath == tls_hosting_power_certificate_filepath # decrypting power restored_decrypting_power = restored_keyring.derive_crypto_power( power_class=DecryptingPower) assert restored_decrypting_power.public_key().hex( ) == decrypting_power_public_key_hex assert restored_decrypting_power.keypair.fingerprint( ) == decrypting_power_fingerprint # signing power restored_signing_power = restored_keyring.derive_crypto_power( power_class=SigningPower) assert restored_signing_power.public_key().hex( ) == signing_power_public_key_hex assert restored_signing_power.keypair.fingerprint( ) == signing_power_fingerprint