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.
|
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,
|
# Alternate Constructors
|
||||||
self.rest_server.rest_interface.host,
|
#
|
||||||
self.rest_server.rest_interface.port)
|
|
||||||
|
@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
|
@classmethod
|
||||||
def from_tls_hosting_power(cls, tls_hosting_power: TLSHostingPower, *args, **kwargs) -> 'Teacher':
|
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
|
certificate = tls_hosting_power.keypair.certificate
|
||||||
return cls(certificate=certificate, certificate_filepath=certificate_filepath, *args, **kwargs)
|
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):
|
def sorted_nodes(self):
|
||||||
nodes_to_consider = list(self.known_nodes.values()) + [self]
|
nodes_to_consider = list(self.known_nodes.values()) + [self]
|
||||||
return sorted(nodes_to_consider, key=lambda n: n.checksum_public_address)
|
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):
|
def update_snapshot(self, checksum, updated):
|
||||||
# TODO: Kind of an interesting pattern here - with VerifiableNode increasingly looking like it will be Teacher.
|
# 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
|
# 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_updated = updated
|
||||||
self.fleet_state_icon = icon_from_checksum(self.fleet_state_checksum,
|
self.fleet_state_icon = icon_from_checksum(self.fleet_state_checksum,
|
||||||
nickname_metadata=self.fleet_state_nickname_metadata)
|
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):
|
def stamp_is_valid(self):
|
||||||
"""
|
"""
|
||||||
|
@ -776,19 +905,6 @@ class Teacher:
|
||||||
else:
|
else:
|
||||||
raise self.InvalidNode
|
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):
|
def verify_id(self, ursula_id, digest_factory=bytes):
|
||||||
self.verify()
|
self.verify()
|
||||||
if not ursula_id == digest_factory(self.canonical_public_address):
|
if not ursula_id == digest_factory(self.canonical_public_address):
|
||||||
|
@ -855,6 +971,23 @@ class Teacher:
|
||||||
signature = blockchain_power.sign_message(bytes(self.stamp))
|
signature = blockchain_power.sign_message(bytes(self.stamp))
|
||||||
self._evidence_of_decentralized_identity = signature
|
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):
|
def _signable_interface_info_message(self):
|
||||||
message = self.canonical_public_address + self.rest_information()[0]
|
message = self.canonical_public_address + self.rest_information()[0]
|
||||||
return message
|
return message
|
||||||
|
@ -885,102 +1018,9 @@ class Teacher:
|
||||||
def timestamp_bytes(self):
|
def timestamp_bytes(self):
|
||||||
return self.timestamp.epoch.to_bytes(4, 'big')
|
return self.timestamp.epoch.to_bytes(4, 'big')
|
||||||
|
|
||||||
@classmethod
|
#
|
||||||
def from_seednode_metadata(cls,
|
# Nicknames
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nickname_icon(self):
|
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
|
You should have received a copy of the GNU General Public License
|
||||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
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
|
from bytestring_splitter import VariableLengthBytestring
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,6 +27,26 @@ class SuspiciousActivity(RuntimeError):
|
||||||
"""raised when an action appears to amount to malicious conduct."""
|
"""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:
|
class InterfaceInfo:
|
||||||
expected_bytes_length = lambda: VariableLengthBytestring
|
expected_bytes_length = lambda: VariableLengthBytestring
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import os
|
import os
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from twisted.logger import Logger
|
from twisted.logger import Logger
|
||||||
|
|
||||||
from apistar import Route, App
|
from apistar import Route, App
|
||||||
|
|
Loading…
Reference in New Issue