diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py index e36374cf2..363a622e6 100644 --- a/nucypher/network/middleware.py +++ b/nucypher/network/middleware.py @@ -104,19 +104,26 @@ class RestMiddleware: url, certificate_filepath, announce_nodes=None, - nodes_i_need=None): + nodes_i_need=None, + client=requests, + fleet_checksum=None): if nodes_i_need: # TODO: This needs to actually do something. # Include node_ids in the request; if the teacher node doesn't know about the # nodes matching these ids, then it will ask other nodes. pass + if fleet_checksum: + params = {'fleet': fleet_checksum} + else: + params = {} + if announce_nodes: payload = bytes().join(bytes(n) for n in announce_nodes) - response = requests.post("https://{}/node_metadata".format(url), + response = client.post("https://{}/node_metadata".format(url), verify=certificate_filepath, - data=payload, timeout=2) + data=payload, timeout=2, params=params) else: - response = requests.get("https://{}/node_metadata".format(url), - verify=certificate_filepath, timeout=2) + response = client.get("https://{}/node_metadata".format(url), + verify=certificate_filepath, timeout=2, params=params) return response diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 5f557d2c8..9c199b387 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -542,7 +542,8 @@ class Learner: 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, - certificate_filepath=certificate_filepath) + certificate_filepath=certificate_filepath, + fleet_checksum=self.known_nodes.checksum) except requests.exceptions.ConnectionError as e: unresponsive_nodes.add(current_teacher) teacher_rest_info = current_teacher.rest_information()[0] @@ -550,9 +551,9 @@ class Learner: # TODO: This error isn't necessarily "no repsonse" - let's maybe pass on the text of the exception here. self.log.info("No Response from teacher: {}:{}.".format(teacher_rest_info.host, teacher_rest_info.port)) self.cycle_teacher_node() - return + raise False - if response.status_code != 200: + if response.status_code not in (200, 204): raise RuntimeError("Bad response from teacher: {} - {}".format(response, response.content)) signature, node_payload = signature_splitter(response.content, return_remainder=True) @@ -561,12 +562,20 @@ class Learner: self.verify_from(current_teacher, node_payload, signature=signature) except current_teacher.InvalidSignature: # TODO: What to do if the teacher improperly signed the node payload? - raise + raise False fleet_state_checksum_bytes, fleet_state_updated_bytes, nodes = FleetState.snapshot_splitter(node_payload, return_remainder=True) + current_teacher.last_seen = maya.now() + current_teacher.update_snapshot(checksum=fleet_state_checksum_bytes.hex(), + updated=maya.MayaDT(int.from_bytes(fleet_state_updated_bytes, byteorder="big"))) + + self.cycle_teacher_node() # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node. from nucypher.characters.lawful import Ursula + if response.status_code == 204: + return constants.FLEET_STATES_MATCH + node_list = Ursula.batch_from_bytes(nodes, federated_only=self.federated_only) # TODO: 466 new_nodes = [] @@ -595,11 +604,6 @@ class Learner: self._adjust_learning(new_nodes) learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new." - current_teacher.last_seen = maya.now() - current_teacher.update_snapshot(checksum=fleet_state_checksum_bytes.hex(), - updated=maya.MayaDT(int.from_bytes(fleet_state_updated_bytes, byteorder="big"))) - - self.cycle_teacher_node() self.log.info(learning_round_log_message.format(self._learning_round, current_teacher, len(node_list), diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 9e9550c57..b85a3498b 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -169,10 +169,20 @@ class ProxyRESTRoutes: return Response(bytes(signature) + payload, headers=headers) def node_metadata_exchange(self, request: Request, query_params: QueryParams): + # If these nodes already have the same fleet state, no exchange is necessary. + learner_fleet_state = query_params.get('fleet') + if learner_fleet_state == self._node_tracker.checksum: + self.log.debug("Learner already knew fleet state {}; doing nothing.".format(learner_fleet_state)) + headers = {'Content-Type': 'application/octet-stream'} + payload = self._node_tracker.snapshot() + signature = self._stamp(payload) + return Response(bytes(signature) + payload, headers=headers, status_code=204) + nodes = self._node_class.batch_from_bytes(request.body, federated_only=self.federated_only, # TODO: 466 ) - # TODO: This logic is basically repeated in learn_from_teacher_node. Let's find a better way. + + # TODO: This logic is basically repeated in learn_from_teacher_node and remember_node. Let's find a better way. 555 for node in nodes: if node.checksum_public_address in self._node_tracker: diff --git a/nucypher/utilities/sandbox/middleware.py b/nucypher/utilities/sandbox/middleware.py index 45b311a80..0ba0f1a06 100644 --- a/nucypher/utilities/sandbox/middleware.py +++ b/nucypher/utilities/sandbox/middleware.py @@ -80,24 +80,8 @@ class MockRestMiddleware(RestMiddleware): response = mock_client.get("http://localhost/public_information") return response - def get_nodes_via_rest(self, url, certificate_filepath, announce_nodes=None, nodes_i_need=None): - - mock_client = self._get_mock_client_by_url(url) - - if nodes_i_need: - # TODO: This needs to actually do something. - # Include node_ids in the request; if the teacher node doesn't know about the - # nodes matching these ids, then it will ask other nodes. - pass - - if announce_nodes: - response = mock_client.post("https://{}/node_metadata".format(url), - verify=certificate_filepath, - data=bytes().join(bytes(n) for n in announce_nodes)) - else: - response = mock_client.get("https://{}/node_metadata".format(url), - verify=certificate_filepath) - return response + def get_nodes_via_rest(self, url, *args, **kwargs): + return super().get_nodes_via_rest(url, client=self._get_mock_client_by_url(url), *args, **kwargs) def put_treasure_map_on_node(self, node, map_id, map_payload): mock_client = self._get_mock_client_by_ursula(node) diff --git a/tests/learning/test_fleet_state.py b/tests/learning/test_fleet_state.py index fbef5c2c1..bd1cf6037 100644 --- a/tests/learning/test_fleet_state.py +++ b/tests/learning/test_fleet_state.py @@ -1,3 +1,6 @@ +from constant_sorrow.constants import FLEET_STATES_MATCH + + def test_all_nodes_have_same_fleet_state(federated_ursulas): checksums = [u.known_nodes.checksum for u in federated_ursulas] assert len(set(checksums)) == 1 # There is only 1 unique value. @@ -17,4 +20,15 @@ def test_teacher_nodes_cycle(federated_ursulas): ursula.learn_from_teacher_node() second_teacher = ursula._current_teacher_node - assert first_teacher != second_teacher \ No newline at end of file + assert first_teacher != second_teacher + + +def test_nodes_with_equal_fleet_state_do_not_send_anew(federated_ursulas): + some_ursula = list(federated_ursulas)[2] + another_ursula = list(federated_ursulas)[3] + + # These two have the same fleet state. + assert some_ursula.known_nodes.checksum == another_ursula.known_nodes.checksum + some_ursula._current_teacher_node = another_ursula + result = some_ursula.learn_from_teacher_node() + assert result is FLEET_STATES_MATCH \ No newline at end of file