From 1c86a07a133cec9584d062540420669346afdccf Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Sun, 25 Nov 2018 20:41:31 -0800 Subject: [PATCH] Integrate FleetState into NodeConfiguration; checksum_address -> checksum_public_address --- nucypher/characters/base.py | 18 +-- nucypher/characters/lawful.py | 148 ++++++++++++++++-- nucypher/characters/unlawful.py | 2 +- nucypher/cli/main.py | 95 ++--------- nucypher/cli/painting.py | 86 +++++++--- nucypher/cli/protocol.py | 20 ++- nucypher/cli/types.py | 2 +- nucypher/config/constants.py | 2 +- nucypher/config/node.py | 49 ++++-- nucypher/network/nodes.py | 127 +-------------- nucypher/utilities/sandbox/ursula.py | 2 +- .../test_alice_can_grant_and_revoke.py | 2 +- ...test_crypto_characters_and_their_powers.py | 2 +- tests/cli/commands/test_status.py | 2 +- tests/fixtures.py | 16 +- 15 files changed, 288 insertions(+), 285 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 0d4311527..98bbf0e35 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -70,7 +70,7 @@ class Character(Learner): is_me: bool = True, federated_only: bool = False, blockchain: Blockchain = None, - checksum_address: bytes = NO_BLOCKCHAIN_CONNECTION.bool_value(False), + checksum_public_address: bytes = NO_BLOCKCHAIN_CONNECTION.bool_value(False), network_middleware: RestMiddleware = None, keyring_dir: str = None, crypto_power: CryptoPower = None, @@ -118,7 +118,7 @@ class Character(Learner): else: self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups) - self._checksum_address = checksum_address + self._checksum_address = checksum_public_address # # Self-Character # @@ -161,10 +161,10 @@ class Character(Learner): # Decentralized # if not federated_only: - if not checksum_address: - raise ValueError("No checksum_address provided while running in a non-federated mode.") + if not checksum_public_address: + raise ValueError("No checksum_public_address provided while running in a non-federated mode.") else: - self._checksum_address = checksum_address # TODO: Check that this matches BlockchainPower + self._checksum_address = checksum_public_address # TODO: Check that this matches BlockchainPower # # Federated # @@ -173,11 +173,11 @@ class Character(Learner): self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION - if checksum_address: + if checksum_public_address: # We'll take a checksum address, as long as it matches their singing key - if not checksum_address == self.checksum_public_address: + if not checksum_public_address == self.checksum_public_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." - raise self.SuspiciousActivity(error.format(checksum_address)) + raise self.SuspiciousActivity(error.format(checksum_public_address)) # # Nicknames @@ -254,7 +254,7 @@ class Character(Learner): 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. + # TODO: Need to be federated only until we figure out the best way to get the checksum_public_address in here. """ diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 16c7d2c38..38d63b321 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -20,10 +20,12 @@ from collections import OrderedDict import maya import requests +import socket +import time from cryptography.hazmat.backends import default_backend 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 cryptography.x509 import load_pem_x509_certificate, Certificate, NameOID from eth_utils import to_checksum_address from functools import partial from twisted.internet import threads @@ -33,17 +35,20 @@ from typing import List from bytestring_splitter import BytestringSplitter, VariableLengthBytestring from constant_sorrow import constants +from constant_sorrow.constants import PUBLIC_ONLY from nucypher.blockchain.eth.actors import PolicyAuthor, Miner from nucypher.blockchain.eth.agents import MinerAgent from nucypher.characters.base import Character, Learner -from nucypher.config.storages import NodeStorage +from nucypher.config.storages import NodeStorage, ForgetfulNodeStorage from nucypher.crypto.api import keccak_digest from nucypher.crypto.constants import PUBLIC_ADDRESS_LENGTH, PUBLIC_KEY_LENGTH from nucypher.crypto.powers import SigningPower, EncryptingPower, DelegatingPower, BlockchainPower from nucypher.keystore.keypairs import HostingKeypair +from nucypher.network.middleware import RestMiddleware from nucypher.network.nodes import Teacher -from nucypher.network.protocols import InterfaceInfo +from nucypher.network.protocols import InterfaceInfo, parse_node_uri from nucypher.network.server import ProxyRESTServer, TLSHostingPower, ProxyRESTRoutes +from nucypher.utilities.decorators import validate_checksum_address from umbral.keys import UmbralPublicKey from umbral.signing import Signature @@ -54,11 +59,11 @@ class Alice(Character, PolicyAuthor): def __init__(self, is_me=True, federated_only=False, network_middleware=None, *args, **kwargs) -> None: policy_agent = kwargs.pop("policy_agent", None) - checksum_address = kwargs.pop("checksum_address", None) + checksum_address = kwargs.pop("checksum_public_address", None) Character.__init__(self, is_me=is_me, federated_only=federated_only, - checksum_address=checksum_address, + checksum_public_address=checksum_address, network_middleware=network_middleware, *args, **kwargs) @@ -455,7 +460,7 @@ class Ursula(Teacher, Character, Miner): timestamp=None, # Blockchain - checksum_address: str = None, + checksum_public_address: str = None, # Character password: str = None, @@ -475,7 +480,7 @@ class Ursula(Teacher, Character, Miner): self._work_orders = list() Character.__init__(self, is_me=is_me, - checksum_address=checksum_address, + checksum_public_address=checksum_public_address, start_learning_now=start_learning_now, federated_only=federated_only, crypto_power=crypto_power, @@ -493,7 +498,7 @@ class Ursula(Teacher, Character, Miner): # Staking Ursula # if not federated_only: - Miner.__init__(self, is_me=is_me, checksum_address=checksum_address) + Miner.__init__(self, is_me=is_me, checksum_address=checksum_public_address) # Access staking node via node's transacting keys TODO: Better handle ephemeral staking self ursula blockchain_power = BlockchainPower(blockchain=self.blockchain, account=self.checksum_public_address) @@ -602,7 +607,7 @@ class Ursula(Teacher, Character, Miner): return deployer def rest_server_certificate(self): # TODO: relocate and use reference on TLS hosting power - return self.get_deployer().cert.to_cryptography() + return self.certificate def __bytes__(self): @@ -627,6 +632,124 @@ class Ursula(Teacher, Character, Miner): # Alternate Constructors # + @classmethod + def from_rest_url(cls, + network_middleware: RestMiddleware, + host: str, + port: int, + certificate_filepath, + federated_only: bool = False, + *args, **kwargs + ): + + response = network_middleware.node_information(host, port, certificate_filepath=certificate_filepath) + if not response.status_code == 200: + raise RuntimeError("Got a bad response: {}".format(response)) + + stranger_ursula_from_public_keys = cls.from_bytes(response.content, federated_only=federated_only, *args, **kwargs) + return stranger_ursula_from_public_keys + + + @classmethod + def from_seednode_metadata(cls, + seednode_metadata, + *args, + **kwargs): + """ + Essentially another deserialization method, but this one doesn't reconstruct a complete + node from bytes; instead it's just enough to connect to and verify a node. + """ + + return cls.from_seed_and_stake_info(checksum_public_address=seednode_metadata.checksum_public_address, + host=seednode_metadata.rest_host, + port=seednode_metadata.rest_port, + *args, **kwargs) + + @classmethod + def from_teacher_uri(cls, + federated_only: bool, + teacher_uri: str, + min_stake: int, + ) -> 'Ursula': + + hostname, port, checksum_address = parse_node_uri(uri=teacher_uri) + try: + teacher = cls.from_seed_and_stake_info(host=hostname, + port=port, + federated_only=federated_only, + checksum_public_address=checksum_address, + minimum_stake=min_stake) + + except (socket.gaierror, requests.exceptions.ConnectionError, ConnectionRefusedError): + # self.log.warn("Can't connect to seed node. Will retry.") + time.sleep(5) # TODO: Move this 5 + + else: + return teacher + + @classmethod + @validate_checksum_address + def from_seed_and_stake_info(cls, + host: str, + port: int, + federated_only: bool, + minimum_stake: int = 0, + checksum_public_address: str = None, + network_middleware: RestMiddleware = None, + *args, + **kwargs + ) -> 'Ursula': + + # + # WARNING: xxx Poison xxx + # Let's learn what we can about the ... "seednode". + # + + if network_middleware is None: + network_middleware = RestMiddleware() + + # Fetch the hosts TLS certificate and read the common name + certificate = network_middleware.get_certificate(host=host, port=port) + real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value + temp_node_storage = ForgetfulNodeStorage(federated_only=federated_only) + certificate_filepath = temp_node_storage.store_host_certificate(host=real_host, + certificate=certificate) + # Load the host as a potential seed node + potential_seed_node = cls.from_rest_url( + host=real_host, + port=port, + network_middleware=network_middleware, + certificate_filepath=certificate_filepath, + federated_only=True, + *args, + **kwargs) # TODO: 466 + + if checksum_public_address: + # Ensure this is the specific node we expected + if not checksum_public_address == potential_seed_node.checksum_public_address: + template = "This seed node has a different wallet address: {} (expected {}). Are you sure this is a seednode?" + raise potential_seed_node.SuspiciousActivity(template.format(potential_seed_node.checksum_public_address, + checksum_public_address)) + + # Check the node's stake (optional) + if minimum_stake > 0: + # TODO: check the blockchain to verify that address has more then minimum_stake. #511 + raise NotImplementedError("Stake checking is not implemented yet.") + + # Verify the node's TLS certificate + try: + potential_seed_node.verify_node( + network_middleware=network_middleware, + accept_federated_only=federated_only, + certificate_filepath=certificate_filepath) + + except potential_seed_node.InvalidNode: + raise # TODO: What if our seed node fails verification? + + # OK - everyone get out + temp_node_storage.forget() + return potential_seed_node + @classmethod def from_bytes(cls, ursula_as_bytes: bytes, @@ -684,7 +807,7 @@ class Ursula(Teacher, Character, Miner): }, interface_signature=signature, timestamp=timestamp, - checksum_address=to_checksum_address(public_address), + checksum_public_address=to_checksum_address(public_address), certificate=certificate, rest_host=rest_info.host, rest_port=rest_info.port, @@ -699,8 +822,8 @@ class Ursula(Teacher, Character, Miner): node_storage: NodeStorage, checksum_adress: str, federated_only: bool = False) -> 'Ursula': - return node_storage.get(checksum_address=checksum_adress, - federated_only=federated_only) + + return node_storage.get(checksum_address=checksum_adress, federated_only=federated_only) # # Properties @@ -722,7 +845,6 @@ class Ursula(Teacher, Character, Miner): @property def rest_app(self): rest_app_on_server = self.rest_server.rest_app - if not rest_app_on_server: m = "This Ursula doesn't have a REST app attached. If you want one, init with is_me and attach_server." raise AttributeError(m) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index 8cdeedfad..28ccc44f6 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -58,7 +58,7 @@ class Vladimir(Ursula): rest_port=target_ursula.rest_information()[0].port, certificate=target_ursula.rest_server_certificate(), network_middleware=cls.network_middleware, - checksum_address = cls.fraud_address, + checksum_public_address = cls.fraud_address, ######### Asshole. timestamp=target_ursula._timestamp, interface_signature=target_ursula._interface_signature_object, diff --git a/nucypher/cli/main.py b/nucypher/cli/main.py index 771e27e9e..b1a613ab9 100644 --- a/nucypher/cli/main.py +++ b/nucypher/cli/main.py @@ -26,7 +26,8 @@ from twisted.logger import globalLogPublisher from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD from nucypher.blockchain.eth.constants import MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS -from nucypher.cli.painting import BANNER, paint_configuration +from nucypher.characters.lawful import Ursula +from nucypher.cli.painting import BANNER, paint_configuration, paint_known_nodes, paint_contract_status from nucypher.cli.protocol import UrsulaCommandProtocol from nucypher.cli.types import ( EIP55_CHECKSUM_ADDRESS, @@ -37,8 +38,6 @@ from nucypher.cli.types import ( STAKE_DURATION ) from nucypher.config.characters import UrsulaConfiguration -from nucypher.config.constants import SEEDNODES -from nucypher.network.nodes import Teacher from nucypher.utilities.logging import ( logToSentry, getTextFileObserver, @@ -127,80 +126,11 @@ def status(click_config, config_file): ursula_config.connect_to_blockchain(provider_uri=ursula_config.provider_uri) ursula_config.connect_to_contracts() - contract_payload = """ + # Contracts + paint_contract_status(ursula_config=ursula_config, click_config=click_config) - | NuCypher ETH Contracts | - - Provider URI ............. {provider_uri} - Registry Path ............ {registry_filepath} - - NucypherToken ............ {token} - MinerEscrow .............. {escrow} - PolicyManager ............ {manager} - - """.format(provider_uri=ursula_config.blockchain.interface.provider_uri, - registry_filepath=ursula_config.blockchain.interface.registry.filepath, - token=ursula_config.token_agent.contract_address, - escrow=ursula_config.miner_agent.contract_address, - manager=ursula_config.policy_agent.contract_address, - period=ursula_config.miner_agent.get_current_period()) - click.secho(contract_payload) - - network_payload = """ - | Blockchain Network | - - Current Period ........... {period} - Gas Price ................ {gas_price} - Active Staking Ursulas ... {ursulas} - - """.format(period=click_config.miner_agent.get_current_period(), - gas_price=click_config.blockchain.interface.w3.eth.gasPrice, - ursulas=click_config.miner_agent.get_miner_population()) - click.secho(network_payload) - - # # Known Nodes - # - - # Gather Data - known_nodes = ursula_config.read_known_nodes() - known_certificates = ursula_config.node_storage.all(certificates_only=True, federated_only=ursula_config.federated_only) - number_of_known_nodes = len(known_nodes) - seen_nodes = len(known_certificates) - - # Operating Mode - federated_only = ursula_config.federated_only - if federated_only: - click.secho("Configured in Federated Only mode", fg='green') - - # Heading - label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes) - heading = '\n' + label + " " * (45 - len(label)) + "Last Seen " - click.secho(heading, bold=True, nl=False) - - # Legend - color_index = { - 'self': 'yellow', - 'known': 'white', - 'seednode': 'blue' - } - for node_type, color in color_index.items(): - click.secho('{0:<6} | '.format(node_type), fg=color, nl=False) - click.echo('\n') - - seednode_addresses = list(bn.checksum_address for bn in SEEDNODES) - for node in known_nodes: - row_template = "{} | {} | {}" - node_type = 'known' - if node.checksum_public_address == ursula_config.checksum_address: - node_type = 'self' - row_template += ' ({})'.format(node_type) - if node.checksum_public_address in seednode_addresses: - node_type = 'seednode' - row_template += ' ({})'.format(node_type) - click.secho(row_template.format(node.checksum_public_address, - node.rest_url(), - node.timestamp), fg=color_index[node_type]) + paint_known_nodes(ursula=ursula_config) @nucypher_cli.command() @@ -301,7 +231,7 @@ def ursula(click_config, rest_port=rest_port, db_filepath=db_filepath, federated_only=federated_only, - checksum_address=checksum_address, + checksum_public_address=checksum_address, no_registry=federated_only or no_registry, registry_filepath=registry_filepath, provider_uri=provider_uri) @@ -324,7 +254,7 @@ def ursula(click_config, poa=poa, registry_filepath=registry_filepath, provider_uri=provider_uri, - checksum_address=checksum_address, + checksum_public_address=checksum_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, @@ -338,7 +268,7 @@ def ursula(click_config, # poa = poa, # registry_filepath = registry_filepath, # provider_uri = provider_uri, - # checksum_address = checksum_address, + # checksum_public_address = checksum_public_address, # federated_only = federated_only, # rest_host = rest_host, # rest_port = rest_port, @@ -365,14 +295,13 @@ def ursula(click_config, # teacher_nodes = list() if teacher_uri: - node = Teacher.from_teacher_uri(teacher_uri=teacher_uri, - min_stake=min_stake, - federated_only=federated_only) + node = Ursula.from_teacher_uri(teacher_uri=teacher_uri, min_stake=min_stake, federated_only=federated_only) teacher_nodes.append(node) + # # Produce - Step 2 # - ursula = ursula_config.produce() + ursula = ursula_config.produce(known_nodes=teacher_nodes) ursula_config.log.debug("Initialized Ursula {}".format(ursula), fg='green') # GO! @@ -488,7 +417,7 @@ def stake(click_config, if not checksum_address: - if config.accounts == NO_BLOCKCHAIN_CONNECTION: + if click_config.accounts == NO_BLOCKCHAIN_CONNECTION: click.echo('No account found.') raise click.Abort() diff --git a/nucypher/cli/painting.py b/nucypher/cli/painting.py index ab45e9807..692ed2d05 100644 --- a/nucypher/cli/painting.py +++ b/nucypher/cli/painting.py @@ -47,6 +47,23 @@ BANNER = """ # Paint # +def build_fleet_state_status(ursula) -> str: + # Build FleetState status line + if ursula.known_nodes.checksum is not NO_KNOWN_NODES: + fleet_state_checksum = ursula.known_nodes.checksum[:7] + fleet_state_nickname = ursula.known_nodes.nickname + fleet_state_icon = ursula.known_nodes.icon + fleet_state = '{checksum} ⇀{nickname}↽ {icon}'.format(icon=fleet_state_icon, + nickname=fleet_state_nickname, + checksum=fleet_state_checksum) + elif ursula.known_nodes.checksum is not NO_KNOWN_NODES: + fleet_state = 'No Known Nodes' + else: + fleet_state = 'Unknown' + + return fleet_state + + def paint_configuration(config_filepath: str) -> None: json_config = UrsulaConfiguration._read_configuration_file(filepath=config_filepath) click.secho("\n======== Ursula Configuration ======== \n", bold=True) @@ -68,15 +85,7 @@ def paint_node_status(ursula, start_time): teacher = 'Current Teacher ..... {}'.format(ursula._current_teacher_node) # Build FleetState status line - if ursula.known_nodes.checksum is not NO_KNOWN_NODES: - fleet_state_checksum = ursula.known_nodes.checksum[:7] - fleet_state_nickname = ursula.known_nodes.nickname - fleet_state_icon = ursula.known_nodes.icon - fleet_state = '{2} {1} ({0})'.format(fleet_state_checksum, fleet_state_nickname, fleet_state_icon) - elif ursula.known_nodes.checksum is not NO_KNOWN_NODES: - fleet_state = 'No Known Nodes' - else: - fleet_state = 'Unknown' + fleet_state = build_fleet_state_status(ursula=ursula) stats = ['⇀URSULA {}↽'.format(ursula.nickname_icon), '{}'.format(ursula), @@ -108,8 +117,13 @@ def paint_known_nodes(ursula) -> None: # Heading label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes) - heading = '\n' + label + " " * (45 - len(label)) + "Last Seen " - click.secho(heading, bold=True, nl=False) + heading = '\n' + label + " " * (45 - len(label)) + click.secho(heading, bold=True, nl=True) + + # Build FleetState status line + fleet_state = build_fleet_state_status(ursula=ursula) + fleet_status_line = 'Fleet State {}'.format(fleet_state) + click.secho(fleet_status_line, fg='blue', bold=True, nl=True) # Legend color_index = { @@ -117,14 +131,16 @@ def paint_known_nodes(ursula) -> None: 'known': 'white', 'seednode': 'blue' } - for node_type, color in color_index.items(): - click.secho('{0:<6} | '.format(node_type), fg=color, nl=False) - click.echo('\n') + + # Ledgend + # for node_type, color in color_index.items(): + # click.secho('{0:<6} | '.format(node_type), fg=color, nl=False) + # click.echo('\n') seednode_addresses = list(bn.checksum_address for bn in SEEDNODES) for node in known_nodes: - row_template = "{} | {} | {} | {} | {}" + row_template = "{} | {}" node_type = 'known' if node.checksum_public_address == ursula.checksum_public_address: node_type = 'self' @@ -132,9 +148,37 @@ def paint_known_nodes(ursula) -> None: elif node.checksum_public_address in seednode_addresses: node_type = 'seednode' row_template += ' ({})'.format(node_type) - click.secho(row_template.format(node.checksum_public_address, - node.rest_url().ljust(20), - node.nickname.ljust(50), - node.timestamp, - node.last_seen, - ), fg=color_index[node_type]) + click.secho(row_template.format(node.rest_url().ljust(20), node), fg=color_index[node_type]) + + +def paint_contract_status(ursula_config, click_config): + contract_payload = """ + + | NuCypher ETH Contracts | + + Provider URI ............. {provider_uri} + Registry Path ............ {registry_filepath} + + NucypherToken ............ {token} + MinerEscrow .............. {escrow} + PolicyManager ............ {manager} + + """.format(provider_uri=ursula_config.blockchain.interface.provider_uri, + registry_filepath=ursula_config.blockchain.interface.registry.filepath, + token=ursula_config.token_agent.contract_address, + escrow=ursula_config.miner_agent.contract_address, + manager=ursula_config.policy_agent.contract_address, + period=ursula_config.miner_agent.get_current_period()) + click.secho(contract_payload) + + network_payload = """ + | Blockchain Network | + + Current Period ........... {period} + Gas Price ................ {gas_price} + Active Staking Ursulas ... {ursulas} + + """.format(period=click_config.miner_agent.get_current_period(), + gas_price=click_config.blockchain.interface.w3.eth.gasPrice, + ursulas=click_config.miner_agent.get_miner_population()) + click.secho(network_payload) diff --git a/nucypher/cli/protocol.py b/nucypher/cli/protocol.py index e3f69e8e3..a02bfd2b7 100644 --- a/nucypher/cli/protocol.py +++ b/nucypher/cli/protocol.py @@ -24,6 +24,8 @@ import maya from twisted.internet import reactor from twisted.protocols.basic import LineReceiver +from nucypher.cli.painting import build_fleet_state_status + class UrsulaCommandProtocol(LineReceiver): @@ -39,12 +41,20 @@ class UrsulaCommandProtocol(LineReceiver): # Expose Ursula functional entry points self.__commands = { - 'stop': reactor.stop, - 'known_nodes': self.paintKnownNodes, + + # Status 'status': self.paintStatus, + 'known_nodes': self.paintKnownNodes, + 'fleet_state': self.paintFleetState, + + # Learning Control 'cycle_teacher': self.ursula.cycle_teacher_node, 'start_learning': self.ursula.start_learning_loop, - 'stop_learning': self.ursula.stop_learning_loop + 'stop_learning': self.ursula.stop_learning_loop, + + # Process Control + 'stop': reactor.stop, + } super().__init__() @@ -61,6 +71,10 @@ class UrsulaCommandProtocol(LineReceiver): from nucypher.cli.painting import paint_node_status paint_node_status(ursula=self.ursula, start_time=self.start_time) + def paintFleetState(self): + line = '{}'.format(build_fleet_state_status(ursula=self.ursula)) + click.secho(line) + def connectionMade(self): message = 'Attached {}@{}'.format( diff --git a/nucypher/cli/types.py b/nucypher/cli/types.py index e6ac2c041..a6413c4cd 100644 --- a/nucypher/cli/types.py +++ b/nucypher/cli/types.py @@ -25,7 +25,7 @@ from nucypher.blockchain.eth.constants import MIN_ALLOWED_LOCKED, MAX_MINTING_PE class ChecksumAddress(click.ParamType): - name = 'checksum_address' + name = 'checksum_public_address' def convert(self, value, param, ctx): if is_checksum_address(value): diff --git a/nucypher/config/constants.py b/nucypher/config/constants.py index 432f51f6a..06b5c18ed 100644 --- a/nucypher/config/constants.py +++ b/nucypher/config/constants.py @@ -34,5 +34,5 @@ DEFAULT_CONFIG_ROOT = APP_DIR.user_data_dir USER_LOG_DIR = APP_DIR.user_log_dir # Static Seednodes -SeednodeMetadata = namedtuple('seednode', ['checksum_address', 'rest_host', 'rest_port']) +SeednodeMetadata = namedtuple('seednode', ['checksum_public_address', 'rest_host', 'rest_port']) SEEDNODES = tuple() diff --git a/nucypher/config/node.py b/nucypher/config/node.py index 33808c6c5..8b10d2fa0 100644 --- a/nucypher/config/node.py +++ b/nucypher/config/node.py @@ -47,6 +47,7 @@ from nucypher.config.keyring import NucypherKeyring from nucypher.config.storages import NodeStorage, ForgetfulNodeStorage, LocalFileBasedNodeStorage from nucypher.crypto.powers import CryptoPowerUp, CryptoPower from nucypher.network.middleware import RestMiddleware +from nucypher.network.nodes import FleetStateTracker from umbral.signing import Signature @@ -99,7 +100,7 @@ class NodeConfiguration(ABC): # Identity is_me: bool = True, - checksum_address: str = None, + checksum_public_address: str = None, crypto_power: CryptoPower = None, # Keyring @@ -126,7 +127,7 @@ class NodeConfiguration(ABC): # Node Storage known_nodes: set = None, node_storage: NodeStorage = None, - load_metadata: bool = True, + reload_metadata: bool = True, save_metadata: bool = True, # Blockchain @@ -195,11 +196,11 @@ class NodeConfiguration(ABC): # Identity # self.is_me = is_me - self.checksum_address = checksum_address + self.checksum_public_address = checksum_public_address if self.is_me is True or dev_mode is True: # Self - if self.checksum_address and dev_mode is False: + if self.checksum_public_address and dev_mode is False: self.attach_keyring() self.network_middleware = network_middleware or self.__DEFAULT_NETWORK_MIDDLEWARE_CLASS() else: @@ -214,12 +215,17 @@ class NodeConfiguration(ABC): # # Learner # - self.known_nodes = known_nodes or set() 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.load_metadata = load_metadata + self.reload_metadata = reload_metadata + + self.__fleet_state = FleetStateTracker() + known_nodes = known_nodes or set() + if known_nodes: + self.known_nodes._nodes.update({node.checksum_public_address: node for node in known_nodes}) + self.known_nodes.record_fleet_state() # # Blockchain @@ -240,7 +246,7 @@ class NodeConfiguration(ABC): # Ephemeral dev settings self.save_metadata = False - self.load_metadata = False + self.reload_metadata = False # Generate one-time alphanumeric development password alphabet = string.ascii_letters + string.digits @@ -258,6 +264,10 @@ class NodeConfiguration(ABC): def dev_mode(self): return self.__dev_mode + @property + def known_nodes(self): + return self.__fleet_state + def connect_to_blockchain(self, provider_uri: str, poa: bool = False, compile_contracts: bool = False): if self.federated_only: raise NodeConfiguration.ConfigurationError("Cannot connect to blockchain in federated mode") @@ -278,7 +288,10 @@ class NodeConfiguration(ABC): self.log.debug("Established connection to nucypher contracts") def read_known_nodes(self): - self.known_nodes.update(self.node_storage.all(federated_only=self.federated_only)) + known_nodes = self.node_storage.all(federated_only=self.federated_only) + known_nodes = {node.checksum_public_address: node for node in known_nodes} + self.known_nodes._nodes.update(known_nodes) + self.known_nodes.record_fleet_state() return self.known_nodes def forget_nodes(self) -> None: @@ -389,7 +402,7 @@ class NodeConfiguration(ABC): # Identity is_me=self.is_me, federated_only=self.federated_only, # TODO: 466 - checksum_address=self.checksum_address, + checksum_public_address=self.checksum_public_address, keyring_dir=self.keyring_dir, # Behavior @@ -403,8 +416,12 @@ class NodeConfiguration(ABC): @property def dynamic_payload(self, **overrides) -> dict: """Exported dynamic configuration values for initializing Ursula""" - if self.load_metadata: - self.known_nodes.update(self.node_storage.all(federated_only=self.federated_only)) + if self.reload_metadata: + known_nodes = self.node_storage.all(federated_only=self.federated_only) + known_nodes = {node.checksum_public_address: node for node in known_nodes} + self.known_nodes._nodes.update(known_nodes) + self.known_nodes.record_fleet_state() + payload = dict(network_middleware=self.network_middleware or self.__DEFAULT_NETWORK_MIDDLEWARE_CLASS(), known_nodes=self.known_nodes, node_storage=self.node_storage, @@ -503,15 +520,15 @@ class NodeConfiguration(ABC): def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None: if self.keyring is not NO_KEYRING_ATTACHED: - if self.keyring.checksum_address != (checksum_address or self.checksum_address): + if self.keyring.checksum_address != (checksum_address or self.checksum_public_address): raise self.ConfigurationError("There is already a keyring attached to this configuration.") return - if (checksum_address or self.checksum_address) is None: + if (checksum_address or self.checksum_public_address) is None: raise self.ConfigurationError("No account specified to unlock keyring") self.keyring = NucypherKeyring(keyring_root=self.keyring_dir, # type: str - account=checksum_address or self.checksum_address, # type: str + account=checksum_address or self.checksum_public_address, # type: str *args, **kwargs) def write_keyring(self, @@ -533,9 +550,9 @@ class NodeConfiguration(ABC): # TODO: Operating mode switch #466 if self.federated_only or not wallet: - self.checksum_address = self.keyring.federated_address + self.checksum_public_address = self.keyring.federated_address else: - self.checksum_address = self.keyring.checksum_address + self.checksum_public_address = self.keyring.checksum_address return self.keyring diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 4ea2129ea..be6cf1259 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -250,8 +250,7 @@ class Learner: self.unresponsive_startup_nodes = list() # TODO: Attempt to use these again later for node in known_nodes: try: - self.remember_node( - node) # TODO: Need to test this better - do we ever init an Ursula-Learner with Node Storage? + self.remember_node(node) # TODO: Need to test this better - do we ever init an Ursula-Learner with Node Storage? except self.UnresponsiveTeacher: self.unresponsive_startup_nodes.append(node) @@ -285,7 +284,7 @@ class Learner: def __attempt_seednode_learning(seednode_metadata, current_attempt=1): from nucypher.characters.lawful import Ursula self.log.debug( - "Seeding from: {}|{}:{}".format(seednode_metadata.checksum_address, + "Seeding from: {}|{}:{}".format(seednode_metadata.checksum_public_address, seednode_metadata.rest_host, seednode_metadata.rest_port)) @@ -725,128 +724,6 @@ class Teacher: Raise when a Character tries to use another Character as decentralized when the latter is federated_only. """ - # - # Alternate Constructors - # - - @classmethod - def from_seednode_metadata(cls, - seednode_metadata, - *args, - **kwargs): - """ - Essentially another deserialization method, but this one doesn't reconstruct a complete - node from bytes; instead it's just enough to connect to and verify a node. - """ - - return cls.from_seed_and_stake_info(checksum_address=seednode_metadata.checksum_address, - host=seednode_metadata.rest_host, - port=seednode_metadata.rest_port, - *args, **kwargs) - - @classmethod - def from_teacher_uri(cls, - federated_only: bool, - teacher_uri: str, - min_stake: int, - ) -> 'Ursula': - - hostname, port, checksum_address = parse_node_uri(uri=teacher_uri) - try: - teacher = cls.from_seed_and_stake_info(host=hostname, - port=port, - federated_only=federated_only, - checksum_address=checksum_address, - minimum_stake=min_stake) - - except (socket.gaierror, requests.exceptions.ConnectionError, ConnectionRefusedError): - # self.log.warn("Can't connect to seed node. Will retry.") - time.sleep(5) # TODO: Move this 5 - - else: - return teacher - - @classmethod - @validate_checksum_address - def from_seed_and_stake_info(cls, - host: str, - port: int, - federated_only: bool, - minimum_stake: int = 0, - checksum_address: str = None, - network_middleware: RestMiddleware = None, - *args, - **kwargs - ) -> 'Teacher': - - # - # WARNING: xxx Poison xxx - # Let's learn what we can about the ... "seednode". - # - - if network_middleware is None: - network_middleware = RestMiddleware() - - # Fetch the hosts TLS certificate and read the common name - certificate = network_middleware.get_certificate(host=host, port=port) - real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value - temp_node_storage = ForgetfulNodeStorage(federated_only=federated_only) - certificate_filepath = temp_node_storage.store_host_certificate(host=real_host, - certificate=certificate) - # Load the host as a potential seed node - potential_seed_node = cls.from_rest_url( - host=real_host, - port=port, - network_middleware=network_middleware, - certificate_filepath=certificate_filepath, - federated_only=True, - *args, - **kwargs) # TODO: 466 - - if checksum_address: - # Ensure this is the specific node we expected - if not checksum_address == potential_seed_node.checksum_public_address: - template = "This seed node has a different wallet address: {} (expected {}). Are you sure this is a seednode?" - raise potential_seed_node.SuspiciousActivity(template.format(potential_seed_node.checksum_public_address, - checksum_address)) - - # Check the node's stake (optional) - if minimum_stake > 0: - # TODO: check the blockchain to verify that address has more then minimum_stake. #511 - raise NotImplementedError("Stake checking is not implemented yet.") - - # Verify the node's TLS certificate - try: - potential_seed_node.verify_node( - network_middleware=network_middleware, - accept_federated_only=federated_only, - certificate_filepath=certificate_filepath) - - except potential_seed_node.InvalidNode: - raise # TODO: What if our seed node fails verification? - - # OK - everyone get out - temp_node_storage.forget() - return potential_seed_node - - @classmethod - def from_rest_url(cls, - network_middleware: RestMiddleware, - host: str, - port: int, - certificate_filepath, - federated_only: bool = False, - *args, **kwargs - ): - - response = network_middleware.node_information(host, port, certificate_filepath=certificate_filepath) - if not response.status_code == 200: - raise RuntimeError("Got a bad response: {}".format(response)) - - stranger_ursula_from_public_keys = cls.from_bytes(response.content, federated_only=federated_only, *args, - **kwargs) - return stranger_ursula_from_public_keys - @classmethod def from_tls_hosting_power(cls, tls_hosting_power: TLSHostingPower, *args, **kwargs) -> 'Teacher': certificate_filepath = tls_hosting_power.keypair.certificate_filepath diff --git a/nucypher/utilities/sandbox/ursula.py b/nucypher/utilities/sandbox/ursula.py index 2697d1ba1..27b7b892d 100644 --- a/nucypher/utilities/sandbox/ursula.py +++ b/nucypher/utilities/sandbox/ursula.py @@ -83,7 +83,7 @@ def make_decentralized_ursulas(ursula_config: UrsulaConfiguration, ursulas = set() for port, checksum_address in enumerate(ether_addresses, start=starting_port): - ursula = ursula_config.produce(checksum_address=checksum_address, + ursula = ursula_config.produce(checksum_public_address=checksum_address, db_filepath=MOCK_URSULA_DB_FILEPATH, rest_port=port + 100, **ursula_overrides) diff --git a/tests/characters/test_alice_can_grant_and_revoke.py b/tests/characters/test_alice_can_grant_and_revoke.py index 3abe8798c..29ba0ba67 100644 --- a/tests/characters/test_alice_can_grant_and_revoke.py +++ b/tests/characters/test_alice_can_grant_and_revoke.py @@ -140,7 +140,7 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir): start_learning_now=False, federated_only=True, save_metadata=False, - load_metadata=False) + reload_metadata=False) # Generate keys and write them the disk alice_config.initialize(password=INSECURE_DEVELOPMENT_PASSWORD) diff --git a/tests/characters/test_crypto_characters_and_their_powers.py b/tests/characters/test_crypto_characters_and_their_powers.py index 96d1c05e3..4af6d8105 100644 --- a/tests/characters/test_crypto_characters_and_their_powers.py +++ b/tests/characters/test_crypto_characters_and_their_powers.py @@ -99,7 +99,7 @@ def test_character_blockchain_power(testerchain): sig_privkey = testerchain.interface.providers[0].ethereum_tester.backend._key_lookup[eth_utils.to_canonical_address(eth_address)] sig_pubkey = sig_privkey.public_key - signer = Character(is_me=True, checksum_address=eth_address) + signer = Character(is_me=True, checksum_public_address=eth_address) signer._crypto_power.consume_power_up(BlockchainPower(testerchain, eth_address)) # Due to testing backend, the account is already unlocked. diff --git a/tests/cli/commands/test_status.py b/tests/cli/commands/test_status.py index 050c8a1c4..ec43ba397 100644 --- a/tests/cli/commands/test_status.py +++ b/tests/cli/commands/test_status.py @@ -59,6 +59,6 @@ def test_empty_federated_status(click_runner, custom_filepath): assert result.exit_code == 0 assert 'Federated Only' in result.output - heading = 'Known Nodes (connected 0 / seen 0) Last Seen self | known | seednode |' + heading = 'Known Nodes (connected 0 / seen 0)' assert heading in result.output assert 'password' not in result.output diff --git a/tests/fixtures.py b/tests/fixtures.py index 4e7022bb1..823d45bbc 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -128,7 +128,7 @@ def ursula_federated_test_config(): federated_only=True, network_middleware=MockRestMiddleware(), save_metadata=False, - load_metadata=False) + reload_metadata=False) yield ursula_config ursula_config.cleanup() @@ -145,7 +145,7 @@ def ursula_decentralized_test_config(three_agents): network_middleware=MockRestMiddleware(), import_seed_registry=False, save_metadata=False, - load_metadata=False) + reload_metadata=False) yield ursula_config ursula_config.cleanup() @@ -159,7 +159,7 @@ def alice_federated_test_config(federated_ursulas): federated_only=True, abort_on_learning_error=True, save_metadata=False, - load_metadata=False) + reload_metadata=False) yield config config.cleanup() @@ -171,13 +171,13 @@ def alice_blockchain_test_config(blockchain_ursulas, three_agents): config = AliceConfiguration(dev_mode=True, is_me=True, - checksum_address=alice_address, + checksum_public_address=alice_address, network_middleware=MockRestMiddleware(), known_nodes=blockchain_ursulas, abort_on_learning_error=True, import_seed_registry=False, save_metadata=False, - load_metadata=False) + reload_metadata=False) yield config config.cleanup() @@ -190,7 +190,7 @@ def bob_federated_test_config(): abort_on_learning_error=True, federated_only=True, save_metadata=False, - load_metadata=False) + reload_metadata=False) yield config config.cleanup() @@ -201,7 +201,7 @@ def bob_blockchain_test_config(blockchain_ursulas, three_agents): etherbase, alice_address, bob_address, *everyone_else = token_agent.blockchain.interface.w3.eth.accounts config = BobConfiguration(dev_mode=True, - checksum_address=bob_address, + checksum_public_address=bob_address, network_middleware=MockRestMiddleware(), known_nodes=blockchain_ursulas, start_learning_now=False, @@ -209,7 +209,7 @@ def bob_blockchain_test_config(blockchain_ursulas, three_agents): federated_only=False, import_seed_registry=False, save_metadata=False, - load_metadata=False) + reload_metadata=False) yield config config.cleanup()