Ensure enough Ursulas are available before retrieval

pull/2730/head
Bogdan Opanchuk 2021-09-02 23:53:34 -07:00
parent a9cc13e825
commit 39dcab3aa1
4 changed files with 37 additions and 171 deletions

View File

@ -541,66 +541,6 @@ class Bob(Character):
card = Card.from_character(self)
return card
def peek_at_treasure_map(self, treasure_map):
"""
Take a quick gander at the TreasureMap to see which
nodes are already known to us.
Don't do any learning, pinging, or anything other than just seeing
whether we know or don't know the nodes.
Return two sets: nodes that are unknown to us, nodes that are known to us.
"""
# The intersection of the map and our known nodes will be the known Ursulas...
known_treasure_ursulas = treasure_map.destinations.keys() & self.known_nodes.addresses()
# while the difference will be the unknown Ursulas.
unknown_treasure_ursulas = treasure_map.destinations.keys() - self.known_nodes.addresses()
return unknown_treasure_ursulas, known_treasure_ursulas
def follow_treasure_map(self,
treasure_map=None,
block=False,
new_thread=False,
timeout=10,
allow_missing=0):
"""
Follows a known TreasureMap.
Determines which Ursulas are known and which are unknown.
If block, will block until either unknown nodes are discovered or until timeout seconds have elapsed.
After timeout seconds, if more than allow_missing nodes are still unknown, raises NotEnoughUrsulas.
If block and new_thread, does the same thing but on a different thread, returning a Deferred which
fires after the blocking has concluded.
Otherwise, returns (unknown_nodes, known_nodes).
# TODO: Check if nodes are up, declare them phantom if not. 567
"""
unknown_ursulas, known_ursulas = self.peek_at_treasure_map(treasure_map=treasure_map)
if unknown_ursulas:
self.learn_about_specific_nodes(unknown_ursulas)
# TODO: what does this even do?
self._push_certain_newly_discovered_nodes_here(known_ursulas, unknown_ursulas)
if block:
if new_thread:
return threads.deferToThread(self.block_until_specific_nodes_are_known,
unknown_ursulas,
timeout=timeout,
allow_missing=allow_missing)
else:
self.block_until_specific_nodes_are_known(unknown_ursulas,
timeout=timeout,
allow_missing=allow_missing,
learn_on_this_thread=True)
def _decrypt_treasure_map(self, encrypted_treasure_map: EncryptedTreasureMap) -> TreasureMap:
publisher = Alice.from_public_keys(verifying_key=encrypted_treasure_map.publisher_verifying_key)

View File

@ -255,7 +255,6 @@ class Learner:
self.learn_on_same_thread = learn_on_same_thread
self._abort_on_learning_error = abort_on_learning_error
self._learning_listeners = defaultdict(list)
self._node_ids_to_learn_about_immediately = set()
self.__known_nodes = self.tracker_class(domain=domain, this_node=self if include_self_in_the_state else None)
@ -463,10 +462,6 @@ class Learner:
# TODO: What about InvalidNode? (for that matter, any SuspiciousActivity) 1714, 567 too really
listeners = self._learning_listeners.pop(node.checksum_address, tuple())
for listener in listeners:
listener.add(node.checksum_address)
self._node_ids_to_learn_about_immediately.discard(node.checksum_address)
if record_fleet_state:
@ -709,14 +704,6 @@ class Learner:
self._LONG_LEARNING_DELAY))
self._learning_task.interval = self._LONG_LEARNING_DELAY
def _push_certain_newly_discovered_nodes_here(self, queue_to_push, node_addresses):
"""
If any node_addresses are discovered, push them to queue_to_push.
"""
for node_address in node_addresses:
self.log.info("Adding listener for {}".format(node_address))
self._learning_listeners[node_address].append(queue_to_push)
def network_bootstrap(self, node_list: list) -> None:
for node_addr, port in node_list:
new_nodes = self.learn_about_nodes_now(node_addr, port)

View File

