Merge pull request #493 from jMyles/seed-node-learning

Seed node learning
pull/501/head
Tux 2018-10-28 00:34:22 +02:00 committed by GitHub
commit 13b564c53c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 213 additions and 120 deletions

View File

@ -1090,21 +1090,21 @@ def status(config,
color_index = {
'self': 'yellow',
'known': 'white',
'bootnode': 'blue'
'seednode': 'blue'
}
for node_type, color in color_index.items():
click.secho('{0:<6} | '.format(node_type), fg=color, nl=False)
click.echo('\n')
bootnode_addresses = list(bn.checksum_address for bn in BOOTNODES)
seednode_addresses = list(bn.checksum_address for bn in BOOTNODES)
for node in known_nodes:
row_template = "{} | {} | {}"
node_type = 'known'
if node.checksum_public_address == config.node_configuration.checksum_address:
node_type = 'self'
row_template += ' ({})'.format(node_type)
if node.checksum_public_address in bootnode_addresses:
node_type = 'bootnode'
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(),
@ -1196,8 +1196,8 @@ def ursula(config,
config.operating_mode = "federated" if ursula_config.federated_only else "decentralized"
click.secho("Running in {} mode".format(config.operating_mode), fg='blue')
# Bootnodes, Seeds, Known Nodes
ursula_config.load_bootnodes()
# seednodes, Seeds, Known Nodes
ursula_config.load_seednodes()
quantity_known_nodes = len(ursula_config.known_nodes)
if quantity_known_nodes > 0:
click.secho("Loaded {} known nodes from storages".format(quantity_known_nodes, fg='blue'))

View File

@ -1,23 +1,24 @@
import random
import time
from collections import defaultdict
from collections import deque
from contextlib import suppress
from logging import Logger
from logging import getLogger
from tempfile import TemporaryDirectory
from typing import Dict, ClassVar, Set
from typing import Tuple
from typing import Union, List
import maya
import requests
import time
from constant_sorrow import constants, default_constant_splitter
from eth_keys import KeyAPI as EthKeyAPI
from eth_utils import to_checksum_address, to_canonical_address
from requests.exceptions import SSLError
from twisted.internet import reactor
from twisted.internet import reactor, defer
from twisted.internet import task
from typing import Dict, ClassVar, Set
from typing import Tuple
from typing import Union, List
from twisted.internet.threads import deferToThread
from umbral.keys import UmbralPublicKey
from umbral.signing import Signature
@ -29,7 +30,6 @@ from nucypher.crypto.powers import CryptoPower, SigningPower, EncryptingPower, N
from nucypher.crypto.signing import signature_splitter, StrangerStamp, SignatureStamp
from nucypher.network.middleware import RestMiddleware
from nucypher.network.nodes import VerifiableNode
from nucypher.network.server import TLSHostingPower
class Learner:
@ -63,12 +63,12 @@ class Learner:
known_nodes: tuple = None,
seed_nodes: Tuple[tuple] = None,
known_certificates_dir: str = None,
node_storage = None,
node_storage=None,
save_metadata: bool = False,
abort_on_learning_error: bool = False
) -> None:
self.log = getLogger("characters") # type: Logger
self.log = getLogger("characters") # type: Logger
self.__common_name = common_name
self.network_middleware = network_middleware
@ -85,7 +85,8 @@ class Learner:
# Read
if node_storage is None:
node_storage = self.__DEFAULT_NODE_STORAGE(federated_only=self.federated_only, #TODO: remove federated_only
node_storage = self.__DEFAULT_NODE_STORAGE(federated_only=self.federated_only,
# TODO: remove federated_only
character_class=self.__class__)
self.node_storage = node_storage
@ -101,9 +102,9 @@ class Learner:
self.unresponsive_startup_nodes.append(node)
self.teacher_nodes = deque()
self._current_teacher_node = None # type: Teacher
self._current_teacher_node = None # type: Teacher
self._learning_task = task.LoopingCall(self.keep_learning_about_nodes)
self._learning_round = 0 # type: int
self._learning_round = 0 # type: int
self._rounds_without_new_nodes = 0 # type: int
self._seed_nodes = seed_nodes or []
@ -120,40 +121,38 @@ class Learner:
retry_rate: int = 2,
timeout=3):
"""
Engage known nodes from storages and pre-fetch hardcoded bootnode certificates for node learning.
Engage known nodes from storages and pre-fetch hardcoded seednode certificates for node learning.
"""
def __attempt_bootnode_learning(bootnode, current_attempt=1):
self.log.debug("Loading Bootnode {}|{}:{}".format(bootnode.checksum_address, bootnode.rest_host, bootnode.rest_port))
try:
seed_node = self.network_middleware.learn_from_seednode(seednode_metadata=bootnode,
timeout=timeout,
accept_federated_only=self.federated_only) # TODO: 466
self.remember_node(seed_node)
except RuntimeError:
if current_attempt == retry_attempts:
message = "No Response from Bootnode {} after {} attempts"
self.log.info(message.format(bootnode.rest_url, retry_attempts))
return
unresponsive_seed_nodes.add(bootnode)
self.log.info("No Response from Bootnode {}. Retrying in {} seconds...".format(bootnode.rest_url, retry_rate))
time.sleep(retry_rate)
__attempt_bootnode_learning(bootnode=bootnode, current_attempt=current_attempt+1)
else:
self.log.info("Successfully learned from {}|{}:{}".format(bootnode.checksum_address, bootnode.rest_host, bootnode.rest_port))
if current_attempt > 1:
unresponsive_seed_nodes.remove(bootnode)
for bootnode in self._seed_nodes:
__attempt_bootnode_learning(bootnode=bootnode)
unresponsive_seed_nodes = set()
if len(unresponsive_seed_nodes) > 0:
self.log.info("No Bootnodes were availible after {} attempts".format(retry_attempts))
def __attempt_seednode_learning(seednode_metadata, current_attempt=1):
self.log.debug(
"Seeding from: {}|{}:{}".format(seednode_metadata.checksum_address,
seednode_metadata.rest_host,
seednode_metadata.rest_port))
seed_node = self.network_middleware.learn_about_seednode(seednode_metadata=seednode_metadata,
known_certs_dir=self.known_certificates_dir,
timeout=timeout,
accept_federated_only=self.federated_only) # TODO: 466
if seed_node is False:
unresponsive_seed_nodes.add(seednode_metadata)
else:
self.remember_node(seed_node)
# if read_storages is True:
# self.read_known_nodes()
for seednode_metadata in self._seed_nodes:
__attempt_seednode_learning(seednode_metadata=seednode_metadata)
if read_storages is True:
self.read_nodes_from_storage()
if not self.known_nodes:
self.log.warning("No seednodes were available after {} attempts".format(retry_attempts))
# TODO: Need some actual logic here for situation with no seed nodes (ie, maybe try again much later)
def read_nodes_from_storage(self) -> set:
stored_nodes = self.node_storage.all(federated_only=self.federated_only) # TODO: 466
for node in stored_nodes:
self.remember_node(node)
def remember_node(self, node, force_verification_check=False):
@ -194,11 +193,17 @@ class Learner:
def start_learning_loop(self, now=False):
if self._learning_task.running:
return False
else:
elif now:
self.load_seednodes()
d = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now)
d.addErrback(self.handle_learning_errors)
return d
else:
seeder_deferred = deferToThread(self.load_seednodes)
learner_deferred = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now)
seeder_deferred.addErrback(self.handle_learning_errors)
learner_deferred.addErrback(self.handle_learning_errors)
return defer.DeferredList([seeder_deferred, learner_deferred])
def handle_learning_errors(self, *args, **kwargs):
failure = args[0]
@ -334,8 +339,9 @@ class Learner:
elif not self._learning_task.running:
raise self.NotEnoughTeachers("The learning loop is not running. Start it with start_learning().")
else:
raise self.NotEnoughTeachers("After {} seconds and {} rounds, didn't find these {} nodes: {}".format(
timeout, rounds_undertaken, len(still_unknown), still_unknown))
raise self.NotEnoughTeachers(
"After {} seconds and {} rounds, didn't find these {} nodes: {}".format(
timeout, rounds_undertaken, len(still_unknown), still_unknown))
else:
time.sleep(.1)
@ -411,7 +417,8 @@ class Learner:
try:
# TODO: Streamline path generation
certificate_filepath = current_teacher.get_certificate_filepath(certificates_dir=self.known_certificates_dir)
certificate_filepath = current_teacher.get_certificate_filepath(
certificates_dir=self.known_certificates_dir)
response = self.network_middleware.get_nodes_via_rest(url=rest_url,
nodes_i_need=self._node_ids_to_learn_about_immediately,
announce_nodes=announce_nodes,
@ -442,7 +449,8 @@ class Learner:
try:
if eager:
certificate_filepath = current_teacher.get_certificate_filepath(certificates_dir=certificate_filepath)
certificate_filepath = current_teacher.get_certificate_filepath(
certificates_dir=certificate_filepath)
node.verify_node(self.network_middleware,
accept_federated_only=self.federated_only, # TODO: 466
certificate_filepath=certificate_filepath)
@ -528,7 +536,7 @@ class Character(Learner):
"""
self.federated_only = federated_only # type: bool
self.federated_only = federated_only # type: bool
#
# Powers
@ -538,7 +546,7 @@ class Character(Learner):
crypto_power_ups = crypto_power_ups or list() # type: list
if crypto_power:
self._crypto_power = crypto_power # type: CryptoPower
self._crypto_power = crypto_power # type: CryptoPower
elif crypto_power_ups:
self._crypto_power = CryptoPower(power_ups=crypto_power_ups)
else:
@ -552,7 +560,7 @@ class Character(Learner):
self.blockchain = blockchain or Blockchain.connect()
self.keyring_dir = keyring_dir # type: str
self.treasure_maps = {} # type: dict
self.treasure_maps = {} # type: dict
self.network_middleware = network_middleware or RestMiddleware()
#
@ -560,7 +568,7 @@ class Character(Learner):
#
try:
signing_power = self._crypto_power.power_ups(SigningPower) # type: SigningPower
self._stamp = signing_power.get_signature_stamp() # type: SignatureStamp
self._stamp = signing_power.get_signature_stamp() # type: SignatureStamp
except NoSigningPower:
self._stamp = constants.NO_SIGNING_POWER

View File

@ -355,11 +355,6 @@ class NodeConfiguration:
self.validate(config_root=self.config_root, no_registry=no_registry or self.federated_only)
return self.config_root
def read_known_nodes(self) -> set:
"""Read known nodes from metadata, and use them when producing a character"""
known_nodes = self.node_storage.all(federated_only=self.federated_only)
return known_nodes
def read_keyring(self, *args, **kwargs):
if self.checksum_address is None:
raise self.ConfigurationError("No account specified to unlock keyring")

View File

@ -1,9 +1,17 @@
import os
import socket
import ssl
import time
import requests
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from umbral.fragments import CapsuleFrag
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding
from twisted.logger import Logger
from umbral.fragments import CapsuleFrag
from nucypher.config.keyring import _write_tls_certificate
class RestMiddleware:
@ -19,16 +27,64 @@ class RestMiddleware:
raise RuntimeError("Bad response: {}".format(response.content))
return response
def _get_certificate(self, hostname, port):
bootnode_certificate = ssl.get_server_certificate(hostname, port)
certificate = x509.load_pem_x509_certificate(bootnode_certificate.encode(),
def learn_about_seednode(self, seednode_metadata, known_certs_dir, timeout=3, accept_federated_only=False):
from nucypher.characters.lawful import Ursula
# Pre-fetch certificate
self.log.info("Fetching seednode {} TLS certificate".format(seednode_metadata.checksum_address))
# TODO: Utilize timeout.
certificate, filepath = self._get_certificate(checksum_address=seednode_metadata.checksum_address,
hostname=seednode_metadata.rest_host,
port=seednode_metadata.rest_port,
certs_dir=known_certs_dir,
timeout=timeout)
if certificate is False:
return False
potential_seed_node = Ursula.from_rest_url(self,
seednode_metadata.rest_host,
seednode_metadata.rest_port,
certificate_filepath=filepath,
federated_only=True) # TODO: 466
if not seednode_metadata.checksum_address == potential_seed_node.checksum_public_address:
raise potential_seed_node.SuspiciousActivity(
"This seed node has a different wallet address: {} (was hoping for {}). Are you sure this is a seed node?".format(
potential_seed_node.checksum_public_address,
seednode_metadata.checksum_address))
try:
potential_seed_node.verify_node(self,
accept_federated_only=accept_federated_only,
certificate_filepath=filepath)
except potential_seed_node.InvalidNode:
raise # TODO: What if our seed node fails verification?
return potential_seed_node
def _get_certificate(self, checksum_address, certs_dir, hostname, port,
timeout=3, retry_attempts: int=3, retry_rate: int = 2,):
socket.setdefaulttimeout(timeout) # Set Socket Timeout
current_attempt = 0
try:
seednode_certificate = ssl.get_server_certificate(addr=(hostname, port))
except socket.timeout:
if current_attempt == retry_attempts:
message = "No Response from seednode {} after {} attempts"
self.log.info(message.format(checksum_address, retry_attempts))
return False, False
self.log.info(
"No Response from seednode {}. Retrying in {} seconds...".format(checksum_address, retry_rate))
time.sleep(retry_rate)
certificate = x509.load_pem_x509_certificate(seednode_certificate.encode(),
backend=default_backend())
# Write certificate
filename = '{}.{}'.format(bootnode.checksum_address, Encoding.PEM.name.lower())
certificate_filepath = os.path.join(self.known_certificates_dir, filename)
filename = '{}.{}'.format(checksum_address, Encoding.PEM.name.lower())
certificate_filepath = os.path.join(certs_dir, filename)
_write_tls_certificate(certificate=certificate, full_filepath=certificate_filepath, force=True)
self.log.info("Saved bootnode {} TLS certificate".format(bootnode.checksum_address))
self.log.info("Saved seednode {} TLS certificate".format(checksum_address))
return certificate, certificate_filepath
def enact_policy(self, ursula, id, payload):
response = requests.post('https://{}/kFrag/{}'.format(ursula.rest_interface, id.hex()), payload,

View File

@ -1,9 +1,4 @@
from urllib.parse import urlparse
from apistar import TestClient
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding
from nucypher.characters.lawful import Ursula
from nucypher.network.middleware import RestMiddleware
@ -37,31 +32,8 @@ class MockRestMiddleware(RestMiddleware):
raise RuntimeError(
"Can't find an Ursula with port {} - did you spin up the right test ursulas?".format(port))
def learn_from_seednode(self, seednode_metadata, timeout=3, accept_federated_only=False):
# Pre-fetch certificate
self.log.info("Fetching bootnode {} TLS certificate".format(seednode_metadata.checksum_address))
certificate, filepath = self._get_certificate(seednode_metadata.rest_host, seednode_metadata.rest_port)
potential_seed_node = Ursula.from_rest_url(self,
seednode_metadata.rest_host,
seednode_metadata.rest_port,
certificate_filepath=filepath,
federated_only=True) # TODO: 466
if not seednode_metadata.checksum_address == potential_seed_node.checksum_public_address:
raise potential_seed_node.SuspiciousActivity(
"This seed node has a different wallet address: {} (was hoping for {}). Are you sure this is a seed node?".format(
potential_seed_node.checksum_public_address,
bootnode.checksum_address))
try:
potential_seed_node.verify_node(self, accept_federated_only=accept_federated_only)
except potential_seed_node.InvalidNode:
raise # TODO: What if our seed node fails verification?
return potential_seed_node
def _get_certificate(self, hostname, port):
def _get_certificate(self, checksum_address, certs_dir, hostname, port, timeout=3, retry_attempts: int = 3,
retry_rate: int = 2, ):
ursula = self._get_ursula_by_port(port)
return ursula.certificate, ursula.certificate_filepath

View File

@ -143,21 +143,21 @@ class UrsulaCommandProtocol(LineReceiver):
color_index = {
'self': 'yellow',
'known': 'white',
'bootnode': 'blue'
'seednode': 'blue'
}
for node_type, color in color_index.items():
click.secho('{0:<6} | '.format(node_type), fg=color, nl=False)
click.echo('\n')
bootnode_addresses = list(bn.checksum_address for bn in BOOTNODES)
seednode_addresses = list(bn.checksum_address for bn in BOOTNODES)
for address, node in known_nodes.items():
row_template = "{} | {} | {}"
node_type = 'known'
if node.checksum_public_address == self.ursula.checksum_public_address:
node_type = 'self'
row_template += ' ({})'.format(node_type)
if node.checksum_public_address in bootnode_addresses:
node_type = 'bootnode'
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(),

View File

@ -1,6 +1,13 @@
from functools import partial
import maya
import pytest
import pytest_twisted
from twisted.internet.threads import deferToThread
from nucypher.network.middleware import RestMiddleware
from nucypher.utilities.sandbox.ursula import make_federated_ursulas
from cryptography.hazmat.primitives import serialization
def test_proper_seed_node_instantiation(ursula_federated_test_config):
@ -10,9 +17,44 @@ def test_proper_seed_node_instantiation(ursula_federated_test_config):
know_each_other=False)
firstula = lonely_ursula_maker().pop()
any_other_ursula = lonely_ursula_maker(seed_nodes=[firstula.seed_node_metadata()]).pop()
firstula_as_seed_node = firstula.seed_node_metadata()
any_other_ursula = lonely_ursula_maker(seed_nodes=[firstula_as_seed_node]).pop()
assert not any_other_ursula.known_nodes
any_other_ursula.start_learning_loop()
any_other_ursula.start_learning_loop(now=True)
assert firstula in any_other_ursula.known_nodes.values()
@pytest_twisted.inlineCallbacks
def test_get_cert_from_running_seed_node(ursula_federated_test_config):
lonely_ursula_maker = partial(make_federated_ursulas,
ursula_config=ursula_federated_test_config,
quantity=1,
know_each_other=False)
firstula = lonely_ursula_maker().pop()
node_deployer = firstula.get_deployer()
node_deployer.addServices()
node_deployer.catalogServers(node_deployer.hendrix)
node_deployer.start()
certificate_as_deployed = node_deployer.cert.to_cryptography()
firstula_as_seed_node = firstula.seed_node_metadata()
any_other_ursula = lonely_ursula_maker(seed_nodes=[firstula_as_seed_node],
network_middleware=RestMiddleware()).pop()
assert not any_other_ursula.known_nodes
def start_lonely_learning_loop():
any_other_ursula.start_learning_loop()
start = maya.now()
while not firstula in any_other_ursula.known_nodes.values():
passed = maya.now() - start
if passed.seconds > 2:
pytest.fail("Didn't find the seed node.")
yield deferToThread(start_lonely_learning_loop)
assert firstula in any_other_ursula.known_nodes.values()
certificate_as_learned = list(any_other_ursula.known_nodes.values())[0].certificate
assert certificate_as_learned == certificate_as_deployed

View File

@ -6,7 +6,7 @@ from moto import mock_s3
from nucypher.characters.lawful import Ursula
from nucypher.config.storages import S3NodeStorage, InMemoryNodeStorage, TemporaryFileBasedNodeStorage, NodeStorage
MOCK_S3_BUCKET_NAME = 'mock-bootnodes'
MOCK_S3_BUCKET_NAME = 'mock-seednodes'
S3_DOMAIN_NAME = 's3.amazonaws.com'

View File

@ -1,6 +1,4 @@
import os
import pytest
import pytest_twisted
import requests
from cryptography.hazmat.primitives import serialization
@ -40,13 +38,7 @@ def test_federated_nodes_connect_via_tls_and_verify(ursula_federated_test_config
try:
with open("test-cert", "wb") as f:
# f.write(cert.tbs_certificate_bytes.hex())
f.write(cert_bytes)
yield threads.deferToThread(check_node_with_cert, node, "test-cert")
finally:
os.remove("test-cert")
@pytest.mark.skip(reason="To be implemented")
def test_node_metadata_contains_proper_cert():
pass

View File

@ -1,6 +1,34 @@
import maya
import pytest
import pytest_twisted
from twisted.internet.threads import deferToThread
from nucypher.utilities.sandbox.ursula import make_federated_ursulas
@pytest.mark.skip("To be implemented.")
def test_eager_learn_from_teacher():
assert False
@pytest_twisted.inlineCallbacks
def test_one_node_stores_a_bunch_of_others(federated_ursulas, ursula_federated_test_config):
the_chosen_seednode = list(federated_ursulas)[2]
seed_node = the_chosen_seednode.seed_node_metadata()
newcomer = make_federated_ursulas(
ursula_config=ursula_federated_test_config,
quantity=1,
know_each_other=False,
save_metadata=True,
seed_nodes=[seed_node]).pop()
assert not newcomer.known_nodes
def start_lonely_learning_loop():
newcomer.start_learning_loop()
start = maya.now()
# Loop until the_chosen_seednode is in storage.
while not the_chosen_seednode in newcomer.node_storage.all(federated_only=True):
passed = maya.now() - start
if passed.seconds > 2:
pytest.fail("Didn't find the seed node.")
yield deferToThread(start_lonely_learning_loop)
# The known_nodes are all saved in storage (and no others have been saved)
assert list(newcomer.known_nodes.values()) == list(newcomer.node_storage.all(True))