mirror of https://github.com/nucypher/nucypher.git
Organize Learner / Server methods; Recompose after diffusing cli utilities.
parent
14ba280294
commit
6cbe8e5f3d
|
@ -725,10 +725,127 @@ class Teacher:
|
|||
Raise when a Character tries to use another Character as decentralized when the latter is federated_only.
|
||||
"""
|
||||
|
||||
def seed_node_metadata(self):
|
||||
return SeednodeMetadata(self.checksum_public_address,
|
||||
self.rest_server.rest_interface.host,
|
||||
self.rest_server.rest_interface.port)
|
||||
#
|
||||
# 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':
|
||||
|
@ -736,20 +853,19 @@ class Teacher:
|
|||
certificate = tls_hosting_power.keypair.certificate
|
||||
return cls(certificate=certificate, certificate_filepath=certificate_filepath, *args, **kwargs)
|
||||
|
||||
#
|
||||
# Known Nodes
|
||||
#
|
||||
|
||||
def seed_node_metadata(self):
|
||||
return SeednodeMetadata(self.checksum_public_address, # type: str
|
||||
self.rest_server.rest_interface.host, # type: str
|
||||
self.rest_server.rest_interface.port) # type: int
|
||||
|
||||
def sorted_nodes(self):
|
||||
nodes_to_consider = list(self.known_nodes.values()) + [self]
|
||||
return sorted(nodes_to_consider, key=lambda n: n.checksum_public_address)
|
||||
|
||||
def _stamp_has_valid_wallet_signature(self):
|
||||
signature_bytes = self._evidence_of_decentralized_identity
|
||||
if signature_bytes is constants.NOT_SIGNED:
|
||||
return False
|
||||
else:
|
||||
signature = EthSignature(signature_bytes)
|
||||
proper_pubkey = signature.recover_public_key_from_msg(bytes(self.stamp))
|
||||
proper_address = proper_pubkey.to_checksum_address()
|
||||
return proper_address == self.checksum_public_address
|
||||
|
||||
def update_snapshot(self, checksum, updated):
|
||||
# TODO: Kind of an interesting pattern here - with VerifiableNode increasingly looking like it will be Teacher.
|
||||
# We update the simple snapshot here, but of course if we're dealing with an instance that is also a Learner, it has
|
||||
|
@ -759,6 +875,19 @@ class Teacher:
|
|||
self.fleet_state_updated = updated
|
||||
self.fleet_state_icon = icon_from_checksum(self.fleet_state_checksum,
|
||||
nickname_metadata=self.fleet_state_nickname_metadata)
|
||||
#
|
||||
# Stamp
|
||||
#
|
||||
|
||||
def _stamp_has_valid_wallet_signature(self):
|
||||
signature_bytes = self._evidence_of_decentralized_identity
|
||||
if signature_bytes is constants.NOT_SIGNED:
|
||||
return False
|
||||
else:
|
||||
signature = EthSignature(signature_bytes)
|
||||
proper_pubkey = signature.recover_public_key_from_msg(bytes(self.stamp))
|
||||
proper_address = proper_pubkey.to_checksum_address()
|
||||
return proper_address == self.checksum_public_address
|
||||
|
||||
def stamp_is_valid(self):
|
||||
"""
|
||||
|
@ -776,19 +905,6 @@ class Teacher:
|
|||
else:
|
||||
raise self.InvalidNode
|
||||
|
||||
def interface_is_valid(self):
|
||||
"""
|
||||
Checks that the interface info is valid for this node's canonical address.
|
||||
"""
|
||||
interface_info_message = self._signable_interface_info_message() # Contains canonical address.
|
||||
message = self.timestamp_bytes() + interface_info_message
|
||||
interface_is_valid = self._interface_signature.verify(message, self.public_keys(SigningPower))
|
||||
self.verified_interface = interface_is_valid
|
||||
if interface_is_valid:
|
||||
return True
|
||||
else:
|
||||
raise self.InvalidNode
|
||||
|
||||
def verify_id(self, ursula_id, digest_factory=bytes):
|
||||
self.verify()
|
||||
if not ursula_id == digest_factory(self.canonical_public_address):
|
||||
|
@ -855,6 +971,23 @@ class Teacher:
|
|||
signature = blockchain_power.sign_message(bytes(self.stamp))
|
||||
self._evidence_of_decentralized_identity = signature
|
||||
|
||||
#
|
||||
# Interface
|
||||
#
|
||||
|
||||
def interface_is_valid(self):
|
||||
"""
|
||||
Checks that the interface info is valid for this node's canonical address.
|
||||
"""
|
||||
interface_info_message = self._signable_interface_info_message() # Contains canonical address.
|
||||
message = self.timestamp_bytes() + interface_info_message
|
||||
interface_is_valid = self._interface_signature.verify(message, self.public_keys(SigningPower))
|
||||
self.verified_interface = interface_is_valid
|
||||
if interface_is_valid:
|
||||
return True
|
||||
else:
|
||||
raise self.InvalidNode
|
||||
|
||||
def _signable_interface_info_message(self):
|
||||
message = self.canonical_public_address + self.rest_information()[0]
|
||||
return message
|
||||
|
@ -885,102 +1018,9 @@ class Teacher:
|
|||
def timestamp_bytes(self):
|
||||
return self.timestamp.epoch.to_bytes(4, 'big')
|
||||
|
||||
@classmethod
|
||||
def from_seednode_metadata(cls,
|
||||
seednode_metadata,
|
||||
node_storage=None,
|
||||
*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,
|
||||
node_storage=node_storage,
|
||||
*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@validate_checksum_address
|
||||
def from_seed_and_stake_info(cls,
|
||||
host,
|
||||
port,
|
||||
federated_only,
|
||||
minimum_stake=__DEFAULT_MIN_SEED_STAKE,
|
||||
checksum_address=None,
|
||||
network_middleware=None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
#
|
||||
# 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:
|
||||
raise potential_seed_node.SuspiciousActivity(
|
||||
"This seed node has a different wallet address: {} (expected {}). Are you sure this is a seednode?"
|
||||
.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
|
||||
#
|
||||
# Nicknames
|
||||
#
|
||||
|
||||
@property
|
||||
def nickname_icon(self):
|
||||
|
|
|
@ -14,6 +14,12 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from eth_utils import is_checksum_address
|
||||
|
||||
from bytestring_splitter import VariableLengthBytestring
|
||||
|
||||
|
||||
|
@ -21,6 +27,26 @@ class SuspiciousActivity(RuntimeError):
|
|||
"""raised when an action appears to amount to malicious conduct."""
|
||||
|
||||
|
||||
def parse_node_uri(uri: str):
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
|
||||
if '@' in uri:
|
||||
checksum_address, uri = uri.split("@")
|
||||
if not is_checksum_address(checksum_address):
|
||||
raise ValueError("{} is not a valid checksum address.".format(checksum_address))
|
||||
else:
|
||||
checksum_address = None # federated
|
||||
|
||||
# HTTPS Explicit Required
|
||||
parsed_uri = urlparse(uri)
|
||||
if not parsed_uri.scheme == "https":
|
||||
raise ValueError("Invalid teacher URI. Is the hostname prefixed with 'https://' ?")
|
||||
|
||||
hostname = parsed_uri.hostname
|
||||
port = parsed_uri.port or UrsulaConfiguration.DEFAULT_REST_PORT
|
||||
return hostname, port, checksum_address
|
||||
|
||||
|
||||
class InterfaceInfo:
|
||||
expected_bytes_length = lambda: VariableLengthBytestring
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
import binascii
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
from twisted.logger import Logger
|
||||
|
||||
from apistar import Route, App
|
||||
|
|
Loading…
Reference in New Issue