@ -251,6 +251,40 @@ class RetrievalClient:
self._learner = learner
self.log = Logger(self.__class__.__name__)
def _ensure_ursula_availability(self, treasure_map: TreasureMap, timeout=10):
"""
Make sure we know enough nodes from the treasure map to decrypt;
otherwise block and wait for them to come online.
"""
# OK, so we're going to need to do some network activity for this retrieval.
# Let's make sure we've seeded.
if not self._learner.done_seeding:
self._learner.learn_from_teacher_node()
ursulas_in_map = treasure_map.destinations.keys()
all_known_ursulas = self._learner.known_nodes.addresses()
# Push all unknown Ursulas from the map in the queue for learning
unknown_ursulas = ursulas_in_map - all_known_ursulas
if unknown_ursulas:
self._learner.learn_about_specific_nodes(unknown_ursulas)
# How many nodes over the threshold we want to know (just in case)
redundancy = 0
# If we know enough to decrypt, we can proceed.
known_ursulas = ursulas_in_map & all_known_ursulas
if len(known_ursulas) - redundancy >= treasure_map.threshold:
return
allow_missing = max(len(unknown_ursulas) - redundancy, 0)
self._learner.block_until_specific_nodes_are_known(unknown_ursulas,
timeout=timeout,
allow_missing=allow_missing,
learn_on_this_thread=True)
def _request_reencryption(self,
ursula: 'Ursula',
reencryption_request: 'ReencryptionRequest',
@ -340,11 +374,7 @@ class RetrievalClient:
policy_encrypting_key: PublicKey, # Key used to create the policy
) -> List[RetrievalResult]:
# TODO: why is it here? This code shouldn't know about these details.
# OK, so we're going to need to do some network activity for this retrieval.
# Let's make sure we've seeded.
if not self._learner.done_seeding:
self._learner.learn_from_teacher_node()
self._ensure_ursula_availability(treasure_map)
retrieval_plan = RetrievalPlan(treasure_map=treasure_map, retrieval_kits=retrieval_kits)

View File

@ -19,105 +19,14 @@ import pytest
import pytest_twisted
from twisted.internet import threads
from nucypher.characters.lawful import Enrico
from nucypher.characters.lawful import Enrico, Bob
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.network.retrieval import RetrievalClient
from nucypher.policy.kits import RetrievalKit
from tests.utils.middleware import MockRestMiddleware, NodeIsDownMiddleware
def test_bob_cannot_follow_the_treasure_map_in_isolation(federated_treasure_map, federated_bob):
# Assume for the moment that Bob has already received a TreasureMap, perhaps via a side channel.
# Bob knows of no Ursulas.
assert len(federated_bob.known_nodes) == 0
# He can't successfully follow the TreasureMap until he learns of a node to ask.
unknown, known = federated_bob.peek_at_treasure_map(treasure_map=federated_treasure_map)
assert len(known) == 0
# TODO: Show that even with learning loop going, nothing happens here.
# Probably use Clock?
federated_bob.follow_treasure_map(treasure_map=federated_treasure_map)
assert len(known) == 0
def test_bob_already_knows_all_nodes_in_treasure_map(enacted_federated_policy,
federated_ursulas,
federated_bob,
federated_alice):
# Bob knows of no Ursulas.
assert len(federated_bob.known_nodes) == 0
# Now we'll inform Bob of some Ursulas.
for ursula in federated_ursulas:
federated_bob.remember_node(ursula)
# Now, Bob can get the TreasureMap all by himself, and doesn't need a side channel.
the_map = federated_bob._decrypt_treasure_map(enacted_federated_policy.treasure_map)
unknown, known = federated_bob.peek_at_treasure_map(treasure_map=the_map)
# He finds that he didn't need to discover any new nodes...
assert len(unknown) == 0
# ...because he already knew of all the Ursulas on the map.
assert len(known) == enacted_federated_policy.shares
@pytest_twisted.inlineCallbacks
def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(federated_treasure_map,
federated_ursulas,
certificates_tempdir):
"""
Similar to above, but this time, we'll show that if Bob can connect to a single node, he can
learn enough to follow the TreasureMap.
Also, we'll get the TreasureMap from the hrac alone (ie, not via a side channel).
"""
from nucypher.characters.lawful import Bob
bob = Bob(network_middleware=MockRestMiddleware(),
domain=TEMPORARY_DOMAIN,
start_learning_now=False,
abort_on_learning_error=True,
federated_only=True)
# Again, let's assume that he received the TreasureMap via a side channel.
# Now, let's create a scenario in which Bob knows of only one node.
assert len(bob.known_nodes) == 0
first_ursula = list(federated_ursulas).pop(0)
bob.remember_node(first_ursula)
assert len(bob.known_nodes) == 1
# This time, when he follows the TreasureMap...
unknown_nodes, known_nodes = bob.peek_at_treasure_map(treasure_map=federated_treasure_map)
# Bob already knew about one node; the rest are unknown.
assert len(unknown_nodes) == len(federated_treasure_map) - 1
# He needs to actually follow the treasure map to get the rest.
bob.follow_treasure_map(treasure_map=federated_treasure_map)
# The nodes in the learning loop are now his top target, but he's not learning yet.
assert not bob._learning_task.running
# ...so he hasn't learned anything (ie, Bob still knows of just one node).
assert len(bob.known_nodes) == 1
# Now, we'll start his learning loop.
bob.start_learning_loop()
# ...and block until the unknown_nodes have all been found.
d = threads.deferToThread(bob.block_until_specific_nodes_are_known, unknown_nodes)
yield d
# ...and he now has no more unknown_nodes.
assert len(bob.known_nodes) == len(federated_treasure_map)
bob.disenchant()
def _policy_info_kwargs(enacted_policy):
return dict(
encrypted_treasure_map=enacted_policy.treasure_map,