diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py
index 2345b99b2..516e831e8 100644
--- a/nucypher/characters/base.py
+++ b/nucypher/characters/base.py
@@ -104,6 +104,8 @@ class Character(Learner):
#
# 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)
diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py
index a7711cf09..6efe10be8 100644
--- a/nucypher/characters/lawful.py
+++ b/nucypher/characters/lawful.py
@@ -17,7 +17,7 @@ along with nucypher. If not, see .
import json
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
import contextlib
import maya
@@ -39,7 +39,6 @@ from constant_sorrow.constants import (
READY,
INVALIDATED
)
-from collections import OrderedDict, defaultdict
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.serialization import Encoding
@@ -59,7 +58,6 @@ from typing import Dict, Iterable, List, Tuple, Union, Optional, Sequence, Set
from umbral import pre
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
-from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature
import nucypher
@@ -101,6 +99,7 @@ from nucypher.network.protocols import InterfaceInfo, parse_node_uri
from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app
from nucypher.network.trackers import AvailabilityTracker
from nucypher.utilities.logging import Logger
+from nucypher.utilities.networking import validate_worker_ip
class Alice(Character, BlockchainPolicyAuthor):
@@ -1258,9 +1257,13 @@ class Ursula(Teacher, Character, Worker):
if result > 0:
self.log.debug(f"Pruned {result} treasure maps.")
+ def __preflight(self) -> None:
+ """Called immediately before running services"""
+ validate_worker_ip(worker_ip=self.rest_interface.host)
+
def run(self,
emitter: StdoutEmitter = None,
- discovery: bool = True,
+ discovery: bool = True, # TODO: see below
availability: bool = True,
worker: bool = True,
pruning: bool = True,
@@ -1272,6 +1275,8 @@ class Ursula(Teacher, Character, Worker):
"""Schedule and start select ursula services, then optionally start the reactor."""
+ self.__preflight()
+
#
# Async loops ordered by schedule priority
#
diff --git a/nucypher/cli/actions/configure.py b/nucypher/cli/actions/configure.py
index 196fff46a..3eb6a77a3 100644
--- a/nucypher/cli/actions/configure.py
+++ b/nucypher/cli/actions/configure.py
@@ -37,10 +37,11 @@ from nucypher.cli.literature import (
COLLECT_URSULA_IPV4_ADDRESS,
CONFIRM_URSULA_IPV4_ADDRESS
)
-from nucypher.cli.types import IPV4_ADDRESS
+from nucypher.cli.types import IPV4_ADDRESS, WORKER_IP
from nucypher.config.characters import StakeHolderConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_WORKER_IP_ADDRESS
from nucypher.config.node import CharacterConfiguration
+from nucypher.utilities.networking import InvalidWorkerIP, validate_worker_ip
from nucypher.utilities.networking import determine_external_ip_address, UnknownIPAddress
@@ -119,12 +120,12 @@ def handle_invalid_configuration_file(emitter: StdoutEmitter,
raise # crash :-(
-def collect_external_ip_address(emitter: StdoutEmitter, network: str, force: bool = False) -> str:
+def collect_worker_ip_address(emitter: StdoutEmitter, network: str, force: bool = False) -> str:
# From environment variable # TODO: remove this environment variable?
ip = os.environ.get(NUCYPHER_ENVVAR_WORKER_IP_ADDRESS)
if ip:
- message = f'Using IP address from {NUCYPHER_ENVVAR_WORKER_IP_ADDRESS} environment variable'
+ message = f'Using IP address ({ip}) from {NUCYPHER_ENVVAR_WORKER_IP_ADDRESS} environment variable'
emitter.message(message, verbosity=2)
return ip
@@ -141,8 +142,9 @@ def collect_external_ip_address(emitter: StdoutEmitter, network: str, force: boo
# Confirmation
if not force:
if not click.confirm(CONFIRM_URSULA_IPV4_ADDRESS.format(rest_host=ip)):
- ip = click.prompt(COLLECT_URSULA_IPV4_ADDRESS, type=IPV4_ADDRESS)
+ ip = click.prompt(COLLECT_URSULA_IPV4_ADDRESS, type=WORKER_IP)
+ validate_worker_ip(worker_ip=ip)
return ip
@@ -157,7 +159,17 @@ def perform_ip_checkup(emitter: StdoutEmitter, ursula: Ursula, force: bool = Fal
message = 'Cannot automatically determine external IP address'
emitter.message(message)
return # TODO: crash, or not to crash... that is the question
- ip_mismatch = external_ip != ursula.rest_interface.host
+ rest_host = ursula.rest_interface.host
+ try:
+ validate_worker_ip(worker_ip=rest_host)
+ except InvalidWorkerIP:
+ message = f'{rest_host} is not a valid or permitted worker IP address. Set the correct external IP then try again\n' \
+ f'automatic configuration -> nucypher ursula config ip-address\n' \
+ f'manual configuration -> nucypher ursula config --rest-host '
+ emitter.message(message)
+ return
+
+ ip_mismatch = external_ip != rest_host
if ip_mismatch and not force:
error = f'\nX External IP address ({external_ip}) does not match configuration ({ursula.rest_interface.host}).\n'
hint = f"Run 'nucypher ursula config ip-address' to reconfigure the IP address then try " \
diff --git a/nucypher/cli/commands/ursula.py b/nucypher/cli/commands/ursula.py
index 1c7ce6fa1..2f36f7513 100644
--- a/nucypher/cli/commands/ursula.py
+++ b/nucypher/cli/commands/ursula.py
@@ -18,12 +18,14 @@
import click
+from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.configure import (
destroy_configuration,
handle_missing_configuration_file,
- get_or_update_configuration, collect_external_ip_address
+ get_or_update_configuration,
+ collect_worker_ip_address
)
from nucypher.cli.actions.configure import forget as forget_nodes, perform_ip_checkup
from nucypher.cli.actions.select import (
@@ -59,7 +61,7 @@ from nucypher.cli.options import (
option_max_gas_price
)
from nucypher.cli.painting.help import paint_new_installation_help
-from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, NETWORK_PORT
+from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, NETWORK_PORT, WORKER_IP
from nucypher.cli.utils import make_cli_character, setup_emitter
from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.constants import (
@@ -176,7 +178,7 @@ class UrsulaConfigOptions:
# Resolve rest host
if not self.rest_host:
- self.rest_host = collect_external_ip_address(emitter, network=self.domain, force=force)
+ self.rest_host = collect_worker_ip_address(emitter, network=self.domain, force=force)
return UrsulaConfiguration.generate(password=get_nucypher_password(confirm=True),
config_root=config_root,
@@ -223,10 +225,10 @@ group_config_options = group_options(
max_gas_price=option_max_gas_price,
worker_address=click.option('--worker-address', help="Run the worker-ursula with a specified address", type=EIP55_CHECKSUM_ADDRESS),
federated_only=option_federated_only,
- rest_host=click.option('--rest-host', help="The host IP address to run Ursula network services on", type=click.STRING),
+ rest_host=click.option('--rest-host', help="The host IP address to run Ursula network services on", type=WORKER_IP),
rest_port=click.option('--rest-port', help="The host port to run Ursula network services on", type=NETWORK_PORT),
db_filepath=option_db_filepath,
- network=option_network(),
+ network=option_network(default=NetworksInventory.DEFAULT),
registry_filepath=option_registry_filepath,
poa=option_poa,
light=option_light,
@@ -422,7 +424,7 @@ def config(general_config, config_options, config_file, force, action):
checksum_address=config_options.worker_address,
config_class=UrsulaConfiguration)
if action == 'ip-address':
- rest_host = collect_external_ip_address(emitter=emitter, network=config_options.domain, force=force)
+ rest_host = collect_worker_ip_address(emitter=emitter, network=config_options.domain, force=force)
config_options.rest_host = rest_host
updates = config_options.get_updates()
get_or_update_configuration(emitter=emitter,
diff --git a/nucypher/cli/literature.py b/nucypher/cli/literature.py
index 2ccdd3222..7d47e9691 100644
--- a/nucypher/cli/literature.py
+++ b/nucypher/cli/literature.py
@@ -377,7 +377,7 @@ DECRYPTING_CHARACTER_KEYRING = 'Authenticating {name}'
CONFIRM_URSULA_IPV4_ADDRESS = "Detected IPv4 address ({rest_host}) - Is this the public-facing address of Ursula?"
-COLLECT_URSULA_IPV4_ADDRESS = "Enter Ursula's public-facing IPv4 address:"
+COLLECT_URSULA_IPV4_ADDRESS = "Enter Ursula's public-facing IPv4 address"
#
diff --git a/nucypher/cli/types.py b/nucypher/cli/types.py
index 763d85451..08dcd2aff 100644
--- a/nucypher/cli/types.py
+++ b/nucypher/cli/types.py
@@ -26,6 +26,7 @@ from nucypher.blockchain.economics import StandardTokenEconomics
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.token import NU
+from nucypher.utilities.networking import validate_worker_ip, InvalidWorkerIP
class ChecksumAddress(click.ParamType):
@@ -46,12 +47,24 @@ class IPv4Address(click.ParamType):
def convert(self, value, param, ctx):
try:
_address = ip_address(value)
- except ValueError as e:
+ except ValueError:
self.fail("Invalid IP Address")
else:
return value
+class WorkerIPAddress(IPv4Address):
+ name = 'worker_ip'
+
+ def convert(self, value, param, ctx):
+ _ip = super().convert(value, param, ctx)
+ try:
+ validate_worker_ip(worker_ip=_ip)
+ except InvalidWorkerIP as e:
+ self.fail(str(e))
+ return value
+
+
class DecimalType(click.ParamType):
name = 'decimal'
@@ -136,6 +149,7 @@ EXISTING_READABLE_FILE = click.Path(exists=True, dir_okay=False, file_okay=True,
# Network
NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False)
IPV4_ADDRESS = IPv4Address()
+WORKER_IP = WorkerIPAddress()
GAS_STRATEGY_CHOICES = click.Choice(list(BlockchainInterface.GAS_STRATEGIES.keys()))
UMBRAL_PUBLIC_KEY_HEX = UmbralPublicKeyHex()
diff --git a/nucypher/config/characters.py b/nucypher/config/characters.py
index e0cc682eb..2f7707061 100644
--- a/nucypher/config/characters.py
+++ b/nucypher/config/characters.py
@@ -39,7 +39,6 @@ class UrsulaConfiguration(CharacterConfiguration):
CHARACTER_CLASS = Ursula
NAME = CHARACTER_CLASS.__name__.lower()
- DEFAULT_REST_HOST = '127.0.0.1'
DEFAULT_REST_PORT = 9151
DEFAULT_DEVELOPMENT_REST_PORT = 10151
__DEFAULT_TLS_CURVE = ec.SECP384R1
@@ -48,10 +47,10 @@ class UrsulaConfiguration(CharacterConfiguration):
LOCAL_SIGNERS_ALLOWED = True
def __init__(self,
+ rest_host: str,
worker_address: str = None,
dev_mode: bool = False,
db_filepath: str = None,
- rest_host: str = None,
rest_port: int = None,
tls_curve: EllipticCurve = None,
certificate: Certificate = None,
@@ -64,7 +63,7 @@ class UrsulaConfiguration(CharacterConfiguration):
else:
rest_port = self.DEFAULT_REST_PORT
self.rest_port = rest_port
- self.rest_host = rest_host or self.DEFAULT_REST_HOST
+ self.rest_host = rest_host
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
self.certificate = certificate
self.db_filepath = db_filepath or UNINITIALIZED_CONFIGURATION
diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py
index 020aa6907..bc6713df2 100644
--- a/nucypher/network/nodes.py
+++ b/nucypher/network/nodes.py
@@ -245,8 +245,7 @@ class Learner:
from nucypher.characters.lawful import Ursula
self.node_class = node_class or Ursula
- self.node_class.set_cert_storage_function(
- node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481
+ self.node_class.set_cert_storage_function(node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481
known_nodes = known_nodes or tuple()
self.unresponsive_startup_nodes = list() # TODO: Buckets - Attempt to use these again later #567
diff --git a/nucypher/utilities/networking.py b/nucypher/utilities/networking.py
index 3a7311b80..973528ae8 100644
--- a/nucypher/utilities/networking.py
+++ b/nucypher/utilities/networking.py
@@ -16,16 +16,15 @@
"""
-from ipaddress import ip_address
-
import random
import requests
+from ipaddress import ip_address
from requests.exceptions import RequestException, HTTPError
from typing import Union
-from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
+from nucypher.config.storages import LocalFileBasedNodeStorage
from nucypher.acumen.perception import FleetSensor
-from nucypher.characters.lawful import Ursula
+from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
from nucypher.network.middleware import RestMiddleware, NucypherMiddlewareClient
from nucypher.utilities.logging import Logger
@@ -34,6 +33,10 @@ class UnknownIPAddress(RuntimeError):
pass
+class InvalidWorkerIP(RuntimeError):
+ """Raised when an Ursula is using an invalid IP address for it's server."""
+
+
RequestErrors = (
# https://requests.readthedocs.io/en/latest/user/quickstart/#errors-and-exceptions
ConnectionError,
@@ -42,9 +45,22 @@ RequestErrors = (
HTTPError
)
+RESERVED_IP_ADDRESSES = (
+ '0.0.0.0',
+ '127.0.0.1',
+ '1.2.3.4'
+)
+
IP_DETECTION_LOGGER = Logger('external-ip-detection')
+
+def validate_worker_ip(worker_ip: str) -> None:
+ if worker_ip in RESERVED_IP_ADDRESSES:
+ raise InvalidWorkerIP(f'{worker_ip} is not a valid or permitted worker IP address. '
+ f'Verify the rest_host is set to the external IPV4 address')
+
+
def __request(url: str, certificate=None) -> Union[str, None]:
"""
Utility function to send a GET request to a URL returning it's
@@ -65,6 +81,8 @@ def get_external_ip_from_default_teacher(network: str,
log: Logger = IP_DETECTION_LOGGER,
registry: BaseContractRegistry = None
) -> Union[str, None]:
+ from nucypher.characters.lawful import Ursula
+
if federated_only and registry:
raise ValueError('Federated mode must not be true if registry is provided.')
base_error = 'Cannot determine IP using default teacher'
@@ -76,13 +94,19 @@ def get_external_ip_from_default_teacher(network: str,
except KeyError:
log.debug(f'{base_error}: Unknown network "{network}".')
return
- if not registry:
- # Registry is needed to perform on-chain staking verification.
- registry = InMemoryContractRegistry.from_latest_publication(network=network)
+
+ ####
+ # TODO: Clean this mess #1481
+ node_storage = LocalFileBasedNodeStorage(federated_only=federated_only)
+ Ursula.set_cert_storage_function(node_storage.store_node_certificate)
+ Ursula.set_federated_mode(federated_only)
+ #####
+
teacher = Ursula.from_teacher_uri(teacher_uri=top_teacher_url,
- registry=registry,
federated_only=federated_only,
min_stake=0) # TODO: Handle customized min stake here.
+
+ # TODO: Pass registry here to verify stake (not essential here since it's a hardcoded node)
client = NucypherMiddlewareClient()
try:
response = client.get(node_or_sprout=teacher, path=f"ping", timeout=2) # TLS certificate logic within
@@ -97,6 +121,8 @@ def get_external_ip_from_default_teacher(network: str,
raise UnknownIPAddress(error)
log.info(f'Fetched external IP address from default teacher ({top_teacher_url} reported {ip}).')
return ip
+ else:
+ log.debug(f'Failed to get external IP from teacher node ({response.status_code})')
def get_external_ip_from_known_nodes(known_nodes: FleetSensor,