From 31f001f2ed329d4a1cbbbfe8a3f506af9e6f825c Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 29 May 2020 15:15:47 -0700 Subject: [PATCH 001/167] This will be the basis of a merge commit in the rebase of these tests. One of them reactivates Vladimir; the other fixes the method for testing the presence of Treasure Maps. --- .../acceptance/network/test_network_actors.py | 31 ++++++++++++++++++- .../network/test_treasure_map_integration.py | 8 +++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index 8dca5db8f..0c5a4a7f2 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -126,6 +126,35 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali vladimir.node_storage.store_node_certificate(certificate=target.certificate) with pytest.raises(vladimir.InvalidNode): - idle_blockchain_policy.consider_arrangement(network_middleware=blockchain_alice.network_middleware, + idle_blockchain_policy.propose_arrangement(network_middleware=blockchain_alice.network_middleware, arrangement=FakeArrangement(), ursula=vladimir) + + +def test_treasure_map_cannot_be_duplicated(blockchain_ursulas, blockchain_alice, blockchain_bob, agency): + + # Setup the policy details + n = 3 + policy_end_datetime = maya.now() + datetime.timedelta(days=5) + label = b"this_is_the_path_to_which_access_is_being_granted" + + # Create the Policy, Granting access to Bob + policy = blockchain_alice.grant(bob=blockchain_bob, + label=label, + m=2, + n=n, + rate=int(1e18), # one ether + expiration=policy_end_datetime) + + u = blockchain_bob.matching_nodes_among(blockchain_alice.known_nodes)[0] + saved_map = u.treasure_maps[bytes.fromhex(policy.treasure_map.public_id())] + assert saved_map == policy.treasure_map + # This Ursula was actually a Vladimir. + # Thus, he has access to the (encrypted) TreasureMap and can use its details to + # try to store his own fake details. + vladimir = Vladimir.from_target_ursula(u) + node_on_which_to_store_bad_map = blockchain_ursulas[1] + with pytest.raises(vladimir.network_middleware.UnexpectedResponse) as e: + vladimir.publish_fraudulent_treasure_map(legit_treasure_map=saved_map, + target_node=node_on_which_to_store_bad_map) + assert e.value.status == 402 diff --git a/tests/integration/network/test_treasure_map_integration.py b/tests/integration/network/test_treasure_map_integration.py index 608cdf151..fde9af5ab 100644 --- a/tests/integration/network/test_treasure_map_integration.py +++ b/tests/integration/network/test_treasure_map_integration.py @@ -40,8 +40,12 @@ def test_alice_sets_treasure_map(enacted_federated_policy, federated_ursulas): """ enacted_federated_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) - treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] - assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map + found = 0 + for node in enacted_federated_policy.bob.matching_nodes_among(enacted_federated_policy.alice.known_nodes): + treasure_map_as_set_on_network = node.treasure_maps[treasure_map_index] + assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map + found += 1 + assert found def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alice, federated_bob, federated_ursulas, From 811549d1b0bf39495d6b75d9a7a878a8c70e7385 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 26 Feb 2020 09:52:35 -0800 Subject: [PATCH 002/167] Get Alice's checksum address for a given Policy. --- nucypher/network/server.py | 3 ++ nucypher/policy/collections.py | 51 ++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 418e0788c..831342128 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -408,6 +408,9 @@ def make_rest_app( do_store = False else: # TODO: If we include the policy ID in this check, does that prevent map spam? 1736 + if not this_node.federated_only: + alice_checksum_address = this_node.policy_agent.contract.functions.getPolicyOwner(treasure_map._hrac[:16]).call() + do_store = treasure_map.public_id() == treasure_map_id if do_store: diff --git a/nucypher/policy/collections.py b/nucypher/policy/collections.py index 4c08e3209..225aca58b 100644 --- a/nucypher/policy/collections.py +++ b/nucypher/policy/collections.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ +import binascii import json from collections import OrderedDict @@ -22,6 +23,10 @@ from collections import OrderedDict import binascii import maya import msgpack +from bytestring_splitter import BytestringSplitter, VariableLengthBytestring, BytestringSplittingError, \ + BytestringKwargifier +from constant_sorrow.constants import CFRAG_NOT_RETAINED +from constant_sorrow.constants import NO_DECRYPTION_PERFORMED from bytestring_splitter import BytestringSplitter, BytestringSplittingError, VariableLengthBytestring from constant_sorrow.constants import CFRAG_NOT_RETAINED, NO_DECRYPTION_PERFORMED from cryptography.hazmat.backends.openssl import backend @@ -50,11 +55,6 @@ class TreasureMap: from nucypher.policy.policies import Arrangement ID_LENGTH = Arrangement.ID_LENGTH # TODO: Unify with Policy / Arrangement - or is this ok? - splitter = BytestringSplitter(Signature, - (bytes, KECCAK_DIGEST_LENGTH), # hrac - (UmbralMessageKit, VariableLengthBytestring) - ) - class NowhereToBeFound(RestMiddleware.NotFound): """ Called when no known nodes have it. @@ -68,7 +68,8 @@ class TreasureMap: node_id_splitter = BytestringSplitter((to_checksum_address, int(PUBLIC_ADDRESS_LENGTH)), ID_LENGTH) - from nucypher.crypto.signing import InvalidSignature # Raised when the public signature (typically intended for Ursula) is not valid. + from nucypher.crypto.signing import \ + InvalidSignature # Raised when the public signature (typically intended for Ursula) is not valid. def __init__(self, m: int = None, @@ -87,11 +88,27 @@ class TreasureMap: self._m = NO_DECRYPTION_PERFORMED self._destinations = NO_DECRYPTION_PERFORMED + self._id = None + self.message_kit = message_kit self._public_signature = public_signature self._hrac = hrac self._payload = None + if message_kit is not None: + self.message_kit = message_kit + self._set_id() + else: + self.message_kit = None + + @classmethod + def splitter(cls): + return BytestringKwargifier(cls, + public_signature=Signature, + hrac=(bytes, KECCAK_DIGEST_LENGTH), + message_kit=(UmbralMessageKit, VariableLengthBytestring) + ) + def prepare_for_publication(self, bob_encrypting_key, bob_verifying_key, @@ -120,6 +137,10 @@ class TreasureMap: self._hrac = keccak_digest(bytes(alice_stamp) + bytes(bob_verifying_key) + label) self._public_signature = alice_stamp(bytes(alice_stamp) + self._hrac) self._set_payload() + self._set_id() + + def _set_id(self): + self._id = keccak_digest(bytes(self._verifying_key) + bytes(self._hrac)).hex() def _set_payload(self): self._payload = self._public_signature + self._hrac + bytes( @@ -215,7 +236,8 @@ class TreasureMap: def check_for_sufficient_destinations(self): if len(self._destinations) < self._m or self._m == 0: - raise self.IsDisorienting(f"TreasureMap lists only {len(self._destinations)} destination, but requires interaction with {self._m} nodes.") + raise self.IsDisorienting( + f"TreasureMap lists only {len(self._destinations)} destination, but requires interaction with {self._m} nodes.") def __eq__(self, other): return bytes(self) == bytes(other) @@ -268,18 +290,18 @@ class PolicyCredential: cred_json = json.loads(data) alice_verifying_key = UmbralPublicKey.from_bytes( - cred_json['alice_verifying_key'], - decoder=bytes().fromhex) + cred_json['alice_verifying_key'], + decoder=bytes().fromhex) label = bytes().fromhex(cred_json['label']) expiration = maya.MayaDT.from_iso8601(cred_json['expiration']) policy_pubkey = UmbralPublicKey.from_bytes( - cred_json['policy_pubkey'], - decoder=bytes().fromhex) + cred_json['policy_pubkey'], + decoder=bytes().fromhex) treasure_map = None if 'treasure_map' in cred_json: treasure_map = TreasureMap.from_bytes( - bytes().fromhex(cred_json['treasure_map'])) + bytes().fromhex(cred_json['treasure_map'])) return cls(alice_verifying_key, label, expiration, policy_pubkey, treasure_map) @@ -292,7 +314,6 @@ class PolicyCredential: class WorkOrder: - class PRETask: def __init__(self, capsule, signature, cfrag=None, cfrag_signature=None): self.capsule = capsule @@ -528,8 +549,8 @@ class Revocation: revocation_splitter = BytestringSplitter((bytes, 7), (bytes, 32), Signature) def __init__(self, arrangement_id: bytes, - signer: 'SignatureStamp' = None, - signature: Signature = None): + signer: 'SignatureStamp' = None, + signature: Signature = None): self.prefix = b'REVOKE-' self.arrangement_id = arrangement_id From 5efde98fb51560c9ecd621f4e67d9e13613d5a5c Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 26 Feb 2020 09:57:34 -0800 Subject: [PATCH 003/167] Sign TreasureMap with wallet keypair... now what? --- nucypher/policy/collections.py | 13 +++---------- nucypher/policy/policies.py | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/nucypher/policy/collections.py b/nucypher/policy/collections.py index 225aca58b..17feb76e2 100644 --- a/nucypher/policy/collections.py +++ b/nucypher/policy/collections.py @@ -188,19 +188,12 @@ class TreasureMap: Ursula will refuse to propagate this if it she can't prove the payload is signed by Alice's public key, which is included in it, """ - # TODO: No reason to keccak this over and over again. Turn into set-once property pattern. - _id = keccak_digest(bytes(self._verifying_key) + bytes(self._hrac)).hex() - return _id + return self._id @classmethod def from_bytes(cls, bytes_representation, verify=True): - signature, hrac, tmap_message_kit = cls.splitter(bytes_representation) - - treasure_map = cls( - message_kit=tmap_message_kit, - public_signature=signature, - hrac=hrac, - ) + splitter = cls.splitter() + treasure_map = splitter(bytes_representation) if verify: treasure_map.public_verify() diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 6518e643a..e1ec0e0b4 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -33,7 +33,7 @@ from nucypher.characters.lawful import Alice, Ursula from nucypher.crypto.api import keccak_digest, secure_random from nucypher.crypto.constants import PUBLIC_KEY_LENGTH from nucypher.crypto.kits import RevocationKit -from nucypher.crypto.powers import DecryptingPower, SigningPower +from nucypher.crypto.powers import DecryptingPower, SigningPower, TransactingPower from nucypher.crypto.utils import construct_policy_id from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware @@ -202,6 +202,7 @@ class Policy(ABC): self.bob = bob # type: Bob self.kfrags = kfrags # type: List[KFrag] self.public_key = public_key + self._id = construct_policy_id(self.label, bytes(self.bob.stamp)) self.treasure_map = TreasureMap(m=m) self.expiration = expiration @@ -229,7 +230,7 @@ class Policy(ABC): @property def id(self) -> bytes: - return construct_policy_id(self.label, bytes(self.bob.stamp)) + return self._id def __repr__(self): return f"{self.__class__.__name__}:{self.id.hex()[:6]}" @@ -669,4 +670,14 @@ class BlockchainPolicy(Policy): for arrangement in self._accepted_arrangements: arrangement.publish_transaction = self.publish_transaction - return super().enact(network_middleware, publish) + super().enact(network_middleware, publish=False) + if publish is True: + self.treasure_map.prepare_for_publication(bob_encrypting_key=self.bob.public_keys(DecryptingPower), + bob_verifying_key=self.bob.public_keys(SigningPower), + alice_stamp=self.alice.stamp, + label=self.label) + # Sign the map. + transacting_power = self.alice._crypto_power.power_ups(TransactingPower) + blockchain_signature = transacting_power.sign_message(bytes(self.treasure_map)) + self.publish_treasure_map(network_middleware=network_middleware) + return From 5a4328f0cc0d2382d40e8228f4e0688959f6d6fc Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 26 Feb 2020 09:57:52 -0800 Subject: [PATCH 004/167] Begining to test to show that blockchain signature prevents duplication. --- .../acceptance/network/test_network_actors.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index 8dca5db8f..540b2e14a 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -14,7 +14,9 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ +import datetime +import maya import pytest from hendrix.experience import crosstown_traffic from hendrix.utils.test_utils import crosstownTaskListDecoratorFactory @@ -57,6 +59,99 @@ def test_blockchain_alice_finds_ursula_via_rest(blockchain_alice, blockchain_urs assert ursula in blockchain_alice.known_nodes +def test_alice_creates_policy_with_correct_hrac(idle_federated_policy): + """ + Alice creates a Policy. It has the proper HRAC, unique per her, Bob, and the label + """ + alice = idle_federated_policy.alice + bob = idle_federated_policy.bob + + assert idle_federated_policy.hrac() == keccak_digest(bytes(alice.stamp) + + bytes(bob.stamp) + + idle_federated_policy.label) + + +def test_alice_sets_treasure_map(enacted_federated_policy, federated_ursulas): + """ + Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and ...... TODO + """ + enacted_federated_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) + treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) + treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] + assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map + + +def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alice, federated_bob, federated_ursulas, + enacted_federated_policy): + """ + The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it. + """ + + treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) + treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] + + hrac_by_bob = federated_bob.construct_policy_hrac(federated_alice.stamp, enacted_federated_policy.label) + assert enacted_federated_policy.hrac() == hrac_by_bob + + hrac, map_id_by_bob = federated_bob.construct_hrac_and_map_id(federated_alice.stamp, enacted_federated_policy.label) + assert map_id_by_bob == treasure_map_as_set_on_network.public_id() + + +def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_policy, federated_ursulas): + """ + Above, we showed that the TreasureMap saved on the network is the correct one for Bob. Here, we show + that Bob can retrieve it with only the information about which he is privy pursuant to the PolicyGroup. + """ + bob = enacted_federated_policy.bob + + # Of course, in the real world, Bob has sufficient information to reconstitute a PolicyGroup, gleaned, we presume, + # through a side-channel with Alice. + + # If Bob doesn't know about any Ursulas, he can't find the TreasureMap via the REST swarm: + with pytest.raises(bob.NotEnoughTeachers): + treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp, + enacted_federated_policy.label) + + # Bob finds out about one Ursula (in the real world, a seed node) + bob.remember_node(list(federated_ursulas)[0]) + + # ...and then learns about the rest of the network. + bob.learn_from_teacher_node(eager=True) + + # Now he'll have better success finding that map. + treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp, + enacted_federated_policy.label) + + assert enacted_federated_policy.treasure_map == treasure_map_from_wire + + +def test_treasure_map_is_legit(enacted_federated_policy): + """ + Sure, the TreasureMap can get to Bob, but we also need to know that each Ursula in the TreasureMap is on the network. + """ + for ursula_address, _node_id in enacted_federated_policy.treasure_map: + assert ursula_address in enacted_federated_policy.bob.known_nodes.addresses() + + +@pytest.mark.usefixtures('blockchain_ursulas') +def test_treasure_map_cannot_be_duplicated(blockchain_alice, blockchain_bob, agency): + + # Setup the policy details + n = 3 + policy_end_datetime = maya.now() + datetime.timedelta(days=5) + label = b"this_is_the_path_to_which_access_is_being_granted" + + # Create the Policy, Granting access to Bob + policy = blockchain_alice.grant(bob=blockchain_bob, + label=label, + m=2, + n=n, + rate=int(1e18), # one ether + expiration=policy_end_datetime) + + assert False + + @pytest.mark.skip("See Issue #1075") # TODO: Issue #1075 def test_vladimir_illegal_interface_key_does_not_propagate(blockchain_ursulas): """ From 995eceb7f3e84d827db8d13c29322c07aa77f638 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 26 Feb 2020 14:53:58 -0800 Subject: [PATCH 005/167] Add blockchain signature verification call in do_store logic. --- nucypher/network/server.py | 18 +++++++++++------- nucypher/policy/collections.py | 34 ++++++++++++++++++++++++++++++++-- nucypher/policy/policies.py | 15 +++++++++------ 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 831342128..bc57ffc89 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -400,19 +400,23 @@ def make_rest_app( @rest_app.route('/treasure_map/', methods=['POST']) def receive_treasure_map(treasure_map_id): - from nucypher.policy.collections import TreasureMap + if not this_node.federated_only: + from nucypher.policy.collections import DecentralizedTreasureMap as _MapClass + else: + from nucypher.policy.collections import TreasureMap as _MapClass try: - treasure_map = TreasureMap.from_bytes(bytes_representation=request.data, verify=True) - except TreasureMap.InvalidSignature: + treasure_map = _MapClass.from_bytes(bytes_representation=request.data, verify=True) + except _MapClass.InvalidSignature: do_store = False else: - # TODO: If we include the policy ID in this check, does that prevent map spam? 1736 - if not this_node.federated_only: - alice_checksum_address = this_node.policy_agent.contract.functions.getPolicyOwner(treasure_map._hrac[:16]).call() - do_store = treasure_map.public_id() == treasure_map_id + if do_store and not this_node.federated_only: + alice_checksum_address = this_node.policy_agent.contract.functions.getPolicyOwner( + treasure_map._hrac[:16]).call() + do_store = treasure_map.verify_blockchain_signature(checksum_address=alice_checksum_address) + if do_store: log.info("{} storing TreasureMap {}".format(this_node, treasure_map_id)) diff --git a/nucypher/policy/collections.py b/nucypher/policy/collections.py index 17feb76e2..f4a3a28f4 100644 --- a/nucypher/policy/collections.py +++ b/nucypher/policy/collections.py @@ -25,6 +25,7 @@ import maya import msgpack from bytestring_splitter import BytestringSplitter, VariableLengthBytestring, BytestringSplittingError, \ BytestringKwargifier +from constant_sorrow.constants import CFRAG_NOT_RETAINED, NO_DECRYPTION_PERFORMED, NOT_SIGNED from constant_sorrow.constants import CFRAG_NOT_RETAINED from constant_sorrow.constants import NO_DECRYPTION_PERFORMED from bytestring_splitter import BytestringSplitter, BytestringSplittingError, VariableLengthBytestring @@ -40,6 +41,8 @@ from umbral.keys import UmbralPublicKey from umbral.pre import Capsule from nucypher.characters.lawful import Bob, Character +from nucypher.crypto.api import keccak_digest, encrypt_and_sign, verify_eip_191 +from nucypher.crypto.constants import PUBLIC_ADDRESS_LENGTH, KECCAK_DIGEST_LENGTH from nucypher.crypto.api import encrypt_and_sign, keccak_digest from nucypher.crypto.constants import KECCAK_DIGEST_LENGTH, PUBLIC_ADDRESS_LENGTH from nucypher.crypto.kits import UmbralMessageKit @@ -52,8 +55,7 @@ from nucypher.network.middleware import RestMiddleware class TreasureMap: - from nucypher.policy.policies import Arrangement - ID_LENGTH = Arrangement.ID_LENGTH # TODO: Unify with Policy / Arrangement - or is this ok? + ID_LENGTH = 32 class NowhereToBeFound(RestMiddleware.NotFound): """ @@ -245,6 +247,34 @@ class TreasureMap: return f"{self.__class__.__name__}:{self.public_id()[:6]}" +class DecentralizedTreasureMap(TreasureMap): + + def __init__(self, blockchain_signature=NOT_SIGNED, *args, **kwargs): + self._blockchain_signature = blockchain_signature + return super().__init__(*args, **kwargs) + + @classmethod + def splitter(cls): + return BytestringKwargifier(cls, + blockchain_signature=65, + public_signature=Signature, + hrac=(bytes, KECCAK_DIGEST_LENGTH), + message_kit=(UmbralMessageKit, VariableLengthBytestring) + ) + + def include_blockchain_signature(self, blockchain_signer): + self._blockchain_signature = blockchain_signer(super().__bytes__()) + + def verify_blockchain_signature(self, checksum_address): + self._set_payload() + return verify_eip_191(message=self._payload, + signature=self._blockchain_signature, + address=checksum_address) + + def __bytes__(self): + return self._blockchain_signature + super().__bytes__() + + class PolicyCredential: """ A portable structure that contains information necessary for Alice or Bob diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index e1ec0e0b4..7f16eb1ae 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -17,6 +17,7 @@ along with nucypher. If not, see . import random from collections import OrderedDict, deque +from typing import Generator, Set, List, Callable import maya from abc import ABC, abstractmethod @@ -195,15 +196,13 @@ class Policy(ABC): :param kfrags: A list of KFrags to distribute per this Policy. :param label: The identity of the resource to which Bob is granted access. """ - from nucypher.policy.collections import TreasureMap # TODO: Circular Import - self.alice = alice # type: Alice self.label = label # type: bytes self.bob = bob # type: Bob self.kfrags = kfrags # type: List[KFrag] self.public_key = public_key self._id = construct_policy_id(self.label, bytes(self.bob.stamp)) - self.treasure_map = TreasureMap(m=m) + self.treasure_map = self._treasure_map_class(m=m) self.expiration = expiration # Keep track of this stuff @@ -255,11 +254,14 @@ class Policy(ABC): """ return keccak_digest(bytes(self.alice.stamp) + bytes(self.bob.stamp) + self.label) - def publish_treasure_map(self, network_middleware: RestMiddleware) -> dict: + def publish_treasure_map(self, network_middleware: RestMiddleware, blockchain_signer: Callable=None) -> dict: self.treasure_map.prepare_for_publication(self.bob.public_keys(DecryptingPower), self.bob.public_keys(SigningPower), self.alice.stamp, self.label) + if blockchain_signer is not None: + self.treasure_map.include_blockchain_signature(blockchain_signer) + if not self.alice.known_nodes: # TODO: Optionally, block. raise RuntimeError("Alice hasn't learned of any nodes. Thus, she can't push the TreasureMap.") @@ -468,6 +470,7 @@ class Policy(ABC): class FederatedPolicy(Policy): _arrangement_class = Arrangement + from nucypher.policy.collections import TreasureMap as _treasure_map_class # TODO: Circular Import def make_arrangements(self, *args, **kwargs) -> None: try: @@ -499,6 +502,7 @@ class BlockchainPolicy(Policy): A collection of n BlockchainArrangements representing a single Policy """ _arrangement_class = BlockchainArrangement + from nucypher.policy.collections import DecentralizedTreasureMap as _treasure_map_class # TODO: Circular Import class NoSuchPolicy(Exception): pass @@ -678,6 +682,5 @@ class BlockchainPolicy(Policy): label=self.label) # Sign the map. transacting_power = self.alice._crypto_power.power_ups(TransactingPower) - blockchain_signature = transacting_power.sign_message(bytes(self.treasure_map)) - self.publish_treasure_map(network_middleware=network_middleware) + self.publish_treasure_map(network_middleware=network_middleware, blockchain_signer=transacting_power.sign_message) return From 7bc2ec65347722f3fc4edd2cd389719ccf01d7ac Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 26 Feb 2020 14:54:51 -0800 Subject: [PATCH 006/167] Different map classes for different policy modes. --- nucypher/policy/policies.py | 46 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 7f16eb1ae..62f1ecbd5 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -124,13 +124,12 @@ class BlockchainArrangement(Arrangement): expiration: maya.MayaDT, duration_periods: int, *args, **kwargs): - super().__init__(alice=alice, ursula=ursula, expiration=expiration, *args, **kwargs) # The relationship exists between two addresses - self.author = alice # type: BlockchainPolicyAuthor + self.author = alice # type: BlockchainPolicyAuthor self.policy_agent = alice.policy_agent # type: PolicyManagerAgent - self.staker = ursula # type: Ursula + self.staker = ursula # type: Ursula # Arrangement rate and duration in periods self.rate = rate @@ -196,10 +195,10 @@ class Policy(ABC): :param kfrags: A list of KFrags to distribute per this Policy. :param label: The identity of the resource to which Bob is granted access. """ - self.alice = alice # type: Alice - self.label = label # type: bytes - self.bob = bob # type: Bob - self.kfrags = kfrags # type: List[KFrag] + self.alice = alice # type: Alice + self.label = label # type: bytes + self.bob = bob # type: Bob + self.kfrags = kfrags # type: List[KFrag] self.public_key = public_key self._id = construct_policy_id(self.label, bytes(self.bob.stamp)) self.treasure_map = self._treasure_map_class(m=m) @@ -208,9 +207,9 @@ class Policy(ABC): # Keep track of this stuff self.selection_buffer = 1 - self._accepted_arrangements = set() # type: Set[Arrangement] - self._rejected_arrangements = set() # type: Set[Arrangement] - self._spare_candidates = set() # type: Set[Ursula] + self._accepted_arrangements = set() # type: Set[Arrangement] + self._rejected_arrangements = set() # type: Set[Arrangement] + self._spare_candidates = set() # type: Set[Ursula] self._enacted_arrangements = OrderedDict() self._published_arrangements = OrderedDict() @@ -254,7 +253,7 @@ class Policy(ABC): """ return keccak_digest(bytes(self.alice.stamp) + bytes(self.bob.stamp) + self.label) - def publish_treasure_map(self, network_middleware: RestMiddleware, blockchain_signer: Callable=None) -> dict: + def publish_treasure_map(self, network_middleware: RestMiddleware, blockchain_signer: Callable = None) -> dict: self.treasure_map.prepare_for_publication(self.bob.public_keys(DecryptingPower), self.bob.public_keys(SigningPower), self.alice.stamp, @@ -311,7 +310,6 @@ class Policy(ABC): return PolicyCredential(self.alice.stamp, self.label, self.expiration, self.public_key, treasure_map) - def __assign_kfrags(self) -> Generator[Arrangement, None, None]: if len(self._accepted_arrangements) < self.n: @@ -356,7 +354,7 @@ class Policy(ABC): # OK, let's check: if two or more Ursulas claimed we didn't pay, # we need to re-evaulate our situation here. arrangement_statuses = [a.status for a in self._accepted_arrangements] - number_of_claims_of_freeloading = sum(status==402 for status in arrangement_statuses) + number_of_claims_of_freeloading = sum(status == 402 for status in arrangement_statuses) if number_of_claims_of_freeloading > 2: raise self.alice.NotEnoughNodes # TODO: Clean this up and enable re-tries. @@ -457,7 +455,7 @@ class Policy(ABC): accepted = len(self._accepted_arrangements) if accepted == self.n and not consider_everyone: try: - spares = set(list(candidate_ursulas)[index+1::]) + spares = set(list(candidate_ursulas)[index + 1::]) self._spare_candidates.update(spares) except IndexError: self._spare_candidates = set() @@ -468,7 +466,6 @@ class Policy(ABC): class FederatedPolicy(Policy): - _arrangement_class = Arrangement from nucypher.policy.collections import TreasureMap as _treasure_map_class # TODO: Circular Import @@ -586,12 +583,12 @@ class BlockchainPolicy(Policy): target_quantity: int, timeout: int = 10) -> set: # TODO #843: Make timeout configurable - start_time = maya.now() # marker for timeout calculation + start_time = maya.now() # marker for timeout calculation found_ursulas, unknown_addresses = set(), deque() - while len(found_ursulas) < target_quantity: # until there are enough Ursulas + while len(found_ursulas) < target_quantity: # until there are enough Ursulas - delta = maya.now() - start_time # check for a timeout + delta = maya.now() - start_time # check for a timeout if delta.total_seconds() >= timeout: missing_nodes = ', '.join(a for a in unknown_addresses) raise RuntimeError("Timed out after {} seconds; Cannot find {}.".format(timeout, missing_nodes)) @@ -640,11 +637,11 @@ class BlockchainPolicy(Policy): # Transact # TODO: Move this logic to BlockchainPolicyActor receipt = self.author.policy_agent.create_policy( - policy_id=self.hrac()[:16], # bytes16 _policyID - author_address=self.author.checksum_address, - value=self.value, - end_timestamp=self.expiration.epoch, # uint16 _numberOfPeriods - node_addresses=prearranged_ursulas # address[] memory _nodes + policy_id=self.hrac()[:16], # bytes16 _policyID + author_address=self.author.checksum_address, + value=self.value, + end_timestamp=self.expiration.epoch, # uint16 _numberOfPeriods + node_addresses=prearranged_ursulas # address[] memory _nodes ) # Capture Response @@ -682,5 +679,6 @@ class BlockchainPolicy(Policy): label=self.label) # Sign the map. transacting_power = self.alice._crypto_power.power_ups(TransactingPower) - self.publish_treasure_map(network_middleware=network_middleware, blockchain_signer=transacting_power.sign_message) + self.publish_treasure_map(network_middleware=network_middleware, + blockchain_signer=transacting_power.sign_message) return From d155fe7fdbbb625efeda57f4ec3cf0b36a9e2385 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 09:26:51 -0800 Subject: [PATCH 007/167] Acquiring StakeList only if starting work now. --- nucypher/blockchain/eth/actors.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index d8edfd09e..7b1ccfda7 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -1290,10 +1290,12 @@ class Worker(NucypherTokenActor): if is_me: if block_until_ready: self.block_until_ready() - self.stakes = StakeList(registry=self.registry, checksum_address=self.checksum_address) - self.stakes.refresh() - self.work_tracker = work_tracker or WorkTracker(worker=self) + if start_working_now: + self.stakes = StakeList(registry=self.registry, checksum_address=self.checksum_address) + self.stakes.refresh() + self.work_tracker = work_tracker or WorkTracker(worker=self) + self.work_tracker.start(act_now=False) def block_until_ready(self, poll_rate: int = None, timeout: int = None): """ From aa93a7d150daf637fa75494b32adf79f38c49ed6 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 09:27:10 -0800 Subject: [PATCH 008/167] Reactivating Vladimir. --- nucypher/characters/unlawful.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index 1f7a9df6a..467284a35 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -64,12 +64,15 @@ class Vladimir(Ursula): crypto_power.consume_power_up(SigningPower(pubkey=target_ursula.stamp.as_umbral_pubkey())) if attach_transacting_key: - cls.attach_transacting_key(blockchain=target_ursula.blockchain) + cls.attach_transacting_key(blockchain=target_ursula.policy_agent.blockchain) vladimir = cls(is_me=True, crypto_power=crypto_power, db_filepath=cls.db_filepath, + domains=[':TEMPORARY_DOMAIN:'], + block_until_bonded=False, + start_working_now=False, rest_host=target_ursula.rest_interface.host, rest_port=target_ursula.rest_interface.port, certificate=target_ursula.rest_server_certificate(), From 8a113eaeddb1bd99e3a40c99df5b27d69132021d Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 09:27:31 -0800 Subject: [PATCH 009/167] Vladimir tries to publish a TreasureMap with his own content. --- nucypher/characters/unlawful.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index 467284a35..d0218a79c 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -103,6 +103,22 @@ class Vladimir(Ursula): raise return True + def publish_fraudulent_treasure_map(self, legit_treasure_map, target_node): + """ + If I see a TreasureMap being published, I can substitute my own payload and hope + that Ursula will store it for me for free. + """ + old_message_kit = legit_treasure_map.message_kit + new_message_kit, _signature = self.encrypt_for(self, b"I want to store this message for free.") + legit_treasure_map.message_kit = new_message_kit + # I'll copy Alice's key so that Ursula thinks that the HRAC has been properly signed. + legit_treasure_map.message_kit.sender_verifying_key = old_message_kit.sender_verifying_key + legit_treasure_map._set_payload() + + response = self.network_middleware.put_treasure_map_on_node(node=target_node, + map_id=legit_treasure_map.public_id(), + map_payload=bytes(legit_treasure_map)) + class Amonia(Alice): """ From 6b087f77b37713b6243e2f6c524bdf450fef7007 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 09:28:33 -0800 Subject: [PATCH 010/167] Separate responses for different bad TreasureMap scenarios. --- nucypher/characters/unlawful.py | 1 + nucypher/network/server.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index d0218a79c..1f96294c1 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -193,3 +193,4 @@ class Amonia(Alice): publish_wrong_payee_address_to_blockchain): with patch("nucypher.policy.policies.Policy.enact", self.enact_without_tabulating_responses): return super().grant(handpicked_ursulas=ursulas_to_trick_into_working_for_free, *args, **kwargs) + diff --git a/nucypher/network/server.py b/nucypher/network/server.py index bc57ffc89..040dc1b61 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -400,6 +400,7 @@ def make_rest_app( @rest_app.route('/treasure_map/', methods=['POST']) def receive_treasure_map(treasure_map_id): + # TODO: Any of the codepaths that trigger 4xx Responses here are also SuspiciousActivity. if not this_node.federated_only: from nucypher.policy.collections import DecentralizedTreasureMap as _MapClass else: @@ -408,9 +409,13 @@ def make_rest_app( try: treasure_map = _MapClass.from_bytes(bytes_representation=request.data, verify=True) except _MapClass.InvalidSignature: - do_store = False + log.info("Bad TreasureMap HRAC Signature; not storing {}".format(treasure_map_id)) + return Response("This TreasureMap's HRAC is not properly signed.", status=401) + if treasure_map.public_id() == treasure_map_id: + do_store = True else: - do_store = treasure_map.public_id() == treasure_map_id + return Response("Can't save a TreasureMap with this ID from you.", status=409) + if do_store and not this_node.federated_only: alice_checksum_address = this_node.policy_agent.contract.functions.getPolicyOwner( @@ -425,9 +430,8 @@ def make_rest_app( this_node.treasure_maps[treasure_map_index] = treasure_map return Response(bytes(treasure_map), status=202) else: - # TODO: Make this a proper 500 or whatever. #341 log.info("Bad TreasureMap ID; not storing {}".format(treasure_map_id)) - assert False + return Response("This TreasureMap doesn't match a paid Policy.", status=402) @rest_app.route('/status/', methods=['GET']) def status(): From cca843d71b3cb613e67b9de64f56c0c417f0df2d Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 09:30:19 -0800 Subject: [PATCH 011/167] Test showing that Vladimir's efforts at posting a nonsense TreasureMap will fail. --- tests/acceptance/network/test_network_actors.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index 540b2e14a..3acce4b9b 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -133,8 +133,7 @@ def test_treasure_map_is_legit(enacted_federated_policy): assert ursula_address in enacted_federated_policy.bob.known_nodes.addresses() -@pytest.mark.usefixtures('blockchain_ursulas') -def test_treasure_map_cannot_be_duplicated(blockchain_alice, blockchain_bob, agency): +def test_treasure_map_cannot_be_duplicated(blockchain_ursulas, blockchain_alice, blockchain_bob, agency): # Setup the policy details n = 3 @@ -149,7 +148,18 @@ def test_treasure_map_cannot_be_duplicated(blockchain_alice, blockchain_bob, age rate=int(1e18), # one ether expiration=policy_end_datetime) - assert False + u = blockchain_ursulas[0] + saved_map = u.treasure_maps[bytes.fromhex(policy.treasure_map.public_id())] + assert saved_map == policy.treasure_map + # This Ursula was actually a Vladimir. + # Thus, he has access to the (encrypted) TreasureMap and can use its details to + # try to store his own fake details. + vladimir = Vladimir.from_target_ursula(u) + node_on_which_to_store_bad_map = blockchain_ursulas[1] + with pytest.raises(vladimir.network_middleware.UnexpectedResponse) as e: + vladimir.publish_fraudulent_treasure_map(legit_treasure_map=saved_map, + target_node=node_on_which_to_store_bad_map) + assert e.value.status == 402 @pytest.mark.skip("See Issue #1075") # TODO: Issue #1075 From ab6c15fee992d5e5aca5ab66444e548d0816d785 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 16:03:35 -0800 Subject: [PATCH 012/167] When Bob joins a policy, he needs to deserialize the proper map class. --- nucypher/characters/lawful.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 38e199e2e..073b58617 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -568,7 +568,10 @@ class Bob(Character): Iterate through the nodes we know, asking for the TreasureMap. Return the first one who has it. """ - from nucypher.policy.collections import TreasureMap + if self.federated_only: + from nucypher.policy.collections import TreasureMap as _MapClass + else: + from nucypher.policy.collections import DecentralizedTreasureMap as _MapClass for node in self.known_nodes.shuffled(): try: response = network_middleware.get_treasure_map_from_node(node=node, map_id=map_id) @@ -580,7 +583,7 @@ class Bob(Character): if response.status_code == 200 and response.content: try: - treasure_map = TreasureMap.from_bytes(response.content) + treasure_map = _MapClass.from_bytes(response.content) except InvalidSignature: # TODO: What if a node gives a bunk TreasureMap? NRN raise @@ -590,7 +593,7 @@ class Bob(Character): else: # TODO: Work out what to do in this scenario - # if Bob can't get the TreasureMap, he needs to rest on the learning mutex or something. NRN - raise TreasureMap.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_id} ") + raise _MapClass.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_id} ") return treasure_map From db5a5888b8ef493c25cc43cbafeacf0d2bd08b27 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 16:04:00 -0800 Subject: [PATCH 013/167] Since we set _checksum_address in Worker, Vladimir needs to compose his fradulent address after the fact. --- nucypher/characters/unlawful.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index 1f96294c1..ad6567db1 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -41,6 +41,10 @@ class Vladimir(Ursula): fraud_key = 'a75d701cc4199f7646909d15f22e2e0ef6094b3e2aa47a188f35f47e8932a7b9' db_filepath = ':memory:' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._checksum_address = self.fraud_address + @classmethod def from_target_ursula(cls, target_ursula: Ursula, From 5ffb35655863bd35eb243e73afef9713b76699e9 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 4 Mar 2020 16:04:50 -0800 Subject: [PATCH 014/167] Vlad's APIs are consistent with the real deal again. --- nucypher/characters/unlawful.py | 3 +-- .../test_ursula_prepares_to_act_as_worker.py | 13 +++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index ad6567db1..7657fa6ed 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -65,7 +65,7 @@ class Vladimir(Ursula): crypto_power = CryptoPower(power_ups=target_ursula._default_crypto_powerups) if claim_signing_key: - crypto_power.consume_power_up(SigningPower(pubkey=target_ursula.stamp.as_umbral_pubkey())) + crypto_power.consume_power_up(SigningPower(public_key=target_ursula.stamp.as_umbral_pubkey())) if attach_transacting_key: cls.attach_transacting_key(blockchain=target_ursula.policy_agent.blockchain) @@ -88,7 +88,6 @@ class Vladimir(Ursula): interface_signature=target_ursula._interface_signature, ######### ) - return vladimir @classmethod diff --git a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py index 1149a5d44..5cca9d96d 100644 --- a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py +++ b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py @@ -64,7 +64,6 @@ def test_blockchain_ursula_verifies_stamp(blockchain_ursulas): assert first_ursula.verified_stamp -@pytest.mark.skip("See Issue #1075") # TODO: Issue #1075 def test_vladimir_cannot_verify_interface_with_ursulas_signing_key(blockchain_ursulas): his_target = list(blockchain_ursulas)[4] @@ -78,7 +77,7 @@ def test_vladimir_cannot_verify_interface_with_ursulas_signing_key(blockchain_ur vladimir = Vladimir.from_target_ursula(his_target, claim_signing_key=True) # Vladimir can substantiate the stamp using his own ether address... - vladimir.substantiate_stamp(client_password=INSECURE_DEVELOPMENT_PASSWORD) + vladimir.substantiate_stamp() vladimir.validate_worker = lambda: True vladimir.validate_worker() # lol @@ -95,7 +94,6 @@ def test_vladimir_cannot_verify_interface_with_ursulas_signing_key(blockchain_ur vladimir.validate_metadata() -@pytest.mark.skip("See Issue #1075") # TODO: Issue #1075 def test_vladimir_invalidity_without_stake(testerchain, blockchain_ursulas, blockchain_alice): his_target = list(blockchain_ursulas)[4] vladimir = Vladimir.from_target_ursula(target_ursula=his_target) @@ -103,14 +101,13 @@ def test_vladimir_invalidity_without_stake(testerchain, blockchain_ursulas, bloc message = vladimir._signable_interface_info_message() signature = vladimir._crypto_power.power_ups(SigningPower).sign(vladimir.timestamp_bytes() + message) vladimir._Teacher__interface_signature = signature - vladimir.substantiate_stamp(client_password=INSECURE_DEVELOPMENT_PASSWORD) + vladimir.substantiate_stamp() # However, the actual handshake proves him wrong. with pytest.raises(vladimir.InvalidNode): - vladimir.verify_node(blockchain_alice.network_middleware, certificate_filepath="doesn't matter") + vladimir.verify_node(blockchain_alice.network_middleware.client, certificate_filepath="doesn't matter") -@pytest.mark.skip("See Issue #1075") # TODO: Issue #1075 def test_vladimir_uses_his_own_signing_key(blockchain_alice, blockchain_ursulas): """ Similar to the attack above, but this time Vladimir makes his own interface signature @@ -122,7 +119,7 @@ def test_vladimir_uses_his_own_signing_key(blockchain_alice, blockchain_ursulas) message = vladimir._signable_interface_info_message() signature = vladimir._crypto_power.power_ups(SigningPower).sign(vladimir.timestamp_bytes() + message) vladimir._Teacher__interface_signature = signature - vladimir.substantiate_stamp(client_password=INSECURE_DEVELOPMENT_PASSWORD) + vladimir.substantiate_stamp() vladimir._worker_is_bonded_to_staker = lambda: True vladimir._staker_is_really_staking = lambda: True @@ -133,7 +130,7 @@ def test_vladimir_uses_his_own_signing_key(blockchain_alice, blockchain_ursulas) # However, the actual handshake proves him wrong. with pytest.raises(vladimir.InvalidNode): - vladimir.verify_node(blockchain_alice.network_middleware, certificate_filepath="doesn't matter") + vladimir.verify_node(blockchain_alice.network_middleware.client, certificate_filepath="doesn't matter") # TODO: Change name of this file, extract this test From 1a6e3b09ed8feb28c615694f7b42155650dd6503 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 19 Apr 2020 16:05:43 -0700 Subject: [PATCH 015/167] Splitting test logic to show when the problem is with Bob or with Alice. --- .../test_bob_joins_policy_and_retrieves.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py index 6e61fde2d..b562a8ffa 100644 --- a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py +++ b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py @@ -93,10 +93,24 @@ def test_bob_joins_policy_and_retrieves(federated_alice, assert bob == policy.bob assert label == policy.label - # Now, Bob joins the policy - bob.join_policy(label=label, - alice_verifying_key=federated_alice.stamp, - block=True) + try: + # Now, Bob joins the policy + bob.join_policy(label=label, + alice_verifying_key=federated_alice.stamp, + block=True) + except policy.treasure_map.NowhereToBeFound: + maps = [] + for ursula in federated_ursulas: + for map in ursula.treasure_maps.values(): + maps.append(map) + if policy.treasure_map in maps: + # This is a nice place to put a breakpoint to examine Bob's failure to join a policy. + # bob.join_policy(label=label, + # alice_verifying_key=federated_alice.stamp, + # block=True) + pytest.fail(f"Bob didn't find map {policy.treasure_map} even though it was available. Come on, Bob.") + else: + pytest.fail(f"It seems that Alice didn't publish {policy.treasure_map}. Come on, Alice.") # In the end, Bob should know all the Ursulas assert len(bob.known_nodes) == len(federated_ursulas) From 9e5ee658ff351c00d86606b32248f7e668686f64 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Apr 2020 12:18:50 -0700 Subject: [PATCH 016/167] Major surgery on _set_checksum_address. --- nucypher/characters/base.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 6b60dfd04..2c47c7a32 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -485,7 +485,29 @@ class Character(Learner): power_up = self._crypto_power.power_ups(power_up_class) return power_up.public_key() - def _set_checksum_address(self): + def _set_checksum_address(self, checksum_address=None): + + if checksum_address is not None: + + # + # Decentralized + # + if not self.federated_only: + self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower + + # + # Federated + # + elif self.federated_only: # TODO: What are we doing here? + try: + self._set_checksum_address() # type: str + except NoSigningPower: + self._checksum_address = NO_BLOCKCHAIN_CONNECTION + if checksum_address: + # We'll take a checksum address, as long as it matches their singing key + if not checksum_address == self.checksum_address: + error = "Federated-only Characters derive their address from their Signing key; got {} instead." + raise self.SuspiciousActivity(error.format(checksum_address)) if self.federated_only: verifying_key = self.public_keys(SigningPower) From d92a8d090358afb8ffb91e1be3b74a5228d08026 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Apr 2020 20:02:32 -0700 Subject: [PATCH 017/167] Facility for Bob (or an acquaintance) to find the nodes which are supposed to have his TreasureMaps. --- nucypher/characters/lawful.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 073b58617..f74993eda 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -843,6 +843,19 @@ class Bob(Character): return cleartexts + def matching_nodes_among(self, nodes: FleetStateTracker): + # Look for nodes whose checksum address has the second character of Bob's encrypting key in the first + # few characters. + # Think of it as a cheap knockoff hamming distance. + # The good news is that Bob can construct the list easily. + # And - famous last words incoming - there's no cognizable attack surface. + # Sure, Bob can mine encrypting keypairs until he gets the set of target Ursulas on which Alice can + # store a TreasureMap. And then... ???... profit? + target_hex_match = self.public_keys(DecryptingPower).hex()[1] + # This might be a performance issue above a few thousand nodes. + target_nodes = [node for node in nodes if target_hex_match in node.checksum_address[2:4]] + return target_nodes + def make_web_controller(drone_bob, crash_on_error: bool = False): app_name = bytes(drone_bob.stamp).hex()[:6] From 3d54aaf0d05314728606e981a681df5d1cf10878 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Apr 2020 20:11:39 -0700 Subject: [PATCH 018/167] Bob now waits to learn about new nodes if he can't immediately find the TreasureMap. --- nucypher/characters/lawful.py | 70 ++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index f74993eda..dbfaacc0b 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -16,6 +16,8 @@ along with nucypher. If not, see . """ import json +import random +from base64 import b64encode, b64decode from base64 import b64decode, b64encode from collections import OrderedDict from random import shuffle @@ -72,6 +74,8 @@ from nucypher.datastore.threading import ThreadedSession from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware from nucypher.network.nicknames import nickname_from_seed +from nucypher.network.nodes import NodeSprout, FleetStateTracker +from nucypher.network.nodes import Teacher from nucypher.network.nodes import NodeSprout, Teacher from nucypher.network.protocols import InterfaceInfo, parse_node_uri from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app @@ -88,7 +92,7 @@ class Alice(Character, BlockchainPolicyAuthor): # Mode is_me: bool = True, federated_only: bool = False, - signer = None, + signer=None, # Ownership checksum_address: str = None, @@ -191,7 +195,7 @@ class Alice(Character, BlockchainPolicyAuthor): def create_policy(self, bob: "Bob", label: bytes, **policy_params): """ - Create a Policy to share uri with bob. + Create a Policy so that Bob has access to all resources under label. Generates KFrags and attaches them. """ @@ -563,7 +567,7 @@ class Bob(Character): map_id = keccak_digest(bytes(verifying_key) + hrac).hex() return hrac, map_id - def get_treasure_map_from_known_ursulas(self, network_middleware, map_id): + def get_treasure_map_from_known_ursulas(self, network_middleware, map_id, timeout=3): """ Iterate through the nodes we know, asking for the TreasureMap. Return the first one who has it. @@ -572,30 +576,35 @@ class Bob(Character): from nucypher.policy.collections import TreasureMap as _MapClass else: from nucypher.policy.collections import DecentralizedTreasureMap as _MapClass - for node in self.known_nodes.shuffled(): - try: - response = network_middleware.get_treasure_map_from_node(node=node, map_id=map_id) - except NodeSeemsToBeDown: - continue - except network_middleware.NotFound: - self.log.info(f"Node {node} claimed not to have TreasureMap {map_id}") - continue - if response.status_code == 200 and response.content: + start = maya.now() + while True: + if (start - maya.now()).seconds > timeout: + raise _MapClass.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_id} ") + + nodes_with_map = self.matching_nodes_among(self.known_nodes) + random.shuffle(nodes_with_map) + + for node in nodes_with_map: try: - treasure_map = _MapClass.from_bytes(response.content) - except InvalidSignature: - # TODO: What if a node gives a bunk TreasureMap? NRN - raise - break - else: - continue # TODO: Actually, handle error case here. NRN - else: - # TODO: Work out what to do in this scenario - - # if Bob can't get the TreasureMap, he needs to rest on the learning mutex or something. NRN - raise _MapClass.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_id} ") + response = network_middleware.get_treasure_map_from_node(node=node, map_id=map_id) + except NodeSeemsToBeDown: + continue + except network_middleware.NotFound: + self.log.info(f"Node {node} claimed not to have TreasureMap {map_id}") + continue - return treasure_map + if response.status_code == 200 and response.content: + try: + treasure_map = _MapClass.from_bytes(response.content) + return treasure_map + except InvalidSignature: + # TODO: What if a node gives a bunk TreasureMap? NRN + raise + else: + continue # TODO: Actually, handle error case here. NRN + else: + self.learn_from_teacher_node() def work_orders_for_capsules(self, *capsules, @@ -900,7 +909,6 @@ class Bob(Character): class Ursula(Teacher, Character, Worker): - banner = URSULA_BANNER _alice_class = Alice @@ -938,7 +946,8 @@ class Ursula(Teacher, Character, Worker): decentralized_identity_evidence: bytes = constants.NOT_SIGNED, checksum_address: str = None, worker_address: str = None, # TODO: deprecate, and rename to "checksum_address" - block_until_ready: bool = True, # TODO: Must be true in order to set staker address - Allow for manual staker addr to be passed too! + block_until_ready: bool = True, + # TODO: Must be true in order to set staker address - Allow for manual staker addr to be passed too! work_tracker: WorkTracker = None, start_working_now: bool = True, client_password: str = None, @@ -971,7 +980,8 @@ class Ursula(Teacher, Character, Worker): is_me=is_me, checksum_address=checksum_address, start_learning_now=False, # Handled later in this function to avoid race condition - federated_only=self._federated_only_instances, # TODO: 'Ursula' object has no attribute '_federated_only_instances' if an is_me Ursula is not inited prior to this moment NRN + federated_only=self._federated_only_instances, + # TODO: 'Ursula' object has no attribute '_federated_only_instances' if an is_me Ursula is not inited prior to this moment NRN crypto_power=crypto_power, abort_on_learning_error=abort_on_learning_error, known_nodes=known_nodes, @@ -980,7 +990,6 @@ class Ursula(Teacher, Character, Worker): **character_kwargs) if is_me: - # In-Memory TreasureMap tracking self._stored_treasure_maps = dict() @@ -1011,7 +1020,8 @@ class Ursula(Teacher, Character, Worker): # Use this power to substantiate the stamp self.substantiate_stamp() - self.log.debug(f"Created decentralized identity evidence: {self.decentralized_identity_evidence[:10].hex()}") + self.log.debug( + f"Created decentralized identity evidence: {self.decentralized_identity_evidence[:10].hex()}") decentralized_identity_evidence = self.decentralized_identity_evidence Worker.__init__(self, @@ -1190,7 +1200,7 @@ class Ursula(Teacher, Character, Worker): raise # Crash :-( elif start_reactor: # ... without hendrix - reactor.run() # <--- Blocking Call (Reactor) + reactor.run() # <--- Blocking Call (Reactor) def stop(self, halt_reactor: bool = False) -> None: """Stop services""" From 5b63b670ae369ebac93e84ab161b5af26096d484 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 21 Apr 2020 10:35:25 -0700 Subject: [PATCH 019/167] Now Alice only PUTs the maps on the nodes where Bob will think to look. Fixes #342. --- nucypher/policy/policies.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 62f1ecbd5..f91099127 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -267,13 +267,11 @@ class Policy(ABC): responses = dict() self.log.debug(f"Pushing {self.treasure_map} to all known nodes from {self.alice}") - for node in self.alice.known_nodes: - # TODO: # 342 - It's way overkill to push this to every node we know about. Come up with a system. + treasure_map_id = self.treasure_map.public_id() + + for node in self.bob.matching_nodes_among(self.alice.known_nodes): try: - treasure_map_id = self.treasure_map.public_id() - - # TODO: Certificate filepath needs to be looked up and passed here response = network_middleware.put_treasure_map_on_node(node=node, map_id=treasure_map_id, map_payload=bytes(self.treasure_map)) From 7deab1242dcf08c50cb0a7b070eb10527ff1fc31 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 21 Apr 2020 10:38:25 -0700 Subject: [PATCH 020/167] Mocking one layer out (setting policy) for these tests. --- .../learning/test_discovery_phases.py | 33 +++++++++++-------- tests/mock/performance_mocks.py | 4 +++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 619dc690e..8e8cec45e 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -97,20 +97,25 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, hi def mock_set_policy(id_as_hex): return "" - def mock_receive_treasure_map(treasure_map_id): - return Response(bytes(), status=202) - - with NotARestApp.replace_route("receive_treasure_map", mock_receive_treasure_map): - with NotARestApp.replace_route("set_policy", mock_set_policy): - with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True): - with patch('umbral.keys.UmbralPublicKey.from_bytes', - new=actual_random_key_instead): - with mock_cert_loading, mock_metadata_validation, mock_message_verification: - with mock_secret_source(): - policy = highperf_mocked_alice.grant( - highperf_mocked_bob, b"any label", m=20, n=30, - expiration=maya.when('next week'), - publish_treasure_map=False) + with NotARestApp.replace_route("set_policy", mock_set_policy): + with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True): + with patch('umbral.keys.UmbralPublicKey.from_bytes', + new=actual_random_key_instead): + with mock_cert_loading, mock_metadata_validation, mock_message_verification: + with mock_secret_source(): + policy = highperf_mocked_alice.grant( + highperf_mocked_bob, b"any label", m=20, n=30, + expiration=maya.when('next week'), + publish_treasure_map=False) # TODO: Make some assertions about policy. total_verified = sum(node.verified_node for node in highperf_mocked_alice.known_nodes) assert total_verified == 30 + + with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: + try: + policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) + except Exception as e: + raise + +def test_mass_treasure_map_placement(highperf_mocked_alice): + assert False \ No newline at end of file diff --git a/tests/mock/performance_mocks.py b/tests/mock/performance_mocks.py index 59fde483d..dc8b72b53 100644 --- a/tests/mock/performance_mocks.py +++ b/tests/mock/performance_mocks.py @@ -399,6 +399,10 @@ class NotAPublicKey: self.__dict__ = _umbral_pubkey.__dict__ self.__class__ = _umbral_pubkey.__class__ + def to_cryptography_pubkey(self): + self.i_want_to_be_a_real_boy() + return self.to_cryptography_pubkey() + @property def params(self): # Holy heck, metamock hacking. From e576793e47a1017a46c1a00d8705c727bcce294b Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 22 Apr 2020 23:41:02 -0700 Subject: [PATCH 021/167] Alice now proposes Arrangements; Ursula considers them. Fixes #1924. --- nucypher/network/middleware.py | 2 +- nucypher/policy/policies.py | 28 +++++++++---------- .../acceptance/network/test_network_actors.py | 6 ++-- tests/utils/policy.py | 6 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py index e8a96c921..bb66ddd8d 100644 --- a/nucypher/network/middleware.py +++ b/nucypher/network/middleware.py @@ -186,7 +186,7 @@ class RestMiddleware: backend=default_backend()) return certificate - def consider_arrangement(self, arrangement): + def propose_arrangement(self, arrangement): node = arrangement.ursula response = self.client.post(node_or_sprout=node, path="consider_arrangement", diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index f91099127..da5b6b77b 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -370,8 +370,8 @@ class Policy(ABC): if publish is True: return self.publish_treasure_map(network_middleware=network_middleware) - def consider_arrangement(self, network_middleware, ursula, arrangement) -> bool: - negotiation_response = network_middleware.consider_arrangement(arrangement=arrangement) + def propose_arrangement(self, network_middleware, ursula, arrangement) -> bool: + negotiation_response = network_middleware.propose_arrangement(arrangement=arrangement) # TODO: check out the response: need to assess the result and see if we're actually good to go. arrangement_is_accepted = negotiation_response.status_code == 200 @@ -397,9 +397,9 @@ class Policy(ABC): the Policy.".format(self.n)) # TODO: One of these layers needs to add concurrency. - self._consider_arrangements(network_middleware=network_middleware, - candidate_ursulas=sampled_ursulas, - *args, **kwargs) + self._propose_arrangements(network_middleware=network_middleware, + candidate_ursulas=sampled_ursulas, + *args, **kwargs) if len(self._accepted_arrangements) < self.n: raise self.Rejected(f'Selected Ursulas rejected too many arrangements ' @@ -425,19 +425,19 @@ class Policy(ABC): return selected_ursulas - def _consider_arrangements(self, - network_middleware: RestMiddleware, - candidate_ursulas: Set[Ursula], - consider_everyone: bool = False, - *args, - **kwargs) -> None: + def _propose_arrangements(self, + network_middleware: RestMiddleware, + candidate_ursulas: Set[Ursula], + consider_everyone: bool = False, + *args, + **kwargs) -> None: for index, selected_ursula in enumerate(candidate_ursulas): arrangement = self.make_arrangement(ursula=selected_ursula, *args, **kwargs) try: - is_accepted = self.consider_arrangement(ursula=selected_ursula, - arrangement=arrangement, - network_middleware=network_middleware) + is_accepted = self.propose_arrangement(ursula=selected_ursula, + arrangement=arrangement, + network_middleware=network_middleware) except NodeSeemsToBeDown as e: # TODO: #355 Also catch InvalidNode here? # This arrangement won't be added to the accepted bucket. diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index 3acce4b9b..e2af34d1d 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -231,6 +231,6 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali vladimir.node_storage.store_node_certificate(certificate=target.certificate) with pytest.raises(vladimir.InvalidNode): - idle_blockchain_policy.consider_arrangement(network_middleware=blockchain_alice.network_middleware, - arrangement=FakeArrangement(), - ursula=vladimir) + idle_blockchain_policy.propose_arrangement(network_middleware=blockchain_alice.network_middleware, + arrangement=FakeArrangement(), + ursula=vladimir) diff --git a/tests/utils/policy.py b/tests/utils/policy.py index ba40e5c9f..445fdbb4f 100644 --- a/tests/utils/policy.py +++ b/tests/utils/policy.py @@ -54,9 +54,9 @@ class MockPolicy(Policy): hrac=self.hrac(), expiration=expiration) - self.consider_arrangement(network_middleware=network_middleware, - ursula=ursula, - arrangement=arrangement) + self.propose_arrangement(network_middleware=network_middleware, + ursula=ursula, + arrangement=arrangement) # TODO: Remove. Seems unused class MockPolicyCreation: From 0d6961c336e0220a5082a51796b930a9a827148a Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 24 Apr 2020 03:14:36 -0700 Subject: [PATCH 022/167] Carving out checksum_address check. A first swing at #1930. --- nucypher/characters/base.py | 43 +++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 2c47c7a32..1099e37a7 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -35,6 +35,15 @@ from nucypher.config.keyring import NucypherKeyring from nucypher.config.node import CharacterConfiguration from nucypher.crypto.api import encrypt_and_sign from nucypher.crypto.kits import UmbralMessageKit +from nucypher.crypto.powers import ( + CryptoPower, + SigningPower, + DecryptingPower, + NoSigningPower, + CryptoPowerUp, + DelegatingPower, TransactingPower, NoTransactingPower +) +from nucypher.crypto.signing import signature_splitter, StrangerStamp, SignatureStamp from nucypher.crypto.powers import (CryptoPower, CryptoPowerUp, DecryptingPower, DelegatingPower, NoSigningPower, SigningPower) from nucypher.crypto.signing import SignatureStamp, StrangerStamp, signature_splitter @@ -175,7 +184,8 @@ class Character(Learner): # self.provider_uri = provider_uri if not self.federated_only: - self.registry = registry or InMemoryContractRegistry.from_latest_publication(network=list(domains)[0]) #TODO: #1580 + self.registry = registry or InMemoryContractRegistry.from_latest_publication( + network=list(domains)[0]) # TODO: #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) @@ -192,7 +202,7 @@ class Character(Learner): *args, **kwargs) # - # Stranger-Character + # Stranger-Char acter # else: # Feel like a stranger @@ -207,25 +217,12 @@ class Character(Learner): self.keyring_root = STRANGER self.network_middleware = STRANGER - # - # Decentralized - # - if not federated_only: - self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower - - # - # Federated - # - elif federated_only: - try: - self._set_checksum_address() - except NoSigningPower: - self._checksum_address = NO_BLOCKCHAIN_CONNECTION - if checksum_address: - # We'll take a checksum address, as long as it matches their singing key - if not checksum_address == self.checksum_address: - error = "Federated-only Characters derive their address from their Signing key; got {} instead." - raise self.SuspiciousActivity(error.format(checksum_address)) + # TODO: Figure out when to do this. + try: + _transacting_power = self._crypto_power.power_ups(TransactingPower) + self._set_checksum_address(checksum_address) + except NoTransactingPower: + pass # Hmm, so this Character has no checksum address at all. Is that what we want? # # Nicknames @@ -538,8 +535,8 @@ class Character(Learner): def make_cli_controller(self, crash_on_error: bool = False): app_name = bytes(self.stamp).hex()[:6] controller = CLIController(app_name=app_name, - crash_on_error=crash_on_error, - interface=self.interface) + crash_on_error=crash_on_error, + interface=self.interface) self.controller = controller return controller From f5458fde6cf54ef5dd78b6d963cc1814713370da Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 24 Apr 2020 03:17:54 -0700 Subject: [PATCH 023/167] Evolving the test to show how Bob can find a map much more quickly. --- tests/acceptance/network/test_network_actors.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index e2af34d1d..70f02e969 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -77,8 +77,12 @@ def test_alice_sets_treasure_map(enacted_federated_policy, federated_ursulas): """ enacted_federated_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) - treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] - assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map + found = 0 + for node in enacted_federated_policy.bob.matching_nodes_among(enacted_federated_policy.alice.known_nodes): + treasure_map_as_set_on_network = node.treasure_maps[treasure_map_index] + assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map + found += 1 + assert found def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alice, federated_bob, federated_ursulas, From d65ad8fb99b84c5987fea569644e714d28ce3ceb Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 13:02:16 -0700 Subject: [PATCH 024/167] Docstring update for Character. --- nucypher/characters/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 1099e37a7..af6c7d06c 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -83,7 +83,11 @@ class Character(Learner): """ - Base class for Nucypher protocol actors. + A participant in the cryptological drama (a screenplay, if you like) of NuCypher. + + Characters can represent users, nodes, wallets, offline devices, or other objects of varying levels of abstraction. + + The Named Characters use this class as a Base, and achieve their individuality from additional methods and PowerUps. PowerUps From 7ef3c05969ea6ed2adaff58eb90d3c0852f73cfc Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 13:21:16 -0700 Subject: [PATCH 025/167] We no longer assume that any representation of a decentralized Character has a checksum address. Fixes #1967 --- nucypher/characters/base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index af6c7d06c..a58e1bda4 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -297,7 +297,7 @@ class Character(Learner): @property def checksum_address(self): - if self._checksum_address is NO_BLOCKCHAIN_CONNECTION: + if not self._checksum_address: self._set_checksum_address() return self._checksum_address @@ -518,12 +518,14 @@ class Character(Learner): public_address = verifying_key_as_eth_key.to_checksum_address() else: try: + # TODO: Some circular logic here if we haven't set the canonical address. public_address = to_checksum_address(self.canonical_public_address) except TypeError: - raise TypeError("You can't use a decentralized character without a _checksum_address.") + public_address = NO_BLOCKCHAIN_CONNECTION + # raise TypeError("You can't use a decentralized character without a _checksum_address.") except NotImplementedError: raise TypeError( - "You can't use a plain Character in federated mode - you need to implement ether_address.") + "You can't use a plain Character in federated mode - you need to implement ether_address.") # TODO: update comment self._checksum_address = public_address From 62c6e9c35ed0fa441a3753cf1aae955e858940fe Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 13:23:16 -0700 Subject: [PATCH 026/167] Let PolicyCredential make the decentralized variant of TreasureMap when necessary. --- nucypher/policy/collections.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nucypher/policy/collections.py b/nucypher/policy/collections.py index f4a3a28f4..785f3f9be 100644 --- a/nucypher/policy/collections.py +++ b/nucypher/policy/collections.py @@ -306,10 +306,12 @@ class PolicyCredential: return json.dumps(cred_dict) @classmethod - def from_json(cls, data: str): + def from_json(cls, data: str, federated=False): """ Deserializes the PolicyCredential from JSON. """ + from nucypher.characters.lawful import Ursula + cred_json = json.loads(data) alice_verifying_key = UmbralPublicKey.from_bytes( @@ -323,7 +325,12 @@ class PolicyCredential: treasure_map = None if 'treasure_map' in cred_json: - treasure_map = TreasureMap.from_bytes( + if federated: # I know know. TODO: WTF. 466 and just... you know... whatever. + _MapClass = TreasureMap + else: + _MapClass = DecentralizedTreasureMap + + treasure_map = _MapClass.from_bytes( bytes().fromhex(cred_json['treasure_map'])) return cls(alice_verifying_key, label, expiration, policy_pubkey, From b2362f584e77ca1c97e7e3401fb677a0018bc5f7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 22:35:01 -0700 Subject: [PATCH 027/167] Bob figures out which type of map he needs. --- nucypher/characters/lawful.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index dbfaacc0b..2687c679a 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -709,16 +709,18 @@ class Bob(Character): alice = Alice.from_public_keys(verifying_key=alice_verifying_key) compass = self.make_compass_for_alice(alice) - from nucypher.policy.collections import TreasureMap + if self.federated_only: + from nucypher.policy.collections import TreasureMap as _MapClass + else: + from nucypher.policy.collections import DecentralizedTreasureMap as _MapClass # TODO: This LBYL is ugly and fraught with danger. NRN if isinstance(treasure_map, bytes): - treasure_map = TreasureMap.from_bytes(treasure_map) + treasure_map = _MapClass.from_bytes(treasure_map) if isinstance(treasure_map, str): tmap_bytes = treasure_map.encode() - treasure_map = TreasureMap.from_bytes(b64decode(tmap_bytes)) - + treasure_map = _MapClass.from_bytes(b64decode(tmap_bytes)) treasure_map.orient(compass) _unknown_ursulas, _known_ursulas, m = self.follow_treasure_map(treasure_map=treasure_map, block=True) else: From 6ce384d576d8bcd8f66736992a1b36bdfaa766c0 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 22:35:25 -0700 Subject: [PATCH 028/167] If we try to cast a DecentralizedTreasureMap to bytes without the blockchain signature, we end up with very odd behavior. --- nucypher/policy/collections.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nucypher/policy/collections.py b/nucypher/policy/collections.py index 785f3f9be..e8e40ac4e 100644 --- a/nucypher/policy/collections.py +++ b/nucypher/policy/collections.py @@ -272,6 +272,8 @@ class DecentralizedTreasureMap(TreasureMap): address=checksum_address) def __bytes__(self): + if self._blockchain_signature is NOT_SIGNED: + raise self.InvalidSignature("Can't cast a DecentralizedTreasureMap to bytes until it has a blockchain signature (otherwise, is it really a 'DecentralizedTreasureMap'?") return self._blockchain_signature + super().__bytes__() From 5931076f77461443d3ef10d9c6a952e621053137 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 22:35:49 -0700 Subject: [PATCH 029/167] Including a fake signature for the test. --- .../characters/control/test_rpc_control_blockchain.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/characters/control/test_rpc_control_blockchain.py b/tests/acceptance/characters/control/test_rpc_control_blockchain.py index 6cce8a0d7..606a479d1 100644 --- a/tests/acceptance/characters/control/test_rpc_control_blockchain.py +++ b/tests/acceptance/characters/control/test_rpc_control_blockchain.py @@ -16,6 +16,9 @@ """ from base64 import b64encode +from nucypher.policy.collections import TreasureMap, DecentralizedTreasureMap +from nucypher.crypto.powers import DecryptingPower, SigningPower +from nucypher.characters.lawful import Ursula import pytest @@ -153,13 +156,15 @@ def test_bob_rpc_character_control_retrieve_with_tmap( # Make a wrong (empty) treasure map - wrong_tmap = TreasureMap(m=0) + wrong_tmap = DecentralizedTreasureMap(m=0) wrong_tmap.prepare_for_publication( blockchain_bob.public_keys(DecryptingPower), blockchain_bob.public_keys(SigningPower), blockchain_alice.stamp, b'Wrong!') - tmap_64 = b64encode(bytes(wrong_tmap)).decode() + wrong_tmap._blockchain_signature = b"this is not a signature, but we don't need one for this test....." # ...because it only matters when Ursula looks at it. + tmap_bytes = bytes(wrong_tmap) + tmap_64 = b64encode(tmap_bytes).decode() request_data['params']['treasure_map'] = tmap_64 - with pytest.raises(TreasureMap.IsDisorienting): + with pytest.raises(DecentralizedTreasureMap.IsDisorienting): bob_rpc_controller.send(request_data) From b821b81a89ab3a5ea16760bb2fd427d31ee59d79 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 22:36:14 -0700 Subject: [PATCH 030/167] Amonia needs to remember not to publish TreasureMaps, lest her gig being discovered too soon. --- tests/acceptance/characters/test_freerider_attacks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/characters/test_freerider_attacks.py b/tests/acceptance/characters/test_freerider_attacks.py index 0cb879633..8a9685ea5 100644 --- a/tests/acceptance/characters/test_freerider_attacks.py +++ b/tests/acceptance/characters/test_freerider_attacks.py @@ -50,7 +50,7 @@ def test_policy_simple_sinpa(blockchain_ursulas, blockchain_alice, blockchain_bo def test_try_to_post_free_arrangement_by_hacking_enact(blockchain_ursulas, blockchain_alice, blockchain_bob, agency, testerchain): """ - This time we won't rely on the tabulation in Alice's enact to catch the problem. + This time we won't rely on the tabulation in Alice's enact() to catch the problem. """ amonia = Amonia.from_lawful_alice(blockchain_alice) # Setup the policy details @@ -63,7 +63,8 @@ def test_try_to_post_free_arrangement_by_hacking_enact(blockchain_ursulas, block m=2, n=n, rate=int(1e18), # one ether - expiration=policy_end_datetime) + expiration=policy_end_datetime, + publish_treasure_map=False) for ursula in blockchain_ursulas: # Even though the grant executed without error... @@ -101,7 +102,8 @@ def test_pay_a_flunky_instead_of_the_arranged_ursula(blockchain_alice, blockchai m=2, n=n, rate=int(1e18), # one ether - expiration=policy_end_datetime) + expiration=policy_end_datetime, + publish_treasure_map=False) # Same exact set of assertions as the last test: for ursula in blockchain_ursulas: From f3b159d8e471ed6b0686df1591ebfbc089a1dffe Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 22:47:27 -0700 Subject: [PATCH 031/167] Separating publishing kwargs: one for blockchain, one for TreasureMap (because you might only want to do the former and not the latter in the decentralized scenario, or not do either one in a federated scenario. --- nucypher/characters/lawful.py | 4 +++- nucypher/policy/policies.py | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 2687c679a..92f6c9c02 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -305,7 +305,9 @@ class Alice(Character, BlockchainPolicyAuthor): # REST call happens here, as does population of TreasureMap. self.log.debug(f"Enacting {policy} ... ") - policy.enact(network_middleware=self.network_middleware, publish=publish_treasure_map) + + # TODO: Make it optional to publish to blockchain? Or is this presumptive based on the `Policy` type? + policy.enact(network_middleware=self.network_middleware, publish_treasure_map=publish_treasure_map) return policy # Now with TreasureMap affixed! def get_policy_encrypting_key_from_label(self, label: bytes) -> UmbralPublicKey: diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index da5b6b77b..ec786bca5 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -328,7 +328,7 @@ class Policy(ABC): raise self.MoreKFragsThanArrangements("Not enough accepted arrangements to assign all KFrags.") return - def enact(self, network_middleware, publish=True) -> dict: + def enact(self, network_middleware, publish_treasure_map=True) -> dict: """ Assign kfrags to ursulas_on_network, and distribute them via REST, populating enacted_arrangements @@ -367,7 +367,7 @@ class Policy(ABC): self.revocation_kit = RevocationKit(self, self.alice.stamp) self.alice.add_active_policy(self) - if publish is True: + if publish_treasure_map is True: return self.publish_treasure_map(network_middleware=network_middleware) def propose_arrangement(self, network_middleware, ursula, arrangement) -> bool: @@ -657,20 +657,21 @@ class BlockchainPolicy(Policy): duration_periods=self.duration_periods, *args, **kwargs) - def enact(self, network_middleware, publish=True) -> dict: + def enact(self, network_middleware, publish_to_blockchain=True, publish_treasure_map=True) -> dict: """ Assign kfrags to ursulas_on_network, and distribute them via REST, populating enacted_arrangements """ - if publish is True: + if publish_to_blockchain is True: self.publish_to_blockchain() # Not in love with this block here, but I want 121 closed. for arrangement in self._accepted_arrangements: arrangement.publish_transaction = self.publish_transaction - super().enact(network_middleware, publish=False) - if publish is True: + super().enact(network_middleware, publish_treasure_map=False) + + if publish_treasure_map is True: self.treasure_map.prepare_for_publication(bob_encrypting_key=self.bob.public_keys(DecryptingPower), bob_verifying_key=self.bob.public_keys(SigningPower), alice_stamp=self.alice.stamp, From 2a8989aede3a231314d605c6fafd6d3a9c0f104a Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 22:47:43 -0700 Subject: [PATCH 032/167] Updated kwarg name. --- nucypher/characters/unlawful.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index 7657fa6ed..4d9ea1cca 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -75,7 +75,7 @@ class Vladimir(Ursula): crypto_power=crypto_power, db_filepath=cls.db_filepath, domains=[':TEMPORARY_DOMAIN:'], - block_until_bonded=False, + block_until_ready=False, start_working_now=False, rest_host=target_ursula.rest_interface.host, rest_port=target_ursula.rest_interface.port, From 568541dde87034bd93a531855d7c6d46bfdcb0ac Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 7 May 2020 22:48:05 -0700 Subject: [PATCH 033/167] Decentralized Alice controller test needs, you guessed it, DecentralizedTreasureMap. --- .../characters/control/test_web_control_blockchain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/characters/control/test_web_control_blockchain.py b/tests/acceptance/characters/control/test_web_control_blockchain.py index 995fa8916..7025a3c50 100644 --- a/tests/acceptance/characters/control/test_web_control_blockchain.py +++ b/tests/acceptance/characters/control/test_web_control_blockchain.py @@ -26,7 +26,7 @@ from click.testing import CliRunner import nucypher from nucypher.crypto.kits import UmbralMessageKit from nucypher.crypto.powers import DecryptingPower -from nucypher.policy.collections import TreasureMap +from nucypher.policy.collections import TreasureMap, DecentralizedTreasureMap click_runner = CliRunner() @@ -89,7 +89,7 @@ def test_alice_web_character_control_grant(alice_web_controller_test_client, gra assert 'alice_verifying_key' in response_data['result'] map_bytes = b64decode(response_data['result']['treasure_map']) - encrypted_map = TreasureMap.from_bytes(map_bytes) + encrypted_map = DecentralizedTreasureMap.from_bytes(map_bytes) assert encrypted_map._hrac is not None # Send bad data to assert error returns From 414f48c922ebe8156dbaf33ab8f63f68d83b80d9 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 12 May 2020 19:25:19 -0700 Subject: [PATCH 034/167] Further down the optimization adventuref for NodeSprout. --- nucypher/network/nodes.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 76b244791..82c0025fa 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -166,6 +166,7 @@ class FleetStateTracker: def record_fleet_state(self, additional_nodes_to_track=None): if additional_nodes_to_track: self.additional_nodes_to_track.extend(additional_nodes_to_track) + if not self._nodes: # No news here. return @@ -226,16 +227,21 @@ class NodeSprout(PartiallyKwargifiedBytes): def __init__(self, node_metadata): super().__init__(node_metadata) - self.checksum_address = to_checksum_address(self.public_address) - self.nickname = nickname_from_seed(self.checksum_address)[0] + self._checksum_address = None + self._nickname = None + self._hash = None self.timestamp = maya.MayaDT(self.timestamp) # Weird for this to be in init. maybe this belongs in the splitter also. - self._hash = int.from_bytes(self.public_address, byteorder="big") # stop-propagation logic (ie, only propagate verified, staked nodes) keeps this unique and BFT. - self._repr = f"({self.__class__.__name__})⇀{self.nickname}↽ ({self.checksum_address})" + self._repr = None def __hash__(self): + if not self._hash: + self._hash = int.from_bytes(self.public_address, + byteorder="big") # stop-propagation logic (ie, only propagate verified, staked nodes) keeps this unique and BFT. return self._hash def __repr__(self): + if not self._repr: + self._repr = f"({self.__class__.__name__})⇀{self.nickname}↽ ({self.checksum_address})" return self._repr def __bytes__(self): @@ -251,6 +257,19 @@ class NodeSprout(PartiallyKwargifiedBytes): def stamp(self) -> bytes: return self.processed_objects['verifying_key'][0] + @property + def checksum_address(self): + if not self._checksum_address: + self._checksum_address = to_checksum_address(self.public_address) + return self._checksum_address + + @property + def nickname(self): + if not self._nickname: + self._nickname = nickname_from_seed(self.checksum_address)[0] + return self._nickname + + def mature(self): mature_node = self.finish() From 5cd3e61b356f5cbc9713f03248ae7dc8c275cefb Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 12 May 2020 19:29:29 -0700 Subject: [PATCH 035/167] What happens if we add a little slowness like network latency? (And here's where we'll have to add concurrency). --- nucypher/policy/policies.py | 1 + tests/utils/middleware.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index ec786bca5..4c7bc194d 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -270,6 +270,7 @@ class Policy(ABC): treasure_map_id = self.treasure_map.public_id() for node in self.bob.matching_nodes_among(self.alice.known_nodes): + # TODO: Concurrency here. try: response = network_middleware.put_treasure_map_on_node(node=node, diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 4dc49ead9..10d0b5949 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -14,7 +14,7 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ - +import time import requests import socket @@ -112,6 +112,15 @@ class MockRestMiddlewareForLargeFleetTests(MockRestMiddleware): return r +class SluggishLargeFleetMiddleware(MockRestMiddlewareForLargeFleetTests): + """ + Similar to above, but with added delay to simulate network latency. + """ + def put_treasure_map_on_node(self, *args, **kwargs): + time.sleep(.1) + return super().put_treasure_map_on_node(*args, **kwargs) + + class _MiddlewareClientWithConnectionProblems(_TestMiddlewareClient): def __init__(self, *args, **kwargs): From fb523765b87d840da3ff034fc9cad8c3aff5d7fd Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 12 May 2020 19:30:23 -0700 Subject: [PATCH 036/167] Test using the sluggish middleware; this is the one that will require the concurrency from the last commit. --- .../learning/test_discovery_phases.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 8e8cec45e..dd4a6e4bf 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -14,12 +14,15 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ - +import random +from unittest.mock import patch import maya import pytest import time from flask import Response + +from nucypher.utilities.sandbox.middleware import SluggishLargeFleetMiddleware from umbral.keys import UmbralPublicKey from unittest.mock import patch @@ -48,7 +51,7 @@ This toolchain is not built for that scenario at this time, although it is not a After this, our "Learning Loop" does four other things in sequence which are not part of the offering of node discovery tooling alone: -* Instantiation of an actual Node object (currently, an Ursula object) from node metadata. +* Instantiation of an actual Node object (currently, an Ursula object) from node metadata. TODO * Validation of the node's metadata (non-interactive; shows that the Node's public material is indeed signed by the wallet holder of its Staker). * Verification of the Node itself (interactive; shows that the REST server operating at the Node's interface matches the node's metadata). * Verification of the Stake (reads the blockchain; shows that the Node is sponsored by a Staker with sufficient Stake to support a Policy). @@ -82,9 +85,11 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice): isinstance(u, Ursula) for u in highperf_mocked_alice.known_nodes) < 20 # We haven't instantiated many Ursulas. VerificationTracker.node_verifications = 0 # Cleanup +_POLICY_PRESERVER = [] -@pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [100], indirect=True) -def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, highperf_mocked_alice, +@pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) +def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, + highperf_mocked_alice, highperf_mocked_bob): _umbral_pubkey_from_bytes = UmbralPublicKey.from_bytes @@ -107,15 +112,24 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, hi highperf_mocked_bob, b"any label", m=20, n=30, expiration=maya.when('next week'), publish_treasure_map=False) - # TODO: Make some assertions about policy. + _POLICY_PRESERVER.append(policy) + total_verified = sum(node.verified_node for node in highperf_mocked_alice.known_nodes) assert total_verified == 30 - with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: - try: - policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) - except Exception as e: - raise +@pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) def test_mass_treasure_map_placement(highperf_mocked_alice): - assert False \ No newline at end of file + """ + Large-scale map placement with a middleware that simulates network latency. + """ + + highperf_mocked_alice.network_middleware = SluggishLargeFleetMiddleware() + + policy = _POLICY_PRESERVER[0] + with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: + try: + policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) + except Exception as e: + # Retained for convenient breakpointing during test reuns. + raise \ No newline at end of file From cc00294ee721ba36cabdeb34e722dc709e8a712b Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 13 May 2020 14:25:33 -0700 Subject: [PATCH 037/167] Adding a 'last chance' escape hatch for blocking node learning, so that, if the timeout is reached, we check one last time to see if we've learned about the necessary number of nodes. --- nucypher/network/nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 82c0025fa..1c14c017c 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -657,6 +657,9 @@ class Learner: round_finish = maya.now() elapsed = (round_finish - start).seconds if elapsed > timeout: + if len(self.known_nodes) >= number_of_nodes_to_know: # Last chance! + continue + if not self._learning_task.running: raise RuntimeError("Learning loop is not running. Start it with start_learning().") elif not reactor.running and not learn_on_this_thread: From 4488b59683bd9ad13cc6f699f6a6c2d483ab5434 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 13 May 2020 14:26:05 -0700 Subject: [PATCH 038/167] Getting a little crazy - first swing at concurrent Alice-=>Ursula connection. --- nucypher/policy/policies.py | 47 +++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 4c7bc194d..5fcaf68ae 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -20,6 +20,9 @@ from collections import OrderedDict, deque from typing import Generator, Set, List, Callable import maya +from twisted.internet.defer import DeferredList +from twisted.internet.threads import deferToThread + from abc import ABC, abstractmethod from bytestring_splitter import BytestringSplitter, VariableLengthBytestring from constant_sorrow.constants import NOT_SIGNED, UNKNOWN_KFRAG @@ -265,34 +268,38 @@ class Policy(ABC): # TODO: Optionally, block. raise RuntimeError("Alice hasn't learned of any nodes. Thus, she can't push the TreasureMap.") - responses = dict() + responses = list() self.log.debug(f"Pushing {self.treasure_map} to all known nodes from {self.alice}") treasure_map_id = self.treasure_map.public_id() for node in self.bob.matching_nodes_among(self.alice.known_nodes): # TODO: Concurrency here. - try: - response = network_middleware.put_treasure_map_on_node(node=node, - map_id=treasure_map_id, - map_payload=bytes(self.treasure_map)) - except NodeSeemsToBeDown: - # TODO: Introduce good failure mode here if too few nodes receive the map. - self.log.debug(f"Failed pushing {self.treasure_map} to unresponsive {node}") - continue + responses.append(deferToThread(network_middleware.put_treasure_map_on_node, + node=node, + map_id=treasure_map_id, + map_payload=bytes(self.treasure_map) + )) - if response.status_code == 202: - # TODO: #341 - Handle response wherein node already had a copy of this TreasureMap. - responses[node] = response - self.log.debug(f"{self.treasure_map} successfully pushed to {node}") + # try: + # response = network_middleware.put_treasure_map_on_node() + # except NodeSeemsToBeDown: + # # TODO: Introduce good failure mode here if too few nodes receive the map. + # self.log.debug(f"Failed pushing {self.treasure_map} to unresponsive {node}") + # continue + # + # if response.status_code == 202: + # # TODO: #341 - Handle response wherein node already had a copy of this TreasureMap. + # responses[node] = response + # self.log.debug(f"{self.treasure_map} successfully pushed to {node}") + # + # else: + # # TODO: Do something useful here. + # message = f"Failed pushing {self.treasure_map} to {node}, with status {response.status_code}" + # self.log.debug(message) + # raise RuntimeError(message) - else: - # TODO: Do something useful here. - message = f"Failed pushing {self.treasure_map} to {node}, with status {response.status_code}" - self.log.debug(message) - raise RuntimeError(message) - - return responses + return DeferredList(responses) def credential(self, with_treasure_map=True): """ From d301da3dae6d372171f57949b1d563d36e51b09b Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 13 May 2020 14:26:35 -0700 Subject: [PATCH 039/167] It's slow, but it's a start. This test passes in about 9 seconds right now; subsequent commits will tune it. --- .../learning/test_discovery_phases.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index dd4a6e4bf..a1de85714 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -20,6 +20,8 @@ from unittest.mock import patch import maya import pytest import time + +import pytest_twisted from flask import Response from nucypher.utilities.sandbox.middleware import SluggishLargeFleetMiddleware @@ -118,8 +120,11 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, assert total_verified == 30 +@pytest_twisted.inlineCallbacks @pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) -def test_mass_treasure_map_placement(highperf_mocked_alice): +def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, + highperf_mocked_alice, + highperf_mocked_bob): """ Large-scale map placement with a middleware that simulates network latency. """ @@ -129,7 +134,18 @@ def test_mass_treasure_map_placement(highperf_mocked_alice): policy = _POLICY_PRESERVER[0] with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: try: - policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) + deferreds = policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) except Exception as e: # Retained for convenient breakpointing during test reuns. - raise \ No newline at end of file + raise + + yield deferreds + + nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) + nodes_that_actually_have_the_map = [] + + for ursula in fleet_of_highperf_mocked_ursulas: + if policy.treasure_map in list(ursula.treasure_maps.values()): + nodes_that_actually_have_the_map.append(ursula) + + assert nodes_that_actually_have_the_map == nodes_we_expect_to_have_the_map From 96c8a7d197f3a066bbb75ba3cdef9fad4e4641c1 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 14 May 2020 04:13:33 -0700 Subject: [PATCH 040/167] A way for Alice to move on after the TreasureMap is 10% published and let the rest happen in the background. --- nucypher/policy/policies.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 5fcaf68ae..6fe7ad2a4 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -163,6 +163,22 @@ class BlockchainArrangement(Arrangement): return bytes(self.publish_transaction) + partial_payload +class PolicyPayloadMutex(DeferredList): + + def __init__(self, deferredList, *args, **kwargs): + super().__init__(deferredList, *args, **kwargs) + self.done_when = int(len(deferredList) / 10) + + def _cbDeferred(self, *args, **kwargs): + if self.finishedCount == self.done_when: + self.fireOnOneCallback = True + result = super()._cbDeferred(*args, **kwargs) + self.called = False # Keep running. + return result + + return super()._cbDeferred(*args, **kwargs) + + class Policy(ABC): """ An edict by Alice, arranged with n Ursulas, to perform re-encryption for a specific Bob @@ -281,25 +297,7 @@ class Policy(ABC): map_payload=bytes(self.treasure_map) )) - # try: - # response = network_middleware.put_treasure_map_on_node() - # except NodeSeemsToBeDown: - # # TODO: Introduce good failure mode here if too few nodes receive the map. - # self.log.debug(f"Failed pushing {self.treasure_map} to unresponsive {node}") - # continue - # - # if response.status_code == 202: - # # TODO: #341 - Handle response wherein node already had a copy of this TreasureMap. - # responses[node] = response - # self.log.debug(f"{self.treasure_map} successfully pushed to {node}") - # - # else: - # # TODO: Do something useful here. - # message = f"Failed pushing {self.treasure_map} to {node}, with status {response.status_code}" - # self.log.debug(message) - # raise RuntimeError(message) - - return DeferredList(responses) + return PolicyPayloadMutex(responses) def credential(self, with_treasure_map=True): """ From f21bc0cb72de3c9b28dee910afa8feb4a2543539 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 14 May 2020 04:14:44 -0700 Subject: [PATCH 041/167] This zany test actually passes - now to get that 30 second limit down. --- .../learning/test_discovery_phases.py | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index a1de85714..c35d94ccf 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -89,6 +89,7 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice): _POLICY_PRESERVER = [] + @pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, highperf_mocked_alice, @@ -132,6 +133,8 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, highperf_mocked_alice.network_middleware = SluggishLargeFleetMiddleware() policy = _POLICY_PRESERVER[0] + + with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: try: deferreds = policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) @@ -139,13 +142,40 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, # Retained for convenient breakpointing during test reuns. raise + nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) + + def map_is_probably_about_ten_percent_published(*args, **kwargs): + nodes_that_have_the_map_when_we_unblock = [] + + for ursula in fleet_of_highperf_mocked_ursulas: + if policy.treasure_map in list(ursula.treasure_maps.values()): + nodes_that_have_the_map_when_we_unblock.append(ursula) + + approximate_expected_distribution = int(len(nodes_we_expect_to_have_the_map) / 10) + assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx(approximate_expected_distribution, 2) + + deferreds.addCallback(map_is_probably_about_ten_percent_published) + + # Fun fact: if you outdent this next line, you'll be unable to validate nodes - pytest + # fires the callback of the DeferredList in the context of the yield (as it ought to). yield deferreds - nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) - nodes_that_actually_have_the_map = [] + # Now we're back over here (which will be in the threadpool in the background in the real world, but in the main thread + # for the remainder of this test), distributing the test to the rest of the eligible nodes. + resumed_publication = maya.now() - for ursula in fleet_of_highperf_mocked_ursulas: - if policy.treasure_map in list(ursula.treasure_maps.values()): - nodes_that_actually_have_the_map.append(ursula) + nodes_that_actually_have_the_map_eventually = [] + nodes_we_expect_to_have_the_map_but_which_do_not = [u for u in fleet_of_highperf_mocked_ursulas if u in nodes_we_expect_to_have_the_map] - assert nodes_that_actually_have_the_map == nodes_we_expect_to_have_the_map + while nodes_we_expect_to_have_the_map_but_which_do_not: + for ursula in nodes_we_expect_to_have_the_map_but_which_do_not: + if policy.treasure_map in list(ursula.treasure_maps.values()): + nodes_that_actually_have_the_map_eventually.append(ursula) + nodes_we_expect_to_have_the_map_but_which_do_not.remove(ursula) + time_spent_publishing = maya.now() - resumed_publication + if time_spent_publishing.seconds > 30: + pytest.fail("Treasure Map wasn't published to the rest of the eligible fleet.") + time.sleep(.01) + + # For clarity. + assert nodes_that_actually_have_the_map_eventually == nodes_we_expect_to_have_the_map \ No newline at end of file From ea78f4f81f0ad53b7703b2f553defa128739f7da Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 15 May 2020 16:30:24 -0700 Subject: [PATCH 042/167] Another place with wasteful address calculation. --- nucypher/characters/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index a58e1bda4..e943a95b4 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -289,6 +289,7 @@ class Character(Learner): @property def canonical_public_address(self): + # TODO: This is wasteful. #1995 return to_canonical_address(self._checksum_address) @canonical_public_address.setter From b22e6cc7b67c4b23dc7a5d41078e890b8a353775 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 15 May 2020 16:34:32 -0700 Subject: [PATCH 043/167] A threadpool for alice to use for policy publication network requests. --- nucypher/characters/lawful.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 92f6c9c02..f0f252936 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -24,6 +24,11 @@ from random import shuffle import maya import time + +from twisted.python.threadpool import ThreadPool + +from bytestring_splitter import BytestringKwargifier, BytestringSplittingError +from bytestring_splitter import BytestringSplitter, VariableLengthBytestring from bytestring_splitter import BytestringKwargifier, BytestringSplitter, BytestringSplittingError, \ VariableLengthBytestring from constant_sorrow import constants @@ -123,6 +128,9 @@ class Alice(Character, BlockchainPolicyAuthor): if is_me: self.m = m self.n = n + + self.publication_threadpool = ThreadPool(maxthreads=120, minthreads=20, name="Alice Policy Publication") # In the future, this value is perhaps best set to something like 3-4 times the optimal "high n", whatever we determine that to be. + self.publication_threadpool.start() else: self.m = STRANGER_ALICE self.n = STRANGER_ALICE From d2686ce84ed9d7c8aa206d089be079cf8346d70e Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 15 May 2020 17:06:03 -0700 Subject: [PATCH 044/167] Using the threadpool and the custom mutex - substantially faster test run. --- nucypher/policy/policies.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 6fe7ad2a4..2d580f2e4 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -17,11 +17,13 @@ along with nucypher. If not, see . import random from collections import OrderedDict, deque +from queue import Queue from typing import Generator, Set, List, Callable import maya +from twisted.internet import reactor from twisted.internet.defer import DeferredList -from twisted.internet.threads import deferToThread +from twisted.internet.threads import deferToThread, deferToThreadPool from abc import ABC, abstractmethod from bytestring_splitter import BytestringSplitter, VariableLengthBytestring @@ -165,18 +167,22 @@ class BlockchainArrangement(Arrangement): class PolicyPayloadMutex(DeferredList): - def __init__(self, deferredList, *args, **kwargs): + def __init__(self, deferredList, percent_to_complete_before_release=10, *args, **kwargs): + self.percent_to_complete_before_release = percent_to_complete_before_release + self.q = Queue() + super().__init__(deferredList, *args, **kwargs) - self.done_when = int(len(deferredList) / 10) + self.done_when = int(len(deferredList) / self.percent_to_complete_before_release) def _cbDeferred(self, *args, **kwargs): - if self.finishedCount == self.done_when: - self.fireOnOneCallback = True - result = super()._cbDeferred(*args, **kwargs) - self.called = False # Keep running. - return result + result = super()._cbDeferred(*args, **kwargs) - return super()._cbDeferred(*args, **kwargs) + if self.finishedCount == self.done_when: + self.q.put(None) + + if self.finishedCount == len(self.resultList): + print("********************** DONE!!! **********************") + return result class Policy(ABC): @@ -291,13 +297,14 @@ class Policy(ABC): for node in self.bob.matching_nodes_among(self.alice.known_nodes): # TODO: Concurrency here. - responses.append(deferToThread(network_middleware.put_treasure_map_on_node, - node=node, - map_id=treasure_map_id, - map_payload=bytes(self.treasure_map) - )) + responses.append(deferToThreadPool(reactor, self.alice.publication_threadpool, + network_middleware.put_treasure_map_on_node, + node=node, + map_id=treasure_map_id, + map_payload=bytes(self.treasure_map) + )) - return PolicyPayloadMutex(responses) + return PolicyPayloadMutex(responses, percent_to_complete_before_release=10) def credential(self, with_treasure_map=True): """ From 35b30d5af61c23fa1e81065bcdc022d3483f7d50 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 15 May 2020 17:07:18 -0700 Subject: [PATCH 045/167] Putting the fake latency on either side of the endpoint gives slightly more realistic conditions. --- tests/utils/middleware.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 10d0b5949..17ab9734d 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -116,9 +116,11 @@ class SluggishLargeFleetMiddleware(MockRestMiddlewareForLargeFleetTests): """ Similar to above, but with added delay to simulate network latency. """ - def put_treasure_map_on_node(self, *args, **kwargs): + def put_treasure_map_on_node(self, node, *args, **kwargs): time.sleep(.1) - return super().put_treasure_map_on_node(*args, **kwargs) + result = super().put_treasure_map_on_node(node=node, *args, **kwargs) + time.sleep(.1) + return result class _MiddlewareClientWithConnectionProblems(_TestMiddlewareClient): From 3b309baccd75338a98f9162287d460a3377e6863 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 15 May 2020 17:08:18 -0700 Subject: [PATCH 046/167] Cleaning up here results in faster test run exit. --- tests/fixtures.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 2fd74e297..8892df032 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -949,7 +949,10 @@ def highperf_mocked_alice(fleet_of_highperf_mocked_ursulas): with mock_cert_storage, mock_verify_node, mock_record_fleet_state, mock_message_verification, mock_keep_learning: alice = config.produce(known_nodes=list(fleet_of_highperf_mocked_ursulas)[:1]) - return alice + yield alice + # TODO: Where does this really, truly belong? + alice._learning_task.stop() + alice.publication_threadpool.stop() @pytest.fixture(scope="module") @@ -962,8 +965,10 @@ def highperf_mocked_bob(fleet_of_highperf_mocked_ursulas): save_metadata=False, reload_metadata=False) - with mock_cert_storage, mock_verify_node, mock_record_fleet_state: + with mock_cert_storage, mock_verify_node, mock_record_fleet_state, mock_keep_learning: bob = config.produce(known_nodes=list(fleet_of_highperf_mocked_ursulas)[:1]) + yield bob + bob._learning_task.stop() return bob # From a36c82cfed35aa4a8a7338c734d8064bd0c39b36 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 15 May 2020 17:09:03 -0700 Subject: [PATCH 047/167] Honing in on the most important concurrent test assertions. This passes, but we can do even better. --- .../learning/test_discovery_phases.py | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index c35d94ccf..c030cd9f1 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -23,6 +23,8 @@ import time import pytest_twisted from flask import Response +from twisted.internet import threads, reactor +from twisted.internet.defer import Deferred from nucypher.utilities.sandbox.middleware import SluggishLargeFleetMiddleware from umbral.keys import UmbralPublicKey @@ -121,6 +123,7 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, assert total_verified == 30 + @pytest_twisted.inlineCallbacks @pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, @@ -132,50 +135,40 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, highperf_mocked_alice.network_middleware = SluggishLargeFleetMiddleware() - policy = _POLICY_PRESERVER[0] - + policy = _POLICY_PRESERVER.pop() with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: try: - deferreds = policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) + publisher = policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) except Exception as e: # Retained for convenient breakpointing during test reuns. raise nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) - def map_is_probably_about_ten_percent_published(*args, **kwargs): - nodes_that_have_the_map_when_we_unblock = [] - - for ursula in fleet_of_highperf_mocked_ursulas: - if policy.treasure_map in list(ursula.treasure_maps.values()): - nodes_that_have_the_map_when_we_unblock.append(ursula) - - approximate_expected_distribution = int(len(nodes_we_expect_to_have_the_map) / 10) - assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx(approximate_expected_distribution, 2) - - deferreds.addCallback(map_is_probably_about_ten_percent_published) + # TODO: This is heavy enough to affect this test. + nodes_we_expect_to_have_the_map_but_which_do_not = [u for u in fleet_of_highperf_mocked_ursulas if u in nodes_we_expect_to_have_the_map] + for node in nodes_we_expect_to_have_the_map_but_which_do_not: + node.verify_node(network_middleware_client=policy.alice.network_middleware.client) # Fun fact: if you outdent this next line, you'll be unable to validate nodes - pytest # fires the callback of the DeferredList in the context of the yield (as it ought to). - yield deferreds + # yield threads.deferToThread(lambda: None) + # publisher.q.get() + + nodes_that_have_the_map_when_we_unblock = [] + + for ursula in fleet_of_highperf_mocked_ursulas: + if policy.treasure_map in list(ursula.treasure_maps.values()): + nodes_that_have_the_map_when_we_unblock.append(ursula) # Now we're back over here (which will be in the threadpool in the background in the real world, but in the main thread # for the remainder of this test), distributing the test to the rest of the eligible nodes. - resumed_publication = maya.now() - nodes_that_actually_have_the_map_eventually = [] - nodes_we_expect_to_have_the_map_but_which_do_not = [u for u in fleet_of_highperf_mocked_ursulas if u in nodes_we_expect_to_have_the_map] + def wtf(map_publication_responses): + for was_succssfull, http_response in map_publication_responses: + assert was_succssfull + assert http_response.status_code == 202 - while nodes_we_expect_to_have_the_map_but_which_do_not: - for ursula in nodes_we_expect_to_have_the_map_but_which_do_not: - if policy.treasure_map in list(ursula.treasure_maps.values()): - nodes_that_actually_have_the_map_eventually.append(ursula) - nodes_we_expect_to_have_the_map_but_which_do_not.remove(ursula) - time_spent_publishing = maya.now() - resumed_publication - if time_spent_publishing.seconds > 30: - pytest.fail("Treasure Map wasn't published to the rest of the eligible fleet.") - time.sleep(.01) - - # For clarity. - assert nodes_that_actually_have_the_map_eventually == nodes_we_expect_to_have_the_map \ No newline at end of file + publisher.addCallback(wtf) + yield publisher From 9045c3d7c17d1718287f255bc8ec42c0d29c43ba Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 15 May 2020 21:43:45 -0700 Subject: [PATCH 048/167] Refining both test and threadpooling. Looking pretty good now. --- nucypher/characters/lawful.py | 2 +- nucypher/policy/policies.py | 21 +++--- tests/fixtures.py | 2 +- .../test_bob_joins_policy_and_retrieves.py | 6 +- .../learning/test_discovery_phases.py | 65 +++++++++++++------ 5 files changed, 61 insertions(+), 35 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index f0f252936..409b5f015 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -129,7 +129,7 @@ class Alice(Character, BlockchainPolicyAuthor): self.m = m self.n = n - self.publication_threadpool = ThreadPool(maxthreads=120, minthreads=20, name="Alice Policy Publication") # In the future, this value is perhaps best set to something like 3-4 times the optimal "high n", whatever we determine that to be. + self.publication_threadpool = ThreadPool(maxthreads=120, name="Alice Policy Publication") # In the future, this value is perhaps best set to something like 3-4 times the optimal "high n", whatever we determine that to be. self.publication_threadpool.start() else: self.m = STRANGER_ALICE diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 2d580f2e4..eb4bd955c 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -169,21 +169,24 @@ class PolicyPayloadMutex(DeferredList): def __init__(self, deferredList, percent_to_complete_before_release=10, *args, **kwargs): self.percent_to_complete_before_release = percent_to_complete_before_release - self.q = Queue() + self._policy_locking_queue = Queue() super().__init__(deferredList, *args, **kwargs) - self.done_when = int(len(deferredList) / self.percent_to_complete_before_release) + self._block_until_this_many_are_complete = int(len(deferredList) / self.percent_to_complete_before_release) def _cbDeferred(self, *args, **kwargs): result = super()._cbDeferred(*args, **kwargs) - if self.finishedCount == self.done_when: - self.q.put(None) - - if self.finishedCount == len(self.resultList): - print("********************** DONE!!! **********************") + if self.finishedCount == self._block_until_this_many_are_complete: + self._policy_locking_queue.put(None) # TODO: It'd be rad to return a list of nodes here who were contacted, but it's non-trivial. return result + def block_for_a_little_while(self): + """ + https://www.youtube.com/watch?v=OkSLswPSq2o + """ + return self._policy_locking_queue.get() + class Policy(ABC): """ @@ -278,7 +281,7 @@ class Policy(ABC): """ return keccak_digest(bytes(self.alice.stamp) + bytes(self.bob.stamp) + self.label) - def publish_treasure_map(self, network_middleware: RestMiddleware, blockchain_signer: Callable = None) -> dict: + def publish_treasure_map(self, network_middleware: RestMiddleware, blockchain_signer: Callable = None) -> PolicyPayloadMutex: self.treasure_map.prepare_for_publication(self.bob.public_keys(DecryptingPower), self.bob.public_keys(SigningPower), self.alice.stamp, @@ -287,7 +290,7 @@ class Policy(ABC): self.treasure_map.include_blockchain_signature(blockchain_signer) if not self.alice.known_nodes: - # TODO: Optionally, block. + # TODO: Optionally, block. This is increasingly important. raise RuntimeError("Alice hasn't learned of any nodes. Thus, she can't push the TreasureMap.") responses = list() diff --git a/tests/fixtures.py b/tests/fixtures.py index 8892df032..1c054b4e2 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -934,7 +934,7 @@ def fleet_of_highperf_mocked_ursulas(ursula_federated_test_config, request): for ursula in _ursulas: ursula.known_nodes._nodes = all_ursulas ursula.known_nodes.checksum = b"This is a fleet state checksum..".hex() - return _ursulas + yield _ursulas @pytest.fixture(scope="module") diff --git a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py index b562a8ffa..c8dc920b7 100644 --- a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py +++ b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py @@ -105,9 +105,9 @@ def test_bob_joins_policy_and_retrieves(federated_alice, maps.append(map) if policy.treasure_map in maps: # This is a nice place to put a breakpoint to examine Bob's failure to join a policy. - # bob.join_policy(label=label, - # alice_verifying_key=federated_alice.stamp, - # block=True) + bob.join_policy(label=label, + alice_verifying_key=federated_alice.stamp, + block=True) pytest.fail(f"Bob didn't find map {policy.treasure_map} even though it was available. Come on, Bob.") else: pytest.fail(f"It seems that Alice didn't publish {policy.treasure_map}. Come on, Alice.") diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index c030cd9f1..c1909ffda 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ import random +from datetime import datetime from unittest.mock import patch import maya @@ -25,6 +26,7 @@ import pytest_twisted from flask import Response from twisted.internet import threads, reactor from twisted.internet.defer import Deferred +from twisted.internet.threads import deferToThread from nucypher.utilities.sandbox.middleware import SluggishLargeFleetMiddleware from umbral.keys import UmbralPublicKey @@ -137,24 +139,32 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, policy = _POLICY_PRESERVER.pop() - with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: - try: - publisher = policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) - except Exception as e: - # Retained for convenient breakpointing during test reuns. - raise + started = datetime.now() + with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: + # The nodes who match the map distribution criteria. nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) - # TODO: This is heavy enough to affect this test. - nodes_we_expect_to_have_the_map_but_which_do_not = [u for u in fleet_of_highperf_mocked_ursulas if u in nodes_we_expect_to_have_the_map] - for node in nodes_we_expect_to_have_the_map_but_which_do_not: - node.verify_node(network_middleware_client=policy.alice.network_middleware.client) + # returns instantly. + publisher = policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) - # Fun fact: if you outdent this next line, you'll be unable to validate nodes - pytest - # fires the callback of the DeferredList in the context of the yield (as it ought to). - # yield threads.deferToThread(lambda: None) - # publisher.q.get() + nodes_that_have_the_map_when_we_return = [] + + for ursula in fleet_of_highperf_mocked_ursulas: + if policy.treasure_map in list(ursula.treasure_maps.values()): + nodes_that_have_the_map_when_we_return.append(ursula) + + # Very few have gotten the map yet; it's happening in the background. + # Note: if you put a breakpoint above this line, you will likely need to comment this assertion out. + assert len(nodes_that_have_the_map_when_we_return) <= 5 # Maybe a couple finished already, especially if this is a lightning fast computer. But more than five is weird. + + # Wait until about ten percent of the distribution has occurred. + # We do it in a deferred here in the test because it will block the entire process, but in the real-world, we can do this on the granting thread. + yield deferToThread(publisher.block_for_a_little_while) + initial_blocking_duration = datetime.now() - started + + # Here we'll just count the nodes that have the map. In the real world, we can do a sanity check + # to make sure things haven't gone sideways. nodes_that_have_the_map_when_we_unblock = [] @@ -162,13 +172,26 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, if policy.treasure_map in list(ursula.treasure_maps.values()): nodes_that_have_the_map_when_we_unblock.append(ursula) - # Now we're back over here (which will be in the threadpool in the background in the real world, but in the main thread - # for the remainder of this test), distributing the test to the rest of the eligible nodes. + approximate_number_of_nodes_we_expect_to_have_the_map_already = len(nodes_we_expect_to_have_the_map) / 10 + assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx(approximate_number_of_nodes_we_expect_to_have_the_map_already, .5) - def wtf(map_publication_responses): - for was_succssfull, http_response in map_publication_responses: - assert was_succssfull + # The rest of the distributions is continuing in the background. + + successful_responses = [] + def find_successful_responses(map_publication_responses): + for was_succssful, http_response in map_publication_responses: + assert was_succssful assert http_response.status_code == 202 + successful_responses.append(http_response) - publisher.addCallback(wtf) - yield publisher + publisher.addCallback(find_successful_responses) + yield publisher # This will block until the distribution is complete. + complete_distribution_time = datetime.now() - started + + # We have the same number of successful responses as nodes we expected to have the map. + assert len(successful_responses) == len(nodes_we_expect_to_have_the_map) + + # Before Treasure Island (1741), this process took about 3 minutes. + + assert initial_blocking_duration.total_seconds() < 1.8 + assert complete_distribution_time.total_seconds() < 4.5 From b680cf29685d1f97c77bfb314bebc040cfda2198 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 21 May 2020 20:38:11 -0700 Subject: [PATCH 049/167] Introducing acumen, a package for logic relating to what Characters and nodes know (and can know) about each other. --- nucypher/acumen/__init__.py | 0 .../__init__.py => acumen/nicknames.py} | 0 nucypher/acumen/perception.py | 175 ++++++++++++++++++ .../nicknames => acumen}/web_colors.json | 0 .../{network/nicknames => acumen}/zodiac.json | 0 nucypher/blockchain/eth/actors.py | 1 - nucypher/characters/base.py | 3 +- nucypher/characters/lawful.py | 9 +- nucypher/network/nodes.py | 167 +---------------- nucypher/policy/policies.py | 2 +- 10 files changed, 188 insertions(+), 169 deletions(-) create mode 100644 nucypher/acumen/__init__.py rename nucypher/{network/nicknames/__init__.py => acumen/nicknames.py} (100%) create mode 100644 nucypher/acumen/perception.py rename nucypher/{network/nicknames => acumen}/web_colors.json (100%) rename nucypher/{network/nicknames => acumen}/zodiac.json (100%) diff --git a/nucypher/acumen/__init__.py b/nucypher/acumen/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nucypher/network/nicknames/__init__.py b/nucypher/acumen/nicknames.py similarity index 100% rename from nucypher/network/nicknames/__init__.py rename to nucypher/acumen/nicknames.py diff --git a/nucypher/acumen/perception.py b/nucypher/acumen/perception.py new file mode 100644 index 000000000..8a77c00d2 --- /dev/null +++ b/nucypher/acumen/perception.py @@ -0,0 +1,175 @@ +import binascii +import random + +import maya + +from bytestring_splitter import BytestringSplitter +from constant_sorrow.constants import NO_KNOWN_NODES +from collections import namedtuple +from collections import OrderedDict +from twisted.logger import Logger + +from nucypher.acumen.nicknames import nickname_from_seed +from nucypher.crypto.api import keccak_digest + + +def icon_from_checksum(checksum, + nickname_metadata, + number_of_nodes="Unknown number of "): + if checksum is NO_KNOWN_NODES: + return "NO FLEET STATE AVAILABLE" + icon_template = """ +
+
{number_of_nodes} nodes
+
+ {symbol}︎ +
+
+ {fleet_state_checksum} +
+ """.replace(" ", "").replace('\n', "") + return icon_template.format( + number_of_nodes=number_of_nodes, + color=nickname_metadata[0][0]['hex'], + symbol=nickname_metadata[0][1], + fleet_state_checksum=checksum[0:8] + ) + + +class FleetSensor: + """ + A representation of a fleet of NuCypher nodes. + """ + _checksum = NO_KNOWN_NODES.bool_value(False) + _nickname = NO_KNOWN_NODES + _nickname_metadata = NO_KNOWN_NODES + _tracking = False + most_recent_node_change = NO_KNOWN_NODES + snapshot_splitter = BytestringSplitter(32, 4) + log = Logger("Learning") + FleetState = namedtuple("FleetState", ("nickname", "metadata", "icon", "nodes", "updated")) + + def __init__(self): + self.additional_nodes_to_track = [] + self.updated = maya.now() + self._nodes = OrderedDict() + self.states = OrderedDict() + + def __setitem__(self, key, value): + self._nodes[key] = value + + if self._tracking: + self.log.info("Updating fleet state after saving node {}".format(value)) + self.record_fleet_state() + + def __getitem__(self, item): + return self._nodes[item] + + def __bool__(self): + return bool(self._nodes) + + def __contains__(self, item): + return item in self._nodes.keys() or item in self._nodes.values() + + def __iter__(self): + yield from self._nodes.values() + + def __len__(self): + return len(self._nodes) + + def __eq__(self, other): + return self._nodes == other._nodes + + def __repr__(self): + return self._nodes.__repr__() + + @property + def checksum(self): + return self._checksum + + @checksum.setter + def checksum(self, checksum_value): + self._checksum = checksum_value + self._nickname, self._nickname_metadata = nickname_from_seed(checksum_value, number_of_pairs=1) + + @property + def nickname(self): + return self._nickname + + @property + def nickname_metadata(self): + return self._nickname_metadata + + @property + def icon(self) -> str: + if self.nickname_metadata is NO_KNOWN_NODES: + return str(NO_KNOWN_NODES) + return self.nickname_metadata[0][1] + + def addresses(self): + return self._nodes.keys() + + def icon_html(self): + return icon_from_checksum(checksum=self.checksum, + number_of_nodes=str(len(self)), + nickname_metadata=self.nickname_metadata) + + def snapshot(self): + fleet_state_checksum_bytes = binascii.unhexlify(self.checksum) + fleet_state_updated_bytes = self.updated.epoch.to_bytes(4, byteorder="big") + return fleet_state_checksum_bytes + fleet_state_updated_bytes + + def record_fleet_state(self, additional_nodes_to_track=None): + if additional_nodes_to_track: + self.additional_nodes_to_track.extend(additional_nodes_to_track) + + if not self._nodes: + # No news here. + return + sorted_nodes = self.sorted() + + sorted_nodes_joined = b"".join(bytes(n) for n in sorted_nodes) + checksum = keccak_digest(sorted_nodes_joined).hex() + if checksum not in self.states: + self.checksum = keccak_digest(b"".join(bytes(n) for n in self.sorted())).hex() + self.updated = maya.now() + # For now we store the sorted node list. Someday we probably spin this out into + # its own class, FleetState, and use it as the basis for partial updates. + new_state = self.FleetState(nickname=self.nickname, + metadata=self.nickname_metadata, + nodes=sorted_nodes, + icon=self.icon, + updated=self.updated) + self.states[checksum] = new_state + return checksum, new_state + + def start_tracking_state(self, additional_nodes_to_track=None): + if additional_nodes_to_track is None: + additional_nodes_to_track = list() + self.additional_nodes_to_track.extend(additional_nodes_to_track) + self._tracking = True + self.update_fleet_state() + + def sorted(self): + nodes_to_consider = list(self._nodes.values()) + self.additional_nodes_to_track + return sorted(nodes_to_consider, key=lambda n: n.checksum_address) + + def shuffled(self): + nodes_we_know_about = list(self._nodes.values()) + random.shuffle(nodes_we_know_about) + return nodes_we_know_about + + def abridged_states_dict(self): + abridged_states = {} + for k, v in self.states.items(): + abridged_states[k] = self.abridged_state_details(v) + return abridged_states + + @staticmethod + def abridged_state_details(state): + return {"nickname": state.nickname, + "symbol": state.metadata[0][1], + "color_hex": state.metadata[0][0]['hex'], + "color_name": state.metadata[0][0]['color'], + "updated": state.updated.rfc2822(), + } diff --git a/nucypher/network/nicknames/web_colors.json b/nucypher/acumen/web_colors.json similarity index 100% rename from nucypher/network/nicknames/web_colors.json rename to nucypher/acumen/web_colors.json diff --git a/nucypher/network/nicknames/zodiac.json b/nucypher/acumen/zodiac.json similarity index 100% rename from nucypher/network/nicknames/zodiac.json rename to nucypher/acumen/zodiac.json diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 7b1ccfda7..3485ff983 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -78,7 +78,6 @@ from nucypher.cli.painting.deployment import paint_contract_deployment, paint_in from nucypher.cli.painting.transactions import paint_receipt_summary from nucypher.config.constants import DEFAULT_CONFIG_ROOT from nucypher.crypto.powers import TransactingPower -from nucypher.network.nicknames import nickname_from_seed class BaseActor: diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index e943a95b4..43b6ebf00 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -25,6 +25,8 @@ from cryptography.exceptions import InvalidSignature from eth_keys import KeyAPI as EthKeyAPI from eth_utils import to_canonical_address, to_checksum_address from typing import ClassVar, Dict, List, Optional, Set, Union + +from nucypher.acumen.nicknames import nickname_from_seed from umbral.keys import UmbralPublicKey from umbral.signing import Signature @@ -48,7 +50,6 @@ from nucypher.crypto.powers import (CryptoPower, CryptoPowerUp, DecryptingPower, SigningPower) from nucypher.crypto.signing import SignatureStamp, StrangerStamp, signature_splitter from nucypher.network.middleware import RestMiddleware -from nucypher.network.nicknames import nickname_from_seed from nucypher.network.nodes import Learner diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 409b5f015..cbb96095e 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -47,6 +47,8 @@ from twisted.internet import reactor, stdio, threads from twisted.internet.task import LoopingCall from twisted.logger import Logger from typing import Dict, Iterable, List, Set, Tuple, Union + +from nucypher.acumen.perception import FleetSensor from umbral import pre from umbral.keys import UmbralPublicKey from umbral.kfrags import KFrag @@ -78,8 +80,7 @@ from nucypher.datastore.keypairs import HostingKeypair from nucypher.datastore.threading import ThreadedSession from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware -from nucypher.network.nicknames import nickname_from_seed -from nucypher.network.nodes import NodeSprout, FleetStateTracker +from nucypher.network.nodes import NodeSprout from nucypher.network.nodes import Teacher from nucypher.network.nodes import NodeSprout, Teacher from nucypher.network.protocols import InterfaceInfo, parse_node_uri @@ -864,7 +865,9 @@ class Bob(Character): return cleartexts - def matching_nodes_among(self, nodes: FleetStateTracker): + def matching_nodes_among(self, + nodes: FleetSensor, + no_less_than=8): # Look for nodes whose checksum address has the second character of Bob's encrypting key in the first # few characters. # Think of it as a cheap knockoff hamming distance. diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 1c14c017c..5db7aa38d 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -39,6 +39,9 @@ from typing import Set, Tuple, Union from umbral.signing import Signature import nucypher +from nucypher.acumen.perception import FleetSensor +from umbral.signing import Signature + from nucypher.blockchain.economics import EconomicsFactory from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent from nucypher.blockchain.eth.constants import NULL_ADDRESS @@ -52,172 +55,10 @@ from nucypher.crypto.signing import signature_splitter from nucypher.network import LEARNING_LOOP_VERSION from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware -from nucypher.network.nicknames import nickname_from_seed from nucypher.network.protocols import SuspiciousActivity from nucypher.network.server import TLSHostingPower -def icon_from_checksum(checksum, - nickname_metadata, - number_of_nodes="Unknown number of "): - if checksum is NO_KNOWN_NODES: - return "NO FLEET STATE AVAILABLE" - icon_template = """ -
-
{number_of_nodes} nodes
-
- {symbol}︎ -
-
- {fleet_state_checksum} -
- """.replace(" ", "").replace('\n', "") - return icon_template.format( - number_of_nodes=number_of_nodes, - color=nickname_metadata[0][0]['hex'], - symbol=nickname_metadata[0][1], - fleet_state_checksum=checksum[0:8] - ) - - -class FleetStateTracker: - """ - A representation of a fleet of NuCypher nodes. - """ - _checksum = NO_KNOWN_NODES.bool_value(False) - _nickname = NO_KNOWN_NODES - _nickname_metadata = NO_KNOWN_NODES - _tracking = False - most_recent_node_change = NO_KNOWN_NODES - snapshot_splitter = BytestringSplitter(32, 4) - log = Logger("Learning") - FleetState = namedtuple("FleetState", ("nickname", "metadata", "icon", "nodes", "updated")) - - def __init__(self): - self.additional_nodes_to_track = [] - self.updated = maya.now() - self._nodes = OrderedDict() - self.states = OrderedDict() - - def __setitem__(self, key, value): - self._nodes[key] = value - - if self._tracking: - self.log.info("Updating fleet state after saving node {}".format(value)) - self.record_fleet_state() - - def __getitem__(self, item): - return self._nodes[item] - - def __bool__(self): - return bool(self._nodes) - - def __contains__(self, item): - return item in self._nodes.keys() or item in self._nodes.values() - - def __iter__(self): - yield from self._nodes.values() - - def __len__(self): - return len(self._nodes) - - def __eq__(self, other): - return self._nodes == other._nodes - - def __repr__(self): - return self._nodes.__repr__() - - @property - def checksum(self): - return self._checksum - - @checksum.setter - def checksum(self, checksum_value): - self._checksum = checksum_value - self._nickname, self._nickname_metadata = nickname_from_seed(checksum_value, number_of_pairs=1) - - @property - def nickname(self): - return self._nickname - - @property - def nickname_metadata(self): - return self._nickname_metadata - - @property - def icon(self) -> str: - if self.nickname_metadata is NO_KNOWN_NODES: - return str(NO_KNOWN_NODES) - return self.nickname_metadata[0][1] - - def addresses(self): - return self._nodes.keys() - - def icon_html(self): - return icon_from_checksum(checksum=self.checksum, - number_of_nodes=str(len(self)), - nickname_metadata=self.nickname_metadata) - - def snapshot(self): - fleet_state_checksum_bytes = binascii.unhexlify(self.checksum) - fleet_state_updated_bytes = self.updated.epoch.to_bytes(4, byteorder="big") - return fleet_state_checksum_bytes + fleet_state_updated_bytes - - def record_fleet_state(self, additional_nodes_to_track=None): - if additional_nodes_to_track: - self.additional_nodes_to_track.extend(additional_nodes_to_track) - - if not self._nodes: - # No news here. - return - sorted_nodes = self.sorted() - - sorted_nodes_joined = b"".join(bytes(n) for n in sorted_nodes) - checksum = keccak_digest(sorted_nodes_joined).hex() - if checksum not in self.states: - self.checksum = keccak_digest(b"".join(bytes(n) for n in self.sorted())).hex() - self.updated = maya.now() - # For now we store the sorted node list. Someday we probably spin this out into - # its own class, FleetState, and use it as the basis for partial updates. - new_state = self.FleetState(nickname=self.nickname, - metadata=self.nickname_metadata, - nodes=sorted_nodes, - icon=self.icon, - updated=self.updated) - self.states[checksum] = new_state - return checksum, new_state - - def start_tracking_state(self, additional_nodes_to_track=None): - if additional_nodes_to_track is None: - additional_nodes_to_track = list() - self.additional_nodes_to_track.extend(additional_nodes_to_track) - self._tracking = True - self.update_fleet_state() - - def sorted(self): - nodes_to_consider = list(self._nodes.values()) + self.additional_nodes_to_track - return sorted(nodes_to_consider, key=lambda n: n.checksum_address) - - def shuffled(self): - nodes_we_know_about = list(self._nodes.values()) - random.shuffle(nodes_we_know_about) - return nodes_we_know_about - - def abridged_states_dict(self): - abridged_states = {} - for k, v in self.states.items(): - abridged_states[k] = self.abridged_state_details(v) - return abridged_states - - @staticmethod - def abridged_state_details(state): - return {"nickname": state.nickname, - "symbol": state.metadata[0][1], - "color_hex": state.metadata[0][0]['hex'], - "color_name": state.metadata[0][0]['color'], - "updated": state.updated.rfc2822(), - } - class NodeSprout(PartiallyKwargifiedBytes): """ @@ -302,7 +143,7 @@ class Learner: LEARNER_VERSION = LEARNING_LOOP_VERSION node_splitter = BytestringSplitter(VariableLengthBytestring) version_splitter = BytestringSplitter((int, 2, {"byteorder": "big"})) - tracker_class = FleetStateTracker + tracker_class = FleetSensor invalid_metadata_message = "{} has invalid metadata. The node's stake may have ended, or it is transitioning to a new interface. Ignoring." unknown_version_message = "{} purported to be of version {}, but we're only version {}. Is there a new version of NuCypher?" diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index eb4bd955c..f20cdbbdb 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -673,7 +673,7 @@ class BlockchainPolicy(Policy): duration_periods=self.duration_periods, *args, **kwargs) - def enact(self, network_middleware, publish_to_blockchain=True, publish_treasure_map=True) -> dict: + def enact(self, network_middleware, publish_to_blockchain=True, publish_treasure_map=True) -> PolicyPayloadMutex: """ Assign kfrags to ursulas_on_network, and distribute them via REST, populating enacted_arrangements From 41049d00a23415a4e786414282357874669f17d7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 22 May 2020 12:52:23 -0700 Subject: [PATCH 050/167] Don't try to find matching nodes until we've blocked long enough to even have that many. --- nucypher/blockchain/eth/actors.py | 43 ++++++++++++++++++------------- nucypher/characters/lawful.py | 21 ++++++++++++--- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 3485ff983..a65ebbb60 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -15,17 +15,17 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ -import json -import traceback - -import click import csv -import maya +import json import os import sys import time -from constant_sorrow.constants import FULL, NO_WORKER_BONDED, WORKER_NOT_RUNNING +import traceback from decimal import Decimal +from typing import Dict, List, Optional, Tuple + +import click +import maya from eth_tester.exceptions import TransactionFailed as TestTransactionFailed from eth_utils import to_canonical_address, to_checksum_address from twisted.logger import Logger @@ -33,6 +33,8 @@ from typing import Dict, Iterable, List, Optional, Tuple from web3 import Web3 from web3.exceptions import ValidationError +from constant_sorrow.constants import FULL, NO_WORKER_BONDED, WORKER_NOT_RUNNING +from nucypher.acumen.nicknames import nickname_from_seed from nucypher.blockchain.economics import BaseEconomics, EconomicsFactory, StandardTokenEconomics from nucypher.blockchain.eth.agents import ( AdjudicatorAgent, @@ -518,7 +520,6 @@ class ContractAdministrator(NucypherTokenActor): class Allocator: - class AllocationInputError(Exception): """Raised when the allocation data file doesn't have the correct format""" @@ -628,7 +629,7 @@ class Allocator: dry_run=True, gas_limit=gas_limit) except (TestTransactionFailed, ValidationError, ValueError): # TODO: 1950 - self.log.debug(f"Batch of {len(test_batch)} is too big. Let's stick to {len(test_batch)-1} then") + self.log.debug(f"Batch of {len(test_batch)} is too big. Let's stick to {len(test_batch) - 1} then") break else: self.log.debug(f"Batch of {len(test_batch)} stakers fits in single TX ({estimated_gas} gas). " @@ -682,7 +683,8 @@ class Trustee(MultiSigActor): *args, **kwargs): super().__init__(checksum_address=checksum_address, *args, **kwargs) self.authorizations = dict() - self.executive_addresses = tuple(self.multisig_agent.owners) # TODO: Investigate unresolved reference to .owners (linter) + self.executive_addresses = tuple( + self.multisig_agent.owners) # TODO: Investigate unresolved reference to .owners (linter) if client_password: # TODO: Consider an is_transacting parameter self.transacting_power = TransactingPower(password=client_password, account=checksum_address) @@ -727,7 +729,8 @@ class Trustee(MultiSigActor): # TODO: check for inconsistencies (nonce, etc.) r, s, v = self._combine_authorizations() - receipt = self.multisig_agent.execute(sender_address=self.checksum_address, # TODO: Investigate unresolved reference to .execute + receipt = self.multisig_agent.execute(sender_address=self.checksum_address, + # TODO: Investigate unresolved reference to .execute v=v, r=r, s=s, destination=proposal.target_address, value=proposal.value, @@ -917,7 +920,8 @@ class Staker(NucypherTokenActor): # Calculate stake duration in periods if expiration: - additional_periods = datetime_to_period(datetime=expiration, seconds_per_period=self.economics.seconds_per_period) - current_stake.final_locked_period + additional_periods = datetime_to_period(datetime=expiration, + seconds_per_period=self.economics.seconds_per_period) - current_stake.final_locked_period if additional_periods <= 0: raise Stake.StakingError(f"New expiration {expiration} must be at least 1 period from the " f"current stake's end period ({current_stake.final_locked_period}).") @@ -1000,7 +1004,8 @@ class Staker(NucypherTokenActor): # Calculate stake duration in periods if expiration: - additional_periods = datetime_to_period(datetime=expiration, seconds_per_period=self.economics.seconds_per_period) - current_stake.final_locked_period + additional_periods = datetime_to_period(datetime=expiration, + seconds_per_period=self.economics.seconds_per_period) - current_stake.final_locked_period if additional_periods <= 0: raise Stake.StakingError(f"New expiration {expiration} must be at least 1 period from the " f"current stake's end period ({current_stake.final_locked_period}).") @@ -1344,9 +1349,11 @@ class Worker(NucypherTokenActor): delta = now - start if delta.total_seconds() >= timeout: if staking_address == NULL_ADDRESS: - raise self.UnbondedWorker(f"Worker {self.__worker_address} not bonded after waiting {timeout} seconds.") + raise self.UnbondedWorker( + f"Worker {self.__worker_address} not bonded after waiting {timeout} seconds.") elif not ether_balance: - raise RuntimeError(f"Worker {self.__worker_address} has no ether after waiting {timeout} seconds.") + raise RuntimeError( + f"Worker {self.__worker_address} has no ether after waiting {timeout} seconds.") # Increment time.sleep(poll_rate) @@ -1508,6 +1515,7 @@ class Wallet: """ Account management abstraction on top of blockchain providers and external signers """ + class UnknownAccount(KeyError): pass @@ -1578,10 +1586,8 @@ class Wallet: class StakeHolder(Staker): - banner = STAKEHOLDER_BANNER - # # StakeHolder # @@ -1799,7 +1805,8 @@ class Bidder(NucypherTokenActor): def _get_max_bonus_bid_from_max_stake(self) -> int: """Returns maximum allowed bid calculated from maximum allowed locked tokens""" max_bonus_tokens = self.economics.maximum_allowed_locked - self.economics.minimum_allowed_locked - bonus_eth_supply = sum(self._all_bonus_bidders.values()) if self._all_bonus_bidders else self.worklock_agent.get_bonus_eth_supply() + bonus_eth_supply = sum( + self._all_bonus_bidders.values()) if self._all_bonus_bidders else self.worklock_agent.get_bonus_eth_supply() bonus_worklock_supply = self.worklock_agent.get_bonus_lot_value() max_bonus_bid = max_bonus_tokens * bonus_eth_supply // bonus_worklock_supply return max_bonus_bid @@ -1846,7 +1853,7 @@ class Bidder(NucypherTokenActor): a = min_whale_bonus_bid * bonus_worklock_supply - max_bonus_tokens * bonus_eth_supply b = bonus_worklock_supply - max_bonus_tokens * len(whales) - refund = -(-a//b) # div ceil + refund = -(-a // b) # div ceil min_whale_bonus_bid -= refund whales = dict.fromkeys(whales.keys(), min_whale_bonus_bid) self._all_bonus_bidders.update(whales) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index cbb96095e..4e524876e 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -593,7 +593,8 @@ class Bob(Character): if (start - maya.now()).seconds > timeout: raise _MapClass.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_id} ") - nodes_with_map = self.matching_nodes_among(self.known_nodes) + self.block_until_number_of_known_nodes_is(8, timeout=2, learn_on_this_thread=True) + nodes_with_map = self.matching_nodes_among(self.known_nodes, no_less_than=8) random.shuffle(nodes_with_map) for node in nodes_with_map: @@ -875,9 +876,23 @@ class Bob(Character): # And - famous last words incoming - there's no cognizable attack surface. # Sure, Bob can mine encrypting keypairs until he gets the set of target Ursulas on which Alice can # store a TreasureMap. And then... ???... profit? + + # Sanity check - do we even have enough nodes? + if len(nodes) < no_less_than: + raise ValueError(f"Can't select {no_less_than} from {len(nodes)} (Fleet state: {nodes.FleetState}") + + search_boundary = 2 + target_nodes = [] target_hex_match = self.public_keys(DecryptingPower).hex()[1] - # This might be a performance issue above a few thousand nodes. - target_nodes = [node for node in nodes if target_hex_match in node.checksum_address[2:4]] + while len(target_nodes) < 8: # Arbitrary floor. Is 8 good? + target_nodes = [] + search_boundary += 2 + + if search_boundary > 42: + raise self.NotEnoughNodes + # TODO: 1995 all throughout here (we might not (need to) know the checksum address yet; canonical will do.) + # This might be a performance issue above a few thousand nodes. + target_nodes = [node for node in nodes if target_hex_match in node.checksum_address[2:search_boundary]] return target_nodes def make_web_controller(drone_bob, crash_on_error: bool = False): From 701070cb389d181bc0d7a5ffafbf7c3ce2e6e0d5 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 22 May 2020 12:53:28 -0700 Subject: [PATCH 051/167] Refactor and reorg. --- nucypher/network/nodes.py | 33 ++++++++++++++++----------------- nucypher/network/server.py | 1 - nucypher/policy/collections.py | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 5db7aa38d..d8919bd50 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -16,39 +16,36 @@ along with nucypher. If not, see . """ import contextlib -import random -from collections import OrderedDict, defaultdict, deque, namedtuple +import time +from collections import defaultdict +from collections import deque from contextlib import suppress +from typing import Set, Tuple, Union -import binascii import maya import requests -import time -from bytestring_splitter import BytestringSplitter, BytestringSplittingError, PartiallyKwargifiedBytes, \ - VariableLengthBytestring -from constant_sorrow import constant_or_bytes -from constant_sorrow.constants import (CERTIFICATE_NOT_SAVED, FLEET_STATES_MATCH, NEVER_SEEN, NOT_SIGNED, - NO_KNOWN_NODES, NO_STORAGE_AVAILIBLE, UNKNOWN_FLEET_STATE) from cryptography.x509 import Certificate from eth_utils import to_checksum_address from requests.exceptions import SSLError from twisted.internet import defer, reactor, task from twisted.internet.threads import deferToThread from twisted.logger import Logger -from typing import Set, Tuple, Union -from umbral.signing import Signature import nucypher -from nucypher.acumen.perception import FleetSensor -from umbral.signing import Signature - +from bytestring_splitter import BytestringSplitter, BytestringSplittingError, PartiallyKwargifiedBytes, \ + VariableLengthBytestring +from constant_sorrow import constant_or_bytes +from constant_sorrow.constants import (CERTIFICATE_NOT_SAVED, FLEET_STATES_MATCH, NEVER_SEEN, NOT_SIGNED, + NO_KNOWN_NODES, NO_STORAGE_AVAILIBLE, UNKNOWN_FLEET_STATE) +from nucypher.acumen.nicknames import nickname_from_seed +from nucypher.acumen.perception import FleetSensor, icon_from_checksum from nucypher.blockchain.economics import EconomicsFactory from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent from nucypher.blockchain.eth.constants import NULL_ADDRESS from nucypher.blockchain.eth.registry import BaseContractRegistry from nucypher.config.constants import SeednodeMetadata from nucypher.config.storages import ForgetfulNodeStorage -from nucypher.crypto.api import keccak_digest, recover_address_eip_191, verify_eip_191 +from nucypher.crypto.api import recover_address_eip_191, verify_eip_191 from nucypher.crypto.kits import UmbralMessageKit from nucypher.crypto.powers import DecryptingPower, NoSigningPower, SigningPower, TransactingPower from nucypher.crypto.signing import signature_splitter @@ -57,7 +54,7 @@ from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware from nucypher.network.protocols import SuspiciousActivity from nucypher.network.server import TLSHostingPower - +from umbral.signing import Signature class NodeSprout(PartiallyKwargifiedBytes): @@ -627,6 +624,8 @@ class Learner: def learn_from_teacher_node(self, eager=False): """ Sends a request to node_url to find out about known nodes. + + TODO: Does this (and related methods) belong on FleetSensor for portability? """ self._learning_round += 1 @@ -703,7 +702,7 @@ class Learner: self.log.warn(f"Invalid signature ({signature}) received from teacher {current_teacher} for payload {node_payload}") # End edge case handling. - fleet_state_checksum_bytes, fleet_state_updated_bytes, node_payload = FleetStateTracker.snapshot_splitter( + fleet_state_checksum_bytes, fleet_state_updated_bytes, node_payload = FleetSensor.snapshot_splitter( node_payload, return_remainder=True) diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 040dc1b61..da2436e90 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -416,7 +416,6 @@ def make_rest_app( else: return Response("Can't save a TreasureMap with this ID from you.", status=409) - if do_store and not this_node.federated_only: alice_checksum_address = this_node.policy_agent.contract.functions.getPolicyOwner( treasure_map._hrac[:16]).call() diff --git a/nucypher/policy/collections.py b/nucypher/policy/collections.py index e8e40ac4e..b0054f09e 100644 --- a/nucypher/policy/collections.py +++ b/nucypher/policy/collections.py @@ -182,7 +182,7 @@ class TreasureMap: def add_arrangement(self, arrangement): if self.destinations == NO_DECRYPTION_PERFORMED: raise TypeError("This TreasureMap is encrypted. You can't add another node without decrypting it.") - self.destinations[arrangement.ursula.checksum_address] = arrangement.id + self.destinations[arrangement.ursula.checksum_address] = arrangement.id # TODO: 1995 def public_id(self) -> str: """ From 9374947d6177ff4c1d26af979d3049c1fe12fae2 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 22 May 2020 12:54:11 -0700 Subject: [PATCH 052/167] Comment out this block for a moment; Character tests pass in this config. --- nucypher/policy/policies.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index f20cdbbdb..e36e18789 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -353,6 +353,7 @@ class Policy(ABC): arrangement_message_kit = arrangement.encrypt_payload_for_ursula() try: + # TODO: Concurrency response = network_middleware.enact_policy(arrangement.ursula, arrangement.id, arrangement_message_kit.to_bytes()) @@ -361,7 +362,7 @@ class Policy(ABC): else: arrangement.status = response.status_code - # Assuming response is what we hope for. + # TODO: Handle problem here - if the arrangement is bad, deal with it. self.treasure_map.add_arrangement(arrangement) else: @@ -694,6 +695,7 @@ class BlockchainPolicy(Policy): label=self.label) # Sign the map. transacting_power = self.alice._crypto_power.power_ups(TransactingPower) - self.publish_treasure_map(network_middleware=network_middleware, + publisher = self.publish_treasure_map(network_middleware=network_middleware, blockchain_signer=transacting_power.sign_message) - return + # publisher.block_for_a_little_while() + return publisher From 09e03d156e9ef7da5f133dbca9c8d360205c355f Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 22 May 2020 12:54:29 -0700 Subject: [PATCH 053/167] Stop Alice's publication threadpool at the end of fixtures; otherwise tests hang. --- tests/fixtures.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 1c054b4e2..f94d65643 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -343,14 +343,16 @@ def random_policy_label(): @pytest.fixture(scope="module") def federated_alice(alice_federated_test_config): - _alice = alice_federated_test_config.produce() - return _alice + alice = alice_federated_test_config.produce() + yield alice + alice.publication_threadpool.stop() @pytest.fixture(scope="module") def blockchain_alice(alice_blockchain_test_config, testerchain): - _alice = alice_blockchain_test_config.produce() - return _alice + alice = alice_blockchain_test_config.produce() + yield alice + alice.publication_threadpool.stop() @pytest.fixture(scope="module") From d4807654b19f2663a371d7677a040a3e85a74dcc Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 22 May 2020 14:07:32 -0700 Subject: [PATCH 054/167] Changing references to acumen materials. --- nucypher/cli/painting/status.py | 2 +- tests/acceptance/learning/test_fault_tolerance.py | 5 ++++- tests/acceptance/network/test_network_actors.py | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/nucypher/cli/painting/status.py b/nucypher/cli/painting/status.py index b9a96a359..0bfc01155 100644 --- a/nucypher/cli/painting/status.py +++ b/nucypher/cli/painting/status.py @@ -28,7 +28,7 @@ from nucypher.blockchain.eth.constants import NULL_ADDRESS from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory from nucypher.blockchain.eth.token import NU from nucypher.blockchain.eth.utils import prettify_eth_amount -from nucypher.network.nicknames import nickname_from_seed +from nucypher.acumen.nicknames import nickname_from_seed def paint_contract_status(registry, emitter): diff --git a/tests/acceptance/learning/test_fault_tolerance.py b/tests/acceptance/learning/test_fault_tolerance.py index 1e452e5f5..ddf351376 100644 --- a/tests/acceptance/learning/test_fault_tolerance.py +++ b/tests/acceptance/learning/test_fault_tolerance.py @@ -20,6 +20,9 @@ from constant_sorrow.constants import NOT_SIGNED from twisted.logger import LogLevel, globalLogPublisher from nucypher.crypto.powers import TransactingPower +from nucypher.acumen.nicknames import nickname_from_seed +from nucypher.acumen.perception import FleetSensor +from nucypher.network.nodes import Learner from nucypher.network.nodes import FleetStateTracker, Learner from tests.utils.middleware import MockRestMiddleware from tests.utils.ursula import make_ursula_for_staker @@ -44,7 +47,7 @@ def test_blockchain_ursula_stamp_verification_tolerance(blockchain_ursulas, mock unsigned._Teacher__decentralized_identity_evidence = NOT_SIGNED # Wipe known nodes! - lonely_blockchain_learner._Learner__known_nodes = FleetStateTracker() + lonely_blockchain_learner._Learner__known_nodes = FleetSensor() lonely_blockchain_learner._current_teacher_node = blockchain_teacher lonely_blockchain_learner.remember_node(blockchain_teacher) diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index 70f02e969..3ec9a15c3 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -25,8 +25,8 @@ from nucypher.characters.lawful import Ursula from nucypher.characters.unlawful import Vladimir from nucypher.crypto.api import keccak_digest from nucypher.crypto.powers import SigningPower -from nucypher.network.nicknames import nickname_from_seed -from nucypher.network.nodes import FleetStateTracker +from nucypher.acumen.nicknames import nickname_from_seed +from nucypher.acumen.perception import FleetSensor from tests.constants import INSECURE_DEVELOPMENT_PASSWORD from tests.utils.middleware import MockRestMiddleware @@ -49,7 +49,7 @@ def test_all_blockchain_ursulas_know_about_all_other_ursulas(blockchain_ursulas, @pytest.mark.slow() def test_blockchain_alice_finds_ursula_via_rest(blockchain_alice, blockchain_ursulas): # Imagine alice knows of nobody. - blockchain_alice._Learner__known_nodes = FleetStateTracker() + blockchain_alice._Learner__known_nodes = FleetSensor() blockchain_alice.remember_node(blockchain_ursulas[0]) blockchain_alice.learn_from_teacher_node() From 88b53fff155aa68728efca386b488a824c953d6d Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 13:57:59 -0700 Subject: [PATCH 055/167] Relative import as the start of the question to keep this compatible as a package. --- nucypher/acumen/perception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/acumen/perception.py b/nucypher/acumen/perception.py index 8a77c00d2..c7a86d33f 100644 --- a/nucypher/acumen/perception.py +++ b/nucypher/acumen/perception.py @@ -9,7 +9,7 @@ from collections import namedtuple from collections import OrderedDict from twisted.logger import Logger -from nucypher.acumen.nicknames import nickname_from_seed +from .nicknames import nickname_from_seed from nucypher.crypto.api import keccak_digest From be372ec0b608a92aa56c6b180c189e547c2fe203 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 13:58:18 -0700 Subject: [PATCH 056/167] Alice's threadpool stops when the reactor stops. --- nucypher/characters/lawful.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 4e524876e..3c9d66e3a 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -132,6 +132,7 @@ class Alice(Character, BlockchainPolicyAuthor): self.publication_threadpool = ThreadPool(maxthreads=120, name="Alice Policy Publication") # In the future, this value is perhaps best set to something like 3-4 times the optimal "high n", whatever we determine that to be. self.publication_threadpool.start() + reactor.addSystemEventTrigger("before", "shutdown", self.publication_threadpool.stop) # TODO: Congregate Character Stop activity. else: self.m = STRANGER_ALICE self.n = STRANGER_ALICE From b0bd5165c3b61be5b37b5fd39c7c92c11a309f78 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 13:58:41 -0700 Subject: [PATCH 057/167] Using 'no_less_than' kwarg. --- nucypher/characters/lawful.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 3c9d66e3a..e38447b77 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -317,7 +317,7 @@ class Alice(Character, BlockchainPolicyAuthor): self.log.debug(f"Enacting {policy} ... ") # TODO: Make it optional to publish to blockchain? Or is this presumptive based on the `Policy` type? - policy.enact(network_middleware=self.network_middleware, publish_treasure_map=publish_treasure_map) + _publisher = policy.enact(network_middleware=self.network_middleware, publish_treasure_map=publish_treasure_map) return policy # Now with TreasureMap affixed! def get_policy_encrypting_key_from_label(self, label: bytes) -> UmbralPublicKey: @@ -869,7 +869,7 @@ class Bob(Character): def matching_nodes_among(self, nodes: FleetSensor, - no_less_than=8): + no_less_than=7): # Look for nodes whose checksum address has the second character of Bob's encrypting key in the first # few characters. # Think of it as a cheap knockoff hamming distance. @@ -885,7 +885,7 @@ class Bob(Character): search_boundary = 2 target_nodes = [] target_hex_match = self.public_keys(DecryptingPower).hex()[1] - while len(target_nodes) < 8: # Arbitrary floor. Is 8 good? + while len(target_nodes) < no_less_than: # Arbitrary floor. Is 8 good? target_nodes = [] search_boundary += 2 From acd7e41452d7b91e37735cf8369e0eceffd31fc0 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 13:59:47 -0700 Subject: [PATCH 058/167] Fire off a learning task at the beginning of one of the blocking methods. This substantially increases the chance that the method won't need to block for multiple iterations (causes a 4.5s speedup on two tests on my laptop). --- nucypher/network/nodes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index d8919bd50..be5ef4128 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -475,6 +475,10 @@ class Learner: start = maya.now() starting_round = self._learning_round + if not learn_on_this_thread: + # Get a head start by firing the looping call now. If it's very fast, maybe we'll have enough nodes on the first iteration. + self._learning_task() + while True: rounds_undertaken = self._learning_round - starting_round if len(self.known_nodes) >= number_of_nodes_to_know: @@ -516,6 +520,10 @@ class Learner: start = maya.now() starting_round = self._learning_round + if not learn_on_this_thread: + # Get a head start by firing the looping call now. If it's very fast, maybe we'll have enough nodes on the first iteration. + self._learning_task() + while True: if self._crashed: return self._crashed From f84c8ab269dd17b178ad37b12d3f17e65be5e515 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 14:00:16 -0700 Subject: [PATCH 059/167] Map comparisons are better done just by public_id than the full bytes, since it's possible to compare maps in various states of secrecy. --- nucypher/policy/collections.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nucypher/policy/collections.py b/nucypher/policy/collections.py index b0054f09e..be782d63e 100644 --- a/nucypher/policy/collections.py +++ b/nucypher/policy/collections.py @@ -235,7 +235,10 @@ class TreasureMap: f"TreasureMap lists only {len(self._destinations)} destination, but requires interaction with {self._m} nodes.") def __eq__(self, other): - return bytes(self) == bytes(other) + try: + return self.public_id() == other.public_id() + except AttributeError: + raise TypeError(f"Can't compare f{other} to a TreasureMap (it needs to implement public_id() )") def __iter__(self): return iter(self.destinations.items()) From 48362fe5861dafa1ba0893b1e9df103c51f1b677 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 14:00:45 -0700 Subject: [PATCH 060/167] Snaking mutex through a few tests; helpful for debugging. --- nucypher/policy/policies.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index e36e18789..1e8edb03c 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -244,6 +244,8 @@ class Policy(ABC): self.alice_signature = alice_signature # TODO: This is unused / To Be Implemented? + self.publishing_mutex = None + class MoreKFragsThanArrangements(TypeError): """ Raised when a Policy has been used to generate Arrangements with Ursulas insufficient number @@ -297,8 +299,8 @@ class Policy(ABC): self.log.debug(f"Pushing {self.treasure_map} to all known nodes from {self.alice}") treasure_map_id = self.treasure_map.public_id() - for node in self.bob.matching_nodes_among(self.alice.known_nodes): - # TODO: Concurrency here. + self.alice.block_until_number_of_known_nodes_is(8, timeout=2, learn_on_this_thread=True) + for node in self.bob.matching_nodes_among(self.alice.known_nodes, no_less_than=8): responses.append(deferToThreadPool(reactor, self.alice.publication_threadpool, network_middleware.put_treasure_map_on_node, @@ -306,8 +308,8 @@ class Policy(ABC): map_id=treasure_map_id, map_payload=bytes(self.treasure_map) )) - - return PolicyPayloadMutex(responses, percent_to_complete_before_release=10) + self.publishing_mutex = PolicyPayloadMutex(responses, percent_to_complete_before_release=10) + # return self.publishing_mutex # I dunno.. return this? Why not just use the composed version? def credential(self, with_treasure_map=True): """ @@ -686,7 +688,7 @@ class BlockchainPolicy(Policy): for arrangement in self._accepted_arrangements: arrangement.publish_transaction = self.publish_transaction - super().enact(network_middleware, publish_treasure_map=False) + publisher = super().enact(network_middleware, publish_treasure_map=False) if publish_treasure_map is True: self.treasure_map.prepare_for_publication(bob_encrypting_key=self.bob.public_keys(DecryptingPower), From 8e7d83134eb261e4fdfe6abfa43c2f83e2afa6a1 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 14:01:19 -0700 Subject: [PATCH 061/167] Instead of the first Ursula, choose the first Ursula who is supposed to have the map. --- tests/acceptance/network/test_network_actors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index 3ec9a15c3..79ee5b790 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -152,7 +152,7 @@ def test_treasure_map_cannot_be_duplicated(blockchain_ursulas, blockchain_alice, rate=int(1e18), # one ether expiration=policy_end_datetime) - u = blockchain_ursulas[0] + u = blockchain_bob.matching_nodes_among(blockchain_alice.known_nodes)[0] saved_map = u.treasure_maps[bytes.fromhex(policy.treasure_map.public_id())] assert saved_map == policy.treasure_map # This Ursula was actually a Vladimir. From 62b7ccd9bb936addc2e70d38498b933c8f1f0f81 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 23 May 2020 15:38:34 -0700 Subject: [PATCH 062/167] Fixing locations and formats. --- nucypher/characters/lawful.py | 1 + .../learning/test_discovery_phases.py | 36 +++++++++---------- tests/mock/performance_mocks.py | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index e38447b77..74694722b 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -48,6 +48,7 @@ from twisted.internet.task import LoopingCall from twisted.logger import Logger from typing import Dict, Iterable, List, Set, Tuple, Union +from nucypher.acumen.nicknames import nickname_from_seed from nucypher.acumen.perception import FleetSensor from umbral import pre from umbral.keys import UmbralPublicKey diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index c1909ffda..db4546210 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -14,25 +14,21 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ -import random +import time from datetime import datetime from unittest.mock import patch import maya import pytest -import time - import pytest_twisted -from flask import Response -from twisted.internet import threads, reactor -from twisted.internet.defer import Deferred from twisted.internet.threads import deferToThread -from nucypher.utilities.sandbox.middleware import SluggishLargeFleetMiddleware -from umbral.keys import UmbralPublicKey -from unittest.mock import patch - from nucypher.characters.lawful import Ursula +from tests.performance_mocks import NotAPublicKey, NotARestApp, VerificationTracker, mock_cert_loading, \ + mock_cert_storage, mock_message_verification, mock_metadata_validation, mock_pubkey_from_bytes, mock_secret_source, \ + mock_signature_bytes, mock_stamp_call, mock_verify_node +from tests.utils.middleware import SluggishLargeFleetMiddleware +from umbral.keys import UmbralPublicKey from tests.mock.performance_mocks import ( NotAPublicKey, NotARestApp, @@ -76,7 +72,8 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice): # doesn't take up all the time. _teacher = highperf_mocked_alice.current_teacher_node() _teacher_known_nodes_bytestring = _teacher.bytestring_of_known_nodes() - _teacher.bytestring_of_known_nodes = lambda *args, **kwargs: _teacher_known_nodes_bytestring # TODO: Formalize this? #1537 + _teacher.bytestring_of_known_nodes = lambda *args, ** kwargs: _teacher_known_nodes_bytestring # TODO: Formalize this? #1537 + with mock_cert_storage, mock_cert_loading, mock_verify_node, mock_message_verification, mock_metadata_validation: with mock_pubkey_from_bytes(), mock_stamp_call, mock_signature_bytes: @@ -91,6 +88,7 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice): isinstance(u, Ursula) for u in highperf_mocked_alice.known_nodes) < 20 # We haven't instantiated many Ursulas. VerificationTracker.node_verifications = 0 # Cleanup + _POLICY_PRESERVER = [] @@ -125,7 +123,6 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, assert total_verified == 30 - @pytest_twisted.inlineCallbacks @pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, @@ -146,7 +143,7 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) # returns instantly. - publisher = policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) + policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) nodes_that_have_the_map_when_we_return = [] @@ -156,11 +153,12 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, # Very few have gotten the map yet; it's happening in the background. # Note: if you put a breakpoint above this line, you will likely need to comment this assertion out. - assert len(nodes_that_have_the_map_when_we_return) <= 5 # Maybe a couple finished already, especially if this is a lightning fast computer. But more than five is weird. + assert len( + nodes_that_have_the_map_when_we_return) <= 5 # Maybe a couple finished already, especially if this is a lightning fast computer. But more than five is weird. # Wait until about ten percent of the distribution has occurred. # We do it in a deferred here in the test because it will block the entire process, but in the real-world, we can do this on the granting thread. - yield deferToThread(publisher.block_for_a_little_while) + yield deferToThread(policy.publishing_mutex.block_for_a_little_while) initial_blocking_duration = datetime.now() - started # Here we'll just count the nodes that have the map. In the real world, we can do a sanity check @@ -173,19 +171,21 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, nodes_that_have_the_map_when_we_unblock.append(ursula) approximate_number_of_nodes_we_expect_to_have_the_map_already = len(nodes_we_expect_to_have_the_map) / 10 - assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx(approximate_number_of_nodes_we_expect_to_have_the_map_already, .5) + assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx( + approximate_number_of_nodes_we_expect_to_have_the_map_already, .5) # The rest of the distributions is continuing in the background. successful_responses = [] + def find_successful_responses(map_publication_responses): for was_succssful, http_response in map_publication_responses: assert was_succssful assert http_response.status_code == 202 successful_responses.append(http_response) - publisher.addCallback(find_successful_responses) - yield publisher # This will block until the distribution is complete. + policy.publishing_mutex.addCallback(find_successful_responses) + yield policy.publishing_mutex # This will block until the distribution is complete. complete_distribution_time = datetime.now() - started # We have the same number of successful responses as nodes we expected to have the map. diff --git a/tests/mock/performance_mocks.py b/tests/mock/performance_mocks.py index dc8b72b53..5a387a680 100644 --- a/tests/mock/performance_mocks.py +++ b/tests/mock/performance_mocks.py @@ -35,7 +35,7 @@ def fake_keep_learning(learner, *args, **kwargs): mock_keep_learning = patch('nucypher.network.nodes.Learner.keep_learning_about_nodes', new=fake_keep_learning) -mock_record_fleet_state = patch("nucypher.network.nodes.FleetStateTracker.record_fleet_state", +mock_record_fleet_state = patch("nucypher.acumen.perception.FleetSensor.record_fleet_state", new=lambda *args, **kwargs: None) """ From 365464adc743e57ca5d2747fdb6385748f6539b8 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 25 May 2020 14:15:38 -0700 Subject: [PATCH 063/167] Toward a more coherent depiction of 1547, and how NO_BLOCKCHAIN_CONNECTION can be understood when init'ing a Character. --- nucypher/characters/base.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 43b6ebf00..2a0e5b2eb 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -161,8 +161,6 @@ class Character(Learner): else: self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups) - self._checksum_address = checksum_address - # Fleet and Blockchain Connection (Everyone) if not domains: domains = {CharacterConfiguration.DEFAULT_DOMAIN} @@ -225,9 +223,12 @@ class Character(Learner): # TODO: Figure out when to do this. try: _transacting_power = self._crypto_power.power_ups(TransactingPower) - self._set_checksum_address(checksum_address) except NoTransactingPower: - pass # Hmm, so this Character has no checksum address at all. Is that what we want? + self._checksum_address = checksum_address + if checksum_address is None: + assert True # Hmm, so this Character has no checksum address at all. Is that what we want? + else: + self._set_checksum_address(checksum_address) # # Nicknames @@ -239,7 +240,12 @@ class Character(Learner): self.nickname = self.nickname_metadata = NO_NICKNAME else: try: - self.nickname, self.nickname_metadata = nickname_from_seed(self.checksum_address) + # TODO: It's possible that this is NO_BLOCKCHAIN_CONNECTION. + if self.checksum_address is NO_BLOCKCHAIN_CONNECTION: + self.nickname = self.nickname_metadata = NO_NICKNAME + else: + # This can call _set_checksum_address. + self.nickname, self.nickname_metadata = nickname_from_seed(self.checksum_address) except SigningPower.not_found_error: # TODO: Handle NO_BLOCKCHAIN_CONNECTION more coherently - #1547 if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME From d3c6d121d480ccd07abaeaf3d7a2df3daf404efc Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 25 May 2020 15:44:24 -0700 Subject: [PATCH 064/167] No need to reset checksum_address in Worker - let's try to set it in as few places as possible. #1547. --- nucypher/blockchain/eth/actors.py | 2 +- nucypher/characters/base.py | 1 + nucypher/characters/lawful.py | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index a65ebbb60..09a96f3d0 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -1275,7 +1275,7 @@ class Worker(NucypherTokenActor): self.is_me = is_me - self._checksum_address = None # Stake Address + # self._checksum_address = None # Stake Address # TODO - wait, why? Why are we setting this to None when it may have already been set in an outer method? self.__worker_address = worker_address # Agency diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 2a0e5b2eb..160e90b23 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -502,6 +502,7 @@ class Character(Learner): # Decentralized # if not self.federated_only: + # TODO: And why not return here then? self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower # diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 74694722b..4f0f2b7c9 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1044,11 +1044,12 @@ class Ursula(Teacher, Character, Worker): if is_me and not federated_only: # TODO: #429 # Prepare a TransactingPower from worker node's transacting keys - self.transacting_power = TransactingPower(account=worker_address, + _transacting_power = TransactingPower(account=worker_address, password=client_password, signer=self.signer, cache=True) - self._crypto_power.consume_power_up(self.transacting_power) + self._crypto_power.consume_power_up(_transacting_power) + self._set_checksum_address(_transacting_power.account) # Use this power to substantiate the stamp self.substantiate_stamp() From 1170c27b63651a1f56a0cb7287d023db165d746e Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 28 May 2020 22:05:08 -0700 Subject: [PATCH 065/167] The connectivity problem will now manfiest itself as NotEnoughNodes, as Bob won't have learned in time to try to get the map. --- nucypher/characters/lawful.py | 4 ++-- nucypher/policy/policies.py | 2 +- tests/acceptance/network/test_network_actors.py | 2 +- tests/fixtures.py | 5 +++-- tests/integration/learning/test_discovery_phases.py | 5 +---- tests/integration/learning/test_learning_upgrade.py | 2 +- tests/integration/network/test_failure_modes.py | 2 +- tests/integration/network/test_treasure_map_integration.py | 1 + 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 4f0f2b7c9..e6cd716a4 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -596,13 +596,13 @@ class Bob(Character): raise _MapClass.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_id} ") self.block_until_number_of_known_nodes_is(8, timeout=2, learn_on_this_thread=True) - nodes_with_map = self.matching_nodes_among(self.known_nodes, no_less_than=8) + nodes_with_map = self.matching_nodes_among(self.known_nodes) random.shuffle(nodes_with_map) for node in nodes_with_map: try: response = network_middleware.get_treasure_map_from_node(node=node, map_id=map_id) - except NodeSeemsToBeDown: + except (NodeSeemsToBeDown, self.NotEnoughNodes): continue except network_middleware.NotFound: self.log.info(f"Node {node} claimed not to have TreasureMap {map_id}") diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 1e8edb03c..4cedd40e6 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -167,7 +167,7 @@ class BlockchainArrangement(Arrangement): class PolicyPayloadMutex(DeferredList): - def __init__(self, deferredList, percent_to_complete_before_release=10, *args, **kwargs): + def __init__(self, deferredList, percent_to_complete_before_release=5, *args, **kwargs): self.percent_to_complete_before_release = percent_to_complete_before_release self._policy_locking_queue = Queue() diff --git a/tests/acceptance/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py index 79ee5b790..2f615167e 100644 --- a/tests/acceptance/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -112,7 +112,7 @@ def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_poli # through a side-channel with Alice. # If Bob doesn't know about any Ursulas, he can't find the TreasureMap via the REST swarm: - with pytest.raises(bob.NotEnoughTeachers): + with pytest.raises(bob.NotEnoughNodes): treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp, enacted_federated_policy.label) diff --git a/tests/fixtures.py b/tests/fixtures.py index f94d65643..488ccce54 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -357,8 +357,9 @@ def blockchain_alice(alice_blockchain_test_config, testerchain): @pytest.fixture(scope="module") def federated_bob(bob_federated_test_config): - _bob = bob_federated_test_config.produce() - return _bob + bob = bob_federated_test_config.produce() + _d = bob.start_learning_loop() + return bob @pytest.fixture(scope="module") diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index db4546210..f7bcd4d60 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -24,9 +24,6 @@ import pytest_twisted from twisted.internet.threads import deferToThread from nucypher.characters.lawful import Ursula -from tests.performance_mocks import NotAPublicKey, NotARestApp, VerificationTracker, mock_cert_loading, \ - mock_cert_storage, mock_message_verification, mock_metadata_validation, mock_pubkey_from_bytes, mock_secret_source, \ - mock_signature_bytes, mock_stamp_call, mock_verify_node from tests.utils.middleware import SluggishLargeFleetMiddleware from umbral.keys import UmbralPublicKey from tests.mock.performance_mocks import ( @@ -170,7 +167,7 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, if policy.treasure_map in list(ursula.treasure_maps.values()): nodes_that_have_the_map_when_we_unblock.append(ursula) - approximate_number_of_nodes_we_expect_to_have_the_map_already = len(nodes_we_expect_to_have_the_map) / 10 + approximate_number_of_nodes_we_expect_to_have_the_map_already = len(nodes_we_expect_to_have_the_map) / 5 assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx( approximate_number_of_nodes_we_expect_to_have_the_map_already, .5) diff --git a/tests/integration/learning/test_learning_upgrade.py b/tests/integration/learning/test_learning_upgrade.py index 5d08bc314..f93d95a83 100644 --- a/tests/integration/learning/test_learning_upgrade.py +++ b/tests/integration/learning/test_learning_upgrade.py @@ -22,8 +22,8 @@ from bytestring_splitter import VariableLengthBytestring from eth_utils.address import to_checksum_address from twisted.logger import LogLevel, globalLogPublisher +from nucypher.acumen.nicknames import nickname_from_seed from nucypher.characters.base import Character -from nucypher.network.nicknames import nickname_from_seed from tests.utils.middleware import MockRestMiddleware from tests.utils.ursula import make_federated_ursulas diff --git a/tests/integration/network/test_failure_modes.py b/tests/integration/network/test_failure_modes.py index 1788b6d5f..fb5c2c6d6 100644 --- a/tests/integration/network/test_failure_modes.py +++ b/tests/integration/network/test_failure_modes.py @@ -44,7 +44,7 @@ def test_bob_does_not_let_a_connection_error_stop_him(enacted_federated_policy, federated_bob.network_middleware = NodeIsDownMiddleware() federated_bob.network_middleware.node_is_down(ursula1) - with pytest.raises(TreasureMap.NowhereToBeFound): + with pytest.raises(federated_bob.NotEnoughNodes): federated_bob.get_treasure_map(federated_alice.stamp, enacted_federated_policy.label) federated_bob.remember_node(ursula2) diff --git a/tests/integration/network/test_treasure_map_integration.py b/tests/integration/network/test_treasure_map_integration.py index 608cdf151..86933d6d5 100644 --- a/tests/integration/network/test_treasure_map_integration.py +++ b/tests/integration/network/test_treasure_map_integration.py @@ -40,6 +40,7 @@ def test_alice_sets_treasure_map(enacted_federated_policy, federated_ursulas): """ enacted_federated_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) + # TODO: Ensure that this is... an actual... hmm... treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map From 9d8af87680700992b6e93c79b305c865aad7a8e6 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 29 May 2020 18:11:19 -0700 Subject: [PATCH 066/167] Adding acumen to TOC so that the apidoc builder finds it. --- MANIFEST.in | 2 +- docs/source/index.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index e31be9d2c..924437825 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,4 +11,4 @@ global-exclude *.py[cod] recursive-include nucypher/blockchain/eth/contract_registry *.json *.md prune nucypher/blockchain/eth/contract_registry/historical recursive-include nucypher/network/templates *.html *.j2 -recursive-include nucypher/network/nicknames/ *json +recursive-include nucypher/acumen/ *json diff --git a/docs/source/index.rst b/docs/source/index.rst index a09f32890..232d4f057 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -166,6 +166,7 @@ Whitepapers api/nucypher.network api/nucypher.datastore api/nucypher.crypto + api/nucypher.acumen .. toctree:: :maxdepth: 1 From 2065262a02886d486d4fe102e0f56ae6d1f923ff Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 29 May 2020 19:52:48 -0700 Subject: [PATCH 067/167] Setting checksum_address to staker address, and looking up TransactingPower in the usual (albeit private) way. --- nucypher/characters/lawful.py | 2 +- tests/utils/ursula.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index e6cd716a4..deab60486 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1049,7 +1049,7 @@ class Ursula(Teacher, Character, Worker): signer=self.signer, cache=True) self._crypto_power.consume_power_up(_transacting_power) - self._set_checksum_address(_transacting_power.account) + self._set_checksum_address(checksum_address) # Use this power to substantiate the stamp self.substantiate_stamp() diff --git a/tests/utils/ursula.py b/tests/utils/ursula.py index 97c0d325d..449afb4da 100644 --- a/tests/utils/ursula.py +++ b/tests/utils/ursula.py @@ -27,6 +27,7 @@ from nucypher.blockchain.eth.actors import Staker from nucypher.blockchain.eth.interfaces import BlockchainInterface from nucypher.characters.lawful import Ursula from nucypher.config.characters import UrsulaConfiguration +from nucypher.crypto.powers import TransactingPower from tests.constants import ( MOCK_URSULA_DB_FILEPATH, NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK @@ -107,7 +108,10 @@ def make_decentralized_ursulas(ursula_config: UrsulaConfiguration, rest_port=port + 100, **ursula_overrides) if commit_to_next_period: - ursula.transacting_power.activate() + # TODO: Is _crypto_power trying to be public? Or is there a way to expose *something* public about TransactingPower? + # Do we need to revisit the concept of "public material"? Or does this rightly belong as a method? + tx_power = ursula._crypto_power.power_ups(TransactingPower) + tx_power.activate() ursula.commit_to_next_period() ursulas.append(ursula) From d4ec70115ebd5318c3a11ddbff47b568d5c9e002 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 9 Jun 2020 21:30:51 -0700 Subject: [PATCH 068/167] It's also now possible for checksum_address to be NO_BLOCKCHAIN_CONNECTION at this point. --- nucypher/blockchain/eth/decorators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nucypher/blockchain/eth/decorators.py b/nucypher/blockchain/eth/decorators.py index 2f22b423a..b5ddcb926 100644 --- a/nucypher/blockchain/eth/decorators.py +++ b/nucypher/blockchain/eth/decorators.py @@ -22,7 +22,8 @@ from constant_sorrow.constants import ( CONTRACT_ATTRIBUTE, CONTRACT_CALL, TRANSACTION, - UNKNOWN_CONTRACT_INTERFACE + UNKNOWN_CONTRACT_INTERFACE, + NO_BLOCKCHAIN_CONNECTION ) from datetime import datetime from twisted.logger import Logger @@ -80,7 +81,7 @@ def validate_checksum_address(func: Callable) -> Callable: signature = inspect.signature(func) parameter_is_optional = signature.parameters[parameter_name].default is None - if parameter_is_optional and checksum_address is None: + if parameter_is_optional and checksum_address is None or checksum_address is NO_BLOCKCHAIN_CONNECTION: continue address_is_valid = eth_utils.is_checksum_address(checksum_address) From 8697a22047294776ed2f8d1e36a8fc56bbeaca3f Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 10 Jun 2020 16:13:15 -0700 Subject: [PATCH 069/167] No need to catch this exception anymore; let it raise. --- nucypher/network/nodes.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index be5ef4128..cc539e5c4 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -637,11 +637,7 @@ class Learner: """ self._learning_round += 1 - try: - current_teacher = self.current_teacher_node() - except self.NotEnoughTeachers as e: - self.log.warn("Can't learn right now: {}".format(e.args[0])) - return + current_teacher = self.current_teacher_node() # Will raise if there's no available teacher. if Teacher in self.__class__.__bases__: announce_nodes = [self] From 1b5917d72838b935ffbd629637aad1c440569e36 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 10 Jun 2020 16:13:44 -0700 Subject: [PATCH 070/167] Clarifying failure message; this makes it easier to understand why this test occasionally fails. --- tests/integration/network/test_treasure_map_integration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/network/test_treasure_map_integration.py b/tests/integration/network/test_treasure_map_integration.py index fde9af5ab..f8efd4c3e 100644 --- a/tests/integration/network/test_treasure_map_integration.py +++ b/tests/integration/network/test_treasure_map_integration.py @@ -80,6 +80,7 @@ def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_poli enacted_federated_policy.label) # Bob finds out about one Ursula (in the real world, a seed node) + # TODO: Real seed-node logic here? bob.remember_node(list(federated_ursulas)[0]) # ...and then learns about the rest of the network. @@ -97,7 +98,8 @@ def test_treasure_map_is_legit(enacted_federated_policy): Sure, the TreasureMap can get to Bob, but we also need to know that each Ursula in the TreasureMap is on the network. """ for ursula_address, _node_id in enacted_federated_policy.treasure_map: - assert ursula_address in enacted_federated_policy.bob.known_nodes.addresses() + if ursula_address not in enacted_federated_policy.bob.known_nodes.addresses(): + pytest.fail(f"Bob didn't know about {ursula_address}") def test_alice_does_not_update_with_old_ursula_info(federated_alice, federated_ursulas): From b3ac5a6e3e0247e3d019b8754f29fac12970e120 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 10 Jun 2020 16:14:20 -0700 Subject: [PATCH 071/167] If the port is too high, try again. Attempting to deal with #1546. --- tests/utils/ursula.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/ursula.py b/tests/utils/ursula.py index 449afb4da..31b006d5c 100644 --- a/tests/utils/ursula.py +++ b/tests/utils/ursula.py @@ -47,7 +47,7 @@ def select_test_port() -> int: open_socket.bind(('localhost', 0)) port = open_socket.getsockname()[1] - if port == UrsulaConfiguration.DEFAULT_REST_PORT: + if port == UrsulaConfiguration.DEFAULT_REST_PORT or port > 64000: return select_test_port() open_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) From fd086458eb791b8f28471680fa2fd338f37af2d4 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 11 Jun 2020 14:12:34 -0700 Subject: [PATCH 072/167] Matching node floor is now 7; this will have no impact in production, but makes the tests pass. --- nucypher/characters/lawful.py | 7 ++++--- nucypher/policy/policies.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index deab60486..742b3f55c 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -870,7 +870,7 @@ class Bob(Character): def matching_nodes_among(self, nodes: FleetSensor, - no_less_than=7): + no_less_than=7): # Somewhat arbitrary floor here. # Look for nodes whose checksum address has the second character of Bob's encrypting key in the first # few characters. # Think of it as a cheap knockoff hamming distance. @@ -886,12 +886,13 @@ class Bob(Character): search_boundary = 2 target_nodes = [] target_hex_match = self.public_keys(DecryptingPower).hex()[1] - while len(target_nodes) < no_less_than: # Arbitrary floor. Is 8 good? + while len(target_nodes) < no_less_than: target_nodes = [] search_boundary += 2 - if search_boundary > 42: + if search_boundary > 42: # We've searched the entire string and can't match any. TODO: Portable learning is a nice idea here. raise self.NotEnoughNodes + # TODO: 1995 all throughout here (we might not (need to) know the checksum address yet; canonical will do.) # This might be a performance issue above a few thousand nodes. target_nodes = [node for node in nodes if target_hex_match in node.checksum_address[2:search_boundary]] diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 4cedd40e6..a42940d59 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -300,7 +300,7 @@ class Policy(ABC): treasure_map_id = self.treasure_map.public_id() self.alice.block_until_number_of_known_nodes_is(8, timeout=2, learn_on_this_thread=True) - for node in self.bob.matching_nodes_among(self.alice.known_nodes, no_less_than=8): + for node in self.bob.matching_nodes_among(self.alice.known_nodes): responses.append(deferToThreadPool(reactor, self.alice.publication_threadpool, network_middleware.put_treasure_map_on_node, From 263aa1fab50866d8eec1a6d01b733e692cb419f8 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 11 Jun 2020 15:05:24 -0700 Subject: [PATCH 073/167] The first node in the fixture might not always be among the matching nodes. --- tests/integration/network/test_treasure_map_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/network/test_treasure_map_integration.py b/tests/integration/network/test_treasure_map_integration.py index f8efd4c3e..6f433e0e4 100644 --- a/tests/integration/network/test_treasure_map_integration.py +++ b/tests/integration/network/test_treasure_map_integration.py @@ -55,7 +55,7 @@ def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alic """ treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) - treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] + treasure_map_as_set_on_network = federated_bob.matching_nodes_among(federated_ursulas)[0].treasure_maps[treasure_map_index] hrac_by_bob = federated_bob.construct_policy_hrac(federated_alice.stamp, enacted_federated_policy.label) assert enacted_federated_policy.hrac() == hrac_by_bob From a9c1763e1b75ccf9c61c04f691e601b4c1ce926e Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 22 Jun 2020 21:09:04 -0700 Subject: [PATCH 074/167] Exception tuple needs to be unpacked here; TypeError otherwise. --- nucypher/characters/lawful.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 742b3f55c..fcb033eea 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -602,7 +602,7 @@ class Bob(Character): for node in nodes_with_map: try: response = network_middleware.get_treasure_map_from_node(node=node, map_id=map_id) - except (NodeSeemsToBeDown, self.NotEnoughNodes): + except (*NodeSeemsToBeDown, self.NotEnoughNodes): continue except network_middleware.NotFound: self.log.info(f"Node {node} claimed not to have TreasureMap {map_id}") From b491a613a19aacdff9528e4c7056d36f787edc21 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 22 Jun 2020 21:19:20 -0700 Subject: [PATCH 075/167] Allow other Characters to be --lonely. --- nucypher/cli/commands/alice.py | 10 +++++++--- nucypher/cli/commands/ursula.py | 1 - nucypher/cli/options.py | 1 + nucypher/network/nodes.py | 3 ++- tests/acceptance/cli/test_alice.py | 4 ++-- tests/acceptance/cli/ursula/test_federated_ursula.py | 1 + 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/nucypher/cli/commands/alice.py b/nucypher/cli/commands/alice.py index 5861542ac..5ca4dff20 100644 --- a/nucypher/cli/commands/alice.py +++ b/nucypher/cli/commands/alice.py @@ -52,7 +52,7 @@ from nucypher.cli.options import ( option_provider_uri, option_registry_filepath, option_signer_uri, - option_teacher_uri + option_teacher_uri, option_lonely ) from nucypher.cli.painting.help import paint_new_installation_help from nucypher.cli.processes import get_geth_provider_process @@ -89,7 +89,8 @@ class AliceConfigOptions: registry_filepath: str, middleware: RestMiddleware, gas_strategy: str, - signer_uri: str + signer_uri: str, + lonely: bool, ): if federated_only and geth: @@ -115,6 +116,7 @@ class AliceConfigOptions: self.discovery_port = discovery_port self.registry_filepath = registry_filepath self.middleware = middleware + self.lonely = lonely def create_config(self, emitter, config_file): @@ -172,6 +174,7 @@ group_config_options = group_options( pay_with=option_pay_with, registry_filepath=option_registry_filepath, middleware=option_middleware, + lonely=option_lonely, ) @@ -286,7 +289,8 @@ class AliceCharacterOptions: min_stake=self.min_stake, client_password=client_password, load_preferred_teachers=load_seednodes, - start_learning_now=load_seednodes) + start_learning_now=load_seednodes, + lonely=self.config_options.lonely) return ALICE except NucypherKeyring.AuthenticationFailed as e: diff --git a/nucypher/cli/commands/ursula.py b/nucypher/cli/commands/ursula.py index e42bf0ed4..714d7a97c 100644 --- a/nucypher/cli/commands/ursula.py +++ b/nucypher/cli/commands/ursula.py @@ -294,7 +294,6 @@ class UrsulaCharacterOptions: group_character_options = group_options( UrsulaCharacterOptions, config_options=group_config_options, - lonely=click.option('--lonely', help="Do not connect to seednodes", is_flag=True), teacher_uri=option_teacher_uri, min_stake=option_min_stake ) diff --git a/nucypher/cli/options.py b/nucypher/cli/options.py index 0d17660e7..f4decc6fb 100644 --- a/nucypher/cli/options.py +++ b/nucypher/cli/options.py @@ -46,6 +46,7 @@ option_force = click.option('--force', help="Don't ask for confirmation", is_fla option_geth = click.option('--geth', '-G', help="Run using the built-in geth node", is_flag=True) option_hw_wallet = click.option('--hw-wallet/--no-hw-wallet') option_light = click.option('--light', help="Indicate that node is light", is_flag=True, default=None) +option_lonely = click.option('--lonely', help="Do not connect to seednodes", is_flag=True) option_m = click.option('--m', help="M-Threshold KFrags", type=click.INT) option_min_stake = click.option('--min-stake', help="The minimum stake the teacher must have to be a teacher", type=click.INT, default=0) option_n = click.option('--n', help="N-Total KFrags", type=click.INT) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index cc539e5c4..4be0057e7 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -227,7 +227,8 @@ class Learner: self._seed_nodes = seed_nodes or [] self.unresponsive_seed_nodes = set() - if self.start_learning_now: + if self.start_learning_now and not self.lonely: + self.load_seednodes() self.start_learning_loop(now=self.learn_on_same_thread) @property diff --git a/tests/acceptance/cli/test_alice.py b/tests/acceptance/cli/test_alice.py index 93e3c8209..252390650 100644 --- a/tests/acceptance/cli/test_alice.py +++ b/tests/acceptance/cli/test_alice.py @@ -73,7 +73,7 @@ def test_alice_control_starts_with_mocked_keyring(click_runner, mocker, monkeypa mocker.patch.object(AliceConfiguration, "attach_keyring", return_value=None) good_enough_config = AliceConfiguration(dev_mode=True, federated_only=True, keyring=MockKeyring) mocker.patch.object(AliceConfiguration, "from_configuration_file", return_value=good_enough_config) - init_args = ('alice', 'run', '-x', '--network', TEMPORARY_DOMAIN) + init_args = ('alice', 'run', '-x', '--lonely', '--network', TEMPORARY_DOMAIN) result = click_runner.invoke(nucypher_cli, init_args, input=FAKE_PASSWORD_CONFIRMED) assert result.exit_code == 0, result.exception @@ -110,7 +110,7 @@ def test_initialize_alice_with_custom_configuration_root(custom_filepath, click_ def test_alice_control_starts_with_preexisting_configuration(click_runner, custom_filepath): custom_config_filepath = os.path.join(custom_filepath, AliceConfiguration.generate_filename()) - run_args = ('alice', 'run', '--dry-run', '--config-file', custom_config_filepath) + run_args = ('alice', 'run', '--dry-run', '--lonely', '--config-file', custom_config_filepath) result = click_runner.invoke(nucypher_cli, run_args, input=FAKE_PASSWORD_CONFIRMED) assert result.exit_code == 0 diff --git a/tests/acceptance/cli/ursula/test_federated_ursula.py b/tests/acceptance/cli/ursula/test_federated_ursula.py index c2258fc99..9da01ebdb 100644 --- a/tests/acceptance/cli/ursula/test_federated_ursula.py +++ b/tests/acceptance/cli/ursula/test_federated_ursula.py @@ -139,6 +139,7 @@ def test_run_federated_ursula_from_config_file(custom_filepath, click_runner): run_args = ('ursula', 'run', '--dry-run', '--interactive', + '--lonely', '--config-file', custom_config_filepath) result = click_runner.invoke(nucypher_cli, run_args, From 65607953dcf562434e8b0e15d1a511e1a113aad5 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 02:20:18 -0700 Subject: [PATCH 076/167] All sorts of out-comments to get the lonely thing kickin'. --- nucypher/characters/lawful.py | 8 ++++---- nucypher/cli/utils.py | 17 +++++++++-------- nucypher/network/nodes.py | 21 ++++++++++++++------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index fcb033eea..5505c2ed8 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1180,10 +1180,10 @@ class Ursula(Teacher, Character, Worker): if emitter: emitter.message(f"✓ Database pruning", color='green') - if learning: - self.start_learning_loop(now=self._start_learning_now) - if emitter: - emitter.message(f"✓ Node Discovery ({','.join(self.learning_domains)})", color='green') + # if learning: + # self.start_learning_loop(now=self._start_learning_now) + # if emitter: + # emitter.message(f"✓ Node Discovery ({','.join(self.learning_domains)})", color='green') if self._availability_check and availability: self._availability_tracker.start(now=False) # wait... diff --git a/nucypher/cli/utils.py b/nucypher/cli/utils.py index 0de8e7197..f8b9bd3d6 100644 --- a/nucypher/cli/utils.py +++ b/nucypher/cli/utils.py @@ -73,15 +73,16 @@ def make_cli_character(character_config, password=get_nucypher_password(confirm=False)) # Handle Teachers + # TODO: Is this still relevant? Is it better to DRY this up by doing it later? teacher_nodes = list() - if load_preferred_teachers: - teacher_nodes = load_seednodes(emitter, - teacher_uris=[teacher_uri] if teacher_uri else None, - min_stake=min_stake, - federated_only=character_config.federated_only, - network_domains=character_config.domains, - network_middleware=character_config.network_middleware, - registry=character_config.registry) + # if load_preferred_teachers: + # teacher_nodes = load_seednodes(emitter, + # teacher_uris=[teacher_uri] if teacher_uri else None, + # min_stake=min_stake, + # federated_only=character_config.federated_only, + # network_domains=character_config.domains, + # network_middleware=character_config.network_middleware, + # registry=character_config.registry) # # Character Init diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 4be0057e7..73a10d842 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -354,19 +354,26 @@ class Learner: elif now: self.log.info("Starting Learning Loop NOW.") - if self.lonely: - self.done_seeding = True - self.read_nodes_from_storage() + # if self.lonely: + # self.done_seeding = True + # self.read_nodes_from_storage() + # + # else: + # self.load_seednodes() + try: + self.learn_from_teacher_node() + except self.NotEnoughTeachers: + if self.lonely: + assert False + else: + assert False - else: - self.load_seednodes() - - self.learn_from_teacher_node() self.learning_deferred = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY) self.learning_deferred.addErrback(self.handle_learning_errors) return self.learning_deferred else: self.log.info("Starting Learning Loop.") + self.cycle_teacher_node() learning_deferreds = list() if not self.lonely: From 61b87f93bfac13cb89d1640fa7403f443d6c0d67 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 02:27:24 -0700 Subject: [PATCH 077/167] Getting --lonely through each Character where it's used. --- nucypher/cli/commands/alice.py | 3 ++- nucypher/cli/commands/bob.py | 14 ++++++++++---- nucypher/cli/commands/ursula.py | 18 +++++++++++------- tests/acceptance/cli/test_bob.py | 4 ++-- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/nucypher/cli/commands/alice.py b/nucypher/cli/commands/alice.py index 5ca4dff20..83e3ee055 100644 --- a/nucypher/cli/commands/alice.py +++ b/nucypher/cli/commands/alice.py @@ -52,7 +52,8 @@ from nucypher.cli.options import ( option_provider_uri, option_registry_filepath, option_signer_uri, - option_teacher_uri, option_lonely + option_teacher_uri, + option_lonely ) from nucypher.cli.painting.help import paint_new_installation_help from nucypher.cli.processes import get_geth_provider_process diff --git a/nucypher/cli/commands/bob.py b/nucypher/cli/commands/bob.py index c67b1dd2a..09c70584b 100644 --- a/nucypher/cli/commands/bob.py +++ b/nucypher/cli/commands/bob.py @@ -47,7 +47,8 @@ from nucypher.cli.options import ( option_provider_uri, option_registry_filepath, option_signer_uri, - option_teacher_uri + option_teacher_uri, + option_lonely ) from nucypher.cli.painting.help import paint_new_installation_help from nucypher.cli.utils import make_cli_character, setup_emitter @@ -71,7 +72,9 @@ class BobConfigOptions: middleware: RestMiddleware, federated_only: bool, gas_strategy: str, - signer_uri: str): + signer_uri: str, + lonely: bool, + ): self.provider_uri = provider_uri self.signer_uri = signer_uri @@ -83,6 +86,7 @@ class BobConfigOptions: self.dev = dev self.middleware = middleware self.federated_only = federated_only + self.lonely = lonely def create_config(self, emitter: StdoutEmitter, config_file: str) -> BobConfiguration: if self.dev: @@ -158,7 +162,8 @@ group_config_options = group_options( discovery_port=option_discovery_port(), dev=option_dev, middleware=option_middleware, - federated_only=option_federated_only + federated_only=option_federated_only, + lonely=option_lonely, ) @@ -177,7 +182,8 @@ class BobCharacterOptions: emitter=emitter, unlock_keyring=not self.config_options.dev, teacher_uri=self.teacher_uri, - min_stake=self.min_stake) + min_stake=self.min_stake, + lonely=self.config_options.lonely) group_character_options = group_options( diff --git a/nucypher/cli/commands/ursula.py b/nucypher/cli/commands/ursula.py index 714d7a97c..1e1f7b6c7 100644 --- a/nucypher/cli/commands/ursula.py +++ b/nucypher/cli/commands/ursula.py @@ -58,7 +58,8 @@ from nucypher.cli.options import ( option_provider_uri, option_registry_filepath, option_signer_uri, - option_teacher_uri + option_teacher_uri, + option_lonely ) from nucypher.cli.painting.help import paint_new_installation_help from nucypher.cli.painting.transactions import paint_receipt_summary @@ -94,7 +95,9 @@ class UrsulaConfigOptions: light, gas_strategy, signer_uri, - availability_check): + availability_check, + lonely: bool + ): if federated_only: if geth: @@ -125,6 +128,7 @@ class UrsulaConfigOptions: self.light = light self.gas_strategy = gas_strategy self.availability_check = availability_check + self.lonely = lonely def create_config(self, emitter, config_file): if self.dev: @@ -246,7 +250,8 @@ group_config_options = group_options( poa=option_poa, light=option_light, dev=option_dev, - availability_check=click.option('--availability-check/--disable-availability-check', help="Enable or disable self-health checks while running", is_flag=True, default=None) + availability_check=click.option('--availability-check/--disable-availability-check', help="Enable or disable self-health checks while running", is_flag=True, default=None), + lonely=option_lonely, ) @@ -254,9 +259,8 @@ class UrsulaCharacterOptions: __option_name__ = 'character_options' - def __init__(self, config_options: UrsulaConfigOptions, lonely, teacher_uri, min_stake): + def __init__(self, config_options: UrsulaConfigOptions, teacher_uri, min_stake): self.config_options = config_options - self.lonely = lonely self.teacher_uri = teacher_uri self.min_stake = min_stake @@ -279,9 +283,9 @@ class UrsulaCharacterOptions: min_stake=self.min_stake, teacher_uri=self.teacher_uri, unlock_keyring=not self.config_options.dev, - lonely=self.lonely, + lonely=self.config_options.lonely, client_password=client_password, - load_preferred_teachers=load_seednodes and not self.lonely, + # load_preferred_teachers=load_seednodes and not self.lonely, start_learning_now=load_seednodes) return ursula_config, URSULA diff --git a/tests/acceptance/cli/test_bob.py b/tests/acceptance/cli/test_bob.py index 71ecf2fc9..f1b99edf7 100644 --- a/tests/acceptance/cli/test_bob.py +++ b/tests/acceptance/cli/test_bob.py @@ -88,7 +88,7 @@ def test_initialize_bob_with_custom_configuration_root(custom_filepath, click_ru def test_bob_control_starts_with_preexisting_configuration(click_runner, custom_filepath): custom_config_filepath = os.path.join(custom_filepath, BobConfiguration.generate_filename()) - init_args = ('bob', 'run', '--dry-run', '--config-file', custom_config_filepath) + init_args = ('bob', 'run', '--dry-run', '--lonely', '--config-file', custom_config_filepath) result = click_runner.invoke(nucypher_cli, init_args, input=FAKE_PASSWORD_CONFIRMED) assert result.exit_code == 0, result.exception assert "Bob Verifying Key" in result.output @@ -107,7 +107,7 @@ def test_bob_view_with_preexisting_configuration(click_runner, custom_filepath): def test_bob_public_keys(click_runner): - derive_key_args = ('bob', 'public-keys', '--dev') + derive_key_args = ('bob', 'public-keys', '--lonely', '--dev') result = click_runner.invoke(nucypher_cli, derive_key_args, catch_exceptions=False) assert result.exit_code == 0 assert "bob_encrypting_key" in result.output From 4ca6688a8d7e6b86c4a68770b6aa4813cdb3daa7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 02:30:02 -0700 Subject: [PATCH 078/167] Moving hardcoded seednodes to middleware. --- nucypher/network/middleware.py | 5 +++++ nucypher/network/teachers.py | 21 --------------------- tests/utils/middleware.py | 6 ++++++ 3 files changed, 11 insertions(+), 21 deletions(-) delete mode 100644 nucypher/network/teachers.py diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py index bb66ddd8d..f5ec43e78 100644 --- a/nucypher/network/middleware.py +++ b/nucypher/network/middleware.py @@ -35,6 +35,7 @@ class NucypherMiddlewareClient: library = requests timeout = 1.2 + def __init__(self, registry=None, *args, **kwargs): self.registry = registry @@ -143,6 +144,10 @@ class RestMiddleware: _client_class = NucypherMiddlewareClient + TEACHER_NODES = { + 'ibex': ('https://ibex.nucypher.network:9151',), + } + class UnexpectedResponse(Exception): def __init__(self, message, status, *args, **kwargs): super().__init__(message, *args, **kwargs) diff --git a/nucypher/network/teachers.py b/nucypher/network/teachers.py deleted file mode 100644 index eca95cba7..000000000 --- a/nucypher/network/teachers.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -This file is part of nucypher. - -nucypher is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -nucypher is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with nucypher. If not, see . -""" - -# Hardcoded bootstrapping teacher nodes keyed by network domain -TEACHER_NODES = { - 'ibex': ('https://ibex.nucypher.network:9151', ), -} diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 17ab9734d..da6d730da 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -89,6 +89,12 @@ class MockRestMiddleware(RestMiddleware): class NotEnoughMockUrsulas(Ursula.NotEnoughUrsulas): pass + class TEACHER_NODES: + + @classmethod + def get(_cls, item, default): + return tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values()) + def get_certificate(self, host, port, timeout=3, retry_attempts: int = 3, retry_rate: int = 2, current_attempt: int = 0): ursula = self.client._get_ursula_by_port(port) From 3c58a0ce60b7577d3b4506e0c0cd23421a032072 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 02:32:01 -0700 Subject: [PATCH 079/167] Moving seednode loading logic inward. --- nucypher/network/nodes.py | 20 ++++++++++++++++++-- nucypher/utilities/seednodes.py | 4 +--- tests/acceptance/cli/test_cli_lifecycle.py | 3 ++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 73a10d842..c5d3024a1 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -238,12 +238,28 @@ class Learner: def load_seednodes(self, read_storage: bool = True, retry_attempts: int = 3): """ Engage known nodes from storages and pre-fetch hardcoded seednode certificates for node learning. + + TODO: Dehydrate this with nucypher.utilities.seednodes.load_seednodes """ if self.done_seeding: self.log.debug("Already done seeding; won't try again.") return - + from nucypher.utilities.seednodes import aggregate_seednode_uris # TODO: Ugh. + # teacher_uris = aggregate_seednode_uris(domains=self.learning_domains) + canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(tuple(self.learning_domains)[0], ()) # TODO: Are we done with multiple domains? + # TODO: Is this better as a sprout? from nucypher.characters.lawful import Ursula + ############################ + for uri in canonical_sage_uris: + # Not catching any errors here; we want to fail fast if there are bad hardcoded teachers. + # This is essentially an __active-fire__ if it's anything but a fleeting one-off. + sage_node = Ursula.from_teacher_uri(teacher_uri=uri, + min_stake=0, # TODO: Where to get this? + federated_only=self.federated_only, + network_middleware=self.network_middleware, + registry=self.registry) + self.remember_node(sage_node) + ################ for seednode_metadata in self._seed_nodes: self.log.debug( @@ -275,7 +291,7 @@ class Learner: def read_nodes_from_storage(self) -> None: stored_nodes = self.node_storage.all(federated_only=self.federated_only) # TODO: #466 for node in stored_nodes: - self.remember_node(node) + self.remember_node(node) # TODO: Validity status 1866 def remember_node(self, node, diff --git a/nucypher/utilities/seednodes.py b/nucypher/utilities/seednodes.py index e335cd4f8..be9bbfa9a 100644 --- a/nucypher/utilities/seednodes.py +++ b/nucypher/utilities/seednodes.py @@ -33,8 +33,6 @@ from nucypher.cli.literature import ( from nucypher.config.constants import DEFAULT_CONFIG_ROOT from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware -from nucypher.network.nodes import Teacher -from nucypher.network.teachers import TEACHER_NODES def load_static_nodes(domains: Set[str], filepath: Optional[str] = None) -> Dict[str, 'Ursula']: @@ -118,7 +116,7 @@ def load_seednodes(emitter, except NodeSeemsToBeDown: emitter.message(UNREADABLE_SEEDNODE_ADVISORY.format(uri=uri)) continue - except Teacher.NotStaking: + except Ursula.NotStaking: emitter.message(SEEDNODE_NOT_STAKING_WARNING.format(uri=uri)) continue teacher_nodes.append(teacher_node) diff --git a/tests/acceptance/cli/test_cli_lifecycle.py b/tests/acceptance/cli/test_cli_lifecycle.py index 63d997b31..babc4247b 100644 --- a/tests/acceptance/cli/test_cli_lifecycle.py +++ b/tests/acceptance/cli/test_cli_lifecycle.py @@ -213,6 +213,7 @@ def _cli_lifecycle(click_runner, bob_configuration_file_location = os.path.join(bob_config_root, BobConfiguration.generate_filename()) bob_view_args = ('bob', 'public-keys', '--json-ipc', + '--mock-networking', # TODO: It's absurd for this public-keys command to connect at all. 1710 '--config-file', bob_configuration_file_location) bob_view_result = click_runner.invoke(nucypher_cli, bob_view_args, catch_exceptions=False, env=envvars) @@ -226,7 +227,7 @@ def _cli_lifecycle(click_runner, side_channel.save_bob_public_keys(bob_public_keys) """ - Scene 3: Alice derives a policy keypair, and saves it's public key to a sidechannel. + Scene 3: Alice derives a policy keypair, and saves its public key to a sidechannel. """ random_label = random_policy_label.decode() # Unicode string From e845dc3b5b5d8b2530f6d352e91f6287ecdad002 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 02:32:33 -0700 Subject: [PATCH 080/167] We're not connecting to seednodes at this stage anymore. --- tests/acceptance/cli/ursula/test_federated_ursula.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/cli/ursula/test_federated_ursula.py b/tests/acceptance/cli/ursula/test_federated_ursula.py index 9da01ebdb..a7e1fa505 100644 --- a/tests/acceptance/cli/ursula/test_federated_ursula.py +++ b/tests/acceptance/cli/ursula/test_federated_ursula.py @@ -149,7 +149,7 @@ def test_run_federated_ursula_from_config_file(custom_filepath, click_runner): # CLI Output assert result.exit_code == 0 assert 'Federated' in result.output, 'WARNING: Federated ursula is not running in federated mode' - assert 'Connecting' in result.output + # assert 'Connecting' in result.output assert 'Running' in result.output assert "'help' or '?'" in result.output From f708b77a572b9276d1495dc4fe1629ac02a658c0 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 02:33:40 -0700 Subject: [PATCH 081/167] Going back to defined ports in the hopes of working out #1546 / #1689. --- tests/utils/ursula.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/ursula.py b/tests/utils/ursula.py index 31b006d5c..ea1fed925 100644 --- a/tests/utils/ursula.py +++ b/tests/utils/ursula.py @@ -164,4 +164,4 @@ def start_pytest_ursula_services(ursula: Ursula) -> Certificate: MOCK_KNOWN_URSULAS_CACHE = dict() -MOCK_URSULA_STARTING_PORT = select_test_port() +MOCK_URSULA_STARTING_PORT = 51000 # select_test_port() From b277436cc44e7b973874487039118df6ec8842ff Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 15:57:01 -0700 Subject: [PATCH 082/167] Note here that this fails sometimes. Perhaps needs to become an Issue. --- nucypher/characters/lawful.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 5505c2ed8..2998c0369 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -891,6 +891,7 @@ class Bob(Character): search_boundary += 2 if search_boundary > 42: # We've searched the entire string and can't match any. TODO: Portable learning is a nice idea here. + # TODO: This fails in tests once in a great while because there aren't matching nodes among the few test nodes. Not sure what to do. raise self.NotEnoughNodes # TODO: 1995 all throughout here (we might not (need to) know the checksum address yet; canonical will do.) From 03a4bcd75258118ca5b450f4ac4ae7365b302a5b Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 15:58:49 -0700 Subject: [PATCH 083/167] Moving lonely up to the main config instead of each Character config. --- nucypher/cli/commands/alice.py | 2 -- nucypher/cli/commands/bob.py | 5 +---- nucypher/config/node.py | 3 +++ 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/nucypher/cli/commands/alice.py b/nucypher/cli/commands/alice.py index 83e3ee055..d6d16623c 100644 --- a/nucypher/cli/commands/alice.py +++ b/nucypher/cli/commands/alice.py @@ -91,7 +91,6 @@ class AliceConfigOptions: middleware: RestMiddleware, gas_strategy: str, signer_uri: str, - lonely: bool, ): if federated_only and geth: @@ -117,7 +116,6 @@ class AliceConfigOptions: self.discovery_port = discovery_port self.registry_filepath = registry_filepath self.middleware = middleware - self.lonely = lonely def create_config(self, emitter, config_file): diff --git a/nucypher/cli/commands/bob.py b/nucypher/cli/commands/bob.py index 09c70584b..24a714bd5 100644 --- a/nucypher/cli/commands/bob.py +++ b/nucypher/cli/commands/bob.py @@ -73,7 +73,6 @@ class BobConfigOptions: federated_only: bool, gas_strategy: str, signer_uri: str, - lonely: bool, ): self.provider_uri = provider_uri @@ -86,7 +85,6 @@ class BobConfigOptions: self.dev = dev self.middleware = middleware self.federated_only = federated_only - self.lonely = lonely def create_config(self, emitter: StdoutEmitter, config_file: str) -> BobConfiguration: if self.dev: @@ -182,8 +180,7 @@ class BobCharacterOptions: emitter=emitter, unlock_keyring=not self.config_options.dev, teacher_uri=self.teacher_uri, - min_stake=self.min_stake, - lonely=self.config_options.lonely) + min_stake=self.min_stake) group_character_options = group_options( diff --git a/nucypher/config/node.py b/nucypher/config/node.py index 9f53e4154..73f84e6dc 100644 --- a/nucypher/config/node.py +++ b/nucypher/config/node.py @@ -93,6 +93,7 @@ class CharacterConfiguration(BaseConfiguration): domains: Set[str] = None, # TODO: Mapping between learning domains and "registry" domains - #1580 interface_signature: Signature = None, network_middleware: RestMiddleware = None, + lonely: bool = False, # Node Storage known_nodes: set = None, @@ -152,6 +153,7 @@ class CharacterConfiguration(BaseConfiguration): self.save_metadata = save_metadata self.reload_metadata = reload_metadata self.known_nodes = known_nodes or set() # handpicked + self.lonely = lonely # Configuration self.__dev_mode = dev_mode @@ -400,6 +402,7 @@ class CharacterConfiguration(BaseConfiguration): start_learning_now=self.start_learning_now, save_metadata=self.save_metadata, node_storage=self.node_storage.payload(), + lonely=self.lonely, ) # Optional values (mode) From e8e3e9ab4e004ce4f931cbb97a292c673e50b681 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 15:59:11 -0700 Subject: [PATCH 084/167] Fixing up fixtures to fail fast and delete Ursulas when done. --- tests/fixtures.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 488ccce54..538240d96 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -110,7 +110,8 @@ from tests.utils.config import ( ) from tests.utils.middleware import MockRestMiddleware, MockRestMiddlewareForLargeFleetTests from tests.utils.policy import generate_random_label -from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, make_decentralized_ursulas, make_federated_ursulas +from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, make_decentralized_ursulas, make_federated_ursulas, \ + MOCK_KNOWN_URSULAS_CACHE test_logger = Logger("test-logger") @@ -358,7 +359,6 @@ def blockchain_alice(alice_blockchain_test_config, testerchain): @pytest.fixture(scope="module") def federated_bob(bob_federated_test_config): bob = bob_federated_test_config.produce() - _d = bob.start_learning_loop() return bob @@ -374,6 +374,9 @@ def federated_ursulas(ursula_federated_test_config): quantity=NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK) yield _ursulas + for ursula in _ursulas: + del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] + # # Blockchain From 33f71f6e5217eee8b45ff55e2d7349684c3e66d7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 15:59:39 -0700 Subject: [PATCH 085/167] The number of known nodes might be a bit variable at this point; we're really concerned that he has only connected to 2. --- .../characters/test_bob_joins_policy_and_retrieves.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py index c8dc920b7..cfa862b20 100644 --- a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py +++ b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py @@ -74,8 +74,8 @@ def test_bob_joins_policy_and_retrieves(federated_alice, known_nodes=a_couple_of_ursulas, ) - # Bob only knows a couple of Ursulas initially - assert len(bob.known_nodes) == 2 + # Bob has only connected to 2 nodes. + assert sum(node.verified_node for node in bob.known_nodes) == 2 # Alice creates a policy granting access to Bob # Just for fun, let's assume she distributes KFrags among Ursulas unknown to Bob From 935ac06d2cced786f1fc31f827cfa305a0cb3638 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 16:00:05 -0700 Subject: [PATCH 086/167] A Character created from blank config will try to connect to seednodes without lonely. --- tests/integration/config/test_character_configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/config/test_character_configuration.py b/tests/integration/config/test_character_configuration.py index c6612c9f0..5baf0576e 100644 --- a/tests/integration/config/test_character_configuration.py +++ b/tests/integration/config/test_character_configuration.py @@ -54,7 +54,7 @@ all_configurations = tuple(configurations + blockchain_only_configurations) @pytest.mark.parametrize("character,configuration", characters_and_configurations) def test_federated_development_character_configurations(character, configuration): - config = configuration(dev_mode=True, federated_only=True, domains={TEMPORARY_DOMAIN}) + config = configuration(dev_mode=True, federated_only=True, lonely=True, domains={TEMPORARY_DOMAIN}) assert config.is_me is True assert config.dev_mode is True assert config.keyring == NO_KEYRING_ATTACHED From 1d75ced0459f777b0fc76dc4a7fc70d9cb060154 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 23 Jun 2020 16:00:35 -0700 Subject: [PATCH 087/167] We need to fetch the actual Ursula from the cache to get the bytestring; this will be a sprout nowadays. --- tests/integration/learning/test_discovery_phases.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index f7bcd4d60..caa03615a 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -25,6 +25,7 @@ from twisted.internet.threads import deferToThread from nucypher.characters.lawful import Ursula from tests.utils.middleware import SluggishLargeFleetMiddleware +from tests.utils.ursula import MOCK_KNOWN_URSULAS_CACHE from umbral.keys import UmbralPublicKey from tests.mock.performance_mocks import ( NotAPublicKey, @@ -68,8 +69,10 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice): # A quick setup so that the bytes casting of Ursulas (on what in the real world will be the remote node) # doesn't take up all the time. _teacher = highperf_mocked_alice.current_teacher_node() - _teacher_known_nodes_bytestring = _teacher.bytestring_of_known_nodes() - _teacher.bytestring_of_known_nodes = lambda *args, ** kwargs: _teacher_known_nodes_bytestring # TODO: Formalize this? #1537 + actual_ursula = MOCK_KNOWN_URSULAS_CACHE[_teacher.rest_interface.port] + + _teacher_known_nodes_bytestring = actual_ursula.bytestring_of_known_nodes() + actual_ursula.bytestring_of_known_nodes = lambda *args, ** kwargs: _teacher_known_nodes_bytestring # TODO: Formalize this? #1537 with mock_cert_storage, mock_cert_loading, mock_verify_node, mock_message_verification, mock_metadata_validation: From e7387352b2a4fb47847c6b443e362b5a7653ae25 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 14:37:52 -0700 Subject: [PATCH 088/167] Reworked logic to determine good serials for mocks. --- tests/fixtures.py | 3 +- tests/mock/performance_mocks.py | 358 +++----------------------------- tests/mock/serials.py | 1 + 3 files changed, 36 insertions(+), 326 deletions(-) create mode 100644 tests/mock/serials.py diff --git a/tests/fixtures.py b/tests/fixtures.py index 538240d96..490608af5 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -100,7 +100,7 @@ from tests.mock.performance_mocks import ( mock_remember_node, mock_rest_app_creation, mock_secret_source, - mock_verify_node + mock_verify_node, _determine_good_serials ) from tests.utils.blockchain import TesterBlockchain, token_airdrop from tests.utils.config import ( @@ -927,6 +927,7 @@ def mock_transacting_power_activation(testerchain): @pytest.fixture(scope="module") def fleet_of_highperf_mocked_ursulas(ursula_federated_test_config, request): + # good_serials = _determine_good_serials(10000, 50000) try: quantity = request.param except AttributeError: diff --git a/tests/mock/performance_mocks.py b/tests/mock/performance_mocks.py index 5a387a680..2f2425c6f 100644 --- a/tests/mock/performance_mocks.py +++ b/tests/mock/performance_mocks.py @@ -16,13 +16,14 @@ """ from contextlib import contextmanager - -from umbral.config import default_params -from umbral.keys import UmbralPublicKey -from umbral.signing import Signature from unittest.mock import patch from nucypher.network.server import make_rest_app +from tests.mock.serials import good_serials +from tests.utils.ursula import MOCK_KNOWN_URSULAS_CACHE +from umbral.config import default_params +from umbral.keys import UmbralPublicKey +from umbral.signing import Signature mock_cert_storage = patch("nucypher.config.storages.ForgetfulNodeStorage.store_node_certificate", new=lambda *args, **kwargs: "this_might_normally_be_a_filepath") @@ -48,335 +49,22 @@ The problem is that, for any mock key string, there are going to be some bytes t The only other obvious way to have a test this fast is to hardcode 20k keypairs into the codebase (and even then, it will be far, far less performant than this). """ -serials_which_produce_bytes_which_are_not_viable_as_a_pubkey = (10001, 10003, 10004, 10005, 10007, 10013, 10014, 10018, - 10020, 10022, 10029, 10031, 10033, 10034, 10035, 10036, - 10037, 10038, 10040, 10041, 10043, 10045, 10049, 10051, - 10052, 10053, 10054, 10055, 10056, 10057, 10058, 10060, - 10064, 10065, 10068, 10069, 10070, 10071, 10074, 10077, - 10078, 10082, 10083, 10086, 10089, 10090, 10091, 10092, - 10093, 10095, 10096, 10099, 10100, 10101, 10102, 10103, - 10107, 10108, 10110, 10112, 10113, 10116, 10119, 10120, - 10122, 10124, 10125, 10128, 10130, 10131, 10134, 10140, - 10141, 10142, 10143, 10145, 10146, 10149, 10150, 10151, - 10152, 10153, 10154, 10155, 10158, 10159, 10160, 10161, - 10164, 10166, 10167, 10169, 10171, 10173, 10174, 10175, - 10180, 10184, 10187, 10190, 10191, 10193, 10197, 10198, - 10199, 10201, 10202, 10205, 10207, 10209, 10212, 10215, - 10218, 10219, 10225, 10226, 10229, 10230, 10233, 10234, - 10237, 10239, 10240, 10241, 10243, 10244, 10245, 10251, - 10252, 10253, 10260, 10262, 10263, 10264, 10266, 10267, - 10269, 10277, 10283, 10284, 10291, 10292, 10293, 10294, - 10296, 10297, 10302, 10303, 10305, 10306, 10312, 10315, - 10317, 10318, 10319, 10321, 10322, 10326, 10329, 10332, - 10340, 10344, 10345, 10348, 10349, 10351, 10352, 10353, - 10354, 10355, 10357, 10359, 10360, 10361, 10362, 10363, - 10364, 10365, 10367, 10369, 10371, 10373, 10374, 10376, - 10377, 10380, 10383, 10385, 10386, 10387, 10388, 10389, - 10392, 10393, 10394, 10395, 10396, 10399, 10400, 10403, - 10404, 10405, 10406, 10407, 10409, 10410, 10415, 10416, - 10418, 10419, 10420, 10424, 10425, 10429, 10430, 10431, - 10432, 10433, 10436, 10439, 10440, 10444, 10452, 10454, - 10456, 10458, 10463, 10467, 10468, 10470, 10471, 10472, - 10473, 10476, 10477, 10478, 10480, 10483, 10488, 10489, - 10490, 10493, 10494, 10496, 10497, 10498, 10499, 10501, - 10503, 10504, 10507, 10512, 10514, 10515, 10516, 10518, - 10522, 10523, 10524, 10526, 10527, 10529, 10531, 10533, - 10537, 10538, 10539, 10546, 10547, 10551, 10553, 10554, - 10555, 10556, 10557, 10559, 10561, 10563, 10565, 10567, - 10568, 10570, 10574, 10575, 10577, 10578, 10580, 10588, - 10590, 10592, 10593, 10596, 10598, 10599, 10601, 10602, - 10604, 10607, 10609, 10610, 10613, 10616, 10618, 10623, - 10624, 10628, 10630, 10631, 10637, 10639, 10640, 10641, - 10642, 10643, 10647, 10651, 10652, 10653, 10655, 10656, - 10657, 10660, 10661, 10662, 10664, 10666, 10668, 10671, - 10674, 10676, 10677, 10679, 10680, 10686, 10692, 10695, - 10696, 10697, 10699, 10701, 10703, 10704, 10705, 10707, - 10708, 10711, 10713, 10715, 10717, 10722, 10724, 10731, - 10732, 10733, 10734, 10736, 10739, 10740, 10741, 10742, - 10746, 10747, 10753, 10754, 10755, 10758, 10759, 10761, - 10763, 10764, 10765, 10767, 10768, 10771, 10772, 10773, - 10774, 10775, 10777, 10779, 10780, 10785, 10786, 10789, - 10792, 10793, 10798, 10802, 10803, 10806, 10807, 10808, - 10809, 10810, 10812, 10813, 10815, 10816, 10817, 10818, - 10821, 10822, 10827, 10828, 10832, 10834, 10836, 10837, - 10839, 10844, 10846, 10848, 10849, 10851, 10853, 10855, - 10858, 10859, 10860, 10861, 10862, 10863, 10864, 10865, - 10866, 10867, 10870, 10872, 10873, 10877, 10878, 10880, - 10881, 10883, 10884, 10887, 10889, 10892, 10893, 10895, - 10896, 10898, 10899, 10900, 10901, 10902, 10908, 10910, - 10915, 10916, 10920, 10921, 10922, 10923, 10924, 10926, - 10931, 10932, 10934, 10935, 10936, 10937, 10939, 10940, - 10948, 10952, 10953, 10954, 10958, 10959, 10961, 10963, - 10964, 10965, 10966, 10968, 10969, 10970, 10972, 10974, - 10975, 10976, 10977, 10978, 10979, 10981, 10982, 10983, - 10984, 10985, 10986, 10987, 10988, 10992, 10993, 10996, - 10997, 10998, 10999, 11001, 11002, 11005, 11010, 11011, - 11015, 11016, 11019, 11025, 11026, 11027, 11029, 11030, - 11034, 11035, 11039, 11041, 11043, 11046, 11050, 11054, - 11055, 11057, 11058, 11064, 11066, 11067, 11068, 11070, - 11071, 11073, 11074, 11076, 11078, 11082, 11084, 11086, - 11087, 11089, 11092, 11093, 11094, 11095, 11097, 11098, - 11100, 11103, 11104, 11105, 11108, 11111, 11114, 11115, - 11116, 11118, 11119, 11120, 11122, 11124, 11125, 11127, - 11128, 11131, 11132, 11135, 11137, 11139, 11142, 11143, - 11145, 11148, 11150, 11151, 11153, 11155, 11156, 11158, - 11160, 11164, 11165, 11167, 11169, 11171, 11176, 11179, - 11181, 11182, 11183, 11184, 11186, 11187, 11188, 11189, - 11194, 11197, 11198, 11200, 11205, 11206, 11208, 11209, - 11212, 11213, 11214, 11217, 11221, 11224, 11228, 11230, - 11238, 11239, 11241, 11242, 11243, 11245, 11249, 11250, - 11251, 11252, 11253, 11259, 11261, 11263, 11264, 11266, - 11267, 11272, 11273, 11278, 11279, 11280, 11282, 11283, - 11286, 11287, 11289, 11291, 11295, 11296, 11298, 11299, - 11304, 11307, 11308, 11309, 11310, 11312, 11319, 11321, - 11323, 11327, 11328, 11330, 11331, 11332, 11337, 11340, - 11345, 11347, 11349, 11350, 11351, 11352, 11353, 11354, - 11355, 11359, 11360, 11361, 11362, 11363, 11364, 11366, - 11368, 11369, 11371, 11374, 11377, 11380, 11382, 11383, - 11384, 11386, 11387, 11388, 11390, 11393, 11394, 11396, - 11398, 11401, 11403, 11406, 11411, 11412, 11417, 11422, - 11423, 11428, 11429, 11430, 11431, 11433, 11434, 11435, - 11442, 11444, 11449, 11451, 11453, 11456, 11457, 11458, - 11461, 11463, 11464, 11465, 11466, 11469, 11470, 11471, - 11472, 11476, 11477, 11478, 11479, 11481, 11485, 11488, - 11490, 11492, 11494, 11495, 11496, 11498, 11500, 11501, - 11502, 11503, 11505, 11509, 11510, 11513, 11514, 11515, - 11516, 11518, 11521, 11523, 11524, 11525, 11527, 11530, - 11531, 11532, 11533, 11535, 11537, 11538, 11539, 11540, - 11544, 11546, 11548, 11551, 11552, 11553, 11559, 11560, - 11562, 11564, 11568, 11569, 11575, 11576, 11577, 11579, - 11581, 11584, 11585, 11586, 11587, 11591, 11592, 11593, - 11596, 11597, 11598, 11600, 11601, 11604, 11605, 11608, - 11609, 11610, 11611, 11612, 11616, 11618, 11621, 11622, - 11623, 11624, 11628, 11633, 11634, 11637, 11638, 11641, - 11642, 11644, 11645, 11646, 11650, 11655, 11658, 11661, - 11664, 11665, 11666, 11668, 11670, 11671, 11672, 11673, - 11674, 11675, 11679, 11681, 11684, 11685, 11687, 11688, - 11689, 11690, 11691, 11692, 11695, 11696, 11697, 11698, - 11699, 11700, 11701, 11702, 11706, 11709, 11710, 11712, - 11716, 11718, 11719, 11724, 11726, 11732, 11733, 11736, - 11737, 11738, 11742, 11744, 11745, 11746, 11747, 11748, - 11749, 11750, 11751, 11756, 11757, 11759, 11763, 11764, - 11768, 11771, 11772, 11773, 11775, 11778, 11779, 11780, - 11782, 11784, 11785, 11786, 11787, 11788, 11789, 11790, - 11791, 11794, 11798, 11800, 11802, 11804, 11811, 11815, - 11818, 11821, 11824, 11829, 11831, 11833, 11834, 11837, - 11839, 11840, 11846, 11847, 11848, 11849, 11851, 11852, - 11853, 11860, 11863, 11865, 11866, 11867, 11868, 11869, - 11870, 11873, 11877, 11878, 11881, 11883, 11885, 11886, - 11888, 11889, 11893, 11894, 11895, 11896, 11899, 11902, - 11905, 11906, 11909, 11910, 11912, 11913, 11916, 11919, - 11920, 11924, 11925, 11927, 11931, 11933, 11934, 11936, - 11937, 11938, 11943, 11947, 11948, 11953, 11954, 11955, - 11956, 11959, 11960, 11963, 11969, 11970, 11972, 11973, - 11975, 11980, 11981, 11983, 11985, 11991, 11992, 11996, - 11999, 12002, 12003, 12004, 12007, 12008, 12009, 12010, - 12011, 12012, 12013, 12014, 12016, 12017, 12018, 12020, - 12022, 12026, 12028, 12029, 12030, 12031, 12034, 12036, - 12043, 12045, 12047, 12050, 12051, 12053, 12054, 12056, - 12057, 12058, 12060, 12061, 12063, 12065, 12067, 12069, - 12070, 12071, 12074, 12075, 12076, 12077, 12079, 12080, - 12082, 12083, 12084, 12086, 12087, 12089, 12090, 12091, - 12092, 12093, 12094, 12095, 12096, 12097, 12098, 12099, - 12102, 12103, 12104, 12111, 12113, 12114, 12116, 12122, - 12123, 12124, 12125, 12126, 12127, 12128, 12129, 12130, - 12132, 12135, 12136, 12137, 12138, 12139, 12140, 12141, - 12145, 12147, 12148, 12151, 12152, 12153, 12155, 12156, - 12157, 12159, 12164, 12167, 12170, 12171, 12173, 12174, - 12176, 12178, 12179, 12181, 12182, 12183, 12185, 12190, - 12193, 12194, 12195, 12198, 12200, 12201, 12202, 12204, - 12205, 12208, 12211, 12212, 12214, 12215, 12218, 12219, - 12221, 12222, 12223, 12226, 12227, 12229, 12230, 12232, - 12233, 12238, 12244, 12245, 12246, 12249, 12254, 12255, - 12257, 12258, 12262, 12263, 12264, 12265, 12266, 12272, - 12273, 12274, 12276, 12277, 12278, 12280, 12281, 12283, - 12284, 12286, 12288, 12293, 12294, 12297, 12299, 12300, - 12301, 12302, 12303, 12304, 12308, 12309, 12310, 12313, - 12314, 12315, 12316, 12317, 12320, 12321, 12323, 12324, - 12328, 12330, 12331, 12333, 12334, 12335, 12338, 12339, - 12340, 12346, 12350, 12354, 12357, 12360, 12361, 12362, - 12363, 12365, 12368, 12369, 12375, 12376, 12378, 12382, - 12385, 12386, 12388, 12389, 12396, 12398, 12403, 12404, - 12408, 12409, 12411, 12413, 12420, 12421, 12422, 12424, - 12426, 12427, 12429, 12431, 12432, 12435, 12436, 12437, - 12438, 12442, 12443, 12445, 12446, 12447, 12448, 12455, - 12457, 12458, 12460, 12463, 12464, 12466, 12467, 12468, - 12469, 12470, 12472, 12473, 12474, 12475, 12476, 12480, - 12481, 12484, 12485, 12486, 12487, 12489, 12490, 12491, - 12492, 12496, 12497, 12499, 12502, 12504, 12505, 12507, - 12508, 12509, 12514, 12515, 12516, 12518, 12520, 12521, - 12523, 12540, 12544, 12546, 12548, 12549, 12553, 12555, - 12556, 12558, 12559, 12560, 12563, 12564, 12566, 12568, - 12569, 12572, 12574, 12575, 12576, 12577, 12579, 12580, - 12581, 12582, 12585, 12586, 12587, 12589, 12590, 12591, - 12593, 12597, 12599, 12601, 12602, 12604, 12605, 12607, - 12608, 12609, 12610, 12613, 12615, 12616, 12617, 12620, - 12623, 12624, 12631, 12632, 12633, 12634, 12635, 12636, - 12637, 12638, 12639, 12640, 12644, 12648, 12650, 12651, - 12652, 12657, 12658, 12659, 12663, 12665, 12666, 12667, - 12668, 12669, 12672, 12676, 12679, 12680, 12681, 12682, - 12683, 12684, 12685, 12687, 12688, 12690, 12691, 12693, - 12694, 12697, 12699, 12702, 12703, 12705, 12708, 12709, - 12710, 12712, 12714, 12715, 12716, 12721, 12728, 12730, - 12731, 12732, 12735, 12738, 12739, 12742, 12746, 12748, - 12750, 12753, 12755, 12757, 12758, 12764, 12765, 12774, - 12776, 12778, 12779, 12781, 12785, 12786, 12787, 12788, - 12791, 12799, 12800, 12802, 12804, 12805, 12808, 12813, - 12816, 12817, 12818, 12819, 12820, 12824, 12825, 12827, - 12828, 12830, 12831, 12832, 12833, 12836, 12838, 12839, - 12840, 12841, 12843, 12844, 12847, 12848, 12850, 12851, - 12853, 12855, 12857, 12858, 12860, 12862, 12864, 12865, - 12866, 12867, 12868, 12870, 12875, 12876, 12877, 12878, - 12879, 12881, 12882, 12883, 12889, 12890, 12891, 12892, - 12893, 12898, 12902, 12904, 12905, 12908, 12909, 12910, - 12911, 12916, 12917, 12918, 12919, 12920, 12922, 12923, - 12925, 12926, 12927, 12928, 12931, 12934, 12935, 12938, - 12942, 12943, 12944, 12945, 12951, 12952, 12953, 12954, - 12955, 12956, 12957, 12958, 12960, 12961, 12962, 12963, - 12965, 12966, 12969, 12970, 12971, 12976, 12977, 12980, - 12982, 12983, 12984, 12985, 12991, 12992, 12994, 12996, - 13003, 13004, 13005, 13006, 13007, 13010, 13011, 13013, - 13015, 13016, 13018, 13021, 13022, 13025, 13028, 13030, - 13031, 13033, 13034, 13036, 13038, 13039, 13041, 13045, - 13046, 13049, 13051, 13052, 13054, 13055, 13057, 13058, - 13059, 13061, 13062, 13063, 13067, 13071, 13072, 13078, - 13081, 13082, 13084, 13087, 13090, 13091, 13093, 13095, - 13096, 13098, 13099, 13100, 13101, 13102, 13103, 13105, - 13106, 13108, 13109, 13112, 13114, 13115, 13116, 13118, - 13120, 13121, 13123, 13124, 13125, 13126, 13129, 13130, - 13131, 13132, 13133, 13134, 13136, 13137, 13138, 13139, - 13141, 13144, 13145, 13146, 13148, 13149, 13151, 13152, - 13153, 13154, 13155, 13156, 13157, 13158, 13160, 13161, - 13164, 13165, 13166, 13167, 13168, 13172, 13173, 13177, - 13178, 13179, 13180, 13182, 13184, 13185, 13190, 13194, - 13195, 13199, 13200, 13201, 13203, 13205, 13206, 13212, - 13214, 13216, 13217, 13218, 13219, 13220, 13221, 13225, - 13226, 13227, 13241, 13242, 13243, 13244, 13245, 13247, - 13253, 13256, 13257, 13258, 13259, 13261, 13262, 13263, - 13265, 13267, 13269, 13271, 13272, 13274, 13276, 13277, - 13279, 13280, 13282, 13284, 13285, 13286, 13287, 13290, - 13293, 13294, 13296, 13298, 13302, 13305, 13308, 13309, - 13310, 13311, 13317, 13318, 13320, 13322, 13323, 13325, - 13327, 13329, 13330, 13333, 13338, 13339, 13340, 13342, - 13345, 13346, 13347, 13350, 13353, 13355, 13357, 13358, - 13359, 13364, 13365, 13366, 13368, 13372, 13374, 13376, - 13379, 13380, 13383, 13384, 13386, 13388, 13389, 13390, - 13392, 13393, 13394, 13395, 13396, 13398, 13399, 13402, - 13403, 13405, 13406, 13408, 13410, 13412, 13414, 13416, - 13417, 13419, 13421, 13422, 13423, 13425, 13428, 13432, - 13434, 13435, 13437, 13438, 13439, 13440, 13442, 13443, - 13444, 13445, 13452, 13453, 13454, 13455, 13457, 13458, - 13459, 13462, 13464, 13465, 13470, 13474, 13477, 13479, - 13482, 13484, 13486, 13488, 13489, 13491, 13492, 13497, - 13499, 13501, 13502, 13503, 13505, 13506, 13507, 13510, - 13511, 13514, 13517, 13521, 13522, 13524, 13525, 13527, - 13529, 13531, 13533, 13534, 13535, 13539, 13540, 13544, - 13546, 13547, 13552, 13553, 13554, 13562, 13565, 13570, - 13575, 13576, 13577, 13578, 13581, 13582, 13583, 13585, - 13587, 13589, 13591, 13592, 13593, 13597, 13598, 13600, - 13601, 13602, 13603, 13605, 13607, 13609, 13610, 13617, - 13619, 13620, 13624, 13625, 13631, 13634, 13637, 13640, - 13641, 13643, 13644, 13645, 13648, 13653, 13656, 13658, - 13659, 13661, 13662, 13669, 13670, 13671, 13672, 13674, - 13676, 13681, 13684, 13686, 13687, 13690, 13691, 13697, - 13698, 13700, 13702, 13704, 13708, 13710, 13711, 13713, - 13715, 13719, 13722, 13723, 13724, 13725, 13732, 13733, - 13734, 13735, 13741, 13742, 13744, 13746, 13747, 13748, - 13754, 13755, 13757, 13759, 13762, 13766, 13770, 13771, - 13772, 13774, 13778, 13780, 13781, 13783, 13784, 13785, - 13789, 13790, 13792, 13793, 13794, 13795, 13796, 13799, - 13801, 13803, 13804, 13806, 13808, 13810, 13811, 13817, - 13819, 13820, 13826, 13828, 13834, 13835, 13837, 13838, - 13839, 13841, 13842, 13844, 13845, 13846, 13854, 13855, - 13860, 13861, 13863, 13864, 13867, 13868, 13869, 13870, - 13872, 13876, 13879, 13881, 13884, 13888, 13890, 13895, - 13896, 13897, 13899, 13901, 13903, 13905, 13906, 13907, - 13913, 13915, 13918, 13920, 13921, 13923, 13924, 13929, - 13930, 13931, 13935, 13937, 13938, 13939, 13942, 13946, - 13948, 13949, 13950, 13951, 13953, 13959, 13961, 13962, - 13963, 13965, 13968, 13969, 13978, 13980, 13982, 13990, - 13991, 13992, 13994, 13995, 13996, 13998, 13999, 14000, - 14005, 14008, 14010, 14012, 14015, 14016, 14018, 14019, - 14025, 14027, 14028, 14031, 14032, 14034, 14035, 14036, - 14038, 14039, 14040, 14041, 14044, 14045, 14047, 14048, - 14049, 14050, 14051, 14052, 14055, 14056, 14058, 14063, - 14064, 14066, 14068, 14070, 14071, 14072, 14073, 14074, - 14078, 14083, 14084, 14085, 14089, 14090, 14091, 14093, - 14094, 14096, 14097, 14098, 14099, 14100, 14101, 14102, - 14106, 14109, 14110, 14111, 14119, 14120, 14121, 14122, - 14123, 14124, 14125, 14126, 14128, 14130, 14132, 14133, - 14134, 14136, 14137, 14138, 14140, 14143, 14144, 14146, - 14147, 14148, 14151, 14152, 14157, 14167, 14168, 14169, - 14171, 14173, 14177, 14179, 14181, 14184, 14185, 14188, - 14189, 14190, 14191, 14197, 14200, 14201, 14203, 14206, - 14208, 14211, 14212, 14213, 14216, 14218, 14220, 14222, - 14227, 14228, 14229, 14230, 14231, 14232, 14233, 14234, - 14235, 14237, 14239, 14243, 14245, 14247, 14248, 14252, - 14258, 14261, 14264, 14265, 14266, 14267, 14268, 14269, - 14271, 14275, 14277, 14278, 14280, 14281, 14282, 14283, - 14284, 14285, 14286, 14289, 14290, 14292, 14293, 14294, - 14296, 14297, 14302, 14307, 14309, 14310, 14311, 14314, - 14316, 14317, 14318, 14326, 14331, 14332, 14334, 14338, - 14341, 14343, 14346, 14348, 14349, 14350, 14352, 14353, - 14354, 14356, 14359, 14361, 14362, 14365, 14367, 14369, - 14370, 14371, 14373, 14376, 14377, 14382, 14384, 14386, - 14387, 14388, 14391, 14392, 14393, 14394, 14396, 14399, - 14400, 14401, 14403, 14404, 14405, 14407, 14409, 14410, - 14411, 14412, 14414, 14415, 14416, 14419, 14420, 14423, - 14424, 14426, 14427, 14429, 14431, 14433, 14434, 14436, - 14437, 14439, 14440, 14441, 14442, 14443, 14444, 14446, - 14449, 14455, 14457, 14458, 14460, 14461, 14464, 14468, - 14469, 14470, 14471, 14475, 14476, 14480, 14482, 14483, - 14485, 14486, 14488, 14492, 14495, 14496, 14497, 14498, - 14499, 14501, 14502, 14504, 14505, 14506, 14509, 14510, - 14511, 14514, 14516, 14517, 14518, 14519, 14521, 14526, - 14529, 14530, 14534, 14535, 14537, 14538, 14539, 14541, - 14543, 14544, 14545, 14546, 14551, 14552, 14555, 14558, - 14562, 14563, 14564, 14568, 14569, 14571, 14572, 14573, - 14575, 14578, 14579, 14580, 14581, 14584, 14586, 14590, - 14591, 14593, 14594, 14595, 14596, 14597, 14598, 14599, - 14600, 14603, 14606, 14609, 14611, 14613, 14617, 14619, - 14620, 14622, 14624, 14627, 14629, 14631, 14633, 14634, - 14635, 14637, 14640, 14644, 14645, 14647, 14648, 14652, - 14654, 14656, 14658, 14659, 14661, 14666, 14668, 14669, - 14671, 14673, 14677, 14680, 14681, 14682, 14686, 14688, - 14689, 14693, 14695, 14698, 14700, 14701, 14703, 14709, - 14711, 14714, 14715, 14716, 14717, 14718, 14719, 14722, - 14723, 14725, 14727, 14730, 14731, 14732, 14733, 14734, - 14735, 14736, 14739, 14741, 14743, 14750, 14753, 14754, - 14755, 14759, 14760, 14761, 14767, 14770, 14771, 14772, - 14773, 14777, 14781, 14782, 14783, 14788, 14789, 14790, - 14791, 14792, 14795, 14799, 14800, 14807, 14809, 14810, - 14813, 14814, 14817, 14819, 14820, 14822, 14823, 14824, - 14825, 14827, 14828, 14829, 14830, 14832, 14834, 14835, - 14837, 14838, 14840, 14841, 14843, 14844, 14845, 14848, - 14849, 14850, 14852, 14853, 14854, 14856, 14857, 14861, - 14864, 14867, 14868, 14872, 14873, 14874, 14881, 14884, - 14885, 14886, 14887, 14888, 14889, 14890, 14891, 14898, - 14899, 14900, 14905, 14906, 14910, 14912, 14915, 14916, - 14918, 14919, 14921, 14922, 14923, 14924, 14925, 14926, - 14929, 14931, 14932, 14933, 14934, 14935, 14937, 14939, - 14941, 14942, 14943, 14947, 14949, 14950, 14958, 14959, - 14960, 14964, 14967, 14968, 14969, 14970, 14972, 14973, - 14974, 14975, 14978, 14983, 14984, 14985, 14986, 14989, - 14991, 14992, 14994, 14997, 14999, 15000, 15002) - class NotAPublicKey: + _serial_bytes_length = 5 _serial = 10000 _umbral_pubkey_from_bytes = UmbralPublicKey.from_bytes - @classmethod - def tick(cls): - cls._serial += 1 - while cls._serial in serials_which_produce_bytes_which_are_not_viable_as_a_pubkey: - cls._serial += 1 + def _tick(): + for serial in good_serials: + yield serial + tick = _tick() def __init__(self, serial=None): if serial is None: - self.tick() - self.serial = str(self._serial).encode() + serial_int = next(self.tick) + self.serial = serial_int.to_bytes(self._serial_bytes_length, byteorder="big") else: self.serial = serial @@ -385,12 +73,16 @@ class NotAPublicKey: @classmethod def reset(cls): - cls._serial = 10000 + cls.tick = cls._tick() @classmethod def from_bytes(cls, some_bytes): return cls(serial=some_bytes[-5:]) + @classmethod + def from_int(cls, serial): + return cls(serial.to_bytes(cls._serial_bytes_length, byteorder="big")) + def to_bytes(self, *args, **kwargs): return b"this is not a public key... but it is 64 bytes.. so, ya know" + self.serial @@ -554,5 +246,21 @@ def mock_pubkey_from_bytes(*args, **kwargs): yield NotAPublicKey.reset() + mock_stamp_call = patch('nucypher.crypto.signing.SignatureStamp.__call__', new=NotAPrivateKey.stamp) mock_signature_bytes = patch('umbral.signing.Signature.__bytes__', new=NotAPrivateKey.signature_bytes) + + +def _determine_good_serials(start, end): + ''' + Figure out which serials are good to use in mocks because they won't result in non-viable public keys. + ''' + good_serials = [] + for i in range(start, end): + try: + NotAPublicKey.from_int(i).i_want_to_be_a_real_boy() + except Exception as e: + continue + else: + good_serials.append(i) + return good_serials diff --git a/tests/mock/serials.py b/tests/mock/serials.py new file mode 100644 index 000000000..d5e2851be --- /dev/null +++ b/tests/mock/serials.py @@ -0,0 +1 @@ +good_serials = (10000, 10001, 10005, 10006, 10009, 10011, 10012, 10013, 10015, 10019, 10020, 10022, 10025, 10026, 10029, 10036, 10037, 10038, 10040, 10044, 10045, 10048, 10049, 10050, 10051, 10053, 10054, 10055, 10056, 10058, 10059, 10061, 10064, 10067, 10068, 10069, 10070, 10072, 10074, 10075, 10076, 10080, 10081, 10083, 10085, 10086, 10088, 10089, 10092, 10094, 10097, 10100, 10101, 10102, 10103, 10104, 10105, 10107, 10108, 10110, 10111, 10112, 10119, 10120, 10121, 10123, 10126, 10128, 10131, 10132, 10133, 10134, 10137, 10139, 10140, 10141, 10142, 10151, 10153, 10154, 10155, 10158, 10166, 10169, 10172, 10173, 10175, 10177, 10178, 10179, 10181, 10185, 10187, 10188, 10192, 10194, 10196, 10197, 10200, 10202, 10203, 10206, 10207, 10208, 10209, 10211, 10212, 10216, 10220, 10221, 10227, 10228, 10232, 10233, 10234, 10235, 10236, 10237, 10238, 10239, 10240, 10242, 10243, 10245, 10246, 10247, 10248, 10249, 10250, 10251, 10253, 10254, 10255, 10256, 10258, 10259, 10262, 10265, 10266, 10267, 10269, 10271, 10272, 10273, 10274, 10275, 10276, 10282, 10283, 10285, 10286, 10287, 10291, 10294, 10295, 10298, 10299, 10301, 10302, 10304, 10305, 10310, 10316, 10317, 10318, 10319, 10322, 10326, 10327, 10333, 10336, 10337, 10339, 10344, 10346, 10347, 10348, 10349, 10353, 10356, 10358, 10359, 10360, 10363, 10367, 10368, 10369, 10371, 10372, 10374, 10375, 10380, 10381, 10382, 10385, 10386, 10389, 10390, 10392, 10394, 10395, 10397, 10398, 10399, 10400, 10401, 10402, 10404, 10406, 10407, 10409, 10410, 10411, 10413, 10414, 10420, 10422, 10423, 10425, 10426, 10427, 10431, 10432, 10436, 10438, 10440, 10442, 10444, 10446, 10450, 10451, 10453, 10455, 10456, 10457, 10458, 10459, 10461, 10462, 10463, 10466, 10468, 10470, 10471, 10473, 10474, 10475, 10480, 10482, 10483, 10484, 10485, 10487, 10488, 10490, 10491, 10494, 10495, 10497, 10499, 10500, 10501, 10502, 10504, 10506, 10507, 10510, 10515, 10517, 10518, 10525, 10526, 10530, 10534, 10535, 10536, 10537, 10538, 10540, 10541, 10542, 10543, 10544, 10546, 10547, 10548, 10550, 10551, 10552, 10554, 10555, 10556, 10557, 10558, 10559, 10562, 10564, 10566, 10567, 10568, 10571, 10577, 10579, 10586, 10590, 10591, 10596, 10599, 10601, 10605, 10608, 10610, 10613, 10614, 10615, 10618, 10619, 10620, 10622, 10624, 10626, 10627, 10629, 10630, 10631, 10632, 10633, 10636, 10637, 10640, 10642, 10643, 10644, 10646, 10647, 10650, 10651, 10652, 10654, 10655, 10656, 10658, 10660, 10663, 10666, 10667, 10669, 10670, 10671, 10672, 10677, 10678, 10679, 10680, 10681, 10682, 10683, 10685, 10686, 10690, 10691, 10692, 10696, 10700, 10702, 10703, 10705, 10707, 10708, 10710, 10711, 10714, 10715, 10718, 10719, 10720, 10721, 10723, 10724, 10726, 10727, 10728, 10729, 10731, 10732, 10734, 10735, 10736, 10737, 10738, 10742, 10744, 10745, 10746, 10747, 10749, 10751, 10753, 10754, 10756, 10757, 10759, 10760, 10762, 10763, 10765, 10767, 10769, 10772, 10775, 10777, 10778, 10779, 10780, 10786, 10789, 10793, 10794, 10795, 10796, 10797, 10799, 10800, 10805, 10806, 10807, 10811, 10812, 10813, 10814, 10817, 10825, 10826, 10827, 10828, 10830, 10831, 10832, 10835, 10838, 10843, 10844, 10845, 10849, 10852, 10853, 10855, 10856, 10857, 10860, 10861, 10862, 10863, 10865, 10866, 10867, 10869, 10870, 10876, 10879, 10880, 10881, 10882, 10886, 10887, 10888, 10889, 10892, 10893, 10899, 10900, 10903, 10904, 10906, 10908, 10909, 10910, 10911, 10912, 10914, 10916, 10917, 10918, 10919, 10921, 10922, 10924, 10926, 10927, 10929, 10931, 10934, 10935, 10938, 10941, 10942, 10944, 10950, 10951, 10953, 10955, 10957, 10960, 10962, 10965, 10966, 10969, 10973, 10976, 10977, 10978, 10980, 10981, 10984, 10987, 10991, 10992, 10993, 10994, 10995, 10996, 10997, 10998, 10999, 11003, 11008, 11009, 11010, 11015, 11017, 11018, 11020, 11024, 11025, 11026, 11029, 11031, 11032, 11034, 11035, 11037, 11039, 11040, 11042, 11046, 11049, 11050, 11051, 11052, 11053, 11057, 11058, 11059, 11060, 11061, 11062, 11063, 11064, 11068, 11069, 11070, 11071, 11073, 11074, 11075, 11076, 11077, 11078, 11079, 11080, 11081, 11082, 11087, 11089, 11090, 11091, 11094, 11097, 11098, 11100, 11101, 11102, 11105, 11108, 11109, 11110, 11114, 11118, 11121, 11122, 11123, 11124, 11125, 11129, 11130, 11131, 11132, 11133, 11136, 11137, 11139, 11141, 11146, 11148, 11153, 11155, 11156, 11159, 11160, 11163, 11164, 11168, 11169, 11170, 11171, 11172, 11173, 11180, 11182, 11184, 11186, 11187, 11188, 11196, 11199, 11200, 11201, 11204, 11207, 11208, 11209, 11213, 11218, 11220, 11224, 11225, 11227, 11229, 11230, 11233, 11237, 11238, 11241, 11244, 11246, 11247, 11250, 11254, 11256, 11257, 11259, 11260, 11263, 11267, 11268, 11269, 11270, 11273, 11274, 11275, 11276, 11278, 11279, 11282, 11283, 11285, 11290, 11291, 11292, 11293, 11298, 11299, 11302, 11303, 11305, 11307, 11308, 11311, 11312, 11313, 11316, 11319, 11320, 11321, 11323, 11329, 11330, 11331, 11333, 11335, 11337, 11339, 11342, 11343, 11344, 11345, 11346, 11349, 11353, 11354, 11355, 11359, 11361, 11365, 11366, 11367, 11369, 11374, 11375, 11376, 11378, 11379, 11385, 11386, 11389, 11391, 11392, 11395, 11399, 11401, 11403, 11406, 11409, 11410, 11411, 11412, 11416, 11421, 11422, 11423, 11429, 11431, 11432, 11433, 11435, 11436, 11438, 11439, 11441, 11442, 11443, 11446, 11447, 11450, 11451, 11453, 11454, 11455, 11456, 11457, 11458, 11459, 11461, 11462, 11463, 11464, 11465, 11468, 11474, 11475, 11476, 11477, 11478, 11479, 11480, 11484, 11487, 11488, 11490, 11492, 11494, 11496, 11497, 11499, 11502, 11503, 11510, 11518, 11519, 11521, 11525, 11529, 11530, 11531, 11533, 11536, 11537, 11538, 11540, 11541, 11542, 11543, 11545, 11547, 11551, 11553, 11554, 11555, 11561, 11563, 11564, 11566, 11567, 11572, 11574, 11575, 11577, 11581, 11582, 11584, 11585, 11586, 11588, 11589, 11590, 11591, 11598, 11603, 11605, 11608, 11613, 11614, 11615, 11617, 11621, 11623, 11624, 11625, 11626, 11627, 11628, 11629, 11630, 11635, 11636, 11637, 11639, 11641, 11642, 11643, 11644, 11646, 11647, 11648, 11649, 11656, 11659, 11662, 11663, 11665, 11666, 11667, 11668, 11671, 11674, 11675, 11677, 11678, 11679, 11681, 11684, 11685, 11686, 11687, 11689, 11690, 11691, 11692, 11694, 11696, 11700, 11701, 11702, 11703, 11705, 11706, 11709, 11710, 11713, 11715, 11718, 11720, 11722, 11723, 11727, 11728, 11731, 11732, 11734, 11736, 11737, 11739, 11741, 11744, 11745, 11747, 11749, 11751, 11754, 11756, 11757, 11759, 11764, 11765, 11766, 11767, 11768, 11770, 11771, 11777, 11778, 11781, 11782, 11784, 11787, 11790, 11791, 11792, 11795, 11797, 11799, 11800, 11801, 11803, 11805, 11806, 11807, 11808, 11809, 11810, 11812, 11815, 11821, 11823, 11824, 11825, 11830, 11832, 11833, 11834, 11835, 11836, 11837, 11838, 11842, 11843, 11844, 11845, 11847, 11848, 11849, 11851, 11852, 11860, 11864, 11874, 11876, 11881, 11884, 11886, 11895, 11896, 11904, 11908, 11909, 11912, 11913, 11915, 11916, 11919, 11920, 11925, 11926, 11927, 11929, 11931, 11932, 11936, 11937, 11938, 11940, 11941, 11942, 11944, 11945, 11948, 11949, 11950, 11951, 11955, 11960, 11961, 11962, 11965, 11969, 11970, 11972, 11974, 11975, 11977, 11978, 11984, 11985, 11987, 11989, 11990, 11991, 11993, 11994, 11996, 11999, 12002, 12006, 12007, 12009, 12010, 12012, 12015, 12016, 12018, 12019, 12020, 12021, 12022, 12023, 12024, 12025, 12027, 12028, 12032, 12033, 12035, 12037, 12038, 12039, 12040, 12041, 12043, 12044, 12048, 12051, 12052, 12053, 12055, 12056, 12060, 12061, 12062, 12063, 12066, 12067, 12068, 12071, 12072, 12076, 12077, 12078, 12080, 12081, 12082, 12085, 12087, 12088, 12092, 12094, 12101, 12102, 12103, 12105, 12106, 12107, 12109, 12111, 12113, 12114, 12115, 12120, 12121, 12122, 12123, 12124, 12125, 12128, 12129, 12131, 12132, 12133, 12135, 12136, 12138, 12139, 12141, 12142, 12143, 12146, 12147, 12148, 12149, 12150, 12152, 12153, 12154, 12156, 12157, 12159, 12161, 12163, 12166, 12168, 12171, 12173, 12174, 12175, 12176, 12178, 12184, 12188, 12191, 12193, 12194, 12195, 12197, 12200, 12201, 12202, 12203, 12207, 12209, 12214, 12218, 12220, 12221, 12223, 12225, 12226, 12227, 12232, 12233, 12234, 12240, 12241, 12243, 12245, 12247, 12249, 12252, 12254, 12256, 12257, 12259, 12261, 12265, 12267, 12268, 12269, 12272, 12273, 12274, 12275, 12276, 12277, 12278, 12279, 12280, 12282, 12283, 12284, 12286, 12287, 12290, 12292, 12293, 12294, 12295, 12296, 12297, 12298, 12299, 12303, 12308, 12310, 12311, 12314, 12315, 12316, 12319, 12320, 12321, 12330, 12332, 12333, 12334, 12336, 12339, 12341, 12345, 12347, 12349, 12350, 12354, 12358, 12362, 12363, 12364, 12365, 12366, 12367, 12372, 12373, 12377, 12378, 12381, 12382, 12383, 12387, 12391, 12392, 12393, 12394, 12395, 12397, 12398, 12399, 12401, 12403, 12404, 12406, 12407, 12411, 12412, 12414, 12416, 12421, 12422, 12424, 12427, 12430, 12433, 12434, 12436, 12437, 12439, 12440, 12441, 12444, 12445, 12447, 12448, 12449, 12453, 12455, 12458, 12459, 12462, 12464, 12465, 12466, 12467, 12468, 12469, 12471, 12474, 12475, 12479, 12480, 12483, 12487, 12493, 12495, 12502, 12504, 12507, 12508, 12509, 12510, 12513, 12514, 12515, 12516, 12518, 12519, 12521, 12523, 12526, 12528, 12529, 12533, 12535, 12536, 12538, 12540, 12541, 12542, 12543, 12545, 12546, 12547, 12548, 12549, 12553, 12557, 12558, 12559, 12560, 12561, 12562, 12563, 12564, 12565, 12567, 12569, 12572, 12573, 12577, 12581, 12586, 12587, 12589, 12591, 12592, 12593, 12594, 12595, 12596, 12598, 12601, 12603, 12606, 12608, 12610, 12612, 12615, 12617, 12619, 12621, 12622, 12623, 12625, 12627, 12631, 12632, 12635, 12636, 12640, 12641, 12642, 12643, 12644, 12647, 12648, 12649, 12652, 12655, 12659, 12660, 12661, 12662, 12663, 12666, 12667, 12668, 12670, 12672, 12673, 12675, 12676, 12677, 12678, 12683, 12684, 12685, 12686, 12689, 12690, 12692, 12693, 12695, 12696, 12697, 12699, 12702, 12703, 12706, 12707, 12708, 12709, 12710, 12711, 12713, 12716, 12718, 12721, 12725, 12726, 12729, 12730, 12731, 12737, 12738, 12739, 12740, 12743, 12745, 12747, 12750, 12751, 12752, 12754, 12755, 12756, 12761, 12763, 12764, 12767, 12768, 12772, 12774, 12776, 12777, 12778, 12779, 12781, 12782, 12783, 12784, 12785, 12787, 12789, 12792, 12795, 12796, 12798, 12799, 12800, 12802, 12805, 12806, 12807, 12808, 12809, 12810, 12812, 12816, 12818, 12819, 12823, 12827, 12828, 12829, 12830, 12835, 12839, 12840, 12841, 12843, 12845, 12848, 12850, 12852, 12853, 12854, 12855, 12858, 12861, 12863, 12866, 12867, 12868, 12869, 12873, 12877, 12879, 12880, 12883, 12884, 12886, 12890, 12897, 12899, 12900, 12901, 12903, 12909, 12912, 12913, 12915, 12916, 12918, 12920, 12922, 12923, 12924, 12926, 12928, 12930, 12932, 12934, 12935, 12936, 12937, 12939, 12944, 12945, 12947, 12950, 12951, 12952, 12954, 12956, 12958, 12959, 12960, 12962, 12963, 12964, 12966, 12967, 12968, 12971, 12972, 12976, 12977, 12981, 12982, 12983, 12984, 12985, 12986, 12990, 12991, 12992, 12993, 12994, 12996, 12999, 13001, 13002, 13003, 13008, 13009, 13011, 13013, 13017, 13018, 13019, 13021, 13023, 13024, 13026, 13028, 13034, 13037, 13038, 13039, 13040, 13043, 13046, 13047, 13048, 13050, 13051, 13052, 13056, 13059, 13064, 13067, 13068, 13075, 13077, 13078, 13080, 13081, 13082, 13083, 13084, 13086, 13087, 13088, 13089, 13091, 13095, 13098, 13100, 13102, 13103, 13104, 13107, 13109, 13112, 13114, 13115, 13123, 13124, 13125, 13127, 13128, 13129, 13135, 13141, 13142, 13143, 13145, 13149, 13150, 13151, 13154, 13163, 13164, 13166, 13168, 13170, 13171, 13172, 13175, 13179, 13180, 13181, 13183, 13185, 13186, 13189, 13190, 13191, 13195, 13200, 13202, 13203, 13210, 13211, 13212, 13216, 13218, 13219, 13222, 13226, 13227, 13228, 13229, 13230, 13231, 13232, 13234, 13236, 13240, 13241, 13242, 13243, 13244, 13246, 13247, 13248, 13249, 13250, 13252, 13256, 13259, 13260, 13261, 13265, 13267, 13269, 13272, 13273, 13274, 13275, 13277, 13279, 13284, 13287, 13288, 13290, 13293, 13295, 13296, 13298, 13301, 13302, 13305, 13306, 13307, 13309, 13310, 13312, 13315, 13316, 13318, 13319, 13322, 13324, 13326, 13327, 13328, 13329, 13330, 13331, 13333, 13335, 13336, 13338, 13339, 13341, 13342, 13343, 13345, 13348, 13349, 13355, 13356, 13357, 13362, 13368, 13369, 13370, 13373, 13374, 13376, 13377, 13378, 13380, 13381, 13383, 13384, 13385, 13390, 13391, 13392, 13394, 13396, 13399, 13401, 13402, 13403, 13405, 13406, 13407, 13408, 13409, 13410, 13412, 13415, 13416, 13418, 13419, 13420, 13422, 13423, 13424, 13426, 13428, 13429, 13430, 13433, 13438, 13441, 13444, 13445, 13446, 13447, 13449, 13450, 13451, 13453, 13455, 13457, 13458, 13461, 13463, 13472, 13474, 13475, 13476, 13479, 13485, 13486, 13488, 13491, 13492, 13493, 13494, 13496, 13499, 13501, 13504, 13505, 13506, 13508, 13510, 13513, 13514, 13516, 13517, 13518, 13520, 13530, 13532, 13533, 13536, 13537, 13540, 13543, 13544, 13545, 13546, 13547, 13548, 13549, 13555, 13556, 13557, 13558, 13560, 13567, 13569, 13570, 13571, 13572, 13573, 13574, 13576, 13578, 13580, 13581, 13583, 13585, 13587, 13588, 13592, 13594, 13595, 13596, 13597, 13599, 13600, 13601, 13607, 13611, 13612, 13613, 13620, 13624, 13626, 13629, 13631, 13632, 13633, 13635, 13636, 13637, 13638, 13641, 13644, 13645, 13646, 13648, 13654, 13660, 13663, 13665, 13668, 13672, 13673, 13676, 13677, 13683, 13685, 13686, 13688, 13689, 13690, 13692, 13693, 13694, 13698, 13701, 13703, 13705, 13707, 13708, 13709, 13710, 13711, 13712, 13713, 13714, 13715, 13717, 13718, 13721, 13723, 13724, 13725, 13727, 13729, 13731, 13732, 13734, 13737, 13738, 13739, 13741, 13742, 13743, 13744, 13745, 13747, 13748, 13750, 13751, 13757, 13758, 13760, 13761, 13763, 13764, 13765, 13766, 13767, 13768, 13769, 13773, 13775, 13779, 13780, 13783, 13784, 13786, 13787, 13789, 13790, 13793, 13794, 13796, 13804, 13810, 13811, 13815, 13822, 13823, 13825, 13826, 13836, 13837, 13838, 13839, 13840, 13841, 13843, 13845, 13847, 13850, 13852, 13853, 13855, 13857, 13858, 13859, 13860, 13861, 13862, 13866, 13867, 13870, 13871, 13873, 13874, 13875, 13878, 13880, 13882, 13883, 13886, 13888, 13889, 13891, 13892, 13895, 13898, 13899, 13901, 13902, 13903, 13906, 13911, 13912, 13913, 13914, 13915, 13916, 13917, 13919, 13925, 13927, 13928, 13929, 13931, 13932, 13935, 13936, 13940, 13941, 13946, 13949, 13952, 13953, 13954, 13955, 13959, 13960, 13963, 13973, 13977, 13978, 13979, 13982, 13983, 13984, 13987, 13991, 13994, 13997, 13998, 13999, 14000, 14001, 14002, 14003, 14005, 14006, 14008, 14009, 14010, 14011, 14012, 14015, 14016, 14020, 14023, 14024, 14027, 14030, 14036, 14038, 14039, 14040, 14041, 14043, 14046, 14047, 14049, 14050, 14053, 14054, 14057, 14059, 14060, 14062, 14064, 14065, 14066, 14068, 14069, 14071, 14072, 14075, 14077, 14078, 14080, 14088, 14089, 14091, 14092, 14096, 14097, 14099, 14100, 14101, 14103, 14106, 14107, 14110, 14111, 14114, 14115, 14117, 14120, 14122, 14124, 14125, 14126, 14127, 14133, 14137, 14139, 14141, 14142, 14143, 14144, 14145, 14146, 14147, 14156, 14157, 14158, 14159, 14160, 14163, 14165, 14166, 14170, 14171, 14173, 14176, 14177, 14184, 14185, 14187, 14188, 14192, 14194, 14196, 14198, 14199, 14202, 14203, 14204, 14206, 14211, 14213, 14214, 14215, 14216, 14217, 14219, 14220, 14222, 14224, 14226, 14227, 14230, 14233, 14234, 14235, 14236, 14238, 14239, 14241, 14242, 14244, 14245, 14246, 14248, 14250, 14252, 14253, 14254, 14255, 14256, 14257, 14258, 14260, 14262, 14263, 14264, 14265, 14268, 14269, 14270, 14271, 14272, 14276, 14277, 14282, 14285, 14286, 14287, 14288, 14289, 14291, 14292, 14294, 14296, 14299, 14301, 14302, 14303, 14305, 14309, 14310, 14311, 14312, 14318, 14319, 14320, 14321, 14322, 14323, 14328, 14329, 14330, 14331, 14332, 14334, 14339, 14342, 14345, 14350, 14354, 14356, 14360, 14361, 14364, 14366, 14367, 14369, 14377, 14378, 14380, 14383, 14385, 14386, 14387, 14388, 14389, 14390, 14391, 14392, 14393, 14394, 14395, 14396, 14398, 14399, 14401, 14402, 14403, 14404, 14406, 14407, 14410, 14411, 14413, 14414, 14415, 14416, 14417, 14418, 14419, 14420, 14424, 14425, 14426, 14427, 14435, 14437, 14441, 14443, 14445, 14446, 14447, 14448, 14451, 14452, 14453, 14454, 14456, 14459, 14460, 14465, 14474, 14475, 14476, 14479, 14483, 14485, 14486, 14488, 14489, 14490, 14492, 14496, 14498, 14511, 14512, 14513, 14514, 14515, 14516, 14519, 14522, 14523, 14524, 14527, 14528, 14530, 14531, 14532, 14533, 14536, 14538, 14540, 14541, 14542, 14543, 14545, 14546, 14549, 14552, 14555, 14556, 14557, 14558, 14561, 14564, 14565, 14567, 14572, 14577, 14579, 14581, 14582, 14586, 14587, 14591, 14592, 14593, 14597, 14599, 14600, 14601, 14602, 14605, 14607, 14608, 14611, 14613, 14614, 14616, 14619, 14623, 14625, 14626, 14627, 14628, 14629, 14630, 14631, 14633, 14634, 14635, 14637, 14639, 14643, 14644, 14647, 14649, 14650, 14654, 14656, 14658, 14660, 14662, 14664, 14665, 14667, 14669, 14670, 14671, 14672, 14673, 14676, 14677, 14679, 14681, 14686, 14687, 14689, 14691, 14693, 14694, 14698, 14701, 14702, 14708, 14710, 14711, 14712, 14713, 14715, 14717, 14721, 14725, 14727, 14728, 14731, 14732, 14736, 14741, 14743, 14744, 14748, 14749, 14750, 14753, 14756, 14757, 14758, 14759, 14761, 14762, 14763, 14764, 14765, 14766, 14767, 14768, 14769, 14771, 14772, 14774, 14777, 14778, 14779, 14780, 14781, 14784, 14785, 14786, 14794, 14796, 14797, 14799, 14800, 14806, 14807, 14808, 14811, 14812, 14814, 14815, 14817, 14819, 14820, 14823, 14824, 14825, 14827, 14828, 14829, 14830, 14831, 14834, 14835, 14836, 14838, 14839, 14841, 14843, 14844, 14845, 14849, 14852, 14853, 14854, 14857, 14860, 14862, 14864, 14868, 14872, 14875, 14879, 14881, 14882, 14884, 14885, 14887, 14892, 14893, 14894, 14896, 14897, 14898, 14899, 14901, 14902, 14903, 14906, 14909, 14910, 14912, 14913, 14915, 14918, 14922, 14925, 14929, 14930, 14931, 14932, 14933, 14935, 14938, 14939, 14941, 14944, 14946, 14947, 14948, 14949, 14950, 14953, 14954, 14956, 14960, 14961, 14965, 14966, 14967, 14968, 14973, 14974, 14975, 14976, 14979, 14982, 14985, 14986, 14987, 14988, 14989, 14991, 14993, 14994, 14995, 14996, 14997, 14998, 15000, 15001, 15003, 15005, 15006, 15009, 15011, 15012, 15013, 15014, 15018, 15020, 15023, 15026, 15029, 15030, 15031, 15032, 15033, 15034, 15035, 15036, 15037, 15039, 15041, 15043, 15044, 15046, 15048, 15049, 15050, 15051, 15053, 15055, 15056, 15057, 15058, 15063, 15064, 15065, 15067, 15068, 15072, 15073, 15077, 15078, 15080, 15081, 15083, 15085, 15086, 15087, 15090, 15091, 15092, 15094, 15095, 15097, 15098, 15101, 15102, 15103, 15104, 15105, 15107, 15108, 15109, 15116, 15120, 15121, 15122, 15124, 15125, 15126, 15128, 15137, 15138, 15139, 15141, 15142, 15144, 15152, 15155, 15158, 15160, 15161, 15162, 15165, 15166, 15168, 15169, 15170, 15172, 15173, 15175, 15176, 15177, 15181, 15182, 15184, 15185, 15186, 15187, 15194, 15196, 15197, 15200, 15205, 15210, 15212, 15214, 15215, 15217, 15218, 15219, 15220, 15222, 15224, 15225, 15226, 15229, 15230, 15231, 15233, 15235, 15236, 15238, 15241, 15242, 15243, 15244, 15245, 15249, 15250, 15253, 15254, 15255, 15256, 15258, 15259, 15263, 15264, 15267, 15268, 15270, 15273, 15274, 15277, 15278, 15280, 15285, 15286, 15288, 15289, 15290, 15291, 15292, 15294, 15297, 15298, 15302, 15304, 15305, 15309, 15310, 15315, 15319, 15323, 15326, 15327, 15328, 15331, 15332, 15333, 15334, 15337, 15338, 15340, 15341, 15343, 15346, 15348, 15350, 15351, 15352, 15354, 15357, 15358, 15360, 15361, 15366, 15367, 15368, 15369, 15372, 15373, 15374, 15375, 15376, 15378, 15379, 15380, 15381, 15383, 15384, 15385, 15386, 15387, 15391, 15392, 15393, 15396, 15398, 15400, 15402, 15403, 15405, 15406, 15412, 15413, 15414, 15416, 15419, 15421, 15422, 15423, 15424, 15427, 15428, 15432, 15433, 15435, 15436, 15437, 15439, 15441, 15442, 15443, 15445, 15446, 15449, 15450, 15451, 15452, 15453, 15456, 15462, 15463, 15464, 15466, 15468, 15470, 15474, 15477, 15479, 15481, 15483, 15485, 15486, 15488, 15489, 15491, 15492, 15493, 15494, 15497, 15499, 15501, 15503, 15510, 15511, 15514, 15515, 15520, 15521, 15523, 15525, 15526, 15527, 15528, 15529, 15534, 15535, 15537, 15539, 15546, 15547, 15549, 15551, 15552, 15554, 15557, 15560, 15561, 15563, 15565, 15567, 15569, 15570, 15571, 15573, 15574, 15575, 15577, 15578, 15579, 15583, 15585, 15587, 15588, 15590, 15593, 15594, 15595, 15597, 15598, 15602, 15605, 15606, 15608, 15609, 15613, 15614, 15616, 15619, 15625, 15629, 15630, 15632, 15633, 15635, 15641, 15646, 15647, 15648, 15651, 15654, 15659, 15663, 15665, 15666, 15667, 15668, 15669, 15670, 15671, 15673, 15675, 15677, 15680, 15683, 15684, 15685, 15687, 15688, 15689, 15692, 15696, 15699, 15703, 15704, 15705, 15708, 15710, 15711, 15713, 15714, 15718, 15719, 15722, 15724, 15726, 15728, 15729, 15730, 15732, 15733, 15736, 15739, 15740, 15743, 15744, 15745, 15746, 15747, 15748, 15750, 15752, 15754, 15761, 15763, 15765, 15768, 15772, 15773, 15774, 15775, 15777, 15778, 15779, 15783, 15784, 15786, 15787, 15788, 15789, 15792, 15795, 15796, 15799, 15800, 15801, 15802, 15804, 15805, 15806, 15807, 15808, 15810, 15811, 15814, 15815, 15818, 15824, 15827, 15828, 15831, 15832, 15835, 15836, 15838, 15840, 15841, 15842, 15844, 15846, 15847, 15848, 15849, 15850, 15851, 15852, 15853, 15854, 15857, 15858, 15859, 15860, 15862, 15863, 15869, 15871, 15874, 15875, 15877, 15880, 15883, 15884, 15889, 15891, 15892, 15894, 15896, 15903, 15904, 15905, 15906, 15907, 15908, 15915, 15917, 15918, 15920, 15922, 15924, 15927, 15929, 15933, 15934, 15936, 15937, 15939, 15941, 15943, 15944, 15945, 15948, 15950, 15951, 15955, 15956, 15958, 15959, 15961, 15962, 15963, 15965, 15968, 15971, 15973, 15975, 15976, 15978, 15979, 15980, 15981, 15985, 15987, 15988, 15989, 15990, 15991, 15994, 15997, 16003, 16006, 16007, 16010, 16011, 16013, 16015, 16016, 16019, 16020, 16023, 16025, 16026, 16029, 16030, 16031, 16032, 16036, 16037, 16038, 16040, 16043, 16044, 16045, 16048, 16052, 16060, 16062, 16063, 16065, 16069, 16070, 16071, 16072, 16076, 16077, 16078, 16080, 16081, 16082, 16083, 16084, 16088, 16089, 16091, 16092, 16094, 16095, 16096, 16098, 16102, 16103, 16105, 16107, 16108, 16111, 16114, 16115, 16116, 16117, 16120, 16121, 16122, 16126, 16127, 16130, 16132, 16134, 16136, 16139, 16140, 16146, 16149, 16151, 16156, 16157, 16158, 16161, 16163, 16165, 16166, 16167, 16168, 16169, 16177, 16180, 16181, 16182, 16183, 16188, 16189, 16192, 16193, 16195, 16196, 16198, 16199, 16201, 16202, 16203, 16206, 16210, 16211, 16214, 16215, 16217, 16222, 16223, 16226, 16228, 16229, 16232, 16234, 16235, 16236, 16237, 16238, 16239, 16243, 16251, 16252, 16258, 16261, 16263, 16268, 16270, 16273, 16274, 16275, 16279, 16280, 16281, 16285, 16287, 16290, 16292, 16294, 16295, 16296, 16299, 16302, 16304, 16305, 16306, 16307, 16309, 16312, 16318, 16320, 16322, 16326, 16329, 16330, 16331, 16337, 16340, 16342, 16343, 16344, 16345, 16346, 16350, 16351, 16352, 16353, 16354, 16356, 16358, 16359, 16360, 16364, 16366, 16370, 16372, 16375, 16377, 16379, 16381, 16386, 16388, 16391, 16394, 16395, 16398, 16399, 16401, 16405, 16409, 16414, 16419, 16422, 16426, 16427, 16429, 16430, 16434, 16435, 16436, 16440, 16443, 16445, 16446, 16448, 16455, 16456, 16457, 16460, 16461, 16462, 16466, 16467, 16469, 16470, 16471, 16472, 16474, 16481, 16483, 16484, 16485, 16487, 16490, 16491, 16493, 16494, 16495, 16496, 16497, 16499, 16501, 16502, 16503, 16504, 16505, 16506, 16508, 16509, 16515, 16516, 16518, 16521, 16522, 16523, 16524, 16529, 16530, 16531, 16533, 16535, 16537, 16539, 16540, 16544, 16545, 16546, 16551, 16557, 16559, 16561, 16567, 16568, 16570, 16571, 16572, 16576, 16580, 16581, 16582, 16583, 16585, 16586, 16587, 16588, 16589, 16590, 16591, 16592, 16593, 16596, 16598, 16600, 16601, 16603, 16604, 16606, 16609, 16610, 16619, 16622, 16625, 16626, 16628, 16631, 16638, 16643, 16645, 16647, 16650, 16651, 16656, 16657, 16659, 16660, 16662, 16664, 16665, 16667, 16671, 16672, 16675, 16680, 16681, 16683, 16685, 16686, 16689, 16692, 16693, 16694, 16695, 16697, 16698, 16700, 16703, 16708, 16710, 16711, 16714, 16715, 16716, 16717, 16722, 16729, 16731, 16732, 16733, 16734, 16736, 16737, 16738, 16739, 16742, 16743, 16747, 16751, 16754, 16755, 16758, 16760, 16761, 16762, 16763, 16766, 16768, 16770, 16772, 16773, 16774, 16776, 16778, 16782, 16785, 16786, 16788, 16791, 16793, 16795, 16796, 16797, 16803, 16804, 16808, 16811, 16812, 16813, 16817, 16818, 16819, 16820, 16822, 16825, 16826, 16829, 16831, 16834, 16840, 16846, 16849, 16850, 16853, 16855, 16856, 16858, 16859, 16860, 16861, 16863, 16865, 16866, 16867, 16868, 16869, 16870, 16871, 16872, 16873, 16878, 16879, 16881, 16883, 16887, 16891, 16898, 16901, 16902, 16905, 16907, 16910, 16912, 16914, 16915, 16917, 16919, 16921, 16922, 16923, 16924, 16926, 16927, 16928, 16930, 16931, 16932, 16933, 16934, 16935, 16936, 16938, 16943, 16945, 16947, 16953, 16954, 16955, 16956, 16957, 16959, 16960, 16961, 16962, 16963, 16964, 16965, 16966, 16967, 16968, 16969, 16975, 16978, 16982, 16983, 16990, 16991, 16996, 16998, 16999, 17000, 17002, 17003, 17006, 17008, 17011, 17012, 17015, 17023, 17026, 17027, 17028, 17029, 17030, 17032, 17035, 17036, 17040, 17041, 17042, 17045, 17046, 17048, 17050, 17051, 17053, 17055, 17057, 17058, 17060, 17062, 17063, 17065, 17068, 17074, 17075, 17076, 17077, 17079, 17080, 17082, 17084, 17085, 17086, 17088, 17090, 17092, 17094, 17095, 17096, 17097, 17100, 17101, 17102, 17103, 17107, 17108, 17110, 17112, 17114, 17117, 17119, 17122, 17125, 17128, 17129, 17134, 17136, 17137, 17138, 17139, 17143, 17148, 17149, 17150, 17151, 17153, 17154, 17156, 17157, 17158, 17160, 17163, 17164, 17166, 17167, 17172, 17174, 17175, 17176, 17178, 17179, 17180, 17184, 17186, 17190, 17192, 17193, 17196, 17198, 17200, 17203, 17205, 17207, 17208, 17211, 17212, 17213, 17215, 17216, 17223, 17226, 17227, 17229, 17231, 17232, 17234, 17236, 17237, 17239, 17240, 17242, 17244, 17245, 17246, 17247, 17250, 17252, 17254, 17255, 17260, 17261, 17263, 17266, 17267, 17270, 17271, 17272, 17273, 17274, 17277, 17279, 17280, 17282, 17284, 17286, 17287, 17288, 17289, 17291, 17292, 17293, 17294, 17295, 17296, 17299, 17300, 17304, 17306, 17308, 17310, 17311, 17314, 17316, 17317, 17318, 17322, 17324, 17325, 17326, 17330, 17331, 17332, 17334, 17335, 17338, 17344, 17345, 17348, 17349, 17350, 17352, 17357, 17359, 17361, 17363, 17366, 17370, 17378, 17384, 17387, 17388, 17389, 17390, 17391, 17393, 17396, 17397, 17399, 17406, 17407, 17409, 17410, 17411, 17412, 17414, 17415, 17416, 17419, 17422, 17425, 17426, 17429, 17430, 17431, 17435, 17436, 17437, 17438, 17440, 17441, 17443, 17447, 17450, 17452, 17454, 17456, 17458, 17459, 17461, 17462, 17463, 17464, 17465, 17466, 17467, 17469, 17473, 17475, 17476, 17477, 17482, 17484, 17485, 17486, 17487, 17488, 17490, 17492, 17494, 17498, 17500, 17505, 17506, 17507, 17508, 17509, 17511, 17514, 17515, 17518, 17521, 17523, 17524, 17527, 17529, 17531, 17533, 17534, 17536, 17538, 17539, 17540, 17541, 17542, 17546, 17547, 17550, 17552, 17555, 17558, 17559, 17560, 17564, 17565, 17566, 17567, 17569, 17570, 17571, 17573, 17574, 17575, 17580, 17582, 17583, 17586, 17587, 17589, 17590, 17593, 17596, 17599, 17601, 17602, 17603, 17604, 17606, 17608, 17628, 17633, 17637, 17638, 17639, 17641, 17647, 17648, 17649, 17651, 17652, 17655, 17658, 17659, 17660, 17668, 17674, 17675, 17676, 17679, 17680, 17681, 17682, 17685, 17686, 17687, 17688, 17691, 17692, 17693, 17694, 17695, 17696, 17698, 17701, 17703, 17704, 17705, 17707, 17708, 17709, 17710, 17712, 17715, 17716, 17717, 17721, 17722, 17725, 17726, 17727, 17730, 17731, 17735, 17736, 17739, 17742, 17743, 17744, 17745, 17747, 17749, 17750, 17751, 17755, 17756, 17757, 17758, 17763, 17766, 17772, 17773, 17775, 17776, 17778, 17782, 17783, 17785, 17786, 17787, 17788, 17789, 17790, 17791, 17792, 17793, 17795, 17796, 17797, 17798, 17801, 17804, 17805, 17806, 17808, 17809, 17810, 17812, 17817, 17819, 17820, 17823, 17824, 17825, 17826, 17827, 17833, 17834, 17837, 17838, 17839, 17840, 17841, 17843, 17844, 17847, 17848, 17849, 17852, 17853, 17854, 17855, 17856, 17859, 17860, 17864, 17868, 17869, 17870, 17871, 17872, 17873, 17874, 17875, 17876, 17879, 17881, 17882, 17883, 17884, 17888, 17890, 17892, 17896, 17898, 17900, 17901, 17902, 17903, 17904, 17908, 17914, 17915, 17919, 17920, 17923, 17925, 17926, 17928, 17930, 17931, 17933, 17934, 17935, 17937, 17939, 17940, 17943, 17944, 17946, 17947, 17948, 17949, 17952, 17954, 17955, 17956, 17960, 17964, 17965, 17966, 17969, 17970, 17971, 17972, 17973, 17981, 17982, 17984, 17990, 17991, 17993, 17994, 17995, 17996, 17997, 17999, 18002, 18005, 18006, 18009, 18013, 18016, 18017, 18018, 18021, 18022, 18023, 18024, 18026, 18028, 18032, 18033, 18035, 18037, 18041, 18047, 18048, 18049, 18050, 18051, 18053, 18054, 18056, 18057, 18059, 18060, 18062, 18063, 18065, 18066, 18069, 18072, 18073, 18075, 18077, 18078, 18079, 18082, 18084, 18085, 18090, 18092, 18093, 18095, 18096, 18097, 18099, 18100, 18102, 18103, 18104, 18106, 18107, 18109, 18110, 18111, 18115, 18118, 18122, 18123, 18124, 18125, 18127, 18132, 18134, 18135, 18137, 18138, 18141, 18143, 18145, 18146, 18147, 18149, 18152, 18153, 18157, 18158, 18160, 18161, 18164, 18166, 18167, 18169, 18175, 18177, 18182, 18183, 18184, 18188, 18189, 18191, 18192, 18193, 18194, 18196, 18197, 18199, 18200, 18201, 18203, 18204, 18205, 18209, 18211, 18212, 18214, 18216, 18220, 18223, 18227, 18228, 18229, 18231, 18232, 18233, 18234, 18236, 18243, 18245, 18246, 18247, 18251, 18267, 18268, 18269, 18270, 18271, 18273, 18275, 18277, 18278, 18280, 18284, 18287, 18288, 18289, 18291, 18294, 18295, 18300, 18301, 18302, 18304, 18305, 18307, 18308, 18309, 18314, 18317, 18318, 18320, 18321, 18322, 18323, 18324, 18328, 18331, 18334, 18335, 18337, 18339, 18340, 18345, 18346, 18350, 18352, 18353, 18356, 18358, 18360, 18363, 18364, 18367, 18371, 18372, 18374, 18376, 18378, 18379, 18380, 18383, 18385, 18387, 18390, 18392, 18393, 18394, 18397, 18398, 18401, 18404, 18406, 18408, 18410, 18413, 18414, 18416, 18417, 18418, 18421, 18424, 18425, 18428, 18429, 18434, 18438, 18439, 18441, 18442, 18445, 18446, 18447, 18448, 18449, 18450, 18455, 18458, 18459, 18462, 18464, 18466, 18467, 18471, 18472, 18474, 18475, 18479, 18480, 18481, 18482, 18483, 18484, 18486, 18487, 18494, 18495, 18496, 18498, 18499, 18500, 18505, 18506, 18507, 18508, 18510, 18512, 18513, 18514, 18515, 18517, 18518, 18519, 18521, 18523, 18524, 18525, 18527, 18528, 18529, 18530, 18531, 18532, 18533, 18535, 18537, 18538, 18540, 18542, 18543, 18551, 18552, 18556, 18557, 18558, 18561, 18563, 18566, 18568, 18571, 18574, 18575, 18576, 18578, 18579, 18581, 18582, 18583, 18584, 18585, 18587, 18588, 18592, 18593, 18595, 18596, 18598, 18601, 18607, 18608, 18613, 18616, 18617, 18618, 18619, 18620, 18623, 18625, 18626, 18628, 18631, 18633, 18634, 18635, 18636, 18637, 18644, 18646, 18647, 18648, 18649, 18650, 18654, 18655, 18659, 18661, 18664, 18667, 18669, 18671, 18672, 18673, 18674, 18677, 18680, 18681, 18683, 18686, 18687, 18694, 18697, 18699, 18700, 18702, 18703, 18704, 18705, 18708, 18709, 18711, 18713, 18714, 18715, 18719, 18721, 18722, 18723, 18725, 18726, 18727, 18729, 18731, 18732, 18734, 18736, 18737, 18740, 18741, 18743, 18744, 18746, 18749, 18751, 18752, 18754, 18755, 18758, 18759, 18761, 18763, 18765, 18766, 18767, 18770, 18771, 18772, 18774, 18782, 18783, 18784, 18785, 18789, 18790, 18791, 18792, 18794, 18796, 18805, 18807, 18813, 18815, 18817, 18820, 18823, 18825, 18826, 18829, 18831, 18832, 18833, 18835, 18837, 18840, 18841, 18843, 18844, 18845, 18847, 18848, 18849, 18850, 18851, 18852, 18853, 18855, 18856, 18857, 18858, 18863, 18864, 18865, 18869, 18870, 18872, 18873, 18874, 18875, 18877, 18878, 18879, 18880, 18884, 18885, 18887, 18888, 18889, 18891, 18892, 18894, 18895, 18898, 18899, 18901, 18903, 18904, 18906, 18910, 18911, 18913, 18917, 18919, 18922, 18924, 18925, 18927, 18928, 18929, 18933, 18935, 18942, 18945, 18946, 18948, 18951, 18954, 18955, 18958, 18961, 18965, 18966, 18970, 18974, 18977, 18979, 18981, 18982, 18984, 18985, 18988, 18991, 18993, 18994, 18995, 19000, 19002, 19006, 19008, 19010, 19011, 19013, 19014, 19015, 19016, 19017, 19026, 19027, 19028, 19033, 19034, 19036, 19037, 19038, 19050, 19051, 19052, 19053, 19055, 19058, 19059, 19060, 19062, 19064, 19065, 19066, 19073, 19074, 19077, 19078, 19086, 19087, 19091, 19092, 19094, 19095, 19097, 19100, 19101, 19104, 19106, 19111, 19113, 19115, 19118, 19122, 19124, 19127, 19129, 19131, 19133, 19134, 19135, 19136, 19138, 19140, 19141, 19143, 19145, 19151, 19152, 19154, 19155, 19156, 19157, 19159, 19161, 19163, 19164, 19173, 19175, 19176, 19177, 19179, 19180, 19182, 19183, 19185, 19187, 19188, 19189, 19192, 19193, 19197, 19200, 19203, 19204, 19206, 19208, 19212, 19214, 19216, 19217, 19218, 19220, 19225, 19230, 19232, 19236, 19237, 19240, 19242, 19243, 19244, 19245, 19246, 19248, 19251, 19252, 19253, 19254, 19255, 19256, 19259, 19260, 19261, 19264, 19270, 19273, 19275, 19276, 19280, 19281, 19283, 19286, 19287, 19288, 19290, 19294, 19295, 19300, 19305, 19307, 19308, 19311, 19312, 19313, 19316, 19319, 19325, 19327, 19328, 19335, 19336, 19340, 19341, 19344, 19345, 19346, 19347, 19348, 19349, 19352, 19354, 19355, 19356, 19358, 19359, 19362, 19363, 19364, 19366, 19368, 19370, 19371, 19374, 19375, 19377, 19378, 19379, 19381, 19385, 19387, 19389, 19390, 19392, 19393, 19394, 19396, 19397, 19400, 19401, 19403, 19404, 19405, 19407, 19408, 19410, 19411, 19413, 19416, 19417, 19419, 19420, 19421, 19422, 19423, 19426, 19428, 19429, 19430, 19432, 19435, 19436, 19439, 19441, 19444, 19448, 19451, 19453, 19456, 19457, 19458, 19460, 19461, 19462, 19463, 19464, 19465, 19466, 19467, 19469, 19471, 19472, 19477, 19478, 19480, 19481, 19485, 19486, 19487, 19493, 19494, 19497, 19498, 19499, 19500, 19501, 19502, 19504, 19509, 19513, 19514, 19518, 19520, 19521, 19523, 19524, 19525, 19526, 19527, 19534, 19535, 19536, 19538, 19539, 19541, 19542, 19548, 19549, 19551, 19552, 19553, 19554, 19558, 19560, 19563, 19564, 19566, 19567, 19569, 19570, 19571, 19572, 19573, 19575, 19578, 19580, 19581, 19582, 19584, 19585, 19587, 19589, 19590, 19591, 19593, 19596, 19599, 19602, 19604, 19607, 19608, 19610, 19611, 19613, 19615, 19617, 19618, 19621, 19622, 19623, 19627, 19635, 19636, 19639, 19643, 19644, 19645, 19646, 19651, 19652, 19653, 19655, 19656, 19657, 19659, 19661, 19662, 19663, 19667, 19670, 19671, 19672, 19674, 19675, 19677, 19679, 19684, 19687, 19692, 19695, 19696, 19697, 19698, 19699, 19704, 19707, 19709, 19710, 19711, 19713, 19714, 19720, 19722, 19723, 19724, 19726, 19727, 19729, 19730, 19731, 19733, 19734, 19735, 19741, 19744, 19748, 19749, 19750, 19751, 19755, 19756, 19758, 19762, 19763, 19764, 19765, 19767, 19777, 19778, 19780, 19782, 19783, 19784, 19786, 19787, 19790, 19792, 19793, 19797, 19799, 19800, 19802, 19804, 19807, 19810, 19816, 19820, 19823, 19824, 19825, 19826, 19828, 19829, 19830, 19831, 19837, 19839, 19841, 19844, 19847, 19848, 19849, 19850, 19852, 19854, 19856, 19857, 19859, 19861, 19862, 19864, 19865, 19868, 19870, 19871, 19875, 19876, 19877, 19878, 19880, 19881, 19884, 19885, 19888, 19889, 19890, 19892, 19893, 19895, 19897, 19898, 19901, 19904, 19905, 19906, 19908, 19909, 19910, 19914, 19918, 19919, 19922, 19923, 19925, 19926, 19928, 19929, 19930, 19932, 19935, 19936, 19940, 19941, 19943, 19944, 19945, 19950, 19951, 19952, 19953, 19955, 19956, 19958, 19959, 19962, 19963, 19967, 19968, 19970, 19971, 19973, 19976, 19977, 19979, 19983, 19985, 19987, 19990, 19991, 19992, 19993, 19994, 19996, 19998, 19999, 20000, 20002, 20006, 20007, 20011, 20013, 20014, 20022, 20026, 20027, 20028, 20030, 20032, 20035, 20036, 20037, 20039, 20041, 20044, 20045, 20046, 20051, 20052, 20053, 20054, 20057, 20059, 20060, 20063, 20064, 20066, 20068, 20071, 20073, 20075, 20076, 20079, 20081, 20083, 20086, 20087, 20088, 20089, 20097, 20099, 20101, 20103, 20106, 20116, 20118, 20126, 20127, 20128, 20132, 20133, 20135, 20138, 20142, 20144, 20146, 20147, 20148, 20149, 20150, 20152, 20153, 20155, 20156, 20158, 20159, 20160, 20163, 20167, 20170, 20171, 20173, 20174, 20179, 20181, 20182, 20190, 20192, 20195, 20198, 20199, 20200, 20201, 20204, 20205, 20208, 20210, 20211, 20214, 20215, 20218, 20219, 20223, 20224, 20227, 20228, 20229, 20230, 20232, 20234, 20235, 20239, 20240, 20241, 20242, 20244, 20250, 20257, 20260, 20263, 20265, 20266, 20268, 20269, 20271, 20274, 20275, 20277, 20279, 20280, 20283, 20284, 20285, 20286, 20287, 20288, 20291, 20292, 20293, 20294, 20299, 20300, 20301, 20302, 20303, 20304, 20305, 20306, 20308, 20309, 20311, 20312, 20313, 20314, 20323, 20325, 20326, 20327, 20328, 20329, 20330, 20331, 20334, 20336, 20340, 20341, 20342, 20347, 20349, 20353, 20354, 20358, 20360, 20363, 20364, 20366, 20367, 20371, 20376, 20377, 20382, 20383, 20385, 20386, 20389, 20390, 20394, 20397, 20400, 20401, 20403, 20404, 20405, 20406, 20407, 20409, 20412, 20413, 20414, 20417, 20418, 20419, 20421, 20423, 20424, 20427, 20429, 20432, 20434, 20435, 20437, 20438, 20440, 20442, 20446, 20447, 20448, 20450, 20453, 20454, 20455, 20458, 20461, 20462, 20464, 20467, 20468, 20469, 20471, 20472, 20473, 20476, 20477, 20481, 20485, 20486, 20489, 20492, 20495, 20497, 20498, 20499, 20500, 20501, 20502, 20503, 20504, 20510, 20512, 20514, 20517, 20518, 20521, 20522, 20524, 20525, 20527, 20530, 20531, 20533, 20537, 20538, 20542, 20543, 20547, 20550, 20551, 20554, 20555, 20556, 20557, 20559, 20560, 20562, 20565, 20568, 20569, 20570, 20574, 20576, 20577, 20578, 20584, 20587, 20588, 20589, 20590, 20591, 20592, 20593, 20594, 20595, 20598, 20600, 20602, 20603, 20604, 20605, 20607, 20608, 20609, 20611, 20612, 20614, 20615, 20618, 20622, 20623, 20624, 20626, 20627, 20629, 20633, 20634, 20636, 20637, 20639, 20641, 20642, 20643, 20644, 20650, 20651, 20652, 20653, 20654, 20656, 20663, 20665, 20667, 20668, 20669, 20670, 20671, 20672, 20673, 20678, 20679, 20681, 20682, 20684, 20685, 20686, 20688, 20690, 20694, 20695, 20696, 20697, 20698, 20700, 20701, 20704, 20707, 20708, 20709, 20710, 20717, 20718, 20719, 20720, 20721, 20722, 20724, 20725, 20730, 20731, 20732, 20734, 20735, 20737, 20741, 20743, 20744, 20746, 20747, 20751, 20753, 20754, 20755, 20756, 20759, 20765, 20766, 20768, 20771, 20773, 20774, 20775, 20781, 20783, 20786, 20788, 20789, 20792, 20794, 20798, 20804, 20805, 20808, 20811, 20812, 20814, 20815, 20816, 20818, 20819, 20820, 20823, 20825, 20826, 20827, 20828, 20832, 20834, 20836, 20837, 20839, 20840, 20841, 20843, 20844, 20847, 20848, 20850, 20855, 20857, 20858, 20861, 20864, 20865, 20866, 20867, 20873, 20874, 20877, 20879, 20882, 20884, 20888, 20889, 20890, 20894, 20904, 20905, 20906, 20909, 20910, 20911, 20912, 20913, 20914, 20916, 20917, 20918, 20919, 20920, 20922, 20923, 20925, 20926, 20927, 20932, 20933, 20934, 20938, 20939, 20940, 20943, 20949, 20951, 20952, 20956, 20957, 20959, 20961, 20962, 20964, 20969, 20970, 20972, 20973, 20974, 20975, 20976, 20979, 20980, 20981, 20982, 20984, 20986, 20991, 20993, 20995, 20996, 20998, 20999, 21000, 21001, 21004, 21006, 21007, 21008, 21009, 21010, 21011, 21016, 21021, 21022, 21023, 21025, 21027, 21030, 21033, 21034, 21035, 21037, 21039, 21041, 21046, 21047, 21048, 21049, 21052, 21053, 21054, 21055, 21056, 21065, 21066, 21069, 21071, 21074, 21076, 21078, 21079, 21081, 21082, 21085, 21086, 21087, 21090, 21091, 21092, 21093, 21094, 21095, 21096, 21098, 21100, 21102, 21103, 21105, 21106, 21107, 21108, 21113, 21115, 21117, 21118, 21119, 21121, 21122, 21123, 21124, 21126, 21128, 21129, 21134, 21138, 21141, 21144, 21145, 21147, 21148, 21149, 21152, 21154, 21156, 21157, 21158, 21159, 21161, 21164, 21165, 21166, 21167, 21169, 21171, 21172, 21173, 21183, 21185, 21187, 21188, 21190, 21194, 21195, 21198, 21199, 21201, 21202, 21205, 21206, 21208, 21210, 21211, 21213, 21214, 21215, 21216, 21219, 21222, 21224, 21225, 21227, 21228, 21232, 21233, 21234, 21235, 21237, 21239, 21240, 21241, 21244, 21247, 21252, 21253, 21254, 21255, 21256, 21257, 21258, 21263, 21264, 21267, 21269, 21272, 21277, 21278, 21279, 21280, 21283, 21286, 21287, 21292, 21293, 21297, 21300, 21301, 21302, 21304, 21305, 21309, 21311, 21312, 21314, 21318, 21319, 21320, 21322, 21323, 21327, 21328, 21329, 21332, 21333, 21335, 21338, 21340, 21342, 21346, 21347, 21348, 21349, 21350, 21351, 21353, 21354, 21356, 21357, 21358, 21360, 21362, 21363, 21364, 21367, 21369, 21372, 21373, 21374, 21375, 21376, 21377, 21380, 21382, 21383, 21384, 21385, 21388, 21389, 21393, 21394, 21396, 21398, 21399, 21402, 21406, 21407, 21409, 21411, 21414, 21415, 21416, 21417, 21418, 21419, 21421, 21422, 21423, 21424, 21431, 21432, 21433, 21434, 21437, 21438, 21440, 21446, 21448, 21449, 21450, 21452, 21454, 21460, 21462, 21464, 21466, 21468, 21473, 21475, 21476, 21478, 21479, 21481, 21482, 21483, 21485, 21486, 21492, 21497, 21500, 21501, 21503, 21504, 21506, 21509, 21510, 21513, 21515, 21516, 21517, 21519, 21520, 21522, 21524, 21525, 21526, 21527, 21529, 21530, 21531, 21534, 21535, 21537, 21539, 21544, 21545, 21546, 21548, 21549, 21553, 21554, 21559, 21560, 21562, 21564, 21567, 21569, 21570, 21573, 21574, 21575, 21579, 21580, 21582, 21583, 21584, 21586, 21587, 21588, 21589, 21593, 21594, 21595, 21599, 21601, 21603, 21604, 21606, 21608, 21609, 21610, 21612, 21613, 21616, 21619, 21620, 21624, 21626, 21629, 21631, 21632, 21634, 21636, 21637, 21638, 21639, 21642, 21643, 21645, 21647, 21648, 21650, 21651, 21652, 21653, 21657, 21660, 21661, 21662, 21664, 21665, 21671, 21672, 21673, 21674, 21676, 21678, 21679, 21680, 21681, 21683, 21685, 21687, 21689, 21692, 21694, 21695, 21697, 21698, 21700, 21701, 21702, 21703, 21704, 21705, 21707, 21710, 21713, 21716, 21718, 21719, 21721, 21724, 21726, 21727, 21729, 21732, 21734, 21737, 21741, 21742, 21744, 21746, 21755, 21756, 21757, 21759, 21760, 21763, 21767, 21769, 21770, 21771, 21773, 21776, 21777, 21778, 21779, 21781, 21783, 21788, 21790, 21792, 21793, 21795, 21796, 21797, 21799, 21800, 21803, 21804, 21805, 21806, 21807, 21809, 21812, 21813, 21815, 21816, 21817, 21818, 21819, 21820, 21822, 21823, 21827, 21831, 21833, 21837, 21839, 21842, 21844, 21845, 21847, 21849, 21851, 21852, 21855, 21856, 21857, 21858, 21862, 21864, 21865, 21866, 21868, 21869, 21871, 21873, 21877, 21880, 21881, 21882, 21883, 21888, 21889, 21891, 21893, 21896, 21897, 21903, 21905, 21906, 21908, 21909, 21911, 21912, 21916, 21919, 21921, 21924, 21927, 21929, 21931, 21933, 21934, 21936, 21937, 21938, 21940, 21941, 21942, 21943, 21944, 21945, 21946, 21949, 21950, 21953, 21954, 21955, 21958, 21959, 21960, 21961, 21966, 21967, 21968, 21969, 21971, 21973, 21974, 21975, 21976, 21978, 21979, 21980, 21981, 21982, 21983, 21985, 21986, 21987, 21988, 21991, 21992, 21993, 21996, 21997, 22007, 22012, 22013, 22015, 22017, 22019, 22020, 22021, 22024, 22029, 22030, 22033, 22034, 22037, 22038, 22040, 22041, 22044, 22046, 22047, 22048, 22050, 22051, 22054, 22058, 22059, 22060, 22063, 22065, 22066, 22067, 22070, 22071, 22072, 22075, 22079, 22080, 22081, 22084, 22089, 22092, 22095, 22096, 22097, 22098, 22100, 22103, 22104, 22107, 22108, 22109, 22110, 22112, 22113, 22115, 22116, 22117, 22119, 22121, 22123, 22125, 22128, 22130, 22134, 22135, 22137, 22139, 22140, 22143, 22144, 22145, 22147, 22148, 22150, 22152, 22153, 22154, 22156, 22158, 22161, 22162, 22163, 22164, 22167, 22170, 22171, 22175, 22176, 22178, 22179, 22180, 22181, 22182, 22183, 22184, 22186, 22187, 22188, 22191, 22197, 22198, 22201, 22203, 22204, 22206, 22208, 22209, 22210, 22214, 22215, 22218, 22223, 22224, 22225, 22226, 22229, 22230, 22233, 22234, 22235, 22238, 22240, 22244, 22245, 22246, 22251, 22252, 22255, 22260, 22261, 22264, 22265, 22267, 22269, 22270, 22271, 22273, 22274, 22277, 22278, 22281, 22282, 22286, 22287, 22288, 22289, 22293, 22294, 22295, 22296, 22299, 22302, 22303, 22310, 22311, 22313, 22314, 22315, 22316, 22317, 22318, 22320, 22322, 22324, 22327, 22329, 22330, 22335, 22337, 22338, 22339, 22340, 22343, 22345, 22346, 22347, 22349, 22352, 22353, 22355, 22358, 22359, 22360, 22361, 22363, 22364, 22366, 22367, 22369, 22371, 22373, 22375, 22376, 22377, 22378, 22381, 22384, 22389, 22390, 22391, 22392, 22394, 22398, 22402, 22403, 22410, 22411, 22414, 22415, 22416, 22417, 22419, 22421, 22422, 22428, 22429, 22433, 22437, 22438, 22443, 22446, 22448, 22453, 22454, 22456, 22458, 22459, 22461, 22467, 22468, 22471, 22472, 22478, 22479, 22480, 22483, 22485, 22486, 22488, 22492, 22494, 22496, 22497, 22498, 22499, 22504, 22505, 22509, 22511, 22512, 22513, 22515, 22516, 22517, 22518, 22523, 22525, 22526, 22528, 22529, 22530, 22531, 22532, 22536, 22537, 22539, 22541, 22542, 22544, 22545, 22548, 22550, 22551, 22552, 22553, 22554, 22555, 22556, 22558, 22559, 22563, 22568, 22569, 22571, 22572, 22573, 22575, 22578, 22580, 22582, 22583, 22588, 22589, 22590, 22591, 22592, 22594, 22598, 22599, 22600, 22601, 22602, 22603, 22610, 22614, 22618, 22622, 22623, 22624, 22625, 22626, 22627, 22628, 22630, 22632, 22633, 22634, 22637, 22639, 22640, 22643, 22647, 22648, 22654, 22655, 22656, 22660, 22661, 22665, 22666, 22667, 22673, 22675, 22676, 22681, 22682, 22684, 22686, 22688, 22691, 22692, 22695, 22699, 22700, 22703, 22705, 22707, 22708, 22710, 22711, 22712, 22713, 22720, 22725, 22726, 22727, 22728, 22733, 22734, 22735, 22736, 22740, 22741, 22746, 22747, 22748, 22749, 22752, 22753, 22756, 22757, 22760, 22761, 22762, 22764, 22765, 22766, 22767, 22768, 22777, 22780, 22782, 22787, 22788, 22793, 22795, 22796, 22797, 22799, 22801, 22805, 22806, 22810, 22815, 22816, 22818, 22819, 22822, 22825, 22828, 22830, 22833, 22834, 22841, 22842, 22843, 22848, 22849, 22850, 22851, 22853, 22854, 22856, 22857, 22859, 22861, 22864, 22866, 22867, 22871, 22877, 22880, 22882, 22884, 22885, 22886, 22894, 22897, 22898, 22901, 22903, 22904, 22905, 22906, 22909, 22910, 22911, 22912, 22914, 22916, 22917, 22919, 22920, 22921, 22922, 22923, 22930, 22931, 22932, 22933, 22934, 22936, 22937, 22939, 22941, 22946, 22948, 22949, 22952, 22955, 22956, 22958, 22961, 22962, 22964, 22967, 22970, 22971, 22973, 22974, 22975, 22976, 22978, 22980, 22981, 22987, 22990, 22992, 22994, 22996, 23005, 23009, 23013, 23014, 23018, 23020, 23021, 23023, 23024, 23030, 23034, 23035, 23037, 23039, 23040, 23043, 23045, 23047, 23048, 23050, 23051, 23053, 23054, 23055, 23056, 23058, 23059, 23060, 23061, 23064, 23065, 23067, 23070, 23072, 23075, 23076, 23077, 23078, 23080, 23081, 23082, 23083, 23086, 23087, 23089, 23090, 23091, 23092, 23094, 23096, 23098, 23100, 23101, 23104, 23106, 23109, 23110, 23111, 23113, 23114, 23119, 23120, 23121, 23124, 23125, 23126, 23127, 23129, 23130, 23132, 23135, 23136, 23137, 23140, 23145, 23147, 23150, 23155, 23162, 23163, 23164, 23168, 23171, 23173, 23178, 23179, 23184, 23186, 23187, 23188, 23192, 23193, 23195, 23197, 23200, 23204, 23205, 23211, 23215, 23219, 23221, 23226, 23228, 23229, 23230, 23231, 23232, 23233, 23236, 23239, 23242, 23244, 23246, 23249, 23251, 23253, 23254, 23256, 23258, 23261, 23262, 23263, 23264, 23269, 23272, 23273, 23274, 23276, 23277, 23278, 23280, 23283, 23284, 23285, 23287, 23289, 23291, 23294, 23296, 23298, 23300, 23303, 23305, 23306, 23310, 23311, 23313, 23314, 23315, 23316, 23320, 23323, 23324, 23325, 23326, 23327, 23329, 23331, 23333, 23337, 23338, 23340, 23341, 23345, 23346, 23348, 23351, 23354, 23356, 23357, 23358, 23360, 23361, 23364, 23365, 23367, 23371, 23373, 23374, 23376, 23380, 23381, 23382, 23384, 23388, 23389, 23390, 23392, 23397, 23399, 23401, 23402, 23403, 23406, 23408, 23411, 23414, 23416, 23422, 23423, 23424, 23428, 23430, 23432, 23435, 23437, 23438, 23440, 23441, 23443, 23444, 23447, 23448, 23452, 23454, 23455, 23457, 23459, 23460, 23462, 23463, 23466, 23469, 23470, 23471, 23476, 23477, 23479, 23483, 23485, 23486, 23487, 23488, 23489, 23490, 23491, 23495, 23496, 23500, 23501, 23504, 23505, 23507, 23508, 23509, 23511, 23512, 23516, 23518, 23519, 23523, 23528, 23531, 23532, 23533, 23534, 23535, 23539, 23542, 23543, 23544, 23549, 23551, 23555, 23556, 23557, 23558, 23560, 23562, 23565, 23566, 23567, 23569, 23570, 23571, 23572, 23573, 23574, 23575, 23576, 23579, 23581, 23582, 23583, 23584, 23585, 23586, 23587, 23589, 23593, 23594, 23595, 23599, 23600, 23602, 23612, 23615, 23617, 23622, 23623, 23626, 23628, 23630, 23633, 23635, 23636, 23637, 23639, 23641, 23645, 23646, 23649, 23651, 23652, 23654, 23655, 23660, 23662, 23670, 23671, 23672, 23674, 23676, 23679, 23680, 23682, 23683, 23684, 23687, 23688, 23690, 23691, 23695, 23697, 23701, 23703, 23709, 23710, 23712, 23713, 23715, 23716, 23718, 23719, 23720, 23721, 23723, 23724, 23726, 23727, 23729, 23730, 23732, 23735, 23740, 23741, 23743, 23744, 23746, 23747, 23748, 23749, 23750, 23753, 23757, 23759, 23761, 23762, 23763, 23768, 23770, 23775, 23777, 23778, 23779, 23781, 23785, 23789, 23790, 23792, 23794, 23802, 23803, 23804, 23806, 23809, 23810, 23812, 23813, 23815, 23819, 23821, 23822, 23824, 23825, 23826, 23827, 23829, 23830, 23832, 23833, 23834, 23835, 23836, 23837, 23838, 23839, 23840, 23841, 23843, 23846, 23847, 23848, 23849, 23853, 23854, 23855, 23857, 23858, 23859, 23861, 23862, 23865, 23866, 23867, 23869, 23872, 23876, 23879, 23880, 23882, 23883, 23884, 23886, 23889, 23891, 23895, 23897, 23900, 23903, 23904, 23905, 23906, 23909, 23910, 23911, 23913, 23914, 23915, 23916, 23918, 23919, 23920, 23921, 23922, 23923, 23924, 23925, 23927, 23930, 23932, 23934, 23936, 23939, 23944, 23945, 23946, 23948, 23949, 23950, 23951, 23953, 23958, 23959, 23962, 23966, 23968, 23969, 23970, 23971, 23973, 23975, 23976, 23977, 23980, 23986, 23987, 23988, 23990, 23991, 23992, 23996, 23997, 23999, 24001, 24003, 24006, 24008, 24011, 24013, 24021, 24023, 24026, 24027, 24028, 24030, 24033, 24038, 24043, 24045, 24046, 24047, 24048, 24050, 24051, 24053, 24054, 24055, 24056, 24058, 24059, 24061, 24062, 24064, 24065, 24066, 24069, 24072, 24074, 24076, 24080, 24085, 24086, 24088, 24091, 24092, 24095, 24096, 24097, 24098, 24099, 24106, 24108, 24109, 24112, 24114, 24115, 24119, 24120, 24123, 24124, 24125, 24127, 24128, 24131, 24133, 24145, 24147, 24148, 24149, 24152, 24153, 24154, 24155, 24158, 24160, 24163, 24165, 24168, 24169, 24170, 24171, 24172, 24174, 24178, 24179, 24183, 24187, 24189, 24190, 24193, 24195, 24196, 24199, 24200, 24201, 24202, 24204, 24205, 24206, 24207, 24212, 24215, 24216, 24218, 24219, 24223, 24225, 24226, 24227, 24229, 24233, 24234, 24235, 24236, 24240, 24241, 24242, 24244, 24249, 24251, 24253, 24254, 24255, 24259, 24261, 24263, 24265, 24269, 24270, 24272, 24274, 24275, 24278, 24279, 24281, 24282, 24283, 24284, 24288, 24291, 24293, 24294, 24295, 24297, 24298, 24301, 24303, 24304, 24306, 24308, 24309, 24311, 24312, 24313, 24315, 24317, 24319, 24322, 24325, 24326, 24328, 24329, 24330, 24331, 24333, 24334, 24335, 24339, 24343, 24344, 24345, 24346, 24347, 24349, 24350, 24351, 24352, 24353, 24355, 24357, 24358, 24359, 24360, 24361, 24362, 24364, 24365, 24367, 24368, 24373, 24375, 24382, 24383, 24384, 24385, 24387, 24389, 24390, 24391, 24393, 24395, 24397, 24399, 24401, 24403, 24405, 24406, 24412, 24413, 24414, 24415, 24417, 24419, 24421, 24423, 24424, 24427, 24432, 24434, 24435, 24436, 24437, 24438, 24442, 24443, 24446, 24447, 24449, 24450, 24451, 24455, 24458, 24460, 24464, 24467, 24469, 24471, 24472, 24473, 24474, 24476, 24480, 24482, 24484, 24485, 24488, 24490, 24491, 24492, 24495, 24496, 24497, 24498, 24503, 24506, 24510, 24512, 24514, 24516, 24517, 24519, 24521, 24522, 24523, 24524, 24528, 24529, 24531, 24532, 24534, 24540, 24542, 24543, 24545, 24547, 24548, 24552, 24554, 24556, 24559, 24561, 24564, 24565, 24567, 24572, 24573, 24575, 24584, 24586, 24587, 24591, 24594, 24597, 24598, 24599, 24601, 24602, 24604, 24607, 24608, 24609, 24615, 24619, 24622, 24625, 24626, 24627, 24629, 24634, 24635, 24636, 24638, 24639, 24642, 24643, 24644, 24645, 24646, 24647, 24651, 24653, 24654, 24656, 24657, 24660, 24661, 24662, 24665, 24672, 24673, 24678, 24681, 24683, 24684, 24685, 24686, 24689, 24690, 24692, 24693, 24699, 24700, 24701, 24702, 24703, 24705, 24707, 24710, 24712, 24713, 24715, 24716, 24720, 24721, 24724, 24726, 24729, 24731, 24732, 24734, 24736, 24737, 24738, 24739, 24740, 24742, 24745, 24746, 24747, 24749, 24750, 24751, 24752, 24753, 24754, 24758, 24760, 24761, 24763, 24766, 24767, 24772, 24774, 24776, 24777, 24778, 24780, 24782, 24784, 24785, 24786, 24787, 24790, 24792, 24793, 24797, 24800, 24806, 24807, 24809, 24810, 24813, 24815, 24817, 24819, 24822, 24824, 24825, 24826, 24827, 24829, 24831, 24833, 24834, 24836, 24837, 24838, 24839, 24840, 24841, 24843, 24848, 24850, 24852, 24853, 24855, 24857, 24858, 24860, 24862, 24863, 24864, 24865, 24866, 24867, 24868, 24869, 24871, 24872, 24874, 24876, 24884, 24885, 24888, 24889, 24891, 24892, 24893, 24894, 24897, 24900, 24907, 24909, 24911, 24914, 24919, 24920, 24923, 24925, 24928, 24930, 24931, 24932, 24933, 24936, 24938, 24940, 24941, 24942, 24943, 24946, 24947, 24948, 24949, 24950, 24955, 24958, 24959, 24962, 24966, 24967, 24969, 24971, 24972, 24973, 24977, 24978, 24980, 24983, 24985, 24987, 24991, 24993, 24995, 24997, 24998, 24999, 25000, 25003, 25005, 25008, 25010, 25011, 25012, 25015, 25016, 25017, 25020, 25022, 25023, 25025, 25026, 25030, 25032, 25034, 25035, 25037, 25038, 25039, 25040, 25044, 25046, 25047, 25048, 25050, 25051, 25055, 25058, 25059, 25062, 25063, 25068, 25073, 25075, 25076, 25079, 25082, 25084, 25085, 25086, 25087, 25089, 25091, 25092, 25094, 25096, 25097, 25098, 25100, 25103, 25104, 25105, 25106, 25107, 25108, 25118, 25119, 25121, 25124, 25125, 25126, 25128, 25129, 25138, 25139, 25142, 25144, 25145, 25146, 25148, 25150, 25153, 25155, 25156, 25157, 25159, 25160, 25161, 25162, 25164, 25166, 25167, 25168, 25171, 25175, 25178, 25179, 25180, 25182, 25183, 25187, 25193, 25194, 25195, 25197, 25198, 25201, 25202, 25203, 25206, 25207, 25208, 25209, 25210, 25211, 25212, 25214, 25215, 25216, 25218, 25223, 25225, 25229, 25230, 25231, 25235, 25237, 25238, 25239, 25242, 25243, 25244, 25246, 25249, 25251, 25253, 25255, 25262, 25263, 25266, 25267, 25269, 25271, 25272, 25273, 25275, 25276, 25278, 25281, 25285, 25286, 25287, 25289, 25292, 25293, 25297, 25299, 25300, 25301, 25302, 25304, 25306, 25310, 25314, 25317, 25319, 25321, 25322, 25323, 25324, 25326, 25327, 25328, 25332, 25333, 25336, 25338, 25340, 25342, 25343, 25344, 25345, 25346, 25348, 25354, 25356, 25357, 25358, 25359, 25360, 25364, 25366, 25368, 25370, 25372, 25375, 25376, 25377, 25381, 25385, 25388, 25391, 25394, 25395, 25398, 25403, 25405, 25406, 25407, 25408, 25413, 25414, 25415, 25416, 25419, 25424, 25429, 25431, 25432, 25433, 25437, 25440, 25441, 25442, 25443, 25448, 25449, 25454, 25457, 25458, 25460, 25463, 25466, 25467, 25468, 25469, 25471, 25473, 25475, 25477, 25480, 25481, 25483, 25484, 25487, 25489, 25491, 25492, 25495, 25497, 25503, 25505, 25507, 25508, 25509, 25510, 25516, 25518, 25519, 25521, 25522, 25523, 25525, 25529, 25531, 25532, 25533, 25536, 25538, 25539, 25540, 25542, 25544, 25546, 25550, 25552, 25554, 25556, 25564, 25565, 25567, 25569, 25573, 25574, 25575, 25576, 25577, 25581, 25582, 25583, 25585, 25586, 25589, 25590, 25591, 25598, 25601, 25602, 25603, 25604, 25608, 25609, 25611, 25612, 25613, 25614, 25615, 25616, 25618, 25619, 25621, 25622, 25626, 25628, 25635, 25639, 25641, 25643, 25644, 25645, 25646, 25649, 25652, 25653, 25656, 25657, 25662, 25664, 25665, 25666, 25667, 25668, 25670, 25673, 25675, 25678, 25679, 25680, 25681, 25682, 25685, 25686, 25687, 25692, 25694, 25697, 25699, 25702, 25703, 25704, 25707, 25708, 25710, 25711, 25715, 25717, 25719, 25722, 25723, 25724, 25725, 25726, 25727, 25728, 25729, 25730, 25734, 25735, 25736, 25737, 25738, 25739, 25740, 25741, 25742, 25744, 25745, 25747, 25748, 25750, 25752, 25753, 25757, 25758, 25759, 25761, 25763, 25764, 25765, 25766, 25767, 25770, 25771, 25772, 25773, 25774, 25775, 25776, 25777, 25779, 25782, 25783, 25784, 25785, 25786, 25787, 25789, 25790, 25793, 25794, 25795, 25797, 25799, 25800, 25802, 25803, 25807, 25809, 25810, 25812, 25813, 25815, 25820, 25821, 25822, 25825, 25829, 25830, 25831, 25833, 25835, 25837, 25838, 25840, 25841, 25842, 25843, 25845, 25848, 25850, 25851, 25854, 25855, 25856, 25858, 25859, 25861, 25862, 25866, 25867, 25869, 25870, 25875, 25876, 25878, 25882, 25883, 25886, 25888, 25892, 25893, 25895, 25896, 25899, 25906, 25908, 25910, 25911, 25913, 25915, 25918, 25919, 25922, 25924, 25925, 25928, 25929, 25930, 25932, 25933, 25937, 25940, 25941, 25944, 25947, 25950, 25952, 25953, 25958, 25961, 25962, 25963, 25965, 25966, 25967, 25970, 25973, 25975, 25986, 25987, 25992, 25993, 25996, 25997, 25999, 26002, 26005, 26006, 26008, 26011, 26014, 26015, 26016, 26018, 26020, 26023, 26024, 26025, 26028, 26031, 26032, 26033, 26035, 26036, 26037, 26038, 26039, 26040, 26042, 26044, 26047, 26048, 26051, 26053, 26055, 26060, 26065, 26066, 26068, 26069, 26070, 26071, 26072, 26073, 26074, 26075, 26079, 26081, 26083, 26084, 26086, 26088, 26090, 26091, 26092, 26093, 26095, 26097, 26098, 26099, 26100, 26103, 26104, 26108, 26109, 26111, 26114, 26116, 26120, 26121, 26123, 26124, 26125, 26126, 26129, 26130, 26131, 26132, 26135, 26137, 26140, 26142, 26143, 26148, 26152, 26153, 26156, 26158, 26159, 26161, 26162, 26163, 26165, 26166, 26169, 26170, 26172, 26173, 26178, 26180, 26185, 26189, 26190, 26192, 26193, 26194, 26195, 26204, 26208, 26211, 26212, 26216, 26217, 26218, 26221, 26224, 26227, 26228, 26231, 26233, 26234, 26237, 26238, 26241, 26242, 26243, 26244, 26246, 26247, 26248, 26249, 26251, 26252, 26254, 26258, 26260, 26265, 26266, 26267, 26268, 26271, 26272, 26273, 26274, 26275, 26277, 26279, 26282, 26283, 26288, 26291, 26293, 26297, 26298, 26300, 26301, 26306, 26308, 26310, 26312, 26313, 26314, 26316, 26318, 26321, 26325, 26327, 26329, 26334, 26336, 26337, 26340, 26341, 26344, 26348, 26350, 26352, 26362, 26366, 26370, 26374, 26376, 26380, 26381, 26382, 26383, 26386, 26387, 26391, 26392, 26393, 26394, 26400, 26401, 26407, 26410, 26412, 26413, 26414, 26415, 26416, 26417, 26421, 26422, 26426, 26428, 26431, 26432, 26434, 26435, 26437, 26439, 26440, 26441, 26446, 26447, 26448, 26450, 26451, 26454, 26455, 26456, 26457, 26458, 26460, 26461, 26462, 26464, 26467, 26470, 26473, 26475, 26478, 26480, 26487, 26488, 26490, 26491, 26493, 26494, 26497, 26498, 26504, 26505, 26506, 26510, 26513, 26517, 26518, 26519, 26520, 26521, 26525, 26527, 26528, 26535, 26536, 26537, 26538, 26541, 26547, 26549, 26550, 26551, 26554, 26555, 26557, 26559, 26560, 26561, 26565, 26566, 26572, 26578, 26581, 26583, 26586, 26593, 26595, 26598, 26599, 26601, 26606, 26608, 26609, 26610, 26611, 26612, 26614, 26615, 26617, 26618, 26619, 26622, 26623, 26625, 26628, 26630, 26634, 26635, 26638, 26639, 26641, 26643, 26644, 26645, 26646, 26648, 26649, 26651, 26655, 26656, 26657, 26658, 26661, 26662, 26665, 26666, 26667, 26669, 26670, 26671, 26674, 26675, 26676, 26677, 26678, 26679, 26680, 26681, 26687, 26688, 26693, 26695, 26696, 26700, 26701, 26702, 26707, 26708, 26709, 26710, 26711, 26714, 26715, 26718, 26721, 26723, 26726, 26728, 26731, 26732, 26733, 26734, 26738, 26739, 26742, 26743, 26750, 26751, 26753, 26754, 26756, 26757, 26759, 26760, 26763, 26764, 26766, 26767, 26768, 26769, 26770, 26772, 26773, 26775, 26776, 26782, 26784, 26787, 26791, 26794, 26796, 26797, 26798, 26800, 26802, 26805, 26809, 26813, 26814, 26816, 26818, 26819, 26827, 26828, 26833, 26835, 26841, 26844, 26845, 26846, 26847, 26851, 26852, 26853, 26855, 26856, 26861, 26862, 26863, 26866, 26868, 26869, 26870, 26871, 26872, 26873, 26875, 26877, 26880, 26882, 26883, 26885, 26888, 26889, 26891, 26892, 26896, 26897, 26899, 26900, 26903, 26906, 26907, 26908, 26911, 26912, 26914, 26918, 26919, 26921, 26924, 26925, 26930, 26932, 26934, 26935, 26938, 26940, 26941, 26942, 26943, 26944, 26945, 26946, 26947, 26950, 26956, 26957, 26960, 26962, 26963, 26966, 26968, 26970, 26972, 26973, 26977, 26979, 26985, 26986, 26988, 26989, 26991, 26993, 26995, 26996, 26998, 26999, 27000, 27001, 27002, 27004, 27006, 27009, 27011, 27013, 27016, 27018, 27020, 27021, 27023, 27025, 27026, 27027, 27028, 27029, 27030, 27031, 27033, 27034, 27035, 27036, 27037, 27038, 27039, 27040, 27042, 27043, 27045, 27046, 27048, 27049, 27050, 27052, 27056, 27060, 27062, 27066, 27067, 27068, 27069, 27070, 27072, 27075, 27082, 27084, 27086, 27087, 27088, 27090, 27092, 27093, 27098, 27100, 27102, 27105, 27106, 27107, 27108, 27109, 27111, 27114, 27115, 27121, 27126, 27128, 27133, 27137, 27138, 27140, 27141, 27145, 27147, 27149, 27151, 27152, 27153, 27154, 27155, 27156, 27158, 27159, 27162, 27166, 27167, 27168, 27169, 27171, 27173, 27176, 27177, 27178, 27179, 27180, 27181, 27182, 27183, 27189, 27194, 27196, 27198, 27200, 27202, 27204, 27207, 27215, 27220, 27221, 27224, 27226, 27227, 27228, 27231, 27235, 27239, 27240, 27244, 27245, 27247, 27248, 27249, 27250, 27252, 27253, 27254, 27255, 27256, 27258, 27259, 27260, 27262, 27263, 27265, 27269, 27270, 27271, 27274, 27275, 27279, 27280, 27281, 27283, 27285, 27287, 27288, 27291, 27292, 27296, 27298, 27299, 27302, 27303, 27304, 27305, 27308, 27310, 27311, 27313, 27314, 27315, 27316, 27318, 27322, 27325, 27327, 27328, 27329, 27330, 27331, 27333, 27334, 27338, 27339, 27344, 27349, 27350, 27352, 27354, 27357, 27360, 27361, 27363, 27367, 27370, 27372, 27373, 27374, 27377, 27378, 27379, 27382, 27383, 27384, 27387, 27388, 27390, 27391, 27395, 27396, 27397, 27399, 27402, 27403, 27404, 27405, 27407, 27409, 27415, 27416, 27418, 27420, 27421, 27423, 27424, 27425, 27426, 27428, 27430, 27432, 27433, 27436, 27440, 27441, 27442, 27443, 27445, 27446, 27447, 27449, 27450, 27453, 27455, 27456, 27460, 27466, 27469, 27471, 27472, 27473, 27474, 27477, 27478, 27480, 27481, 27484, 27489, 27490, 27491, 27494, 27495, 27496, 27497, 27498, 27508, 27509, 27511, 27515, 27518, 27520, 27522, 27524, 27525, 27527, 27532, 27535, 27536, 27539, 27543, 27544, 27545, 27547, 27551, 27552, 27554, 27557, 27558, 27562, 27565, 27566, 27568, 27569, 27571, 27573, 27578, 27583, 27584, 27586, 27587, 27588, 27590, 27591, 27592, 27594, 27596, 27597, 27599, 27602, 27608, 27610, 27617, 27619, 27621, 27622, 27627, 27628, 27630, 27634, 27635, 27636, 27640, 27644, 27648, 27649, 27652, 27655, 27656, 27657, 27658, 27660, 27668, 27669, 27671, 27674, 27675, 27676, 27678, 27680, 27681, 27682, 27683, 27685, 27687, 27688, 27691, 27692, 27693, 27694, 27695, 27696, 27700, 27701, 27702, 27703, 27705, 27708, 27709, 27710, 27716, 27718, 27720, 27721, 27723, 27725, 27726, 27727, 27728, 27729, 27730, 27731, 27732, 27735, 27738, 27739, 27740, 27742, 27743, 27745, 27747, 27748, 27751, 27752, 27753, 27756, 27758, 27759, 27761, 27764, 27766, 27772, 27773, 27776, 27777, 27780, 27781, 27783, 27784, 27789, 27791, 27793, 27794, 27796, 27798, 27800, 27803, 27804, 27807, 27808, 27809, 27815, 27817, 27818, 27821, 27822, 27823, 27825, 27828, 27832, 27833, 27834, 27836, 27837, 27838, 27839, 27841, 27843, 27844, 27845, 27846, 27847, 27849, 27850, 27851, 27852, 27853, 27854, 27859, 27861, 27862, 27863, 27864, 27865, 27867, 27868, 27869, 27870, 27871, 27872, 27873, 27878, 27881, 27885, 27887, 27888, 27889, 27891, 27894, 27895, 27896, 27897, 27898, 27900, 27902, 27904, 27905, 27907, 27908, 27909, 27910, 27911, 27912, 27913, 27914, 27915, 27919, 27920, 27921, 27923, 27924, 27926, 27929, 27930, 27931, 27932, 27933, 27935, 27936, 27940, 27942, 27943, 27945, 27946, 27947, 27948, 27949, 27952, 27957, 27960, 27961, 27963, 27964, 27970, 27972, 27974, 27978, 27981, 27986, 27989, 27991, 27994, 27996, 27998, 28000, 28001, 28002, 28004, 28007, 28009, 28010, 28013, 28016, 28021, 28022, 28028, 28029, 28031, 28035, 28036, 28039, 28040, 28043, 28045, 28047, 28048, 28051, 28052, 28055, 28056, 28058, 28059, 28066, 28067, 28068, 28069, 28070, 28072, 28076, 28078, 28079, 28082, 28083, 28084, 28086, 28088, 28089, 28090, 28096, 28097, 28098, 28099, 28104, 28105, 28108, 28113, 28114, 28118, 28121, 28126, 28127, 28128, 28131, 28141, 28142, 28143, 28144, 28145, 28147, 28150, 28151, 28153, 28155, 28159, 28162, 28163, 28166, 28168, 28169, 28170, 28171, 28172, 28174, 28176, 28177, 28179, 28184, 28188, 28189, 28190, 28192, 28193, 28197, 28199, 28200, 28201, 28202, 28206, 28207, 28210, 28212, 28215, 28218, 28221, 28223, 28225, 28231, 28232, 28233, 28234, 28235, 28236, 28241, 28242, 28243, 28246, 28248, 28250, 28251, 28252, 28254, 28255, 28256, 28257, 28267, 28268, 28273, 28275, 28276, 28277, 28278, 28280, 28281, 28282, 28285, 28288, 28291, 28292, 28293, 28294, 28297, 28301, 28302, 28304, 28306, 28307, 28308, 28309, 28311, 28312, 28313, 28314, 28326, 28327, 28328, 28329, 28333, 28335, 28336, 28344, 28345, 28348, 28349, 28350, 28351, 28352, 28353, 28355, 28357, 28363, 28367, 28370, 28371, 28373, 28375, 28376, 28380, 28383, 28384, 28385, 28387, 28389, 28391, 28398, 28399, 28400, 28401, 28404, 28405, 28407, 28408, 28409, 28410, 28411, 28412, 28413, 28415, 28422, 28423, 28428, 28429, 28430, 28431, 28432, 28434, 28435, 28436, 28437, 28440, 28442, 28445, 28446, 28452, 28455, 28460, 28461, 28462, 28463, 28464, 28466, 28468, 28471, 28472, 28475, 28477, 28478, 28479, 28481, 28482, 28483, 28484, 28485, 28486, 28487, 28488, 28489, 28491, 28494, 28495, 28496, 28497, 28501, 28506, 28507, 28510, 28511, 28514, 28515, 28516, 28519, 28522, 28524, 28526, 28528, 28530, 28532, 28533, 28535, 28536, 28538, 28542, 28543, 28544, 28546, 28547, 28548, 28550, 28551, 28553, 28557, 28560, 28561, 28563, 28564, 28565, 28566, 28568, 28569, 28573, 28574, 28575, 28578, 28579, 28583, 28584, 28587, 28589, 28592, 28593, 28595, 28598, 28601, 28602, 28604, 28605, 28608, 28609, 28610, 28611, 28612, 28615, 28616, 28617, 28619, 28620, 28621, 28624, 28626, 28630, 28631, 28632, 28633, 28634, 28635, 28636, 28638, 28644, 28645, 28647, 28648, 28650, 28652, 28654, 28656, 28657, 28659, 28660, 28662, 28663, 28664, 28666, 28667, 28668, 28670, 28672, 28673, 28674, 28676, 28677, 28680, 28681, 28684, 28685, 28686, 28687, 28688, 28689, 28690, 28694, 28695, 28696, 28697, 28698, 28699, 28700, 28702, 28703, 28704, 28706, 28707, 28708, 28710, 28711, 28712, 28714, 28715, 28721, 28723, 28725, 28727, 28732, 28733, 28734, 28735, 28736, 28737, 28738, 28740, 28742, 28743, 28744, 28745, 28746, 28748, 28749, 28750, 28751, 28752, 28753, 28754, 28756, 28757, 28758, 28762, 28763, 28765, 28771, 28776, 28780, 28782, 28784, 28788, 28791, 28794, 28795, 28798, 28799, 28802, 28803, 28804, 28807, 28812, 28815, 28816, 28817, 28820, 28821, 28823, 28828, 28829, 28831, 28832, 28833, 28834, 28835, 28836, 28837, 28839, 28840, 28844, 28845, 28846, 28847, 28848, 28852, 28855, 28856, 28858, 28859, 28861, 28862, 28863, 28864, 28865, 28866, 28868, 28869, 28873, 28874, 28875, 28878, 28880, 28882, 28883, 28884, 28886, 28891, 28892, 28893, 28894, 28896, 28898, 28903, 28904, 28906, 28908, 28909, 28910, 28914, 28916, 28917, 28919, 28924, 28925, 28927, 28928, 28929, 28930, 28932, 28934, 28935, 28936, 28937, 28938, 28939, 28940, 28943, 28946, 28948, 28950, 28951, 28953, 28954, 28956, 28959, 28962, 28964, 28966, 28968, 28970, 28973, 28974, 28976, 28977, 28978, 28984, 28986, 28987, 28988, 28989, 28990, 28992, 28993, 28994, 28995, 28996, 28997, 29000, 29001, 29005, 29007, 29011, 29012, 29015, 29016, 29018, 29019, 29021, 29022, 29023, 29024, 29025, 29028, 29030, 29031, 29036, 29038, 29040, 29041, 29044, 29047, 29048, 29050, 29054, 29055, 29056, 29058, 29059, 29060, 29061, 29062, 29063, 29066, 29067, 29072, 29074, 29080, 29081, 29082, 29083, 29084, 29086, 29087, 29088, 29089, 29090, 29092, 29095, 29097, 29098, 29099, 29101, 29105, 29106, 29112, 29113, 29120, 29121, 29125, 29127, 29128, 29131, 29132, 29133, 29134, 29137, 29138, 29141, 29144, 29146, 29147, 29149, 29151, 29152, 29153, 29154, 29155, 29160, 29162, 29166, 29170, 29171, 29172, 29174, 29175, 29179, 29180, 29182, 29186, 29188, 29190, 29193, 29194, 29198, 29199, 29201, 29202, 29204, 29205, 29206, 29211, 29214, 29215, 29218, 29220, 29221, 29222, 29223, 29227, 29230, 29231, 29233, 29234, 29236, 29237, 29238, 29240, 29241, 29243, 29245, 29247, 29248, 29249, 29250, 29252, 29253, 29255, 29257, 29258, 29260, 29261, 29262, 29266, 29267, 29270, 29271, 29273, 29279, 29280, 29284, 29286, 29291, 29295, 29296, 29298, 29300, 29301, 29303, 29305, 29308, 29309, 29310, 29312, 29314, 29316, 29319, 29322, 29323, 29324, 29328, 29329, 29330, 29331, 29332, 29334, 29335, 29337, 29338, 29339, 29341, 29344, 29348, 29350, 29352, 29353, 29356, 29357, 29359, 29360, 29363, 29369, 29370, 29371, 29373, 29374, 29375, 29376, 29379, 29380, 29381, 29383, 29385, 29386, 29388, 29389, 29392, 29393, 29394, 29395, 29397, 29398, 29400, 29401, 29405, 29410, 29411, 29412, 29416, 29419, 29424, 29425, 29428, 29429, 29430, 29431, 29434, 29437, 29439, 29441, 29447, 29448, 29450, 29452, 29453, 29455, 29460, 29461, 29462, 29464, 29465, 29466, 29469, 29470, 29472, 29475, 29477, 29479, 29482, 29486, 29487, 29489, 29490, 29491, 29492, 29495, 29498, 29499, 29500, 29501, 29502, 29503, 29504, 29508, 29509, 29510, 29511, 29512, 29515, 29516, 29517, 29524, 29525, 29526, 29528, 29530, 29531, 29532, 29537, 29538, 29541, 29543, 29544, 29547, 29548, 29549, 29552, 29553, 29555, 29558, 29560, 29562, 29563, 29564, 29567, 29568, 29570, 29572, 29573, 29575, 29576, 29578, 29579, 29582, 29585, 29589, 29590, 29591, 29595, 29596, 29598, 29601, 29602, 29605, 29606, 29607, 29608, 29610, 29615, 29616, 29617, 29618, 29619, 29620, 29621, 29623, 29631, 29633, 29635, 29636, 29639, 29642, 29643, 29647, 29649, 29650, 29651, 29652, 29653, 29655, 29663, 29664, 29665, 29668, 29671, 29672, 29673, 29676, 29678, 29683, 29685, 29686, 29688, 29691, 29694, 29695, 29697, 29701, 29702, 29704, 29705, 29710, 29711, 29713, 29717, 29718, 29720, 29721, 29726, 29728, 29730, 29732, 29734, 29737, 29751, 29752, 29754, 29755, 29756, 29760, 29761, 29765, 29766, 29767, 29768, 29769, 29773, 29775, 29776, 29777, 29779, 29781, 29782, 29783, 29784, 29786, 29787, 29788, 29790, 29793, 29795, 29796, 29797, 29799, 29801, 29802, 29804, 29806, 29807, 29808, 29810, 29812, 29814, 29818, 29819, 29820, 29823, 29824, 29827, 29829, 29830, 29832, 29833, 29834, 29836, 29837, 29840, 29847, 29849, 29850, 29853, 29856, 29857, 29858, 29860, 29864, 29868, 29870, 29871, 29873, 29874, 29875, 29876, 29879, 29880, 29881, 29883, 29884, 29886, 29889, 29890, 29892, 29893, 29894, 29895, 29897, 29898, 29901, 29904, 29906, 29907, 29908, 29911, 29913, 29914, 29915, 29916, 29920, 29921, 29923, 29927, 29929, 29931, 29932, 29935, 29936, 29938, 29940, 29941, 29945, 29947, 29948, 29957, 29959, 29960, 29963, 29964, 29966, 29967, 29969, 29971, 29972, 29973, 29974, 29977, 29981, 29982, 29983, 29984, 29985, 29986, 29987, 29991, 29992, 29993, 29994, 29998, 29999, 30001, 30002, 30003, 30004, 30006, 30008, 30010, 30011, 30013, 30015, 30020, 30021, 30023, 30024, 30025, 30026, 30027, 30028, 30031, 30033, 30035, 30036, 30038, 30041, 30046, 30049, 30053, 30055, 30056, 30058, 30059, 30060, 30061, 30062, 30063, 30064, 30067, 30068, 30069, 30072, 30073, 30079, 30080, 30081, 30082, 30083, 30086, 30087, 30088, 30089, 30090, 30092, 30097, 30099, 30100, 30102, 30106, 30109, 30110, 30111, 30112, 30115, 30117, 30118, 30119, 30120, 30121, 30122, 30126, 30128, 30132, 30133, 30134, 30137, 30139, 30141, 30142, 30143, 30144, 30145, 30146, 30147, 30150, 30151, 30152, 30155, 30156, 30157, 30158, 30163, 30166, 30170, 30172, 30173, 30174, 30175, 30178, 30179, 30180, 30183, 30187, 30189, 30192, 30194, 30195, 30197, 30198, 30201, 30204, 30206, 30210, 30212, 30215, 30218, 30219, 30220, 30223, 30224, 30225, 30226, 30228, 30229, 30231, 30232, 30237, 30240, 30241, 30243, 30245, 30246, 30248, 30249, 30250, 30251, 30253, 30258, 30261, 30262, 30263, 30269, 30270, 30273, 30275, 30279, 30280, 30281, 30282, 30286, 30288, 30289, 30290, 30291, 30292, 30293, 30294, 30296, 30297, 30299, 30302, 30303, 30304, 30306, 30308, 30309, 30310, 30312, 30314, 30316, 30319, 30320, 30321, 30322, 30323, 30324, 30326, 30329, 30330, 30331, 30336, 30340, 30344, 30345, 30346, 30347, 30349, 30350, 30351, 30355, 30356, 30357, 30360, 30363, 30367, 30371, 30374, 30375, 30377, 30378, 30379, 30385, 30386, 30391, 30392, 30395, 30397, 30400, 30403, 30404, 30405, 30409, 30412, 30413, 30414, 30416, 30417, 30419, 30421, 30427, 30429, 30434, 30436, 30437, 30438, 30439, 30440, 30441, 30442, 30443, 30444, 30446, 30447, 30455, 30456, 30457, 30458, 30463, 30464, 30465, 30467, 30468, 30476, 30478, 30479, 30480, 30482, 30488, 30489, 30497, 30501, 30503, 30505, 30507, 30511, 30512, 30515, 30516, 30517, 30518, 30521, 30522, 30526, 30527, 30529, 30531, 30535, 30539, 30541, 30542, 30547, 30553, 30555, 30556, 30557, 30563, 30564, 30566, 30567, 30568, 30569, 30571, 30575, 30577, 30578, 30579, 30580, 30581, 30582, 30584, 30585, 30588, 30591, 30593, 30596, 30597, 30599, 30600, 30601, 30604, 30605, 30607, 30608, 30609, 30610, 30612, 30613, 30619, 30624, 30625, 30628, 30630, 30633, 30634, 30638, 30640, 30641, 30642, 30643, 30644, 30646, 30647, 30648, 30649, 30650, 30653, 30656, 30658, 30659, 30660, 30661, 30662, 30665, 30667, 30671, 30672, 30677, 30678, 30679, 30680, 30682, 30684, 30685, 30687, 30688, 30691, 30692, 30693, 30694, 30696, 30697, 30698, 30699, 30701, 30703, 30704, 30706, 30707, 30710, 30711, 30712, 30714, 30716, 30719, 30721, 30725, 30729, 30730, 30735, 30736, 30737, 30739, 30740, 30741, 30743, 30744, 30745, 30747, 30748, 30753, 30754, 30755, 30757, 30758, 30760, 30763, 30768, 30769, 30770, 30771, 30773, 30775, 30776, 30777, 30783, 30784, 30785, 30788, 30791, 30792, 30796, 30797, 30798, 30800, 30801, 30802, 30803, 30804, 30805, 30806, 30812, 30814, 30818, 30820, 30827, 30829, 30832, 30833, 30835, 30838, 30840, 30841, 30843, 30844, 30848, 30849, 30850, 30854, 30858, 30859, 30860, 30862, 30863, 30865, 30867, 30869, 30872, 30873, 30875, 30876, 30879, 30880, 30881, 30883, 30884, 30888, 30890, 30891, 30893, 30896, 30898, 30899, 30900, 30901, 30905, 30912, 30916, 30919, 30921, 30923, 30924, 30928, 30929, 30930, 30933, 30934, 30935, 30937, 30939, 30940, 30946, 30947, 30949, 30951, 30952, 30953, 30957, 30959, 30960, 30961, 30962, 30963, 30964, 30965, 30970, 30974, 30975, 30977, 30978, 30979, 30983, 30987, 30988, 30989, 30990, 30995, 30996, 30998, 30999, 31000, 31001, 31011, 31012, 31014, 31017, 31018, 31019, 31023, 31024, 31026, 31028, 31031, 31033, 31034, 31036, 31037, 31039, 31041, 31043, 31045, 31046, 31048, 31049, 31050, 31055, 31056, 31058, 31059, 31060, 31061, 31064, 31067, 31070, 31072, 31077, 31078, 31081, 31084, 31088, 31089, 31090, 31092, 31093, 31094, 31096, 31097, 31102, 31103, 31104, 31105, 31106, 31107, 31108, 31114, 31115, 31116, 31117, 31119, 31120, 31126, 31128, 31129, 31130, 31131, 31134, 31135, 31136, 31137, 31143, 31148, 31151, 31156, 31159, 31166, 31167, 31170, 31172, 31173, 31174, 31179, 31182, 31185, 31186, 31187, 31188, 31189, 31190, 31191, 31192, 31193, 31197, 31199, 31202, 31205, 31206, 31208, 31209, 31210, 31211, 31212, 31214, 31215, 31216, 31217, 31219, 31222, 31226, 31228, 31229, 31233, 31234, 31235, 31238, 31240, 31242, 31247, 31253, 31255, 31256, 31257, 31258, 31261, 31265, 31267, 31268, 31271, 31272, 31275, 31276, 31277, 31278, 31280, 31281, 31282, 31283, 31284, 31289, 31290, 31293, 31294, 31297, 31301, 31305, 31306, 31307, 31309, 31312, 31314, 31318, 31319, 31320, 31321, 31323, 31325, 31326, 31327, 31328, 31331, 31332, 31333, 31335, 31336, 31338, 31339, 31341, 31342, 31343, 31344, 31345, 31346, 31348, 31350, 31353, 31355, 31358, 31360, 31364, 31365, 31367, 31369, 31371, 31372, 31373, 31374, 31375, 31377, 31379, 31380, 31382, 31384, 31385, 31390, 31394, 31395, 31398, 31399, 31408, 31409, 31414, 31415, 31417, 31418, 31419, 31423, 31424, 31425, 31428, 31429, 31430, 31432, 31433, 31435, 31437, 31438, 31440, 31441, 31442, 31443, 31448, 31449, 31451, 31452, 31453, 31458, 31459, 31460, 31461, 31464, 31466, 31467, 31468, 31469, 31470, 31474, 31475, 31477, 31478, 31479, 31481, 31484, 31485, 31489, 31492, 31493, 31494, 31497, 31498, 31499, 31502, 31504, 31506, 31510, 31513, 31521, 31522, 31523, 31528, 31529, 31530, 31532, 31533, 31536, 31538, 31540, 31543, 31547, 31550, 31553, 31556, 31560, 31565, 31566, 31567, 31569, 31570, 31572, 31573, 31575, 31577, 31579, 31580, 31582, 31584, 31586, 31590, 31592, 31593, 31594, 31595, 31596, 31598, 31600, 31602, 31605, 31607, 31609, 31610, 31611, 31617, 31619, 31620, 31628, 31629, 31631, 31632, 31633, 31635, 31637, 31638, 31639, 31640, 31642, 31643, 31646, 31647, 31648, 31650, 31653, 31654, 31655, 31662, 31663, 31664, 31665, 31666, 31667, 31668, 31669, 31671, 31672, 31674, 31676, 31677, 31678, 31679, 31680, 31684, 31685, 31688, 31690, 31691, 31692, 31694, 31695, 31696, 31701, 31702, 31704, 31705, 31711, 31712, 31714, 31715, 31717, 31718, 31720, 31723, 31728, 31731, 31732, 31733, 31735, 31737, 31740, 31746, 31749, 31750, 31752, 31753, 31754, 31757, 31758, 31760, 31761, 31762, 31763, 31765, 31766, 31768, 31770, 31772, 31773, 31774, 31776, 31778, 31782, 31786, 31788, 31789, 31790, 31793, 31795, 31797, 31798, 31800, 31802, 31805, 31812, 31815, 31817, 31819, 31824, 31825, 31826, 31829, 31832, 31834, 31835, 31836, 31839, 31840, 31841, 31843, 31844, 31847, 31850, 31854, 31855, 31856, 31857, 31858, 31862, 31863, 31865, 31867, 31868, 31869, 31872, 31873, 31874, 31875, 31877, 31878, 31879, 31880, 31882, 31883, 31885, 31891, 31893, 31897, 31899, 31905, 31909, 31910, 31912, 31913, 31917, 31919, 31920, 31922, 31923, 31925, 31926, 31928, 31929, 31930, 31931, 31933, 31935, 31940, 31942, 31943, 31944, 31945, 31947, 31949, 31950, 31951, 31952, 31957, 31960, 31961, 31964, 31965, 31967, 31972, 31973, 31974, 31976, 31977, 31978, 31979, 31980, 31982, 31985, 31986, 31987, 31988, 31990, 31991, 31992, 31993, 31994, 31996, 31997, 31999, 32000, 32001, 32004, 32006, 32010, 32013, 32016, 32017, 32018, 32019, 32021, 32022, 32024, 32025, 32026, 32027, 32029, 32030, 32031, 32034, 32036, 32037, 32038, 32039, 32044, 32047, 32048, 32049, 32051, 32053, 32054, 32058, 32059, 32060, 32061, 32064, 32067, 32069, 32071, 32074, 32076, 32077, 32079, 32080, 32081, 32082, 32085, 32087, 32089, 32091, 32092, 32094, 32095, 32102, 32103, 32104, 32106, 32107, 32108, 32110, 32111, 32112, 32114, 32116, 32120, 32122, 32124, 32125, 32127, 32128, 32130, 32131, 32135, 32139, 32140, 32141, 32142, 32143, 32144, 32146, 32147, 32148, 32150, 32153, 32154, 32159, 32160, 32165, 32168, 32171, 32172, 32175, 32177, 32178, 32179, 32181, 32182, 32185, 32187, 32188, 32189, 32195, 32196, 32199, 32200, 32201, 32205, 32206, 32209, 32211, 32213, 32214, 32215, 32217, 32219, 32220, 32222, 32223, 32224, 32228, 32230, 32233, 32235, 32236, 32237, 32238, 32242, 32247, 32248, 32249, 32250, 32252, 32254, 32258, 32259, 32261, 32262, 32265, 32266, 32268, 32269, 32270, 32271, 32272, 32273, 32275, 32276, 32278, 32279, 32282, 32283, 32285, 32287, 32288, 32291, 32294, 32295, 32296, 32297, 32300, 32301, 32303, 32304, 32306, 32312, 32313, 32316, 32318, 32319, 32321, 32322, 32325, 32329, 32330, 32336, 32340, 32343, 32346, 32349, 32352, 32353, 32355, 32357, 32362, 32363, 32364, 32366, 32367, 32369, 32371, 32372, 32374, 32376, 32380, 32384, 32389, 32393, 32395, 32396, 32398, 32399, 32403, 32409, 32410, 32411, 32413, 32414, 32416, 32419, 32420, 32423, 32424, 32426, 32427, 32428, 32430, 32432, 32435, 32438, 32440, 32442, 32444, 32446, 32449, 32451, 32455, 32456, 32457, 32461, 32462, 32470, 32471, 32472, 32473, 32475, 32476, 32477, 32479, 32480, 32481, 32482, 32484, 32485, 32486, 32488, 32489, 32491, 32494, 32496, 32497, 32500, 32501, 32502, 32505, 32506, 32508, 32510, 32511, 32517, 32518, 32519, 32523, 32524, 32526, 32528, 32529, 32530, 32531, 32534, 32538, 32539, 32543, 32544, 32547, 32548, 32549, 32550, 32559, 32561, 32564, 32568, 32569, 32570, 32571, 32572, 32575, 32577, 32578, 32579, 32580, 32581, 32582, 32583, 32587, 32588, 32595, 32596, 32597, 32598, 32600, 32601, 32603, 32606, 32614, 32618, 32619, 32620, 32623, 32627, 32629, 32632, 32633, 32636, 32637, 32638, 32639, 32640, 32643, 32644, 32647, 32648, 32649, 32651, 32653, 32654, 32656, 32657, 32660, 32661, 32662, 32667, 32669, 32670, 32671, 32672, 32673, 32675, 32676, 32677, 32678, 32681, 32682, 32685, 32690, 32693, 32695, 32696, 32697, 32698, 32704, 32705, 32707, 32709, 32710, 32711, 32713, 32715, 32716, 32719, 32720, 32721, 32722, 32723, 32724, 32725, 32726, 32730, 32731, 32732, 32733, 32735, 32736, 32739, 32741, 32743, 32744, 32745, 32746, 32747, 32749, 32751, 32752, 32753, 32755, 32756, 32759, 32761, 32766, 32767, 32768, 32771, 32774, 32775, 32777, 32779, 32785, 32792, 32793, 32794, 32795, 32801, 32804, 32805, 32807, 32811, 32812, 32813, 32814, 32815, 32817, 32821, 32824, 32826, 32828, 32829, 32830, 32831, 32833, 32834, 32837, 32839, 32840, 32842, 32843, 32850, 32851, 32853, 32854, 32855, 32856, 32859, 32862, 32864, 32865, 32868, 32869, 32871, 32872, 32874, 32877, 32878, 32879, 32881, 32883, 32885, 32886, 32888, 32889, 32890, 32891, 32892, 32893, 32894, 32897, 32898, 32899, 32901, 32903, 32906, 32907, 32908, 32917, 32922, 32923, 32924, 32926, 32928, 32929, 32930, 32931, 32932, 32933, 32934, 32935, 32937, 32938, 32941, 32942, 32943, 32944, 32946, 32947, 32948, 32950, 32951, 32952, 32954, 32956, 32957, 32958, 32960, 32961, 32965, 32966, 32969, 32971, 32975, 32976, 32977, 32978, 32982, 32984, 32985, 32989, 32991, 32992, 32998, 33000, 33004, 33008, 33010, 33012, 33013, 33015, 33016, 33020, 33021, 33024, 33025, 33027, 33028, 33030, 33037, 33039, 33041, 33042, 33043, 33044, 33045, 33048, 33053, 33054, 33057, 33060, 33061, 33063, 33064, 33065, 33066, 33071, 33072, 33073, 33076, 33080, 33081, 33085, 33086, 33088, 33089, 33093, 33095, 33096, 33098, 33099, 33100, 33102, 33104, 33107, 33108, 33109, 33111, 33112, 33113, 33114, 33115, 33116, 33118, 33119, 33120, 33123, 33126, 33127, 33130, 33136, 33137, 33138, 33141, 33146, 33148, 33153, 33155, 33156, 33157, 33159, 33161, 33164, 33165, 33171, 33174, 33175, 33177, 33178, 33179, 33180, 33181, 33183, 33184, 33188, 33189, 33190, 33192, 33193, 33194, 33197, 33199, 33202, 33203, 33206, 33207, 33208, 33214, 33216, 33217, 33218, 33219, 33220, 33221, 33224, 33225, 33226, 33227, 33230, 33233, 33234, 33236, 33241, 33244, 33245, 33248, 33250, 33251, 33252, 33253, 33254, 33256, 33258, 33260, 33262, 33263, 33264, 33266, 33267, 33269, 33270, 33272, 33275, 33280, 33281, 33289, 33295, 33300, 33301, 33305, 33306, 33307, 33308, 33310, 33311, 33317, 33318, 33320, 33324, 33328, 33329, 33331, 33333, 33334, 33335, 33336, 33337, 33338, 33340, 33342, 33343, 33345, 33350, 33351, 33353, 33355, 33356, 33357, 33361, 33363, 33364, 33367, 33368, 33370, 33372, 33373, 33378, 33379, 33380, 33383, 33384, 33386, 33387, 33388, 33390, 33391, 33392, 33393, 33396, 33398, 33399, 33403, 33406, 33407, 33408, 33409, 33410, 33411, 33413, 33417, 33418, 33419, 33422, 33427, 33428, 33431, 33433, 33435, 33436, 33439, 33440, 33441, 33442, 33444, 33445, 33446, 33450, 33451, 33452, 33453, 33454, 33456, 33457, 33458, 33459, 33460, 33462, 33464, 33468, 33471, 33472, 33473, 33474, 33479, 33481, 33483, 33485, 33487, 33488, 33489, 33491, 33492, 33493, 33494, 33496, 33498, 33499, 33500, 33502, 33503, 33504, 33506, 33508, 33510, 33514, 33516, 33517, 33519, 33521, 33524, 33525, 33527, 33529, 33531, 33533, 33534, 33535, 33536, 33540, 33544, 33547, 33548, 33549, 33550, 33552, 33553, 33555, 33556, 33559, 33560, 33566, 33568, 33572, 33574, 33575, 33578, 33579, 33580, 33583, 33584, 33587, 33588, 33589, 33592, 33593, 33594, 33595, 33597, 33598, 33599, 33600, 33602, 33603, 33604, 33607, 33609, 33612, 33617, 33618, 33620, 33622, 33623, 33628, 33629, 33630, 33631, 33635, 33637, 33639, 33646, 33648, 33654, 33655, 33657, 33658, 33659, 33661, 33664, 33666, 33669, 33672, 33673, 33674, 33675, 33676, 33678, 33682, 33683, 33684, 33685, 33687, 33688, 33690, 33691, 33692, 33693, 33694, 33699, 33703, 33704, 33705, 33706, 33708, 33710, 33711, 33712, 33714, 33717, 33722, 33723, 33725, 33729, 33731, 33733, 33734, 33735, 33737, 33739, 33741, 33743, 33744, 33747, 33749, 33750, 33753, 33754, 33757, 33763, 33764, 33767, 33770, 33771, 33776, 33778, 33780, 33781, 33782, 33784, 33786, 33788, 33790, 33791, 33795, 33797, 33800, 33801, 33802, 33803, 33804, 33810, 33811, 33813, 33818, 33819, 33820, 33824, 33826, 33828, 33831, 33833, 33834, 33835, 33836, 33838, 33839, 33842, 33846, 33847, 33848, 33849, 33850, 33851, 33854, 33855, 33856, 33859, 33863, 33865, 33870, 33871, 33874, 33877, 33888, 33890, 33891, 33892, 33893, 33894, 33895, 33896, 33898, 33899, 33901, 33902, 33903, 33904, 33905, 33906, 33912, 33913, 33914, 33915, 33916, 33917, 33918, 33920, 33921, 33923, 33924, 33925, 33928, 33933, 33935, 33938, 33939, 33941, 33942, 33945, 33950, 33951, 33953, 33954, 33957, 33958, 33962, 33966, 33968, 33972, 33973, 33974, 33976, 33977, 33982, 33984, 33985, 33987, 33988, 33992, 33995, 33999, 34000, 34002, 34003, 34004, 34006, 34010, 34011, 34014, 34015, 34018, 34019, 34021, 34022, 34025, 34030, 34032, 34037, 34039, 34040, 34045, 34046, 34048, 34049, 34051, 34052, 34054, 34057, 34059, 34060, 34063, 34070, 34071, 34075, 34076, 34077, 34078, 34080, 34085, 34086, 34087, 34089, 34090, 34093, 34094, 34095, 34096, 34097, 34100, 34102, 34103, 34104, 34105, 34106, 34107, 34111, 34118, 34120, 34121, 34122, 34124, 34127, 34128, 34132, 34137, 34144, 34146, 34147, 34148, 34150, 34151, 34152, 34154, 34156, 34157, 34164, 34167, 34169, 34170, 34173, 34174, 34176, 34177, 34179, 34181, 34189, 34191, 34192, 34194, 34195, 34196, 34201, 34204, 34205, 34207, 34210, 34213, 34214, 34215, 34220, 34221, 34222, 34226, 34228, 34229, 34230, 34235, 34238, 34250, 34251, 34253, 34254, 34255, 34257, 34258, 34259, 34261, 34263, 34267, 34271, 34272, 34273, 34274, 34275, 34279, 34280, 34281, 34282, 34284, 34286, 34289, 34291, 34292, 34294, 34295, 34297, 34298, 34299, 34300, 34302, 34308, 34310, 34311, 34312, 34313, 34316, 34317, 34319, 34320, 34322, 34323, 34325, 34326, 34327, 34328, 34331, 34332, 34336, 34337, 34338, 34339, 34342, 34346, 34350, 34351, 34353, 34354, 34355, 34356, 34357, 34358, 34360, 34362, 34363, 34365, 34366, 34367, 34371, 34376, 34377, 34378, 34379, 34380, 34381, 34382, 34383, 34387, 34388, 34391, 34392, 34393, 34397, 34398, 34400, 34403, 34406, 34408, 34409, 34410, 34411, 34412, 34416, 34417, 34419, 34420, 34422, 34423, 34429, 34432, 34437, 34438, 34440, 34442, 34443, 34446, 34447, 34448, 34449, 34451, 34452, 34453, 34455, 34457, 34458, 34459, 34460, 34462, 34463, 34467, 34471, 34473, 34476, 34477, 34480, 34489, 34492, 34495, 34497, 34498, 34499, 34501, 34503, 34507, 34508, 34510, 34511, 34513, 34514, 34517, 34520, 34521, 34522, 34525, 34526, 34527, 34528, 34529, 34530, 34531, 34534, 34537, 34538, 34541, 34543, 34544, 34545, 34546, 34547, 34552, 34557, 34558, 34559, 34560, 34561, 34562, 34564, 34566, 34568, 34569, 34570, 34573, 34574, 34575, 34576, 34577, 34579, 34580, 34581, 34582, 34583, 34584, 34586, 34587, 34589, 34591, 34592, 34593, 34595, 34597, 34598, 34602, 34604, 34606, 34607, 34609, 34610, 34614, 34615, 34616, 34617, 34618, 34619, 34621, 34622, 34624, 34625, 34629, 34631, 34633, 34634, 34637, 34638, 34640, 34642, 34646, 34647, 34649, 34651, 34652, 34653, 34655, 34656, 34657, 34658, 34660, 34661, 34665, 34666, 34668, 34670, 34672, 34674, 34675, 34679, 34680, 34682, 34683, 34684, 34689, 34690, 34693, 34695, 34697, 34698, 34700, 34702, 34704, 34708, 34711, 34714, 34715, 34717, 34718, 34719, 34722, 34724, 34725, 34727, 34729, 34730, 34733, 34736, 34737, 34738, 34739, 34740, 34742, 34744, 34745, 34749, 34750, 34752, 34755, 34756, 34757, 34758, 34759, 34762, 34765, 34767, 34769, 34771, 34772, 34774, 34775, 34776, 34777, 34778, 34779, 34780, 34781, 34784, 34786, 34788, 34789, 34790, 34792, 34793, 34794, 34795, 34797, 34799, 34801, 34802, 34805, 34807, 34808, 34809, 34810, 34812, 34813, 34815, 34820, 34821, 34824, 34826, 34828, 34829, 34830, 34831, 34833, 34834, 34835, 34837, 34839, 34841, 34842, 34843, 34844, 34848, 34859, 34863, 34865, 34866, 34868, 34869, 34871, 34872, 34873, 34874, 34880, 34884, 34885, 34887, 34888, 34889, 34890, 34893, 34894, 34895, 34896, 34897, 34899, 34900, 34901, 34902, 34904, 34906, 34907, 34912, 34914, 34916, 34917, 34918, 34920, 34921, 34923, 34925, 34926, 34927, 34928, 34930, 34931, 34932, 34936, 34938, 34941, 34944, 34946, 34949, 34950, 34953, 34954, 34957, 34958, 34960, 34961, 34964, 34970, 34973, 34974, 34975, 34977, 34978, 34979, 34980, 34981, 34982, 34984, 34985, 34992, 34993, 34994, 34995, 34996, 34997, 34999, 35002, 35004, 35007, 35009, 35010, 35012, 35014, 35017, 35018, 35019, 35022, 35023, 35024, 35025, 35026, 35028, 35029, 35032, 35033, 35035, 35037, 35043, 35046, 35047, 35048, 35049, 35050, 35053, 35055, 35058, 35060, 35061, 35063, 35064, 35066, 35068, 35074, 35078, 35079, 35080, 35082, 35083, 35087, 35088, 35089, 35090, 35091, 35092, 35093, 35094, 35095, 35096, 35099, 35102, 35103, 35105, 35106, 35107, 35110, 35113, 35114, 35116, 35119, 35121, 35123, 35124, 35125, 35126, 35127, 35128, 35130, 35131, 35132, 35133, 35134, 35137, 35138, 35139, 35146, 35147, 35148, 35150, 35151, 35153, 35154, 35156, 35157, 35161, 35162, 35163, 35166, 35167, 35168, 35169, 35173, 35175, 35180, 35184, 35186, 35187, 35188, 35189, 35190, 35194, 35197, 35200, 35201, 35203, 35204, 35205, 35207, 35208, 35210, 35211, 35212, 35213, 35214, 35215, 35216, 35217, 35218, 35219, 35221, 35222, 35223, 35226, 35227, 35228, 35230, 35231, 35234, 35235, 35237, 35238, 35239, 35240, 35247, 35248, 35251, 35252, 35253, 35256, 35259, 35260, 35262, 35266, 35267, 35268, 35274, 35276, 35277, 35281, 35282, 35284, 35285, 35287, 35290, 35293, 35295, 35300, 35306, 35307, 35308, 35311, 35313, 35315, 35317, 35318, 35320, 35321, 35324, 35325, 35326, 35327, 35328, 35330, 35331, 35332, 35333, 35335, 35336, 35339, 35342, 35345, 35347, 35348, 35349, 35350, 35354, 35356, 35358, 35360, 35362, 35363, 35365, 35369, 35371, 35372, 35377, 35378, 35379, 35382, 35384, 35386, 35388, 35391, 35392, 35393, 35395, 35399, 35405, 35409, 35410, 35412, 35414, 35419, 35420, 35422, 35423, 35424, 35425, 35426, 35428, 35431, 35433, 35434, 35436, 35438, 35439, 35440, 35441, 35442, 35444, 35445, 35449, 35453, 35455, 35457, 35459, 35460, 35461, 35462, 35463, 35464, 35465, 35467, 35468, 35469, 35472, 35473, 35477, 35478, 35480, 35482, 35485, 35486, 35487, 35488, 35490, 35491, 35492, 35495, 35497, 35499, 35500, 35502, 35503, 35505, 35508, 35517, 35518, 35519, 35521, 35523, 35526, 35528, 35530, 35531, 35532, 35533, 35534, 35536, 35537, 35538, 35540, 35543, 35547, 35548, 35550, 35551, 35552, 35557, 35558, 35559, 35561, 35562, 35563, 35564, 35565, 35566, 35568, 35570, 35572, 35577, 35581, 35583, 35586, 35587, 35589, 35590, 35591, 35592, 35593, 35595, 35596, 35597, 35598, 35600, 35604, 35605, 35606, 35608, 35611, 35612, 35615, 35618, 35619, 35620, 35623, 35624, 35629, 35630, 35631, 35632, 35633, 35634, 35637, 35638, 35639, 35640, 35641, 35642, 35647, 35649, 35650, 35652, 35653, 35654, 35655, 35656, 35660, 35665, 35666, 35667, 35669, 35670, 35673, 35674, 35675, 35676, 35677, 35678, 35680, 35681, 35685, 35686, 35688, 35689, 35690, 35693, 35697, 35698, 35701, 35702, 35703, 35704, 35705, 35707, 35708, 35711, 35712, 35713, 35714, 35718, 35719, 35723, 35726, 35727, 35729, 35730, 35731, 35732, 35733, 35736, 35739, 35740, 35742, 35743, 35746, 35748, 35751, 35753, 35759, 35762, 35763, 35764, 35765, 35766, 35767, 35771, 35772, 35773, 35776, 35777, 35779, 35784, 35785, 35791, 35792, 35796, 35798, 35799, 35801, 35802, 35807, 35808, 35809, 35810, 35813, 35814, 35815, 35817, 35819, 35820, 35824, 35829, 35833, 35834, 35835, 35837, 35840, 35841, 35843, 35844, 35848, 35849, 35850, 35851, 35852, 35854, 35855, 35856, 35863, 35867, 35872, 35873, 35877, 35879, 35880, 35882, 35883, 35885, 35887, 35888, 35889, 35890, 35891, 35893, 35894, 35895, 35896, 35897, 35901, 35902, 35905, 35906, 35908, 35909, 35911, 35912, 35913, 35915, 35916, 35920, 35921, 35922, 35923, 35925, 35928, 35931, 35934, 35939, 35941, 35942, 35944, 35948, 35949, 35950, 35951, 35952, 35956, 35957, 35962, 35963, 35966, 35967, 35968, 35969, 35973, 35975, 35977, 35978, 35980, 35981, 35983, 35985, 35986, 35988, 35993, 35994, 35996, 35997, 35998, 36004, 36006, 36007, 36008, 36010, 36012, 36015, 36017, 36020, 36021, 36023, 36025, 36028, 36029, 36032, 36036, 36039, 36040, 36041, 36042, 36050, 36051, 36052, 36057, 36058, 36061, 36062, 36066, 36067, 36068, 36069, 36070, 36071, 36072, 36073, 36074, 36075, 36076, 36077, 36078, 36080, 36083, 36085, 36086, 36091, 36093, 36096, 36097, 36099, 36102, 36104, 36105, 36106, 36107, 36111, 36112, 36114, 36115, 36117, 36118, 36120, 36121, 36123, 36124, 36127, 36128, 36132, 36135, 36137, 36138, 36139, 36140, 36141, 36145, 36148, 36149, 36150, 36151, 36156, 36158, 36159, 36163, 36164, 36165, 36168, 36169, 36171, 36172, 36173, 36175, 36177, 36178, 36182, 36183, 36184, 36188, 36189, 36190, 36191, 36192, 36193, 36194, 36195, 36196, 36197, 36199, 36201, 36203, 36205, 36206, 36208, 36210, 36211, 36214, 36215, 36218, 36219, 36221, 36222, 36225, 36227, 36229, 36232, 36239, 36242, 36244, 36245, 36250, 36254, 36256, 36258, 36261, 36262, 36263, 36264, 36265, 36267, 36268, 36269, 36271, 36272, 36273, 36274, 36275, 36277, 36278, 36279, 36280, 36282, 36283, 36285, 36287, 36290, 36291, 36293, 36296, 36298, 36302, 36303, 36304, 36307, 36309, 36310, 36314, 36315, 36316, 36321, 36323, 36324, 36326, 36330, 36331, 36332, 36334, 36335, 36339, 36340, 36344, 36346, 36347, 36348, 36351, 36353, 36354, 36356, 36357, 36359, 36360, 36364, 36365, 36366, 36369, 36370, 36371, 36373, 36374, 36375, 36377, 36378, 36382, 36383, 36384, 36385, 36386, 36387, 36388, 36389, 36391, 36392, 36397, 36400, 36404, 36407, 36411, 36412, 36413, 36414, 36417, 36419, 36420, 36421, 36423, 36424, 36429, 36431, 36433, 36436, 36438, 36439, 36443, 36446, 36448, 36449, 36450, 36458, 36459, 36460, 36461, 36463, 36464, 36465, 36466, 36470, 36471, 36472, 36473, 36474, 36478, 36479, 36480, 36483, 36484, 36486, 36488, 36490, 36491, 36492, 36493, 36494, 36502, 36505, 36508, 36510, 36514, 36517, 36518, 36522, 36523, 36528, 36529, 36532, 36534, 36535, 36536, 36537, 36539, 36540, 36541, 36542, 36548, 36551, 36552, 36554, 36557, 36559, 36560, 36561, 36563, 36564, 36566, 36567, 36568, 36572, 36576, 36580, 36581, 36582, 36583, 36592, 36595, 36596, 36597, 36598, 36600, 36602, 36604, 36605, 36606, 36608, 36609, 36610, 36613, 36614, 36615, 36618, 36619, 36620, 36622, 36625, 36628, 36629, 36634, 36635, 36636, 36637, 36638, 36639, 36642, 36643, 36645, 36646, 36647, 36650, 36651, 36652, 36654, 36656, 36658, 36659, 36661, 36662, 36664, 36666, 36668, 36671, 36673, 36674, 36677, 36678, 36679, 36680, 36681, 36683, 36684, 36686, 36690, 36694, 36695, 36697, 36699, 36700, 36703, 36706, 36707, 36711, 36712, 36713, 36715, 36717, 36718, 36719, 36723, 36724, 36725, 36727, 36731, 36732, 36733, 36737, 36738, 36739, 36740, 36741, 36744, 36745, 36747, 36749, 36750, 36752, 36753, 36754, 36756, 36758, 36759, 36760, 36761, 36762, 36763, 36764, 36766, 36768, 36770, 36771, 36773, 36774, 36776, 36778, 36780, 36782, 36783, 36786, 36789, 36791, 36792, 36794, 36797, 36800, 36806, 36807, 36808, 36813, 36815, 36817, 36819, 36820, 36821, 36822, 36823, 36826, 36827, 36829, 36830, 36833, 36836, 36838, 36840, 36842, 36845, 36846, 36847, 36850, 36851, 36855, 36856, 36865, 36867, 36869, 36874, 36876, 36879, 36882, 36884, 36885, 36889, 36894, 36895, 36896, 36901, 36904, 36905, 36907, 36908, 36912, 36914, 36918, 36919, 36921, 36922, 36923, 36924, 36925, 36932, 36933, 36935, 36936, 36937, 36939, 36944, 36945, 36947, 36948, 36949, 36950, 36952, 36953, 36955, 36956, 36958, 36959, 36960, 36961, 36964, 36965, 36969, 36970, 36971, 36974, 36975, 36977, 36979, 36980, 36981, 36982, 36983, 36986, 36988, 36989, 36991, 36992, 36995, 36997, 36999, 37001, 37002, 37003, 37004, 37005, 37008, 37010, 37012, 37016, 37017, 37020, 37021, 37022, 37023, 37024, 37025, 37027, 37031, 37034, 37036, 37037, 37038, 37039, 37040, 37044, 37046, 37048, 37050, 37056, 37058, 37060, 37061, 37063, 37064, 37069, 37070, 37071, 37074, 37075, 37077, 37081, 37082, 37083, 37084, 37091, 37092, 37093, 37096, 37097, 37099, 37100, 37101, 37102, 37104, 37105, 37106, 37107, 37108, 37109, 37110, 37111, 37112, 37113, 37114, 37115, 37117, 37118, 37119, 37120, 37122, 37124, 37133, 37134, 37135, 37139, 37141, 37142, 37143, 37144, 37145, 37147, 37148, 37149, 37152, 37155, 37157, 37159, 37160, 37161, 37164, 37167, 37169, 37171, 37172, 37173, 37174, 37175, 37176, 37177, 37178, 37179, 37180, 37181, 37184, 37186, 37187, 37189, 37191, 37193, 37195, 37196, 37197, 37198, 37200, 37201, 37202, 37204, 37208, 37211, 37212, 37216, 37217, 37219, 37220, 37223, 37229, 37230, 37232, 37235, 37236, 37238, 37239, 37241, 37243, 37246, 37252, 37253, 37256, 37258, 37260, 37263, 37266, 37267, 37269, 37270, 37272, 37274, 37278, 37280, 37281, 37283, 37284, 37287, 37289, 37291, 37293, 37294, 37299, 37300, 37305, 37311, 37313, 37315, 37318, 37320, 37325, 37327, 37328, 37329, 37330, 37333, 37334, 37337, 37338, 37340, 37342, 37349, 37350, 37351, 37352, 37354, 37356, 37357, 37360, 37363, 37367, 37368, 37370, 37373, 37376, 37377, 37379, 37381, 37382, 37383, 37385, 37386, 37387, 37389, 37393, 37395, 37396, 37398, 37400, 37403, 37404, 37405, 37407, 37415, 37416, 37418, 37420, 37421, 37425, 37427, 37429, 37431, 37433, 37436, 37439, 37444, 37445, 37448, 37449, 37452, 37454, 37459, 37463, 37464, 37469, 37470, 37472, 37473, 37474, 37475, 37478, 37479, 37480, 37482, 37483, 37484, 37488, 37489, 37490, 37491, 37492, 37493, 37494, 37495, 37499, 37500, 37505, 37506, 37507, 37509, 37510, 37511, 37513, 37514, 37518, 37519, 37521, 37523, 37529, 37532, 37533, 37534, 37535, 37537, 37538, 37539, 37540, 37542, 37543, 37544, 37546, 37548, 37550, 37552, 37553, 37554, 37555, 37556, 37560, 37563, 37566, 37569, 37574, 37579, 37583, 37585, 37586, 37588, 37590, 37594, 37596, 37597, 37599, 37601, 37602, 37608, 37609, 37610, 37612, 37613, 37614, 37615, 37617, 37620, 37628, 37630, 37632, 37634, 37635, 37637, 37638, 37642, 37644, 37646, 37647, 37649, 37651, 37652, 37653, 37654, 37655, 37658, 37659, 37660, 37661, 37663, 37664, 37668, 37669, 37671, 37672, 37673, 37674, 37679, 37680, 37684, 37686, 37688, 37689, 37691, 37692, 37693, 37694, 37696, 37699, 37700, 37701, 37702, 37704, 37711, 37712, 37713, 37714, 37716, 37718, 37719, 37720, 37721, 37723, 37725, 37726, 37727, 37728, 37730, 37732, 37739, 37740, 37742, 37744, 37745, 37747, 37749, 37750, 37751, 37752, 37754, 37755, 37756, 37758, 37759, 37760, 37761, 37765, 37767, 37769, 37770, 37771, 37773, 37776, 37777, 37783, 37784, 37786, 37788, 37790, 37792, 37793, 37795, 37796, 37797, 37804, 37805, 37806, 37810, 37812, 37813, 37814, 37815, 37817, 37818, 37820, 37821, 37823, 37827, 37828, 37833, 37834, 37841, 37844, 37845, 37850, 37852, 37853, 37854, 37856, 37857, 37859, 37860, 37865, 37866, 37867, 37868, 37870, 37873, 37875, 37877, 37878, 37881, 37885, 37886, 37887, 37888, 37889, 37890, 37891, 37892, 37899, 37902, 37904, 37905, 37906, 37908, 37910, 37911, 37912, 37915, 37916, 37917, 37919, 37923, 37929, 37930, 37932, 37933, 37935, 37936, 37937, 37938, 37939, 37941, 37944, 37947, 37948, 37950, 37951, 37954, 37957, 37959, 37961, 37963, 37965, 37969, 37971, 37972, 37983, 37984, 37986, 37987, 37988, 37993, 37998, 37999, 38002, 38005, 38006, 38008, 38009, 38010, 38011, 38014, 38018, 38019, 38020, 38021, 38022, 38024, 38026, 38029, 38030, 38033, 38034, 38037, 38040, 38041, 38043, 38047, 38048, 38049, 38050, 38051, 38054, 38056, 38058, 38059, 38060, 38061, 38062, 38066, 38068, 38071, 38075, 38076, 38077, 38078, 38080, 38081, 38083, 38085, 38086, 38088, 38090, 38094, 38095, 38096, 38097, 38099, 38100, 38101, 38104, 38108, 38110, 38114, 38115, 38117, 38121, 38122, 38123, 38125, 38129, 38130, 38131, 38132, 38134, 38135, 38137, 38138, 38139, 38140, 38141, 38143, 38144, 38146, 38148, 38151, 38154, 38156, 38160, 38161, 38163, 38165, 38166, 38167, 38171, 38173, 38178, 38179, 38180, 38185, 38186, 38187, 38188, 38190, 38193, 38194, 38197, 38199, 38201, 38205, 38206, 38207, 38211, 38213, 38214, 38215, 38216, 38218, 38220, 38221, 38224, 38226, 38227, 38228, 38229, 38231, 38232, 38233, 38235, 38237, 38238, 38240, 38244, 38245, 38247, 38249, 38251, 38252, 38254, 38256, 38257, 38259, 38262, 38264, 38267, 38268, 38269, 38271, 38273, 38274, 38275, 38279, 38282, 38284, 38285, 38286, 38289, 38291, 38293, 38295, 38296, 38305, 38310, 38312, 38313, 38316, 38317, 38318, 38320, 38321, 38325, 38326, 38327, 38328, 38330, 38331, 38332, 38335, 38336, 38340, 38344, 38346, 38347, 38348, 38350, 38353, 38354, 38355, 38356, 38357, 38359, 38361, 38364, 38366, 38368, 38369, 38378, 38379, 38381, 38386, 38387, 38388, 38392, 38397, 38399, 38401, 38402, 38403, 38413, 38416, 38419, 38426, 38428, 38429, 38432, 38433, 38434, 38436, 38438, 38439, 38440, 38441, 38442, 38443, 38446, 38450, 38454, 38456, 38457, 38458, 38460, 38461, 38462, 38464, 38466, 38472, 38477, 38478, 38479, 38483, 38485, 38487, 38489, 38490, 38494, 38496, 38501, 38502, 38504, 38505, 38507, 38508, 38509, 38511, 38512, 38513, 38514, 38517, 38521, 38522, 38530, 38533, 38536, 38540, 38541, 38542, 38544, 38546, 38548, 38549, 38550, 38551, 38555, 38557, 38558, 38562, 38563, 38567, 38569, 38570, 38571, 38572, 38576, 38577, 38578, 38579, 38580, 38581, 38582, 38583, 38586, 38588, 38589, 38590, 38591, 38595, 38597, 38599, 38601, 38603, 38604, 38606, 38608, 38609, 38611, 38612, 38613, 38615, 38617, 38618, 38620, 38624, 38625, 38626, 38628, 38632, 38634, 38635, 38637, 38641, 38642, 38643, 38645, 38646, 38648, 38649, 38651, 38652, 38653, 38655, 38656, 38657, 38658, 38659, 38660, 38661, 38663, 38664, 38670, 38675, 38678, 38680, 38681, 38682, 38684, 38685, 38686, 38687, 38689, 38692, 38696, 38700, 38702, 38704, 38707, 38709, 38710, 38711, 38712, 38713, 38714, 38716, 38719, 38720, 38722, 38725, 38728, 38730, 38731, 38733, 38734, 38737, 38738, 38739, 38747, 38750, 38753, 38756, 38759, 38760, 38762, 38765, 38767, 38768, 38772, 38773, 38774, 38777, 38779, 38781, 38785, 38786, 38793, 38794, 38795, 38797, 38799, 38800, 38802, 38804, 38806, 38808, 38815, 38816, 38821, 38823, 38824, 38827, 38828, 38829, 38830, 38831, 38837, 38838, 38839, 38840, 38841, 38845, 38851, 38858, 38862, 38863, 38865, 38869, 38870, 38873, 38874, 38876, 38877, 38878, 38882, 38883, 38884, 38886, 38888, 38889, 38891, 38892, 38897, 38900, 38905, 38908, 38909, 38910, 38911, 38914, 38915, 38916, 38917, 38918, 38922, 38926, 38927, 38928, 38929, 38932, 38933, 38934, 38938, 38940, 38941, 38943, 38948, 38949, 38951, 38952, 38954, 38956, 38960, 38963, 38966, 38971, 38974, 38975, 38976, 38977, 38981, 38986, 38989, 38990, 38992, 38993, 38996, 38997, 38998, 39001, 39003, 39004, 39006, 39007, 39011, 39012, 39013, 39014, 39016, 39017, 39019, 39024, 39026, 39027, 39028, 39029, 39030, 39033, 39036, 39037, 39045, 39048, 39049, 39050, 39051, 39052, 39053, 39056, 39059, 39060, 39061, 39062, 39065, 39066, 39068, 39070, 39072, 39075, 39077, 39078, 39083, 39086, 39088, 39089, 39094, 39096, 39099, 39100, 39101, 39102, 39107, 39108, 39113, 39114, 39115, 39116, 39117, 39118, 39119, 39123, 39124, 39134, 39135, 39136, 39137, 39138, 39139, 39140, 39141, 39142, 39143, 39144, 39145, 39147, 39151, 39152, 39158, 39161, 39162, 39163, 39164, 39165, 39169, 39171, 39175, 39176, 39177, 39178, 39180, 39181, 39182, 39184, 39185, 39186, 39187, 39189, 39190, 39191, 39194, 39199, 39203, 39205, 39208, 39209, 39211, 39215, 39216, 39218, 39219, 39220, 39221, 39222, 39224, 39225, 39227, 39228, 39229, 39232, 39233, 39234, 39237, 39240, 39244, 39245, 39252, 39253, 39254, 39259, 39260, 39261, 39263, 39265, 39267, 39270, 39271, 39273, 39275, 39278, 39279, 39280, 39281, 39282, 39283, 39285, 39290, 39292, 39294, 39295, 39296, 39299, 39300, 39302, 39303, 39309, 39311, 39312, 39313, 39316, 39318, 39319, 39320, 39322, 39324, 39325, 39326, 39327, 39328, 39329, 39331, 39333, 39334, 39335, 39336, 39340, 39342, 39347, 39351, 39352, 39353, 39354, 39357, 39358, 39359, 39360, 39361, 39362, 39363, 39366, 39367, 39368, 39370, 39374, 39376, 39378, 39379, 39380, 39381, 39384, 39385, 39386, 39388, 39390, 39394, 39395, 39401, 39403, 39404, 39405, 39407, 39409, 39410, 39411, 39412, 39413, 39415, 39417, 39418, 39419, 39424, 39425, 39434, 39436, 39440, 39441, 39442, 39443, 39444, 39445, 39447, 39448, 39449, 39450, 39451, 39455, 39456, 39459, 39461, 39466, 39468, 39469, 39470, 39471, 39473, 39474, 39475, 39476, 39484, 39487, 39488, 39489, 39491, 39492, 39493, 39494, 39495, 39496, 39500, 39501, 39502, 39503, 39504, 39506, 39508, 39510, 39512, 39513, 39515, 39516, 39519, 39522, 39523, 39524, 39526, 39530, 39531, 39535, 39538, 39539, 39540, 39541, 39542, 39543, 39544, 39548, 39551, 39552, 39557, 39562, 39564, 39571, 39572, 39573, 39578, 39583, 39585, 39586, 39588, 39590, 39593, 39594, 39595, 39601, 39603, 39604, 39605, 39608, 39609, 39610, 39611, 39613, 39615, 39616, 39618, 39620, 39621, 39622, 39623, 39624, 39625, 39627, 39629, 39630, 39631, 39634, 39638, 39639, 39640, 39642, 39643, 39645, 39651, 39653, 39654, 39655, 39656, 39658, 39659, 39661, 39662, 39663, 39665, 39667, 39672, 39674, 39676, 39677, 39679, 39680, 39681, 39683, 39686, 39688, 39689, 39690, 39692, 39694, 39695, 39698, 39699, 39703, 39707, 39708, 39709, 39713, 39717, 39718, 39726, 39730, 39734, 39736, 39737, 39738, 39742, 39743, 39748, 39752, 39753, 39755, 39756, 39758, 39759, 39762, 39765, 39766, 39770, 39773, 39775, 39777, 39780, 39782, 39783, 39784, 39785, 39786, 39788, 39792, 39793, 39794, 39796, 39803, 39804, 39805, 39806, 39807, 39809, 39810, 39811, 39813, 39814, 39815, 39816, 39821, 39823, 39825, 39826, 39827, 39829, 39831, 39833, 39834, 39835, 39836, 39837, 39838, 39839, 39840, 39841, 39842, 39844, 39845, 39848, 39849, 39854, 39855, 39856, 39857, 39859, 39860, 39862, 39865, 39868, 39869, 39870, 39871, 39873, 39874, 39876, 39879, 39881, 39882, 39884, 39887, 39888, 39892, 39895, 39896, 39898, 39900, 39901, 39903, 39905, 39906, 39908, 39909, 39910, 39918, 39924, 39925, 39926, 39927, 39928, 39930, 39933, 39934, 39939, 39940, 39942, 39943, 39946, 39947, 39951, 39952, 39953, 39958, 39960, 39962, 39965, 39966, 39967, 39968, 39970, 39971, 39973, 39974, 39976, 39977, 39978, 39979, 39980, 39983, 39985, 39986, 39993, 39995, 39996, 39997, 40000, 40001, 40004, 40007, 40009, 40010, 40017, 40018, 40020, 40023, 40024, 40029, 40032, 40033, 40034, 40035, 40037, 40038, 40042, 40044, 40045, 40048, 40049, 40050, 40054, 40055, 40057, 40058, 40060, 40065, 40067, 40068, 40073, 40074, 40075, 40077, 40079, 40080, 40088, 40089, 40090, 40092, 40093, 40094, 40096, 40097, 40100, 40101, 40102, 40103, 40104, 40106, 40109, 40110, 40113, 40115, 40116, 40117, 40118, 40119, 40123, 40125, 40127, 40129, 40131, 40133, 40134, 40136, 40137, 40139, 40141, 40143, 40146, 40147, 40148, 40149, 40150, 40152, 40159, 40162, 40163, 40165, 40166, 40167, 40169, 40171, 40172, 40173, 40177, 40178, 40180, 40181, 40182, 40183, 40184, 40185, 40187, 40190, 40191, 40193, 40194, 40195, 40196, 40197, 40198, 40199, 40200, 40203, 40207, 40208, 40211, 40215, 40218, 40222, 40223, 40226, 40228, 40229, 40230, 40234, 40235, 40239, 40243, 40244, 40245, 40246, 40247, 40248, 40249, 40250, 40251, 40253, 40254, 40255, 40256, 40258, 40260, 40264, 40265, 40266, 40267, 40268, 40269, 40271, 40272, 40273, 40275, 40276, 40279, 40280, 40282, 40283, 40284, 40287, 40289, 40291, 40292, 40293, 40295, 40296, 40297, 40299, 40302, 40303, 40304, 40309, 40312, 40317, 40320, 40322, 40326, 40327, 40328, 40329, 40331, 40332, 40333, 40334, 40335, 40338, 40340, 40342, 40343, 40344, 40348, 40352, 40353, 40358, 40359, 40361, 40362, 40366, 40370, 40374, 40375, 40377, 40379, 40380, 40381, 40383, 40384, 40387, 40390, 40391, 40393, 40394, 40397, 40399, 40403, 40406, 40407, 40409, 40410, 40412, 40414, 40415, 40416, 40417, 40418, 40420, 40421, 40422, 40428, 40430, 40431, 40432, 40434, 40435, 40438, 40441, 40442, 40443, 40444, 40445, 40447, 40448, 40449, 40450, 40452, 40454, 40458, 40461, 40465, 40469, 40470, 40471, 40472, 40475, 40476, 40482, 40483, 40484, 40487, 40491, 40495, 40496, 40497, 40498, 40499, 40500, 40501, 40502, 40504, 40505, 40506, 40508, 40509, 40510, 40513, 40514, 40516, 40518, 40520, 40523, 40526, 40527, 40529, 40530, 40532, 40535, 40539, 40540, 40543, 40545, 40546, 40549, 40551, 40552, 40555, 40556, 40558, 40559, 40560, 40562, 40564, 40565, 40568, 40569, 40572, 40574, 40575, 40578, 40582, 40584, 40586, 40590, 40591, 40594, 40597, 40600, 40601, 40602, 40606, 40607, 40609, 40610, 40611, 40612, 40613, 40614, 40616, 40619, 40621, 40623, 40626, 40628, 40630, 40631, 40635, 40637, 40639, 40640, 40642, 40643, 40644, 40645, 40646, 40647, 40648, 40651, 40653, 40654, 40655, 40658, 40663, 40664, 40666, 40667, 40668, 40670, 40671, 40675, 40676, 40677, 40680, 40681, 40682, 40685, 40686, 40689, 40692, 40693, 40694, 40695, 40697, 40700, 40701, 40704, 40705, 40708, 40711, 40712, 40716, 40718, 40719, 40720, 40723, 40728, 40730, 40731, 40735, 40741, 40743, 40745, 40748, 40749, 40753, 40756, 40757, 40758, 40760, 40761, 40763, 40765, 40766, 40768, 40772, 40773, 40778, 40779, 40781, 40785, 40786, 40787, 40788, 40789, 40793, 40796, 40797, 40802, 40805, 40807, 40808, 40809, 40811, 40812, 40813, 40814, 40815, 40817, 40818, 40820, 40821, 40822, 40825, 40828, 40830, 40832, 40835, 40837, 40838, 40840, 40842, 40844, 40845, 40846, 40849, 40850, 40852, 40855, 40856, 40857, 40859, 40860, 40863, 40864, 40865, 40869, 40870, 40871, 40874, 40876, 40877, 40880, 40881, 40885, 40888, 40889, 40890, 40893, 40895, 40896, 40899, 40902, 40906, 40907, 40909, 40910, 40914, 40915, 40918, 40921, 40922, 40928, 40929, 40931, 40934, 40936, 40942, 40943, 40945, 40946, 40947, 40948, 40949, 40950, 40952, 40954, 40962, 40965, 40968, 40969, 40971, 40973, 40975, 40976, 40978, 40981, 40983, 40989, 40990, 40991, 40992, 40997, 41001, 41002, 41006, 41008, 41009, 41012, 41014, 41016, 41017, 41019, 41020, 41022, 41023, 41025, 41027, 41028, 41029, 41030, 41032, 41035, 41037, 41038, 41039, 41040, 41043, 41047, 41051, 41054, 41055, 41056, 41057, 41060, 41061, 41063, 41065, 41066, 41067, 41069, 41071, 41072, 41073, 41078, 41079, 41082, 41089, 41091, 41092, 41093, 41096, 41098, 41099, 41102, 41103, 41105, 41106, 41107, 41108, 41111, 41112, 41119, 41121, 41124, 41125, 41127, 41131, 41132, 41133, 41135, 41136, 41137, 41138, 41139, 41140, 41141, 41142, 41143, 41144, 41145, 41146, 41147, 41148, 41150, 41151, 41153, 41154, 41157, 41158, 41161, 41163, 41168, 41170, 41171, 41172, 41174, 41175, 41177, 41178, 41179, 41183, 41184, 41186, 41187, 41190, 41191, 41193, 41194, 41198, 41200, 41201, 41202, 41203, 41204, 41205, 41206, 41207, 41210, 41214, 41216, 41217, 41218, 41219, 41224, 41226, 41227, 41236, 41237, 41238, 41240, 41241, 41242, 41247, 41250, 41252, 41255, 41257, 41261, 41262, 41264, 41265, 41266, 41274, 41277, 41281, 41284, 41286, 41287, 41288, 41290, 41291, 41295, 41296, 41302, 41306, 41311, 41312, 41313, 41314, 41315, 41318, 41321, 41322, 41328, 41329, 41331, 41335, 41336, 41338, 41343, 41349, 41352, 41355, 41357, 41358, 41359, 41360, 41362, 41363, 41365, 41373, 41375, 41377, 41379, 41380, 41381, 41382, 41383, 41384, 41385, 41386, 41387, 41389, 41392, 41395, 41396, 41397, 41398, 41400, 41401, 41402, 41403, 41407, 41409, 41410, 41412, 41414, 41415, 41417, 41418, 41421, 41422, 41424, 41427, 41428, 41429, 41432, 41434, 41436, 41437, 41440, 41441, 41443, 41446, 41449, 41450, 41452, 41454, 41455, 41457, 41458, 41462, 41465, 41467, 41469, 41471, 41474, 41475, 41476, 41477, 41478, 41480, 41485, 41486, 41487, 41489, 41490, 41491, 41493, 41495, 41496, 41497, 41498, 41499, 41500, 41502, 41504, 41511, 41512, 41515, 41519, 41523, 41524, 41528, 41529, 41531, 41534, 41537, 41538, 41539, 41540, 41554, 41555, 41556, 41557, 41559, 41563, 41565, 41566, 41567, 41570, 41571, 41573, 41577, 41578, 41580, 41582, 41583, 41585, 41588, 41590, 41591, 41592, 41594, 41597, 41598, 41599, 41600, 41601, 41607, 41611, 41614, 41618, 41621, 41623, 41624, 41625, 41628, 41629, 41633, 41634, 41635, 41638, 41642, 41644, 41645, 41648, 41649, 41650, 41652, 41655, 41657, 41660, 41662, 41663, 41665, 41668, 41669, 41670, 41672, 41675, 41676, 41677, 41678, 41679, 41682, 41683, 41685, 41688, 41691, 41692, 41697, 41698, 41701, 41702, 41703, 41704, 41705, 41706, 41707, 41709, 41714, 41715, 41716, 41718, 41722, 41723, 41724, 41726, 41728, 41729, 41730, 41731, 41733, 41747, 41749, 41751, 41753, 41755, 41758, 41759, 41760, 41761, 41763, 41764, 41767, 41768, 41770, 41772, 41775, 41776, 41780, 41781, 41783, 41788, 41789, 41790, 41791, 41793, 41794, 41795, 41796, 41797, 41798, 41799, 41802, 41803, 41804, 41805, 41807, 41809, 41811, 41812, 41816, 41817, 41818, 41820, 41830, 41831, 41832, 41839, 41842, 41843, 41845, 41846, 41851, 41857, 41858, 41859, 41860, 41862, 41863, 41867, 41871, 41872, 41874, 41875, 41878, 41880, 41882, 41886, 41887, 41889, 41890, 41893, 41899, 41900, 41905, 41912, 41913, 41916, 41918, 41921, 41922, 41924, 41927, 41931, 41934, 41937, 41942, 41943, 41944, 41945, 41946, 41948, 41950, 41951, 41953, 41955, 41957, 41958, 41961, 41963, 41965, 41966, 41967, 41968, 41972, 41977, 41980, 41981, 41982, 41984, 41987, 41989, 41990, 41997, 42002, 42006, 42007, 42011, 42012, 42013, 42014, 42015, 42016, 42018, 42019, 42021, 42022, 42024, 42025, 42030, 42032, 42035, 42036, 42038, 42039, 42040, 42042, 42043, 42048, 42050, 42051, 42052, 42054, 42055, 42057, 42059, 42060, 42062, 42065, 42070, 42073, 42074, 42076, 42079, 42081, 42083, 42085, 42088, 42090, 42091, 42096, 42098, 42099, 42101, 42104, 42107, 42109, 42111, 42112, 42114, 42116, 42117, 42123, 42128, 42129, 42130, 42132, 42133, 42135, 42139, 42140, 42141, 42142, 42145, 42146, 42147, 42150, 42152, 42154, 42157, 42159, 42160, 42161, 42163, 42164, 42166, 42167, 42169, 42170, 42171, 42172, 42173, 42174, 42176, 42177, 42179, 42182, 42185, 42186, 42187, 42188, 42189, 42190, 42191, 42193, 42194, 42196, 42198, 42201, 42207, 42210, 42211, 42213, 42214, 42219, 42220, 42221, 42224, 42227, 42228, 42229, 42230, 42237, 42240, 42241, 42242, 42243, 42245, 42246, 42247, 42248, 42250, 42251, 42257, 42258, 42260, 42261, 42263, 42264, 42265, 42266, 42269, 42272, 42276, 42278, 42280, 42281, 42287, 42288, 42289, 42291, 42292, 42294, 42296, 42301, 42302, 42303, 42304, 42309, 42310, 42312, 42313, 42314, 42315, 42316, 42317, 42318, 42319, 42320, 42322, 42324, 42326, 42327, 42330, 42332, 42335, 42337, 42338, 42339, 42340, 42342, 42348, 42349, 42350, 42352, 42357, 42359, 42360, 42361, 42366, 42367, 42368, 42369, 42373, 42374, 42379, 42380, 42384, 42386, 42387, 42388, 42391, 42395, 42396, 42397, 42399, 42400, 42403, 42404, 42405, 42408, 42410, 42411, 42413, 42414, 42416, 42418, 42422, 42424, 42431, 42434, 42437, 42438, 42444, 42446, 42450, 42451, 42452, 42454, 42458, 42459, 42460, 42465, 42466, 42468, 42469, 42472, 42473, 42475, 42480, 42483, 42484, 42485, 42486, 42487, 42490, 42492, 42497, 42502, 42504, 42505, 42506, 42507, 42509, 42513, 42514, 42515, 42518, 42521, 42523, 42524, 42526, 42527, 42530, 42531, 42533, 42534, 42535, 42537, 42541, 42543, 42545, 42547, 42548, 42554, 42556, 42558, 42560, 42561, 42562, 42564, 42565, 42566, 42570, 42574, 42575, 42576, 42583, 42585, 42587, 42590, 42591, 42594, 42595, 42596, 42597, 42598, 42600, 42601, 42603, 42604, 42607, 42609, 42613, 42614, 42615, 42616, 42617, 42618, 42619, 42620, 42621, 42623, 42627, 42637, 42638, 42639, 42640, 42641, 42642, 42645, 42646, 42647, 42648, 42649, 42650, 42652, 42653, 42657, 42659, 42660, 42661, 42663, 42664, 42665, 42667, 42669, 42677, 42681, 42683, 42684, 42685, 42686, 42688, 42690, 42691, 42693, 42696, 42699, 42700, 42701, 42702, 42704, 42706, 42707, 42708, 42709, 42710, 42711, 42712, 42715, 42716, 42717, 42722, 42726, 42729, 42730, 42735, 42736, 42740, 42742, 42749, 42751, 42754, 42757, 42760, 42763, 42765, 42768, 42769, 42770, 42771, 42776, 42780, 42781, 42784, 42785, 42787, 42792, 42793, 42795, 42797, 42798, 42799, 42801, 42805, 42808, 42809, 42810, 42813, 42814, 42815, 42816, 42819, 42820, 42821, 42822, 42823, 42825, 42827, 42829, 42830, 42831, 42832, 42834, 42837, 42838, 42841, 42842, 42843, 42844, 42845, 42846, 42847, 42851, 42853, 42855, 42857, 42858, 42859, 42860, 42861, 42863, 42864, 42865, 42868, 42869, 42870, 42872, 42873, 42874, 42876, 42877, 42878, 42880, 42881, 42885, 42888, 42890, 42891, 42892, 42896, 42897, 42898, 42899, 42900, 42901, 42902, 42907, 42910, 42912, 42921, 42922, 42924, 42928, 42930, 42931, 42933, 42934, 42936, 42937, 42939, 42941, 42942, 42943, 42947, 42954, 42957, 42958, 42959, 42961, 42963, 42964, 42966, 42967, 42968, 42971, 42972, 42973, 42975, 42976, 42978, 42979, 42982, 42985, 42986, 42987, 42993, 42996, 42999, 43000, 43002, 43004, 43005, 43007, 43008, 43009, 43010, 43011, 43017, 43018, 43019, 43020, 43022, 43023, 43025, 43027, 43029, 43030, 43031, 43032, 43033, 43034, 43035, 43037, 43039, 43042, 43043, 43046, 43047, 43049, 43051, 43053, 43054, 43055, 43062, 43065, 43066, 43067, 43069, 43070, 43071, 43072, 43074, 43076, 43077, 43078, 43079, 43081, 43083, 43084, 43086, 43088, 43090, 43095, 43096, 43098, 43103, 43107, 43109, 43110, 43111, 43113, 43114, 43116, 43119, 43120, 43121, 43122, 43126, 43128, 43131, 43133, 43134, 43135, 43136, 43138, 43144, 43145, 43150, 43151, 43154, 43156, 43157, 43160, 43161, 43162, 43164, 43166, 43167, 43170, 43172, 43177, 43182, 43183, 43184, 43187, 43188, 43190, 43191, 43192, 43193, 43194, 43195, 43196, 43199, 43201, 43203, 43205, 43207, 43210, 43211, 43215, 43217, 43219, 43220, 43221, 43226, 43227, 43229, 43231, 43235, 43241, 43244, 43245, 43246, 43250, 43251, 43253, 43254, 43255, 43256, 43257, 43259, 43260, 43261, 43262, 43264, 43267, 43271, 43274, 43275, 43287, 43288, 43289, 43290, 43291, 43292, 43295, 43296, 43298, 43300, 43302, 43303, 43306, 43307, 43311, 43313, 43315, 43317, 43318, 43321, 43322, 43324, 43327, 43331, 43332, 43333, 43334, 43336, 43338, 43339, 43340, 43341, 43344, 43346, 43347, 43349, 43350, 43351, 43352, 43353, 43354, 43358, 43363, 43366, 43367, 43368, 43369, 43372, 43374, 43375, 43376, 43378, 43381, 43387, 43389, 43390, 43391, 43393, 43398, 43400, 43402, 43404, 43405, 43407, 43408, 43413, 43414, 43415, 43416, 43418, 43420, 43421, 43422, 43423, 43425, 43426, 43427, 43428, 43429, 43431, 43433, 43434, 43437, 43438, 43439, 43441, 43446, 43447, 43448, 43449, 43452, 43453, 43454, 43455, 43456, 43457, 43458, 43459, 43460, 43461, 43462, 43465, 43466, 43469, 43471, 43472, 43477, 43481, 43483, 43485, 43487, 43490, 43491, 43493, 43497, 43500, 43501, 43502, 43503, 43507, 43510, 43511, 43517, 43518, 43520, 43522, 43523, 43524, 43528, 43531, 43535, 43537, 43539, 43540, 43544, 43545, 43547, 43552, 43553, 43559, 43562, 43563, 43565, 43567, 43569, 43570, 43571, 43572, 43575, 43577, 43580, 43581, 43582, 43584, 43585, 43588, 43592, 43595, 43597, 43599, 43600, 43601, 43602, 43603, 43604, 43607, 43608, 43609, 43611, 43612, 43615, 43618, 43619, 43621, 43623, 43628, 43630, 43632, 43634, 43635, 43636, 43637, 43638, 43639, 43641, 43642, 43643, 43644, 43645, 43646, 43647, 43649, 43650, 43651, 43656, 43659, 43660, 43663, 43664, 43665, 43666, 43667, 43670, 43671, 43673, 43674, 43676, 43677, 43678, 43679, 43681, 43683, 43686, 43687, 43688, 43690, 43693, 43694, 43695, 43696, 43698, 43702, 43705, 43707, 43713, 43714, 43716, 43717, 43722, 43723, 43724, 43725, 43728, 43729, 43731, 43732, 43734, 43735, 43736, 43737, 43741, 43743, 43745, 43746, 43748, 43751, 43752, 43757, 43759, 43760, 43762, 43763, 43765, 43766, 43767, 43769, 43772, 43773, 43783, 43786, 43787, 43792, 43795, 43797, 43798, 43800, 43803, 43804, 43805, 43806, 43808, 43810, 43813, 43818, 43822, 43827, 43832, 43835, 43836, 43839, 43840, 43842, 43844, 43845, 43846, 43849, 43850, 43852, 43853, 43855, 43856, 43857, 43858, 43859, 43860, 43861, 43863, 43864, 43870, 43871, 43873, 43874, 43875, 43876, 43877, 43880, 43881, 43882, 43885, 43887, 43888, 43889, 43890, 43892, 43897, 43898, 43899, 43902, 43903, 43905, 43907, 43909, 43912, 43913, 43915, 43916, 43918, 43922, 43937, 43939, 43942, 43944, 43945, 43947, 43948, 43949, 43950, 43951, 43954, 43957, 43958, 43959, 43960, 43962, 43963, 43964, 43966, 43968, 43970, 43975, 43976, 43977, 43978, 43981, 43982, 43983, 43984, 43986, 43991, 43993, 43995, 43997, 43998, 44001, 44006, 44007, 44009, 44010, 44013, 44015, 44016, 44019, 44021, 44022, 44024, 44025, 44026, 44027, 44030, 44031, 44032, 44033, 44035, 44036, 44037, 44041, 44042, 44044, 44045, 44046, 44047, 44048, 44051, 44054, 44059, 44068, 44071, 44072, 44073, 44074, 44075, 44078, 44079, 44081, 44087, 44088, 44089, 44095, 44097, 44098, 44100, 44102, 44103, 44105, 44107, 44109, 44110, 44111, 44112, 44114, 44115, 44119, 44120, 44121, 44122, 44125, 44127, 44131, 44132, 44133, 44136, 44137, 44140, 44141, 44145, 44149, 44150, 44152, 44161, 44163, 44164, 44166, 44167, 44168, 44169, 44170, 44171, 44173, 44175, 44176, 44178, 44179, 44186, 44187, 44189, 44190, 44192, 44194, 44195, 44197, 44198, 44201, 44203, 44204, 44208, 44209, 44211, 44217, 44218, 44222, 44225, 44226, 44228, 44230, 44231, 44235, 44238, 44240, 44241, 44242, 44243, 44246, 44251, 44252, 44253, 44255, 44256, 44258, 44260, 44262, 44263, 44264, 44269, 44270, 44271, 44275, 44276, 44278, 44279, 44280, 44281, 44283, 44286, 44287, 44288, 44289, 44292, 44293, 44294, 44295, 44297, 44298, 44299, 44303, 44304, 44305, 44307, 44309, 44310, 44311, 44312, 44315, 44316, 44317, 44320, 44322, 44323, 44324, 44325, 44327, 44328, 44332, 44334, 44336, 44337, 44339, 44340, 44342, 44343, 44345, 44348, 44349, 44350, 44351, 44352, 44358, 44362, 44364, 44368, 44369, 44370, 44373, 44377, 44381, 44383, 44386, 44387, 44389, 44390, 44392, 44393, 44396, 44397, 44398, 44399, 44400, 44406, 44407, 44408, 44409, 44410, 44415, 44417, 44419, 44422, 44423, 44424, 44425, 44426, 44427, 44428, 44429, 44430, 44431, 44432, 44433, 44436, 44437, 44438, 44443, 44445, 44446, 44449, 44452, 44455, 44457, 44461, 44462, 44466, 44467, 44468, 44470, 44472, 44475, 44476, 44478, 44482, 44484, 44486, 44487, 44490, 44491, 44494, 44495, 44502, 44503, 44510, 44512, 44515, 44518, 44522, 44523, 44526, 44528, 44529, 44532, 44533, 44535, 44537, 44540, 44541, 44542, 44543, 44544, 44546, 44547, 44548, 44549, 44550, 44552, 44554, 44555, 44556, 44559, 44561, 44563, 44564, 44566, 44567, 44568, 44569, 44571, 44575, 44577, 44579, 44581, 44582, 44583, 44586, 44588, 44590, 44592, 44593, 44594, 44595, 44596, 44597, 44599, 44601, 44604, 44607, 44608, 44609, 44611, 44614, 44616, 44617, 44618, 44624, 44626, 44628, 44629, 44633, 44634, 44638, 44640, 44641, 44646, 44651, 44653, 44654, 44655, 44657, 44658, 44659, 44660, 44665, 44667, 44669, 44671, 44672, 44674, 44679, 44680, 44682, 44683, 44684, 44685, 44687, 44689, 44692, 44694, 44697, 44699, 44702, 44707, 44710, 44711, 44712, 44715, 44717, 44718, 44721, 44725, 44726, 44727, 44728, 44729, 44733, 44734, 44736, 44741, 44745, 44746, 44749, 44750, 44752, 44753, 44754, 44755, 44756, 44757, 44763, 44764, 44767, 44771, 44772, 44773, 44774, 44775, 44779, 44780, 44782, 44785, 44789, 44792, 44794, 44796, 44798, 44801, 44806, 44809, 44810, 44811, 44812, 44816, 44817, 44819, 44820, 44821, 44824, 44825, 44827, 44828, 44830, 44833, 44834, 44835, 44838, 44839, 44841, 44843, 44844, 44845, 44846, 44847, 44849, 44850, 44853, 44854, 44855, 44859, 44861, 44862, 44863, 44864, 44865, 44866, 44870, 44871, 44872, 44874, 44875, 44878, 44880, 44881, 44882, 44884, 44886, 44887, 44889, 44893, 44895, 44896, 44897, 44898, 44899, 44901, 44903, 44904, 44906, 44908, 44910, 44911, 44918, 44919, 44920, 44921, 44922, 44925, 44926, 44934, 44935, 44937, 44938, 44939, 44941, 44944, 44945, 44946, 44951, 44955, 44956, 44957, 44958, 44961, 44962, 44963, 44964, 44965, 44966, 44968, 44972, 44973, 44974, 44975, 44976, 44980, 44981, 44982, 44983, 44984, 44985, 44986, 44987, 44988, 44990, 44993, 44994, 44996, 44997, 44999, 45000, 45001, 45007, 45009, 45011, 45012, 45013, 45014, 45015, 45016, 45018, 45021, 45023, 45024, 45025, 45026, 45027, 45032, 45033, 45034, 45036, 45037, 45038, 45041, 45044, 45048, 45049, 45050, 45052, 45055, 45056, 45058, 45059, 45061, 45062, 45064, 45067, 45068, 45070, 45073, 45074, 45078, 45079, 45080, 45084, 45085, 45087, 45088, 45091, 45092, 45095, 45096, 45097, 45098, 45100, 45101, 45105, 45107, 45109, 45110, 45114, 45117, 45119, 45120, 45122, 45124, 45126, 45127, 45128, 45131, 45132, 45133, 45135, 45138, 45141, 45145, 45146, 45147, 45149, 45150, 45151, 45155, 45156, 45157, 45160, 45161, 45163, 45165, 45167, 45168, 45174, 45177, 45179, 45180, 45181, 45182, 45185, 45186, 45187, 45191, 45193, 45194, 45195, 45196, 45197, 45199, 45200, 45201, 45203, 45204, 45208, 45210, 45211, 45219, 45220, 45221, 45222, 45224, 45225, 45228, 45229, 45231, 45232, 45236, 45237, 45240, 45243, 45248, 45249, 45250, 45251, 45252, 45253, 45254, 45256, 45259, 45261, 45264, 45265, 45267, 45271, 45272, 45274, 45276, 45278, 45281, 45286, 45287, 45288, 45289, 45290, 45292, 45293, 45297, 45301, 45302, 45306, 45307, 45308, 45309, 45310, 45311, 45313, 45314, 45323, 45324, 45326, 45327, 45330, 45331, 45332, 45333, 45334, 45336, 45339, 45340, 45343, 45344, 45346, 45347, 45351, 45352, 45357, 45358, 45359, 45360, 45361, 45367, 45368, 45369, 45370, 45371, 45372, 45375, 45376, 45378, 45379, 45380, 45382, 45384, 45387, 45389, 45390, 45391, 45392, 45396, 45397, 45398, 45404, 45406, 45409, 45410, 45415, 45417, 45419, 45421, 45422, 45423, 45425, 45426, 45431, 45433, 45434, 45435, 45436, 45438, 45439, 45443, 45444, 45449, 45451, 45452, 45454, 45456, 45464, 45465, 45466, 45468, 45470, 45472, 45475, 45476, 45478, 45479, 45480, 45481, 45482, 45483, 45485, 45487, 45490, 45492, 45494, 45496, 45497, 45500, 45501, 45505, 45507, 45512, 45514, 45516, 45517, 45519, 45520, 45522, 45523, 45525, 45528, 45533, 45539, 45540, 45541, 45542, 45544, 45550, 45551, 45552, 45553, 45556, 45557, 45558, 45559, 45562, 45564, 45565, 45566, 45569, 45570, 45573, 45574, 45576, 45582, 45584, 45587, 45588, 45589, 45590, 45591, 45594, 45595, 45597, 45599, 45601, 45604, 45605, 45606, 45607, 45609, 45612, 45613, 45614, 45615, 45616, 45619, 45620, 45622, 45623, 45625, 45626, 45630, 45631, 45632, 45633, 45634, 45636, 45637, 45640, 45641, 45646, 45649, 45650, 45652, 45653, 45654, 45658, 45660, 45661, 45664, 45666, 45668, 45675, 45680, 45681, 45685, 45686, 45689, 45690, 45693, 45695, 45696, 45698, 45704, 45707, 45708, 45715, 45716, 45718, 45719, 45721, 45722, 45724, 45727, 45729, 45730, 45734, 45736, 45739, 45743, 45744, 45746, 45748, 45749, 45753, 45760, 45761, 45764, 45765, 45768, 45772, 45774, 45776, 45779, 45780, 45781, 45783, 45784, 45785, 45789, 45790, 45791, 45792, 45796, 45799, 45800, 45802, 45803, 45805, 45806, 45807, 45808, 45811, 45815, 45816, 45820, 45822, 45823, 45824, 45825, 45827, 45829, 45830, 45833, 45835, 45841, 45842, 45845, 45846, 45847, 45849, 45851, 45853, 45854, 45855, 45857, 45858, 45860, 45861, 45862, 45863, 45866, 45868, 45870, 45871, 45872, 45873, 45874, 45881, 45882, 45883, 45888, 45890, 45891, 45893, 45896, 45898, 45899, 45900, 45902, 45903, 45905, 45906, 45907, 45908, 45909, 45910, 45911, 45913, 45914, 45917, 45919, 45922, 45923, 45925, 45927, 45932, 45934, 45936, 45939, 45940, 45941, 45944, 45945, 45949, 45950, 45953, 45954, 45955, 45956, 45957, 45958, 45964, 45965, 45966, 45968, 45970, 45973, 45974, 45975, 45976, 45978, 45979, 45982, 45983, 45985, 45986, 45987, 45989, 45994, 45998, 45999, 46001, 46004, 46005, 46011, 46012, 46014, 46015, 46017, 46019, 46021, 46023, 46024, 46026, 46028, 46030, 46035, 46036, 46038, 46039, 46040, 46043, 46044, 46045, 46050, 46052, 46054, 46056, 46058, 46059, 46062, 46063, 46067, 46069, 46070, 46071, 46072, 46073, 46074, 46076, 46077, 46080, 46083, 46084, 46087, 46089, 46091, 46092, 46093, 46094, 46096, 46098, 46099, 46100, 46101, 46102, 46103, 46104, 46106, 46112, 46114, 46119, 46123, 46128, 46132, 46134, 46135, 46137, 46138, 46140, 46143, 46145, 46147, 46148, 46150, 46152, 46155, 46158, 46159, 46161, 46162, 46166, 46167, 46169, 46170, 46171, 46173, 46175, 46176, 46177, 46181, 46182, 46183, 46187, 46188, 46189, 46190, 46192, 46193, 46198, 46199, 46200, 46204, 46208, 46217, 46218, 46222, 46224, 46225, 46227, 46228, 46229, 46230, 46231, 46236, 46241, 46242, 46244, 46251, 46254, 46256, 46257, 46263, 46265, 46267, 46268, 46270, 46271, 46273, 46274, 46275, 46281, 46286, 46287, 46290, 46292, 46293, 46294, 46295, 46298, 46301, 46302, 46305, 46307, 46308, 46313, 46316, 46318, 46319, 46320, 46321, 46323, 46327, 46328, 46329, 46332, 46333, 46334, 46337, 46339, 46340, 46341, 46342, 46343, 46345, 46348, 46349, 46351, 46357, 46358, 46360, 46361, 46362, 46364, 46366, 46367, 46368, 46371, 46372, 46373, 46375, 46376, 46380, 46382, 46383, 46384, 46385, 46388, 46391, 46393, 46397, 46399, 46400, 46401, 46402, 46403, 46404, 46406, 46408, 46409, 46412, 46417, 46419, 46420, 46422, 46425, 46426, 46427, 46429, 46430, 46432, 46435, 46438, 46444, 46445, 46446, 46447, 46448, 46450, 46451, 46453, 46454, 46455, 46456, 46458, 46461, 46462, 46463, 46464, 46467, 46468, 46469, 46470, 46473, 46474, 46475, 46479, 46480, 46485, 46486, 46488, 46489, 46490, 46493, 46495, 46501, 46504, 46506, 46508, 46510, 46513, 46514, 46516, 46517, 46518, 46519, 46522, 46528, 46530, 46531, 46532, 46534, 46536, 46539, 46543, 46545, 46547, 46548, 46550, 46551, 46552, 46553, 46554, 46556, 46558, 46560, 46561, 46562, 46565, 46566, 46568, 46569, 46571, 46573, 46574, 46575, 46577, 46579, 46580, 46581, 46587, 46588, 46589, 46591, 46595, 46596, 46600, 46602, 46604, 46605, 46606, 46607, 46612, 46613, 46620, 46624, 46626, 46627, 46630, 46636, 46637, 46641, 46642, 46646, 46647, 46648, 46650, 46651, 46657, 46661, 46663, 46665, 46666, 46670, 46671, 46672, 46674, 46679, 46682, 46684, 46685, 46687, 46688, 46690, 46693, 46695, 46701, 46703, 46705, 46707, 46708, 46711, 46712, 46715, 46716, 46717, 46718, 46719, 46722, 46723, 46724, 46726, 46728, 46729, 46730, 46731, 46733, 46738, 46741, 46744, 46747, 46751, 46752, 46754, 46755, 46756, 46758, 46759, 46761, 46762, 46763, 46766, 46767, 46768, 46770, 46772, 46780, 46781, 46782, 46784, 46786, 46787, 46788, 46791, 46792, 46793, 46794, 46796, 46797, 46798, 46800, 46801, 46803, 46808, 46809, 46812, 46815, 46816, 46817, 46818, 46824, 46825, 46828, 46832, 46833, 46834, 46835, 46837, 46838, 46839, 46840, 46842, 46844, 46845, 46851, 46853, 46857, 46859, 46860, 46862, 46865, 46867, 46870, 46873, 46874, 46876, 46877, 46878, 46880, 46884, 46885, 46889, 46891, 46893, 46894, 46895, 46896, 46900, 46901, 46902, 46903, 46904, 46909, 46911, 46912, 46913, 46914, 46916, 46917, 46919, 46922, 46925, 46929, 46933, 46934, 46937, 46939, 46940, 46943, 46945, 46949, 46950, 46951, 46956, 46960, 46961, 46962, 46964, 46965, 46966, 46969, 46970, 46971, 46973, 46974, 46975, 46978, 46982, 46983, 46984, 46985, 46989, 46990, 46994, 46997, 46998, 47000, 47001, 47002, 47003, 47005, 47006, 47008, 47011, 47015, 47017, 47020, 47021, 47023, 47024, 47027, 47028, 47029, 47030, 47031, 47033, 47039, 47041, 47043, 47044, 47046, 47052, 47053, 47057, 47058, 47063, 47070, 47071, 47072, 47076, 47077, 47078, 47079, 47080, 47081, 47082, 47084, 47087, 47088, 47090, 47091, 47092, 47093, 47094, 47095, 47096, 47097, 47098, 47099, 47102, 47103, 47104, 47108, 47111, 47113, 47115, 47118, 47120, 47121, 47123, 47124, 47125, 47126, 47129, 47130, 47131, 47133, 47135, 47136, 47140, 47141, 47142, 47143, 47148, 47149, 47150, 47152, 47153, 47154, 47156, 47157, 47158, 47163, 47165, 47166, 47167, 47171, 47175, 47178, 47180, 47181, 47183, 47184, 47187, 47192, 47193, 47194, 47197, 47199, 47200, 47201, 47203, 47206, 47207, 47211, 47213, 47215, 47217, 47222, 47224, 47226, 47227, 47233, 47235, 47236, 47238, 47239, 47242, 47243, 47245, 47248, 47251, 47256, 47257, 47259, 47260, 47264, 47265, 47267, 47268, 47269, 47270, 47272, 47274, 47275, 47276, 47277, 47279, 47280, 47282, 47284, 47287, 47289, 47291, 47292, 47293, 47295, 47299, 47302, 47304, 47306, 47312, 47316, 47317, 47318, 47319, 47322, 47324, 47325, 47326, 47330, 47333, 47334, 47335, 47336, 47337, 47339, 47340, 47341, 47342, 47343, 47345, 47346, 47347, 47349, 47350, 47352, 47353, 47358, 47359, 47360, 47361, 47362, 47363, 47364, 47366, 47369, 47370, 47373, 47375, 47376, 47378, 47381, 47383, 47385, 47386, 47387, 47389, 47390, 47394, 47395, 47397, 47398, 47399, 47400, 47402, 47405, 47407, 47408, 47409, 47410, 47412, 47414, 47416, 47418, 47426, 47428, 47429, 47430, 47432, 47433, 47434, 47436, 47437, 47439, 47440, 47442, 47444, 47445, 47448, 47449, 47451, 47452, 47453, 47455, 47457, 47468, 47469, 47472, 47473, 47475, 47476, 47477, 47479, 47481, 47482, 47484, 47485, 47486, 47489, 47491, 47492, 47494, 47495, 47497, 47498, 47503, 47504, 47507, 47508, 47510, 47511, 47512, 47515, 47516, 47518, 47524, 47527, 47528, 47529, 47530, 47531, 47534, 47538, 47539, 47545, 47548, 47549, 47550, 47553, 47554, 47557, 47558, 47559, 47560, 47566, 47568, 47570, 47572, 47574, 47575, 47576, 47579, 47581, 47582, 47585, 47586, 47588, 47591, 47592, 47593, 47596, 47599, 47600, 47604, 47609, 47611, 47612, 47616, 47618, 47620, 47621, 47624, 47626, 47628, 47643, 47645, 47647, 47650, 47651, 47655, 47656, 47661, 47663, 47665, 47666, 47668, 47669, 47670, 47673, 47674, 47676, 47681, 47682, 47685, 47688, 47689, 47693, 47694, 47695, 47696, 47704, 47710, 47711, 47712, 47713, 47714, 47718, 47719, 47721, 47723, 47724, 47725, 47726, 47729, 47730, 47731, 47734, 47735, 47736, 47739, 47743, 47745, 47746, 47749, 47753, 47755, 47756, 47761, 47762, 47764, 47768, 47772, 47773, 47774, 47775, 47776, 47783, 47784, 47785, 47786, 47788, 47789, 47793, 47797, 47798, 47806, 47807, 47813, 47814, 47815, 47816, 47817, 47820, 47821, 47823, 47824, 47825, 47826, 47828, 47832, 47833, 47835, 47839, 47842, 47849, 47850, 47852, 47853, 47854, 47857, 47859, 47861, 47864, 47867, 47870, 47871, 47873, 47874, 47875, 47876, 47878, 47879, 47882, 47884, 47886, 47888, 47891, 47895, 47896, 47897, 47898, 47900, 47901, 47903, 47904, 47905, 47906, 47907, 47909, 47911, 47913, 47914, 47915, 47917, 47918, 47919, 47920, 47926, 47927, 47929, 47931, 47934, 47937, 47938, 47939, 47940, 47941, 47943, 47944, 47947, 47951, 47952, 47953, 47955, 47960, 47961, 47962, 47963, 47965, 47966, 47967, 47969, 47970, 47971, 47972, 47974, 47975, 47976, 47977, 47978, 47981, 47985, 47987, 47989, 47990, 47991, 47993, 47994, 48000, 48003, 48006, 48008, 48009, 48011, 48017, 48018, 48019, 48020, 48024, 48025, 48026, 48027, 48028, 48029, 48030, 48035, 48039, 48041, 48050, 48052, 48057, 48058, 48059, 48061, 48063, 48064, 48065, 48067, 48068, 48069, 48070, 48074, 48075, 48078, 48081, 48082, 48084, 48086, 48088, 48090, 48091, 48093, 48094, 48096, 48100, 48103, 48106, 48107, 48108, 48110, 48111, 48112, 48115, 48117, 48119, 48123, 48124, 48126, 48128, 48131, 48132, 48134, 48136, 48137, 48139, 48141, 48142, 48147, 48148, 48150, 48151, 48154, 48155, 48159, 48165, 48167, 48171, 48177, 48178, 48180, 48183, 48184, 48185, 48186, 48188, 48191, 48194, 48195, 48197, 48203, 48204, 48206, 48207, 48208, 48210, 48211, 48212, 48213, 48214, 48218, 48219, 48221, 48223, 48225, 48228, 48230, 48231, 48233, 48234, 48238, 48247, 48249, 48250, 48251, 48253, 48256, 48257, 48259, 48260, 48262, 48263, 48264, 48265, 48270, 48273, 48275, 48276, 48278, 48279, 48280, 48281, 48284, 48285, 48288, 48290, 48291, 48292, 48295, 48297, 48299, 48300, 48302, 48305, 48306, 48307, 48316, 48317, 48321, 48322, 48324, 48325, 48328, 48330, 48331, 48336, 48337, 48338, 48342, 48343, 48345, 48346, 48349, 48350, 48351, 48353, 48359, 48360, 48362, 48363, 48364, 48365, 48366, 48368, 48369, 48370, 48372, 48375, 48376, 48379, 48380, 48382, 48383, 48385, 48386, 48388, 48389, 48393, 48394, 48396, 48399, 48402, 48404, 48407, 48411, 48413, 48416, 48417, 48418, 48421, 48422, 48427, 48428, 48430, 48434, 48437, 48438, 48439, 48440, 48445, 48447, 48448, 48449, 48450, 48454, 48456, 48457, 48458, 48459, 48463, 48466, 48467, 48468, 48471, 48472, 48473, 48474, 48475, 48478, 48481, 48482, 48484, 48486, 48487, 48488, 48491, 48492, 48493, 48494, 48495, 48496, 48497, 48501, 48503, 48506, 48509, 48510, 48512, 48515, 48518, 48519, 48523, 48524, 48528, 48531, 48532, 48533, 48534, 48535, 48536, 48539, 48541, 48544, 48545, 48546, 48549, 48550, 48553, 48554, 48556, 48558, 48559, 48560, 48561, 48562, 48563, 48564, 48565, 48567, 48569, 48573, 48574, 48575, 48577, 48578, 48582, 48585, 48586, 48589, 48590, 48591, 48595, 48596, 48597, 48601, 48604, 48606, 48609, 48612, 48614, 48617, 48619, 48620, 48623, 48624, 48625, 48627, 48628, 48629, 48633, 48636, 48638, 48639, 48642, 48644, 48646, 48650, 48652, 48654, 48655, 48657, 48659, 48663, 48664, 48669, 48672, 48677, 48678, 48679, 48681, 48682, 48684, 48685, 48688, 48689, 48691, 48693, 48694, 48698, 48700, 48701, 48703, 48705, 48708, 48709, 48710, 48711, 48715, 48716, 48719, 48720, 48721, 48723, 48724, 48725, 48726, 48728, 48732, 48733, 48734, 48735, 48739, 48740, 48742, 48743, 48748, 48750, 48751, 48754, 48757, 48758, 48759, 48762, 48763, 48765, 48766, 48768, 48770, 48772, 48774, 48776, 48777, 48778, 48783, 48784, 48785, 48787, 48788, 48790, 48791, 48792, 48794, 48796, 48798, 48799, 48800, 48803, 48805, 48806, 48807, 48811, 48815, 48817, 48821, 48824, 48825, 48830, 48831, 48832, 48833, 48835, 48836, 48837, 48838, 48842, 48843, 48848, 48850, 48851, 48853, 48854, 48855, 48856, 48859, 48860, 48864, 48867, 48868, 48872, 48873, 48874, 48875, 48876, 48877, 48879, 48887, 48888, 48889, 48890, 48891, 48893, 48894, 48895, 48897, 48900, 48906, 48907, 48909, 48912, 48913, 48914, 48915, 48918, 48925, 48926, 48927, 48928, 48929, 48931, 48933, 48935, 48938, 48942, 48946, 48947, 48949, 48954, 48956, 48957, 48958, 48963, 48969, 48970, 48972, 48973, 48976, 48977, 48980, 48983, 48985, 48986, 48987, 48988, 48990, 48991, 48992, 48994, 48996, 48997, 48999, 49001, 49005, 49006, 49007, 49011, 49014, 49015, 49018, 49019, 49020, 49021, 49026, 49027, 49028, 49030, 49032, 49035, 49036, 49037, 49038, 49041, 49042, 49044, 49048, 49050, 49055, 49057, 49060, 49062, 49064, 49065, 49066, 49068, 49070, 49075, 49080, 49082, 49083, 49084, 49087, 49088, 49092, 49093, 49094, 49095, 49096, 49102, 49104, 49105, 49106, 49108, 49109, 49113, 49116, 49117, 49118, 49119, 49120, 49121, 49123, 49125, 49128, 49133, 49134, 49135, 49137, 49139, 49140, 49144, 49145, 49146, 49147, 49148, 49150, 49151, 49152, 49153, 49154, 49155, 49159, 49160, 49161, 49162, 49164, 49167, 49168, 49170, 49171, 49176, 49178, 49180, 49181, 49183, 49184, 49185, 49189, 49190, 49191, 49192, 49193, 49194, 49195, 49198, 49201, 49203, 49204, 49205, 49206, 49207, 49208, 49209, 49211, 49212, 49213, 49214, 49220, 49221, 49223, 49225, 49231, 49234, 49239, 49240, 49241, 49242, 49243, 49247, 49249, 49251, 49252, 49253, 49256, 49257, 49258, 49261, 49263, 49264, 49265, 49266, 49269, 49270, 49271, 49272, 49273, 49274, 49275, 49276, 49277, 49278, 49281, 49282, 49284, 49285, 49295, 49297, 49299, 49302, 49304, 49305, 49307, 49308, 49310, 49312, 49317, 49318, 49322, 49323, 49326, 49327, 49328, 49331, 49333, 49336, 49337, 49339, 49343, 49344, 49348, 49351, 49352, 49353, 49354, 49355, 49357, 49361, 49363, 49366, 49367, 49369, 49370, 49372, 49373, 49374, 49377, 49378, 49379, 49380, 49381, 49383, 49384, 49386, 49388, 49389, 49390, 49392, 49393, 49395, 49397, 49399, 49400, 49401, 49404, 49406, 49410, 49413, 49415, 49420, 49421, 49423, 49424, 49425, 49426, 49427, 49428, 49430, 49434, 49437, 49438, 49440, 49441, 49446, 49447, 49449, 49450, 49453, 49455, 49462, 49465, 49466, 49467, 49468, 49470, 49472, 49473, 49475, 49480, 49484, 49486, 49487, 49489, 49490, 49493, 49494, 49495, 49497, 49498, 49499, 49501, 49504, 49505, 49506, 49507, 49508, 49509, 49511, 49512, 49521, 49523, 49524, 49526, 49527, 49528, 49529, 49531, 49532, 49535, 49536, 49542, 49543, 49545, 49546, 49547, 49549, 49550, 49552, 49555, 49556, 49562, 49565, 49567, 49569, 49570, 49572, 49573, 49574, 49575, 49577, 49580, 49583, 49584, 49585, 49586, 49587, 49588, 49589, 49590, 49592, 49595, 49596, 49598, 49600, 49601, 49602, 49604, 49611, 49614, 49615, 49616, 49620, 49621, 49623, 49626, 49634, 49635, 49636, 49637, 49641, 49642, 49643, 49644, 49645, 49648, 49650, 49652, 49654, 49656, 49659, 49662, 49663, 49667, 49671, 49673, 49675, 49676, 49677, 49679, 49682, 49684, 49685, 49686, 49688, 49689, 49692, 49693, 49696, 49697, 49699, 49700, 49701, 49702, 49703, 49704, 49707, 49709, 49712, 49714, 49716, 49717, 49718, 49721, 49729, 49733, 49736, 49738, 49739, 49741, 49742, 49743, 49745, 49746, 49747, 49751, 49755, 49770, 49774, 49775, 49776, 49777, 49779, 49780, 49784, 49785, 49786, 49789, 49791, 49793, 49796, 49797, 49798, 49799, 49803, 49811, 49812, 49815, 49818, 49823, 49827, 49830, 49831, 49833, 49834, 49837, 49838, 49850, 49851, 49853, 49858, 49859, 49860, 49862, 49863, 49864, 49865, 49866, 49868, 49869, 49871, 49872, 49873, 49874, 49875, 49877, 49879, 49880, 49881, 49885, 49886, 49887, 49888, 49889, 49892, 49893, 49895, 49896, 49897, 49899, 49901, 49902, 49906, 49907, 49911, 49914, 49916, 49919, 49922, 49923, 49925, 49928, 49929, 49932, 49933, 49934, 49935, 49938, 49939, 49941, 49943, 49945, 49946, 49948, 49950, 49957, 49958, 49961, 49965, 49967, 49968, 49972, 49973, 49976, 49977, 49979, 49980, 49981, 49982, 49984, 49985, 49988, 49990, 49994, 49998) From 3a2191248a47b3efbd2ff3d181017b29c5860fdc Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 14:38:44 -0700 Subject: [PATCH 089/167] We no longer need to lower the parameter here; this test can pass in time with all 5000. --- tests/integration/learning/test_discovery_phases.py | 7 ++----- tests/mock/performance_mocks.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index caa03615a..4a2ea631f 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -66,15 +66,14 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice): # TODO: Consider changing this - #1449 assert VerificationTracker.node_verifications == 1 - # A quick setup so that the bytes casting of Ursulas (on what in the real world will be the remote node) - # doesn't take up all the time. _teacher = highperf_mocked_alice.current_teacher_node() actual_ursula = MOCK_KNOWN_URSULAS_CACHE[_teacher.rest_interface.port] + # A quick setup so that the bytes casting of Ursulas (on what in the real world will be the remote node) + # doesn't take up all the time. _teacher_known_nodes_bytestring = actual_ursula.bytestring_of_known_nodes() actual_ursula.bytestring_of_known_nodes = lambda *args, ** kwargs: _teacher_known_nodes_bytestring # TODO: Formalize this? #1537 - with mock_cert_storage, mock_cert_loading, mock_verify_node, mock_message_verification, mock_metadata_validation: with mock_pubkey_from_bytes(), mock_stamp_call, mock_signature_bytes: started = time.time() @@ -92,7 +91,6 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice): _POLICY_PRESERVER = [] -@pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, highperf_mocked_alice, highperf_mocked_bob): @@ -124,7 +122,6 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, @pytest_twisted.inlineCallbacks -@pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [1000], indirect=True) def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, highperf_mocked_alice, highperf_mocked_bob): diff --git a/tests/mock/performance_mocks.py b/tests/mock/performance_mocks.py index 2f2425c6f..058927e83 100644 --- a/tests/mock/performance_mocks.py +++ b/tests/mock/performance_mocks.py @@ -196,8 +196,8 @@ class NotARestApp: def actual_rest_app(self): if self._actual_rest_app is None: self._actual_rest_app, _datastore = make_rest_app(db_filepath="no datastore", - this_node=self.this_node, - serving_domains=(None,)) + this_node=self.this_node, + serving_domains=(None,)) _new_view_functions = self._ViewFunctions(self._actual_rest_app.view_functions) self._actual_rest_app.view_functions = _new_view_functions self._actual_rest_apps.append( From 52564b446fd33a266f8fe7c10fe388eea888f7e7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 19:23:42 -0700 Subject: [PATCH 090/167] No need to verify announced nodes anymore, since we do this lazily now. Fixes #555. --- nucypher/characters/lawful.py | 4 ++-- nucypher/network/server.py | 44 ++--------------------------------- 2 files changed, 4 insertions(+), 44 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 2998c0369..5f4844b8a 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -891,7 +891,7 @@ class Bob(Character): search_boundary += 2 if search_boundary > 42: # We've searched the entire string and can't match any. TODO: Portable learning is a nice idea here. - # TODO: This fails in tests once in a great while because there aren't matching nodes among the few test nodes. Not sure what to do. + # TODO: This fails in tests once in a while because there aren't matching nodes among the few test nodes. Not sure what to do. raise self.NotEnoughNodes # TODO: 1995 all throughout here (we might not (need to) know the checksum address yet; canonical will do.) @@ -1347,7 +1347,7 @@ class Ursula(Teacher, Character, Worker): network_middleware=network_middleware, registry=registry) - except NodeSeemsToBeDown: + except NodeSeemsToBeDown as e: log = Logger(cls.__name__) log.warn( "Can't connect to seed node (attempt {}). Will retry in {} seconds.".format(attempt, interval)) diff --git a/nucypher/network/server.py b/nucypher/network/server.py index da2436e90..f4c694053 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -186,50 +186,10 @@ def make_rest_app( sprouts = _node_class.batch_from_bytes(request.data, registry=this_node.registry) - # TODO: This logic is basically repeated in learn_from_teacher_node and remember_node. - # Let's find a better way. #555 for node in sprouts: - @crosstown_traffic() - def learn_about_announced_nodes(): - if node in this_node.known_nodes: - if node.timestamp <= this_node.known_nodes[node.checksum_address].timestamp: - return + this_node.remember_node(node) - node.mature() - - try: - node.verify_node(this_node.network_middleware.client, - registry=this_node.registry) - - # Suspicion - except node.SuspiciousActivity as e: - # 355 - # TODO: Include data about caller? - # TODO: Account for possibility that stamp, rather than interface, was bad. - # TODO: Maybe also record the bytes representation separately to disk? - message = f"Suspicious Activity about {node}: {str(e)}. Announced via REST." - log.warn(message) - this_node.suspicious_activities_witnessed['vladimirs'].append(node) - except NodeSeemsToBeDown as e: - # This is a rather odd situation - this node *just* contacted us and asked to be verified. Where'd it go? Maybe a NAT problem? - log.info(f"Node announced itself to us just now, but seems to be down: {node}. Response was {e}.") - log.debug(f"Phantom node certificate: {node.certificate}") - # Async Sentinel - except Exception as e: - log.critical(f"This exception really needs to be handled differently: {e}") - raise - - # Believable - else: - log.info("Learned about previously unknown node: {}".format(node)) - this_node.remember_node(node) - # TODO: Record new fleet state - - # Cleanup - finally: - forgetful_node_storage.forget() - - # TODO: What's the right status code here? 202? Different if we already knew about the node? + # TODO: What's the right status code here? 202? Different if we already knew about the node(s)? return all_known_nodes() @rest_app.route('/consider_arrangement', methods=['POST']) From fa927115f1e3ff3b2f689c624d02ca6ce7f1a168 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 19:25:54 -0700 Subject: [PATCH 091/167] Removing fail-slow logic from seednode resolution. --- nucypher/network/nodes.py | 50 +++++++++++---------------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index c5d3024a1..767ea9eea 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -228,7 +228,6 @@ class Learner: self.unresponsive_seed_nodes = set() if self.start_learning_now and not self.lonely: - self.load_seednodes() self.start_learning_loop(now=self.learn_on_same_thread) @property @@ -242,8 +241,7 @@ class Learner: TODO: Dehydrate this with nucypher.utilities.seednodes.load_seednodes """ if self.done_seeding: - self.log.debug("Already done seeding; won't try again.") - return + raise RuntimeError("Already finished seeding. Why try again? Is this a thread safety problem?") from nucypher.utilities.seednodes import aggregate_seednode_uris # TODO: Ugh. # teacher_uris = aggregate_seednode_uris(domains=self.learning_domains) canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(tuple(self.learning_domains)[0], ()) # TODO: Are we done with multiple domains? @@ -251,14 +249,16 @@ class Learner: from nucypher.characters.lawful import Ursula ############################ for uri in canonical_sage_uris: - # Not catching any errors here; we want to fail fast if there are bad hardcoded teachers. - # This is essentially an __active-fire__ if it's anything but a fleeting one-off. - sage_node = Ursula.from_teacher_uri(teacher_uri=uri, - min_stake=0, # TODO: Where to get this? - federated_only=self.federated_only, - network_middleware=self.network_middleware, - registry=self.registry) - self.remember_node(sage_node) + try: + sage_node = Ursula.from_teacher_uri(teacher_uri=uri, + min_stake=0, # TODO: Where to get this? + federated_only=self.federated_only, + network_middleware=self.network_middleware, + registry=self.registry) + except NodeSeemsToBeDown: + self.unresponsive_seed_nodes.add(uri) + else: + self.remember_node(sage_node) ################ for seednode_metadata in self._seed_nodes: @@ -369,20 +369,7 @@ class Learner: return False elif now: self.log.info("Starting Learning Loop NOW.") - - # if self.lonely: - # self.done_seeding = True - # self.read_nodes_from_storage() - # - # else: - # self.load_seednodes() - try: - self.learn_from_teacher_node() - except self.NotEnoughTeachers: - if self.lonely: - assert False - else: - assert False + self.learn_from_teacher_node() self.learning_deferred = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY) self.learning_deferred.addErrback(self.handle_learning_errors) @@ -392,10 +379,6 @@ class Learner: self.cycle_teacher_node() learning_deferreds = list() - if not self.lonely: - seeder_deferred = deferToThread(self.load_seednodes) - seeder_deferred.addErrback(self.handle_learning_errors) - learning_deferreds.append(seeder_deferred) learner_deferred = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now) learner_deferred.addErrback(self.handle_learning_errors) @@ -441,12 +424,8 @@ class Learner: self.teacher_nodes.extend(nodes_we_know_about) def cycle_teacher_node(self): - # To ensure that all the best teachers are available, first let's make sure - # that we have connected to all the seed nodes. - if self.unresponsive_seed_nodes and not self.lonely: - self.log.info("Still have unresponsive seed nodes; trying again to connect.") - self.load_seednodes() # Ideally, this is async and singular. - + if not self.done_seeding: + self.load_seednodes() if not self.teacher_nodes: self.select_teacher_nodes() try: @@ -525,7 +504,6 @@ class Learner: if elapsed > timeout: if len(self.known_nodes) >= number_of_nodes_to_know: # Last chance! continue - if not self._learning_task.running: raise RuntimeError("Learning loop is not running. Start it with start_learning().") elif not reactor.running and not learn_on_this_thread: From dbf910696a725ff6b8d51e4206c28a15a254dbfd Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 19:26:42 -0700 Subject: [PATCH 092/167] Longer time allowance for this test; it will still almost certainly pass in the faster times on CI, but it's a pain to have it fail locally. --- tests/integration/learning/test_discovery_phases.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 4a2ea631f..07825099c 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -189,6 +189,7 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, assert len(successful_responses) == len(nodes_we_expect_to_have_the_map) # Before Treasure Island (1741), this process took about 3 minutes. - - assert initial_blocking_duration.total_seconds() < 1.8 - assert complete_distribution_time.total_seconds() < 4.5 + # On CI, we expect these times to be even less. (Around 1 and 3.5 seconds, respectively) + # But with debuggers and other processes running on laptops, we give a little leeway. + assert initial_blocking_duration.total_seconds() < 3 + assert complete_distribution_time.total_seconds() < 10 From 81a4c623b3702eca35b621e52d357718646ae0e2 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 19:27:08 -0700 Subject: [PATCH 093/167] This is a more acute test with a nonstandard domain, as it assures that the actual seednode logic is used. --- tests/integration/learning/test_firstula_circumstances.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/learning/test_firstula_circumstances.py b/tests/integration/learning/test_firstula_circumstances.py index decdf4e09..85ea7a9ea 100644 --- a/tests/integration/learning/test_firstula_circumstances.py +++ b/tests/integration/learning/test_firstula_circumstances.py @@ -27,7 +27,9 @@ def test_proper_seed_node_instantiation(ursula_federated_test_config): lonely_ursula_maker = partial(make_federated_ursulas, ursula_config=ursula_federated_test_config, quantity=1, - know_each_other=False) + know_each_other=False, + lonely=True, + domains=["useless domain"]) firstula = lonely_ursula_maker().pop() firstula_as_seed_node = firstula.seed_node_metadata() From 169fffd41ee4c11ba9414d4d6de9afab2d83fae7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 19:27:52 -0700 Subject: [PATCH 094/167] This entire test (and part of another) is no longer relevant, as Alice and Bob will learn about seednodes form their hardcoded domain list in such a scenario. --- .../integration/network/test_failure_modes.py | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/tests/integration/network/test_failure_modes.py b/tests/integration/network/test_failure_modes.py index fb5c2c6d6..4aca50191 100644 --- a/tests/integration/network/test_failure_modes.py +++ b/tests/integration/network/test_failure_modes.py @@ -31,30 +31,6 @@ from tests.utils.middleware import EvilMiddleWare, NodeIsDownMiddleware from tests.utils.ursula import make_federated_ursulas -def test_bob_does_not_let_a_connection_error_stop_him(enacted_federated_policy, - federated_ursulas, - federated_bob, - federated_alice): - assert len(federated_bob.known_nodes) == 0 - ursula1 = list(federated_ursulas)[0] - ursula2 = list(federated_ursulas)[1] - - federated_bob.remember_node(ursula1) - - federated_bob.network_middleware = NodeIsDownMiddleware() - federated_bob.network_middleware.node_is_down(ursula1) - - with pytest.raises(federated_bob.NotEnoughNodes): - federated_bob.get_treasure_map(federated_alice.stamp, enacted_federated_policy.label) - - federated_bob.remember_node(ursula2) - - map = federated_bob.get_treasure_map(federated_alice.stamp, enacted_federated_policy.label) - - assert sorted(list(map.destinations.keys())) == sorted( - list(u.checksum_address for u in list(federated_ursulas))) - - def test_alice_can_grant_even_when_the_first_nodes_she_tries_are_down(federated_alice, federated_bob, federated_ursulas): m, n = 2, 3 policy_end_datetime = maya.now() + datetime.timedelta(days=5) @@ -80,22 +56,6 @@ def test_alice_can_grant_even_when_the_first_nodes_she_tries_are_down(federated_ # Go! federated_alice.start_learning_loop() - # Try a first time, failing because no known nodes are up for Alice to even try to learn from. - with pytest.raises(down_node.NotEnoughNodes): - alice_grant_action() - - # Now she learn about one node that *is* up... - reliable_node = list(federated_ursulas)[1] - federated_alice.remember_node(reliable_node) - - # ...amidst a few others that are down. - more_nodes = list(federated_ursulas)[2:10] - for node in more_nodes: - federated_alice.network_middleware.node_is_down(node) - - # Alice still only knows about two nodes (the one that is down and the new one). - assert len(federated_alice.known_nodes) == 2 - # Now we'll have a situation where Alice knows about all 10, # though only one is up. @@ -103,6 +63,10 @@ def test_alice_can_grant_even_when_the_first_nodes_she_tries_are_down(federated_ # Because she has successfully completed learning, but the nodes about which she learned are down, # she'll get a different error. + more_nodes = list(federated_ursulas)[1:10] + for node in more_nodes: + federated_alice.network_middleware.node_is_down(node) + for node in more_nodes: federated_alice.remember_node(node) with pytest.raises(Policy.Rejected): From ca2e84eb2ae6993c2b4c0fd69c788b5b4f00a448 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 24 Jun 2020 19:28:22 -0700 Subject: [PATCH 095/167] Only simulate hardcoded domain nodes for TEMPORARY_DOMAIN, not nonstandard test domains. --- tests/utils/middleware.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index da6d730da..6e22cb73e 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -23,6 +23,7 @@ from constant_sorrow.constants import CERTIFICATE_NOT_SAVED from flask import Response from nucypher.characters.lawful import Ursula +from nucypher.config.constants import TEMPORARY_DOMAIN from nucypher.network.middleware import NucypherMiddlewareClient, RestMiddleware from tests.utils.ursula import MOCK_KNOWN_URSULAS_CACHE @@ -93,7 +94,11 @@ class MockRestMiddleware(RestMiddleware): @classmethod def get(_cls, item, default): - return tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values()) + if item is TEMPORARY_DOMAIN: + nodes = tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values()) + else: + nodes = tuple() + return nodes def get_certificate(self, host, port, timeout=3, retry_attempts: int = 3, retry_rate: int = 2, current_attempt: int = 0): From 2df24bb291ad691ab03ee8e0a2f791547653bb18 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 14:58:29 -0700 Subject: [PATCH 096/167] Loading seednodes is now one-and-done. No more retry attempts. Retry by starting the process over. --- nucypher/network/nodes.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 767ea9eea..08d503863 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -234,7 +234,7 @@ class Learner: def known_nodes(self): return self.__known_nodes - def load_seednodes(self, read_storage: bool = True, retry_attempts: int = 3): + def load_seednodes(self, read_storage: bool = True): """ Engage known nodes from storages and pre-fetch hardcoded seednode certificates for node learning. @@ -284,9 +284,6 @@ class Learner: if read_storage is True: self.read_nodes_from_storage() - if not self.known_nodes: - self.log.warn("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) 567 def read_nodes_from_storage(self) -> None: stored_nodes = self.node_storage.all(federated_only=self.federated_only) # TODO: #466 From 7c177121f265f00830399fb534909ec06b2c717a Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 15:18:35 -0700 Subject: [PATCH 097/167] Record proper fleet state at seednode time. --- nucypher/network/nodes.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 08d503863..d1c9910f5 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -242,15 +242,14 @@ class Learner: """ if self.done_seeding: raise RuntimeError("Already finished seeding. Why try again? Is this a thread safety problem?") - from nucypher.utilities.seednodes import aggregate_seednode_uris # TODO: Ugh. - # teacher_uris = aggregate_seednode_uris(domains=self.learning_domains) + canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(tuple(self.learning_domains)[0], ()) # TODO: Are we done with multiple domains? - # TODO: Is this better as a sprout? - from nucypher.characters.lawful import Ursula - ############################ + + discovered = [] + for uri in canonical_sage_uris: try: - sage_node = Ursula.from_teacher_uri(teacher_uri=uri, + sage_node = self.node_class.from_teacher_uri(teacher_uri=uri, min_stake=0, # TODO: Where to get this? federated_only=self.federated_only, network_middleware=self.network_middleware, @@ -258,8 +257,9 @@ class Learner: except NodeSeemsToBeDown: self.unresponsive_seed_nodes.add(uri) else: - self.remember_node(sage_node) - ################ + new_node = self.remember_node(sage_node, record_fleet_state=False) + discovered.append(new_node) + for seednode_metadata in self._seed_nodes: self.log.debug( @@ -267,14 +267,15 @@ class Learner: seednode_metadata.rest_host, seednode_metadata.rest_port)) - seed_node = Ursula.from_seednode_metadata(seednode_metadata=seednode_metadata, + seed_node = self.node_class.from_seednode_metadata(seednode_metadata=seednode_metadata, network_middleware=self.network_middleware, federated_only=self.federated_only) # TODO: 466 if seed_node is False: self.unresponsive_seed_nodes.add(seednode_metadata) else: self.unresponsive_seed_nodes.discard(seednode_metadata) - self.remember_node(seed_node) + new_node = self.remember_node(seed_node, record_fleet_state=False) + discovered.append(new_node) if not self.unresponsive_seed_nodes: self.log.info("Finished learning about all seednodes.") @@ -282,13 +283,24 @@ class Learner: self.done_seeding = True if read_storage is True: - self.read_nodes_from_storage() + nodes_restored_from_storage = self.read_nodes_from_storage() + + discovered.extend(nodes_restored_from_storage) + + if discovered: + self.known_nodes.record_fleet_state() def read_nodes_from_storage(self) -> None: stored_nodes = self.node_storage.all(federated_only=self.federated_only) # TODO: #466 + + restored_from_disk = [] + for node in stored_nodes: - self.remember_node(node) # TODO: Validity status 1866 + restored_node = self.remember_node(node, record_fleet_state=False) # TODO: Validity status 1866 + restored_from_disk.append(restored_node) + + return restored_from_disk def remember_node(self, node, From 086ff1c3bb4f01961eec7d865e32079f5d0dc8ca Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 15:19:11 -0700 Subject: [PATCH 098/167] This test is no longer relevant; the seednode tests cover this logic equally well. --- tests/integration/learning/test_fleet_state.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/integration/learning/test_fleet_state.py b/tests/integration/learning/test_fleet_state.py index 778f20c16..7454a711d 100644 --- a/tests/integration/learning/test_fleet_state.py +++ b/tests/integration/learning/test_fleet_state.py @@ -23,21 +23,6 @@ from hendrix.utils.test_utils import crosstownTaskListDecoratorFactory from tests.utils.ursula import make_federated_ursulas -def test_learning_from_node_with_no_known_nodes(ursula_federated_test_config): - lonely_ursula_maker = partial(make_federated_ursulas, - ursula_config=ursula_federated_test_config, - quantity=1, - know_each_other=False) - lonely_teacher = lonely_ursula_maker().pop() - lonely_learner = lonely_ursula_maker(known_nodes=[lonely_teacher]).pop() - - learning_callers = [] - crosstown_traffic.decorator = crosstownTaskListDecoratorFactory(learning_callers) - - result = lonely_learner.learn_from_teacher_node() - assert result is NO_KNOWN_NODES - - 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. From 179bd4ae37d375c1bcf4a6378e5f24add4cd5cbd Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 15:19:47 -0700 Subject: [PATCH 099/167] Clarify each fleet state (because one of them is the result of the seednode loading). --- tests/integration/learning/test_fleet_state.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/learning/test_fleet_state.py b/tests/integration/learning/test_fleet_state.py index 7454a711d..851af2acf 100644 --- a/tests/integration/learning/test_fleet_state.py +++ b/tests/integration/learning/test_fleet_state.py @@ -100,6 +100,7 @@ def test_state_is_recorded_after_learning(federated_ursulas, ursula_federated_te some_ursula_in_the_fleet = list(federated_ursulas)[0] lonely_learner.remember_node(some_ursula_in_the_fleet) + assert len(lonely_learner.known_nodes.states) == 1 # Saved a fleet state when we remembered this node. # The rest of the fucking owl. lonely_learner.learn_from_teacher_node() @@ -107,5 +108,5 @@ def test_state_is_recorded_after_learning(federated_ursulas, ursula_federated_te states = list(lonely_learner.known_nodes.states.values()) assert len(states) == 2 - assert len(states[0].nodes) == 2 # This and one other. - assert len(states[1].nodes) == len(federated_ursulas) + 1 # Again, accounting for this Learner. + assert len(states[0].nodes) == 2 # The first fleet state is just us and the one about whom we learned. + assert len(states[1].nodes) == len(federated_ursulas) + 2 # When we ran learn_from_teacher_node, we also loaded the rest of the fleet. From 80ec428442f961069320559f9d47dd9c9458abaa Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 19:27:03 -0700 Subject: [PATCH 100/167] Don't try to remember nodes of an unknown version; we don't want to learn from them anyway. --- nucypher/characters/lawful.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 5f4844b8a..02e6cf30b 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -32,7 +32,7 @@ from bytestring_splitter import BytestringSplitter, VariableLengthBytestring from bytestring_splitter import BytestringKwargifier, BytestringSplitter, BytestringSplittingError, \ VariableLengthBytestring from constant_sorrow import constants -from constant_sorrow.constants import INCLUDED_IN_BYTESTRING, PUBLIC_ONLY, STRANGER_ALICE +from constant_sorrow.constants import INCLUDED_IN_BYTESTRING, PUBLIC_ONLY, STRANGER_ALICE, UNKNOWN_VERSION from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve from cryptography.hazmat.primitives.serialization import Encoding @@ -1434,7 +1434,8 @@ class Ursula(Teacher, Character, Worker): def from_bytes(cls, ursula_as_bytes: bytes, version: int = INCLUDED_IN_BYTESTRING, - registry: BaseContractRegistry = None, + registry: BaseContractRegistry = None, # TODO: Why is this here? It's not being used. + fail_fast=False, ) -> 'Ursula': if version is INCLUDED_IN_BYTESTRING: @@ -1456,11 +1457,21 @@ class Ursula(Teacher, Character, Worker): message = cls.unknown_version_message.format(display_name, version, cls.LEARNER_VERSION) except BytestringSplittingError: message = cls.really_unknown_version_message.format(version, cls.LEARNER_VERSION) - raise cls.IsFromTheFuture(message) - - # Version stuff checked out. Moving on. - node_sprout = cls.internal_splitter(payload, partial=True) - return node_sprout + if fail_fast: + raise cls.IsFromTheFuture(message) + else: + cls.log.warn(message) + return UNKNOWN_VERSION + else: + if fail_fast: + raise cls.IsFromTheFuture(message) + else: + cls.log.warn(message) + return UNKNOWN_VERSION + else: + # Version stuff checked out. Moving on. + node_sprout = cls.internal_splitter(payload, partial=True) + return node_sprout @classmethod def from_processed_bytes(cls, **processed_objects): @@ -1505,11 +1516,21 @@ class Ursula(Teacher, Character, Worker): sprout = cls.from_bytes(node_bytes, version=version, registry=registry) + if sprout is UNKNOWN_VERSION: + continue + except BytestringSplittingError: + message = cls.really_unknown_version_message.format(version, cls.LEARNER_VERSION) + if fail_fast: + raise cls.IsFromTheFuture(message) + else: + cls.log.warn(message) + continue except Ursula.IsFromTheFuture as e: if fail_fast: raise else: cls.log.warn(e.args[0]) + continue else: sprouts.append(sprout) return sprouts From 091074cd9348f680d100b426d261aa9d52726ee9 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 19:27:22 -0700 Subject: [PATCH 101/167] This node might be an unknown version; if so, just move on. --- nucypher/network/nodes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index d1c9910f5..b0ad318fc 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -36,7 +36,7 @@ from bytestring_splitter import BytestringSplitter, BytestringSplittingError, Pa VariableLengthBytestring from constant_sorrow import constant_or_bytes from constant_sorrow.constants import (CERTIFICATE_NOT_SAVED, FLEET_STATES_MATCH, NEVER_SEEN, NOT_SIGNED, - NO_KNOWN_NODES, NO_STORAGE_AVAILIBLE, UNKNOWN_FLEET_STATE) + NO_KNOWN_NODES, NO_STORAGE_AVAILIBLE, UNKNOWN_FLEET_STATE, UNKNOWN_VERSION) from nucypher.acumen.nicknames import nickname_from_seed from nucypher.acumen.perception import FleetSensor, icon_from_checksum from nucypher.blockchain.economics import EconomicsFactory @@ -249,7 +249,7 @@ class Learner: for uri in canonical_sage_uris: try: - sage_node = self.node_class.from_teacher_uri(teacher_uri=uri, + maybe_sage_node = self.node_class.from_teacher_uri(teacher_uri=uri, min_stake=0, # TODO: Where to get this? federated_only=self.federated_only, network_middleware=self.network_middleware, @@ -257,8 +257,11 @@ class Learner: except NodeSeemsToBeDown: self.unresponsive_seed_nodes.add(uri) else: - new_node = self.remember_node(sage_node, record_fleet_state=False) - discovered.append(new_node) + if maybe_sage_node is UNKNOWN_VERSION: + continue + else: + new_node = self.remember_node(maybe_sage_node, record_fleet_state=False) + discovered.append(new_node) for seednode_metadata in self._seed_nodes: From 0d6185e90a2f5f353fb468197f41e8c4262ec404 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 19:27:43 -0700 Subject: [PATCH 102/167] Fixturizing lonely_ursula_maker for cleanup purposes. --- tests/fixtures.py | 57 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 490608af5..c36a714c7 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -15,27 +15,22 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ - import contextlib import json -import random - -import maya import os -import pytest +import random import shutil import tempfile -from click.testing import CliRunner from datetime import datetime, timedelta +from functools import partial +from typing import Tuple + +import maya +import pytest +from click.testing import CliRunner from eth_utils import to_checksum_address -from io import StringIO from sqlalchemy.engine import create_engine from twisted.logger import Logger -from typing import Tuple -from umbral import pre -from umbral.curvebn import CurveBN -from umbral.keys import UmbralPrivateKey -from umbral.signing import Signer from web3 import Web3 from nucypher.blockchain.economics import BaseEconomics, StandardTokenEconomics @@ -100,7 +95,7 @@ from tests.mock.performance_mocks import ( mock_remember_node, mock_rest_app_creation, mock_secret_source, - mock_verify_node, _determine_good_serials + mock_verify_node ) from tests.utils.blockchain import TesterBlockchain, token_airdrop from tests.utils.config import ( @@ -112,6 +107,10 @@ from tests.utils.middleware import MockRestMiddleware, MockRestMiddlewareForLarg from tests.utils.policy import generate_random_label from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, make_decentralized_ursulas, make_federated_ursulas, \ MOCK_KNOWN_URSULAS_CACHE +from umbral import pre +from umbral.curvebn import CurveBN +from umbral.keys import UmbralPrivateKey +from umbral.signing import Signer test_logger = Logger("test-logger") @@ -257,7 +256,7 @@ def idle_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, token_ random_label = generate_random_label() days = token_economics.minimum_locked_periods // 2 now = testerchain.w3.eth.getBlock(block_identifier='latest').timestamp - expiration = maya.MayaDT(now).add(days=days-1) + expiration = maya.MayaDT(now).add(days=days - 1) n = 3 m = 2 policy = blockchain_alice.create_policy(blockchain_bob, @@ -370,12 +369,38 @@ def blockchain_bob(bob_blockchain_test_config, testerchain): @pytest.fixture(scope="module") def federated_ursulas(ursula_federated_test_config): + if MOCK_KNOWN_URSULAS_CACHE: + raise RuntimeError("Ursulas cache was unclear at fixture loading time. Did you use one of the ursula maker functions without cleaning up?") _ursulas = make_federated_ursulas(ursula_config=ursula_federated_test_config, quantity=NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK) + _ports_to_remove = [ursula.rest_interface.port for ursula in _ursulas] yield _ursulas - for ursula in _ursulas: - del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] + for port in _ports_to_remove: + del MOCK_KNOWN_URSULAS_CACHE[port] + + +@pytest.fixture(scope="function") +def lonely_ursula_maker(ursula_federated_test_config): + class _PartialUrsulaMaker: + _partial = partial(make_federated_ursulas, + ursula_config=ursula_federated_test_config, + know_each_other=False, + ) + _made = [] + + def __call__(self, *args, **kwargs): + ursula = self._partial(*args, **kwargs) + self._made.extend(ursula) + return ursula + + def clean(self): + for ursula in self._made: + del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] + _maker = _PartialUrsulaMaker() + yield _maker + _maker.clean() + # From 10b8b13b9b9c38fe93259ca8b38a7e8594b93224 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 19:28:09 -0700 Subject: [PATCH 103/167] Ursula maker fixtures now raise if the cache is unclean. --- tests/fixtures.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index c36a714c7..62691563d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -408,7 +408,6 @@ def lonely_ursula_maker(ursula_federated_test_config): # def make_token_economics(blockchain): - # Get current blocktime now = blockchain.w3.eth.getBlock(block_identifier='latest').timestamp @@ -424,7 +423,7 @@ def make_token_economics(blockchain): economics = StandardTokenEconomics( worklock_boosting_refund_rate=200, worklock_commitment_duration=60, # periods - worklock_supply=10*BaseEconomics._default_maximum_allowed_locked, + worklock_supply=10 * BaseEconomics._default_maximum_allowed_locked, bidding_start_date=bidding_start_date, bidding_end_date=bidding_end_date, cancellation_end_date=cancellation_end_date, @@ -562,11 +561,11 @@ def _make_agency(testerchain, registry=test_registry) worklock_deployer.deploy() - token_agent = token_deployer.make_agent() # 1 Token - staking_agent = staking_escrow_deployer.make_agent() # 2 Staking Escrow - policy_agent = policy_manager_deployer.make_agent() # 3 Policy Agent - _adjudicator_agent = adjudicator_deployer.make_agent() # 4 Adjudicator - _worklock_agent = worklock_deployer.make_agent() # 5 Worklock + token_agent = token_deployer.make_agent() # 1 Token + staking_agent = staking_escrow_deployer.make_agent() # 2 Staking Escrow + policy_agent = policy_manager_deployer.make_agent() # 3 Policy Agent + _adjudicator_agent = adjudicator_deployer.make_agent() # 4 Adjudicator + _worklock_agent = worklock_deployer.make_agent() # 5 Worklock # Set additional parameters minimum, default, maximum = FEE_RATE_RANGE @@ -664,6 +663,8 @@ def stakers(testerchain, agency, token_economics, test_registry): @pytest.fixture(scope="module") def blockchain_ursulas(testerchain, stakers, ursula_decentralized_test_config): + if MOCK_KNOWN_URSULAS_CACHE: + raise RuntimeError("Ursulas cache was unclear at fixture loading time. Did you use one of the ursula maker functions without cleaning up?") _ursulas = make_decentralized_ursulas(ursula_config=ursula_decentralized_test_config, stakers_addresses=testerchain.stakers_accounts, workers_addresses=testerchain.ursulas_accounts, @@ -679,6 +680,9 @@ def blockchain_ursulas(testerchain, stakers, ursula_decentralized_test_config): yield _ursulas + for ursula in _ursulas: + del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] + @pytest.fixture(scope="module") def idle_staker(testerchain, agency): @@ -872,7 +876,8 @@ def manual_staker(testerchain, agency): address = '0xaaa23A5c74aBA6ca5E7c09337d5317A7C4563075' if address not in testerchain.client.accounts: staker_private_key = '13378db1c2af06933000504838afc2d52efa383206454deefb1836f8f4cd86f8' - address = testerchain.provider.ethereum_tester.add_account(staker_private_key, password=INSECURE_DEVELOPMENT_PASSWORD) + address = testerchain.provider.ethereum_tester.add_account(staker_private_key, + password=INSECURE_DEVELOPMENT_PASSWORD) tx = {'to': address, 'from': testerchain.etherbase_account, @@ -968,6 +973,9 @@ def fleet_of_highperf_mocked_ursulas(ursula_federated_test_config, request): ursula.known_nodes.checksum = b"This is a fleet state checksum..".hex() yield _ursulas + for ursula in _ursulas: + del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] + @pytest.fixture(scope="module") def highperf_mocked_alice(fleet_of_highperf_mocked_ursulas): @@ -1003,6 +1011,7 @@ def highperf_mocked_bob(fleet_of_highperf_mocked_ursulas): bob._learning_task.stop() return bob + # # CLI # From d1025e9d15e017d021f530429d3aea760f3698e4 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 19:29:03 -0700 Subject: [PATCH 104/167] Using lonely_ursula_maker in all the right places. --- .../characters/test_ursula_startup.py | 8 ++--- tests/integration/learning/test_domains.py | 17 +++++----- .../learning/test_firstula_circumstances.py | 20 +++--------- .../integration/learning/test_fleet_state.py | 19 ++++-------- .../learning/test_learning_upgrade.py | 31 ++++++++++++------- .../network/test_network_upgrade.py | 4 +-- .../integration/network/test_node_storage.py | 8 ++--- 7 files changed, 44 insertions(+), 63 deletions(-) diff --git a/tests/integration/characters/test_ursula_startup.py b/tests/integration/characters/test_ursula_startup.py index 3b20229c7..3c2916d4d 100644 --- a/tests/integration/characters/test_ursula_startup.py +++ b/tests/integration/characters/test_ursula_startup.py @@ -14,16 +14,14 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ +import pytest from tests.utils.middleware import MockRestMiddleware from tests.utils.ursula import make_federated_ursulas -def test_new_federated_ursula_announces_herself(ursula_federated_test_config): - ursula_in_a_house, ursula_with_a_mouse = make_federated_ursulas(ursula_config=ursula_federated_test_config, - quantity=2, - know_each_other=False, - network_middleware=MockRestMiddleware()) +def test_new_federated_ursula_announces_herself(lonely_ursula_maker): + ursula_in_a_house, ursula_with_a_mouse = lonely_ursula_maker(quantity=2, domains=["useless_domain"]) # Neither Ursula knows about the other. assert ursula_in_a_house.known_nodes == ursula_with_a_mouse.known_nodes diff --git a/tests/integration/learning/test_domains.py b/tests/integration/learning/test_domains.py index 2f359ec39..90a01bb4a 100644 --- a/tests/integration/learning/test_domains.py +++ b/tests/integration/learning/test_domains.py @@ -20,15 +20,12 @@ from functools import partial from tests.utils.ursula import make_federated_ursulas -def test_learner_learns_about_domains_separately(ursula_federated_test_config, caplog): - lonely_ursula_maker = partial(make_federated_ursulas, - ursula_config=ursula_federated_test_config, - quantity=3, - know_each_other=True) +def test_learner_learns_about_domains_separately(lonely_ursula_maker, caplog): + _lonely_ursula_maker = partial(lonely_ursula_maker, know_each_other=True, quantity=3) - global_learners = lonely_ursula_maker(domains={"nucypher1.test_suite"}) - first_domain_learners = lonely_ursula_maker(domains={"nucypher1.test_suite"}) - second_domain_learners = lonely_ursula_maker(domains={"nucypher2.test_suite"}) + global_learners = _lonely_ursula_maker(domains={"nucypher1.test_suite"}) + first_domain_learners = _lonely_ursula_maker(domains={"nucypher1.test_suite"}) + second_domain_learners = _lonely_ursula_maker(domains={"nucypher2.test_suite"}) big_learner = global_learners.pop() @@ -45,8 +42,8 @@ def test_learner_learns_about_domains_separately(ursula_federated_test_config, c # All domain 1 nodes assert len(big_learner.known_nodes) == 5 - new_first_domain_learner = lonely_ursula_maker(domains={"nucypher1.test_suite"}).pop() - _new_second_domain_learner = lonely_ursula_maker(domains={"nucypher2.test_suite"}) + new_first_domain_learner = _lonely_ursula_maker(domains={"nucypher1.test_suite"}).pop() + _new_second_domain_learner = _lonely_ursula_maker(domains={"nucypher2.test_suite"}) new_first_domain_learner._current_teacher_node = big_learner new_first_domain_learner.learn_from_teacher_node() diff --git a/tests/integration/learning/test_firstula_circumstances.py b/tests/integration/learning/test_firstula_circumstances.py index 85ea7a9ea..fc3931e8e 100644 --- a/tests/integration/learning/test_firstula_circumstances.py +++ b/tests/integration/learning/test_firstula_circumstances.py @@ -23,17 +23,11 @@ from nucypher.network.middleware import RestMiddleware from tests.utils.ursula import make_federated_ursulas -def test_proper_seed_node_instantiation(ursula_federated_test_config): - lonely_ursula_maker = partial(make_federated_ursulas, - ursula_config=ursula_federated_test_config, - quantity=1, - know_each_other=False, - lonely=True, - domains=["useless domain"]) - - firstula = lonely_ursula_maker().pop() +def test_proper_seed_node_instantiation(lonely_ursula_maker): + _lonely_ursula_maker = partial(lonely_ursula_maker, quantity=1) + firstula = _lonely_ursula_maker().pop() firstula_as_seed_node = firstula.seed_node_metadata() - any_other_ursula = lonely_ursula_maker(seed_nodes=[firstula_as_seed_node]).pop() + any_other_ursula = _lonely_ursula_maker(seed_nodes=[firstula_as_seed_node], domains=["useless domain"]).pop() assert not any_other_ursula.known_nodes any_other_ursula.start_learning_loop(now=True) @@ -41,11 +35,7 @@ def test_proper_seed_node_instantiation(ursula_federated_test_config): @pt.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) +def test_get_cert_from_running_seed_node(lonely_ursula_maker): firstula = lonely_ursula_maker().pop() node_deployer = firstula.get_deployer() diff --git a/tests/integration/learning/test_fleet_state.py b/tests/integration/learning/test_fleet_state.py index 851af2acf..ba9528b2e 100644 --- a/tests/integration/learning/test_fleet_state.py +++ b/tests/integration/learning/test_fleet_state.py @@ -56,11 +56,7 @@ def test_nodes_with_equal_fleet_state_do_not_send_anew(federated_ursulas): assert result is FLEET_STATES_MATCH -def test_old_state_is_preserved(federated_ursulas, ursula_federated_test_config): - lonely_ursula_maker = partial(make_federated_ursulas, - ursula_config=ursula_federated_test_config, - quantity=1, - know_each_other=False) +def test_old_state_is_preserved(federated_ursulas, lonely_ursula_maker): lonely_learner = lonely_ursula_maker().pop() # This Ursula doesn't know about any nodes. @@ -84,16 +80,13 @@ def test_old_state_is_preserved(federated_ursulas, ursula_federated_test_config) assert lonely_learner.known_nodes.states[checksum_after_learning_two].nodes == proper_second_state -def test_state_is_recorded_after_learning(federated_ursulas, ursula_federated_test_config): +def test_state_is_recorded_after_learning(federated_ursulas, lonely_ursula_maker): """ Similar to above, but this time we show that the Learner records a new state only once after learning about a bunch of nodes. """ - lonely_ursula_maker = partial(make_federated_ursulas, - ursula_config=ursula_federated_test_config, - quantity=1, - know_each_other=False) - lonely_learner = lonely_ursula_maker().pop() + _lonely_ursula_maker = partial(lonely_ursula_maker, quantity=1) + lonely_learner = _lonely_ursula_maker().pop() # This Ursula doesn't know about any nodes. assert len(lonely_learner.known_nodes) == 0 @@ -108,5 +101,5 @@ def test_state_is_recorded_after_learning(federated_ursulas, ursula_federated_te states = list(lonely_learner.known_nodes.states.values()) assert len(states) == 2 - assert len(states[0].nodes) == 2 # The first fleet state is just us and the one about whom we learned. - assert len(states[1].nodes) == len(federated_ursulas) + 2 # When we ran learn_from_teacher_node, we also loaded the rest of the fleet. + assert len(states[0].nodes) == 2 # The first fleet state is just us and the one about whom we learned, which is part of the fleet. + assert len(states[1].nodes) == len(federated_ursulas) + 1 # When we ran learn_from_teacher_node, we also loaded the rest of the fleet. diff --git a/tests/integration/learning/test_learning_upgrade.py b/tests/integration/learning/test_learning_upgrade.py index f93d95a83..9d0d4bddd 100644 --- a/tests/integration/learning/test_learning_upgrade.py +++ b/tests/integration/learning/test_learning_upgrade.py @@ -15,23 +15,20 @@ along with nucypher. If not, see . """ +import os from collections import namedtuple -import os -from bytestring_splitter import VariableLengthBytestring from eth_utils.address import to_checksum_address from twisted.logger import LogLevel, globalLogPublisher +from bytestring_splitter import VariableLengthBytestring from nucypher.acumen.nicknames import nickname_from_seed from nucypher.characters.base import Character from tests.utils.middleware import MockRestMiddleware -from tests.utils.ursula import make_federated_ursulas -def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog): - nodes = make_federated_ursulas(ursula_config=ursula_federated_test_config, - quantity=3, - know_each_other=False) +def test_emit_warning_upon_new_version(lonely_ursula_maker, caplog): + nodes = lonely_ursula_maker(quantity=3) teacher, learner, new_node = nodes learner.remember_node(teacher) @@ -47,13 +44,23 @@ def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog): warnings.append(event) globalLogPublisher.addObserver(warning_trapper) - learner.learn_from_teacher_node() + + # First we'll get a warning, because we're loading a seednode with a version from the future. + learner.load_seednodes() assert len(warnings) == 1 assert warnings[0]['log_format'] == learner.unknown_version_message.format(new_node, new_node.TEACHER_VERSION, learner.LEARNER_VERSION) + # We don't use the above seednode as a teacher, but when our teacher tries to tell us about it, we get another of the same warning. + learner.learn_from_teacher_node() + + assert len(warnings) == 2 + assert warnings[1]['log_format'] == learner.unknown_version_message.format(new_node, + new_node.TEACHER_VERSION, + learner.LEARNER_VERSION) + # Now let's go a little further: make the version totally unrecognizable. # First, there's enough garbage to at least scrape a potential checksum address @@ -76,8 +83,8 @@ def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog): accidental_nickname = nickname_from_seed(accidental_checksum)[0] accidental_node_repr = Character._display_name_template.format("Ursula", accidental_nickname, accidental_checksum) - assert len(warnings) == 2 - assert warnings[1]['log_format'] == learner.unknown_version_message.format(accidental_node_repr, + assert len(warnings) == 3 + assert warnings[2]['log_format'] == learner.unknown_version_message.format(accidental_node_repr, future_version, learner.LEARNER_VERSION) @@ -91,9 +98,9 @@ def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog): learner._current_teacher_node = teacher learner.learn_from_teacher_node() - assert len(warnings) == 3 + assert len(warnings) == 4 # ...so this time we get a "really unknown version message" - assert warnings[2]['log_format'] == learner.really_unknown_version_message.format(future_version, + assert warnings[3]['log_format'] == learner.really_unknown_version_message.format(future_version, learner.LEARNER_VERSION) globalLogPublisher.removeObserver(warning_trapper) diff --git a/tests/integration/network/test_network_upgrade.py b/tests/integration/network/test_network_upgrade.py index e3757b341..1aa2216b6 100644 --- a/tests/integration/network/test_network_upgrade.py +++ b/tests/integration/network/test_network_upgrade.py @@ -38,8 +38,8 @@ def test_alice_enacts_policies_in_policy_group_via_rest(enacted_federated_policy @pytest_twisted.inlineCallbacks -def test_federated_nodes_connect_via_tls_and_verify(ursula_federated_test_config): - node = make_federated_ursulas(ursula_config=ursula_federated_test_config, quantity=1).pop() +def test_federated_nodes_connect_via_tls_and_verify(lonely_ursula_maker): + node = lonely_ursula_maker(quantity=1).pop() node_deployer = node.get_deployer() node_deployer.addServices() diff --git a/tests/integration/network/test_node_storage.py b/tests/integration/network/test_node_storage.py index b4d07dd2c..816230128 100644 --- a/tests/integration/network/test_node_storage.py +++ b/tests/integration/network/test_node_storage.py @@ -20,18 +20,14 @@ import pytest import pytest_twisted as pt from twisted.internet.threads import deferToThread -from tests.utils.ursula import make_federated_ursulas - @pt.inlineCallbacks -def test_one_node_stores_a_bunch_of_others(federated_ursulas, ursula_federated_test_config): +def test_one_node_stores_a_bunch_of_others(federated_ursulas, lonely_ursula_maker): the_chosen_seednode = list(federated_ursulas)[2] # ...neo? seed_node = the_chosen_seednode.seed_node_metadata() - newcomer = make_federated_ursulas( - ursula_config=ursula_federated_test_config, + newcomer = lonely_ursula_maker( quantity=1, - know_each_other=False, save_metadata=True, seed_nodes=[seed_node]).pop() From 31856340ad3faf4ae65904c5b9a83423f76c01ec Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 19:29:27 -0700 Subject: [PATCH 105/167] This test needs to look for the checksum_address specifically, since this will be a stranger node or sprout now. --- tests/integration/network/test_node_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/network/test_node_storage.py b/tests/integration/network/test_node_storage.py index 816230128..8475f2bc8 100644 --- a/tests/integration/network/test_node_storage.py +++ b/tests/integration/network/test_node_storage.py @@ -39,7 +39,7 @@ def test_one_node_stores_a_bunch_of_others(federated_ursulas, lonely_ursula_make newcomer.start_learning_loop() start = maya.now() # Loop until the_chosen_seednode is in storage. - while the_chosen_seednode not in newcomer.node_storage.all(federated_only=True): + while the_chosen_seednode.checksum_address not in [u.checksum_address for u in newcomer.node_storage.all(federated_only=True)]: passed = maya.now() - start if passed.seconds > 2: pytest.fail("Didn't find the seed node.") From 8c6cdf5145aeef186f6284863802913176334e6e Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 25 Jun 2020 23:18:14 -0700 Subject: [PATCH 106/167] Moving federated tests into their own module to keep the cache cleaner. --- .../control/test_rpc_control_blockchain.py | 57 ------------- .../control/test_rpc_control_federated.py | 81 +++++++++++++++++++ .../test_ursula_prepares_to_act_as_worker.py | 1 + tests/fixtures.py | 7 +- .../learning/test_learning_upgrade.py | 3 +- 5 files changed, 88 insertions(+), 61 deletions(-) create mode 100644 tests/acceptance/characters/control/test_rpc_control_federated.py diff --git a/tests/acceptance/characters/control/test_rpc_control_blockchain.py b/tests/acceptance/characters/control/test_rpc_control_blockchain.py index 606a479d1..4b010822a 100644 --- a/tests/acceptance/characters/control/test_rpc_control_blockchain.py +++ b/tests/acceptance/characters/control/test_rpc_control_blockchain.py @@ -111,60 +111,3 @@ def test_alice_rpc_character_control_grant(alice_rpc_test_client, grant_control_ assert validate_json_rpc_response_data(response=response, method_name=method_name, interface=AliceInterface) - - -def test_bob_rpc_character_control_join_policy(bob_rpc_controller, join_control_request, enacted_federated_policy): - - # Simulate passing in a teacher-uri - enacted_federated_policy.bob.remember_node(list(enacted_federated_policy.accepted_ursulas)[0]) - - method_name, params = join_control_request - request_data = {'method': method_name, 'params': params} - response = bob_rpc_controller.send(request_data) - assert validate_json_rpc_response_data(response=response, - method_name=method_name, - interface=BobInterface) - - -def test_enrico_rpc_character_control_encrypt_message(enrico_rpc_controller_test_client, encrypt_control_request): - method_name, params = encrypt_control_request - request_data = {'method': method_name, 'params': params} - response = enrico_rpc_controller_test_client.send(request_data) - assert validate_json_rpc_response_data(response=response, - method_name=method_name, - interface=EnricoInterface) - - -def test_bob_rpc_character_control_retrieve(bob_rpc_controller, retrieve_control_request): - method_name, params = retrieve_control_request - request_data = {'method': method_name, 'params': params} - response = bob_rpc_controller.send(request_data) - assert validate_json_rpc_response_data(response=response, - method_name=method_name, - interface=BobInterface) - - -def test_bob_rpc_character_control_retrieve_with_tmap( - enacted_blockchain_policy, blockchain_bob, blockchain_alice, - bob_rpc_controller, retrieve_control_request): - tmap_64 = b64encode(bytes(enacted_blockchain_policy.treasure_map)).decode() - method_name, params = retrieve_control_request - params['treasure_map'] = tmap_64 - request_data = {'method': method_name, 'params': params} - response = bob_rpc_controller.send(request_data) - assert response.data['result']['cleartexts'][0] == 'Welcome to flippering number 1.' - - # Make a wrong (empty) treasure map - - wrong_tmap = DecentralizedTreasureMap(m=0) - wrong_tmap.prepare_for_publication( - blockchain_bob.public_keys(DecryptingPower), - blockchain_bob.public_keys(SigningPower), - blockchain_alice.stamp, - b'Wrong!') - wrong_tmap._blockchain_signature = b"this is not a signature, but we don't need one for this test....." # ...because it only matters when Ursula looks at it. - tmap_bytes = bytes(wrong_tmap) - tmap_64 = b64encode(tmap_bytes).decode() - request_data['params']['treasure_map'] = tmap_64 - with pytest.raises(DecentralizedTreasureMap.IsDisorienting): - bob_rpc_controller.send(request_data) diff --git a/tests/acceptance/characters/control/test_rpc_control_federated.py b/tests/acceptance/characters/control/test_rpc_control_federated.py new file mode 100644 index 000000000..576da201c --- /dev/null +++ b/tests/acceptance/characters/control/test_rpc_control_federated.py @@ -0,0 +1,81 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + +from base64 import b64encode + +import pytest + +from nucypher.characters.control.interfaces import BobInterface, EnricoInterface +from nucypher.crypto.powers import DecryptingPower, SigningPower +from nucypher.policy.collections import DecentralizedTreasureMap +from tests.acceptance.characters.control.test_rpc_control_blockchain import validate_json_rpc_response_data + + +def test_bob_rpc_character_control_join_policy(bob_rpc_controller, join_control_request, enacted_blockchain_policy): + # Simulate passing in a teacher-uri + enacted_blockchain_policy.bob.remember_node(list(enacted_blockchain_policy.accepted_ursulas)[0]) + + method_name, params = join_control_request + request_data = {'method': method_name, 'params': params} + response = bob_rpc_controller.send(request_data) + assert validate_json_rpc_response_data(response=response, + method_name=method_name, + interface=BobInterface) + + +def test_enrico_rpc_character_control_encrypt_message(enrico_rpc_controller_test_client, encrypt_control_request): + method_name, params = encrypt_control_request + request_data = {'method': method_name, 'params': params} + response = enrico_rpc_controller_test_client.send(request_data) + assert validate_json_rpc_response_data(response=response, + method_name=method_name, + interface=EnricoInterface) + + +def test_bob_rpc_character_control_retrieve(bob_rpc_controller, retrieve_control_request): + method_name, params = retrieve_control_request + request_data = {'method': method_name, 'params': params} + response = bob_rpc_controller.send(request_data) + assert validate_json_rpc_response_data(response=response, + method_name=method_name, + interface=BobInterface) + + +def test_bob_rpc_character_control_retrieve_with_tmap( + enacted_blockchain_policy, blockchain_bob, blockchain_alice, + bob_rpc_controller, retrieve_control_request): + tmap_64 = b64encode(bytes(enacted_blockchain_policy.treasure_map)).decode() + method_name, params = retrieve_control_request + params['treasure_map'] = tmap_64 + request_data = {'method': method_name, 'params': params} + response = bob_rpc_controller.send(request_data) + assert response.data['result']['cleartexts'][0] == 'Welcome to flippering number 1.' + + # Make a wrong (empty) treasure map + + wrong_tmap = DecentralizedTreasureMap(m=0) + wrong_tmap.prepare_for_publication( + blockchain_bob.public_keys(DecryptingPower), + blockchain_bob.public_keys(SigningPower), + blockchain_alice.stamp, + b'Wrong!') + wrong_tmap._blockchain_signature = b"this is not a signature, but we don't need one for this test....." # ...because it only matters when Ursula looks at it. + tmap_bytes = bytes(wrong_tmap) + tmap_64 = b64encode(tmap_bytes).decode() + request_data['params']['treasure_map'] = tmap_64 + with pytest.raises(DecentralizedTreasureMap.IsDisorienting): + bob_rpc_controller.send(request_data) diff --git a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py index 5cca9d96d..e28bb4571 100644 --- a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py +++ b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py @@ -29,6 +29,7 @@ from tests.utils.middleware import NodeIsDownMiddleware from tests.utils.ursula import make_decentralized_ursulas +@pytest.mark.usefixtures("blockchain_ursulas") def test_stakers_bond_to_ursulas(testerchain, test_registry, stakers, ursula_decentralized_test_config): ursulas = make_decentralized_ursulas(ursula_config=ursula_decentralized_test_config, stakers_addresses=testerchain.stakers_accounts, diff --git a/tests/fixtures.py b/tests/fixtures.py index 62691563d..c26c59cd5 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -190,6 +190,8 @@ def ursula_decentralized_test_config(test_registry): rest_port=MOCK_URSULA_STARTING_PORT) yield config config.cleanup() + for k in list(MOCK_KNOWN_URSULAS_CACHE.keys()): + del MOCK_KNOWN_URSULAS_CACHE[k] @pytest.fixture(scope="module") @@ -678,10 +680,11 @@ def blockchain_ursulas(testerchain, stakers, ursula_decentralized_test_config): for ursula_to_learn_about in _ursulas: ursula_to_teach.remember_node(ursula_to_learn_about) + _ports_to_remove = [ursula.rest_interface.port for ursula in _ursulas] yield _ursulas - for ursula in _ursulas: - del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] + for port in _ports_to_remove: + del MOCK_KNOWN_URSULAS_CACHE[port] @pytest.fixture(scope="module") diff --git a/tests/integration/learning/test_learning_upgrade.py b/tests/integration/learning/test_learning_upgrade.py index 9d0d4bddd..eb8ff9b50 100644 --- a/tests/integration/learning/test_learning_upgrade.py +++ b/tests/integration/learning/test_learning_upgrade.py @@ -28,8 +28,7 @@ from tests.utils.middleware import MockRestMiddleware def test_emit_warning_upon_new_version(lonely_ursula_maker, caplog): - nodes = lonely_ursula_maker(quantity=3) - teacher, learner, new_node = nodes + teacher, learner, new_node = lonely_ursula_maker(quantity=3) learner.remember_node(teacher) teacher.remember_node(learner) From 07b1c8eda5116a83d1bad01de35b417c9481e0b5 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 6 Jul 2020 19:53:27 -0700 Subject: [PATCH 107/167] Appropriate response for a map that is already stored. --- nucypher/network/server.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nucypher/network/server.py b/nucypher/network/server.py index f4c694053..cea1dfd18 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -371,6 +371,20 @@ def make_rest_app( except _MapClass.InvalidSignature: log.info("Bad TreasureMap HRAC Signature; not storing {}".format(treasure_map_id)) return Response("This TreasureMap's HRAC is not properly signed.", status=401) + + treasure_map_index = bytes.fromhex(treasure_map_id) + + # First let's see if we already have this map. + + try: + previously_saved_map = this_node.treasure_maps[treasure_map_index] + except KeyError: + pass # We don't have the map. We'll validate and perhaps save it. + else: + if previously_saved_map == treasure_map: + return Response("Already have this map.", status=303) + # Otherwise, if it's a different map with the same ID, we move on to validation. + if treasure_map.public_id() == treasure_map_id: do_store = True else: @@ -383,9 +397,6 @@ def make_rest_app( if do_store: log.info("{} storing TreasureMap {}".format(this_node, treasure_map_id)) - - # TODO 341 - what if we already have this TreasureMap? - treasure_map_index = bytes.fromhex(treasure_map_id) this_node.treasure_maps[treasure_map_index] = treasure_map return Response(bytes(treasure_map), status=202) else: From 42cf16a0eeb654eeeef38c14ab2dc0ddb2304aab Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 6 Jul 2020 19:55:48 -0700 Subject: [PATCH 108/167] Fixed imports for acumen. --- tests/acceptance/learning/test_fault_tolerance.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/acceptance/learning/test_fault_tolerance.py b/tests/acceptance/learning/test_fault_tolerance.py index ddf351376..b6a872d82 100644 --- a/tests/acceptance/learning/test_fault_tolerance.py +++ b/tests/acceptance/learning/test_fault_tolerance.py @@ -16,14 +16,12 @@ """ import pytest -from constant_sorrow.constants import NOT_SIGNED from twisted.logger import LogLevel, globalLogPublisher -from nucypher.crypto.powers import TransactingPower -from nucypher.acumen.nicknames import nickname_from_seed +from constant_sorrow.constants import NOT_SIGNED from nucypher.acumen.perception import FleetSensor +from nucypher.crypto.powers import TransactingPower from nucypher.network.nodes import Learner -from nucypher.network.nodes import FleetStateTracker, Learner from tests.utils.middleware import MockRestMiddleware from tests.utils.ursula import make_ursula_for_staker @@ -73,6 +71,7 @@ def test_blockchain_ursula_stamp_verification_tolerance(blockchain_ursulas, mock lonely_blockchain_learner.learn_from_teacher_node(eager=True) assert len(lonely_blockchain_learner.suspicious_activities_witnessed['vladimirs']) == 1 + @pytest.mark.skip("See Issue #1075") # TODO: Issue #1075 def test_invalid_workers_tolerance(testerchain, test_registry, @@ -156,7 +155,6 @@ def test_invalid_workers_tolerance(testerchain, with pytest.raises(worker.NotStaking): worker.verify_node(force=True, network_middleware=MockRestMiddleware(), certificate_filepath="quietorl") - # Let's learn from this invalid node lonely_blockchain_learner._current_teacher_node = worker globalLogPublisher.addObserver(warning_trapper) From a5c31da1d335ff4bc96133e213f3a7877206b80c Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Tue, 7 Jul 2020 11:48:52 -0700 Subject: [PATCH 109/167] Handles the --lonely CLI option for alice and bob command. --- nucypher/cli/commands/alice.py | 10 ++++++++-- nucypher/cli/commands/bob.py | 14 +++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/nucypher/cli/commands/alice.py b/nucypher/cli/commands/alice.py index d6d16623c..4d3bf95cc 100644 --- a/nucypher/cli/commands/alice.py +++ b/nucypher/cli/commands/alice.py @@ -91,6 +91,7 @@ class AliceConfigOptions: middleware: RestMiddleware, gas_strategy: str, signer_uri: str, + lonely: bool, ): if federated_only and geth: @@ -116,6 +117,7 @@ class AliceConfigOptions: self.discovery_port = discovery_port self.registry_filepath = registry_filepath self.middleware = middleware + self.lonely = lonely def create_config(self, emitter, config_file): @@ -136,7 +138,9 @@ class AliceConfigOptions: provider_uri=self.provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, - federated_only=True) + federated_only=True, + lonely=self.lonely + ) else: try: @@ -152,7 +156,9 @@ class AliceConfigOptions: filepath=config_file, rest_port=self.discovery_port, checksum_address=self.pay_with, - registry_filepath=self.registry_filepath) + registry_filepath=self.registry_filepath, + lonely=self.lonely + ) except FileNotFoundError: return handle_missing_configuration_file( character_config_class=AliceConfiguration, diff --git a/nucypher/cli/commands/bob.py b/nucypher/cli/commands/bob.py index 24a714bd5..6899fa92d 100644 --- a/nucypher/cli/commands/bob.py +++ b/nucypher/cli/commands/bob.py @@ -73,6 +73,7 @@ class BobConfigOptions: federated_only: bool, gas_strategy: str, signer_uri: str, + lonely: bool ): self.provider_uri = provider_uri @@ -85,6 +86,7 @@ class BobConfigOptions: self.dev = dev self.middleware = middleware self.federated_only = federated_only + self.lonely = lonely def create_config(self, emitter: StdoutEmitter, config_file: str) -> BobConfiguration: if self.dev: @@ -97,7 +99,9 @@ class BobConfigOptions: signer_uri=self.signer_uri, federated_only=True, checksum_address=self.checksum_address, - network_middleware=self.middleware) + network_middleware=self.middleware, + lonely=self.lonely + ) else: try: return BobConfiguration.from_configuration_file( @@ -110,7 +114,9 @@ class BobConfigOptions: signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, registry_filepath=self.registry_filepath, - network_middleware=self.middleware) + network_middleware=self.middleware, + lonely=self.lonely + ) except FileNotFoundError: handle_missing_configuration_file(character_config_class=BobConfiguration, config_file=config_file) @@ -133,6 +139,7 @@ class BobConfigOptions: provider_uri=self.provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, + lonely=self.lonely ) def get_updates(self) -> dict: @@ -142,7 +149,8 @@ class BobConfigOptions: registry_filepath=self.registry_filepath, provider_uri=self.provider_uri, signer_uri=self.signer_uri, - gas_strategy=self.gas_strategy + gas_strategy=self.gas_strategy, + lonely=self.lonely ) # Depends on defaults being set on Configuration classes, filtrates None values updates = {k: v for k, v in payload.items() if v is not None} From 8cf42aab036a702f1fd05f69622de9a6122b4d06 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 8 Jul 2020 12:03:19 -0700 Subject: [PATCH 110/167] Separating lifecycle tests for fixture hygiene. --- tests/acceptance/cli/test_cli_lifecycle.py | 404 ------------------ .../cli/test_decentralized_cli_lifecycle.py | 0 .../cli/test_federated_cli_lifecycle.py | 130 ++++++ 3 files changed, 130 insertions(+), 404 deletions(-) delete mode 100644 tests/acceptance/cli/test_cli_lifecycle.py create mode 100644 tests/acceptance/cli/test_decentralized_cli_lifecycle.py create mode 100644 tests/acceptance/cli/test_federated_cli_lifecycle.py diff --git a/tests/acceptance/cli/test_cli_lifecycle.py b/tests/acceptance/cli/test_cli_lifecycle.py deleted file mode 100644 index babc4247b..000000000 --- a/tests/acceptance/cli/test_cli_lifecycle.py +++ /dev/null @@ -1,404 +0,0 @@ -""" - This file is part of nucypher. - - nucypher is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - nucypher is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with nucypher. If not, see . -""" - -import json -from base64 import b64decode -from collections import namedtuple -from json import JSONDecodeError - -import datetime -import maya -import os -import pytest -import pytest_twisted as pt -import shutil -from twisted.internet import threads -from web3 import Web3 - -from nucypher.cli.main import nucypher_cli -from nucypher.config.characters import AliceConfiguration, BobConfiguration -from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN -from nucypher.crypto.kits import UmbralMessageKit -from nucypher.utilities.logging import GlobalLoggerSettings -from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, TEST_PROVIDER_URI -from tests.utils.ursula import start_pytest_ursula_services - -PLAINTEXT = "I'm bereaved, not a sap!" - - -class MockSideChannel: - - PolicyAndLabel = namedtuple('PolicyAndLabel', ['encrypting_key', 'label']) - BobPublicKeys = namedtuple('BobPublicKeys', ['bob_encrypting_key', 'bob_verifying_key']) - - class NoMessageKits(Exception): - pass - - class NoPolicies(Exception): - pass - - def __init__(self): - self.__message_kits = [] - self.__policies = [] - self.__alice_public_keys = [] - self.__bob_public_keys = [] - - def save_message_kit(self, message_kit: str) -> None: - self.__message_kits.append(message_kit) - - def fetch_message_kit(self) -> UmbralMessageKit: - if self.__message_kits: - message_kit = self.__message_kits.pop() - return message_kit - raise self.NoMessageKits - - def save_policy(self, policy: PolicyAndLabel): - self.__policies.append(policy) - - def fetch_policy(self) -> PolicyAndLabel: - if self.__policies: - policy = self.__policies[0] - return policy - raise self.NoPolicies - - def save_alice_pubkey(self, public_key): - self.__alice_public_keys.append(public_key) - - def fetch_alice_pubkey(self): - policy = self.__alice_public_keys.pop() - return policy - - def save_bob_public_keys(self, public_keys: BobPublicKeys): - self.__bob_public_keys.append(public_keys) - - def fetch_bob_public_keys(self) -> BobPublicKeys: - policy = self.__bob_public_keys.pop() - return policy - - -@pt.inlineCallbacks -def test_federated_cli_lifecycle(click_runner, - testerchain, - random_policy_label, - federated_ursulas, - custom_filepath, - custom_filepath_2): - yield _cli_lifecycle(click_runner, - testerchain, - random_policy_label, - federated_ursulas, - custom_filepath, - custom_filepath_2) - - -@pt.inlineCallbacks -def test_decentralized_cli_lifecycle(click_runner, - testerchain, - random_policy_label, - blockchain_ursulas, - custom_filepath, - custom_filepath_2, - agency_local_registry): - - yield _cli_lifecycle(click_runner, - testerchain, - random_policy_label, - blockchain_ursulas, - custom_filepath, - custom_filepath_2, - agency_local_registry.filepath) - - -def _cli_lifecycle(click_runner, - testerchain, - random_policy_label, - ursulas, - custom_filepath, - custom_filepath_2, - registry_filepath=None): - """ - This is an end to end integration test that runs each cli call - in it's own process using only CLI character control entry points, - and a mock side channel that runs in the control process - """ - federated = list(ursulas)[0].federated_only - - # Boring Setup Stuff - alice_config_root = str(custom_filepath) - bob_config_root = str(custom_filepath_2) - envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD} - - # A side channel exists - Perhaps a dApp - side_channel = MockSideChannel() - - shutil.rmtree(str(custom_filepath), ignore_errors=True) - shutil.rmtree(str(custom_filepath_2), ignore_errors=True) - - """ - Scene 1: Alice Installs nucypher to a custom filepath and examines her configuration - """ - - # Alice performs an installation for the first time - alice_init_args = ('alice', 'init', - '--network', TEMPORARY_DOMAIN, - '--config-root', alice_config_root) - if federated: - alice_init_args += ('--federated-only', ) - else: - alice_init_args += ('--provider', TEST_PROVIDER_URI, - '--pay-with', testerchain.alice_account, - '--registry-filepath', str(registry_filepath)) - - alice_init_response = click_runner.invoke(nucypher_cli, alice_init_args, catch_exceptions=False, env=envvars) - assert alice_init_response.exit_code == 0 - - # Prevent previous global logger settings set by aboce command from writing non-IPC messages to stdout - GlobalLoggerSettings.stop_console_logging() - - # Alice uses her configuration file to run the character "view" command - alice_configuration_file_location = os.path.join(alice_config_root, AliceConfiguration.generate_filename()) - alice_view_args = ('alice', 'public-keys', - '--json-ipc', - '--config-file', alice_configuration_file_location) - - alice_view_result = click_runner.invoke(nucypher_cli, - alice_view_args, - input=INSECURE_DEVELOPMENT_PASSWORD, - catch_exceptions=False, - env=envvars) - - assert alice_view_result.exit_code == 0 - - try: - alice_view_response = json.loads(alice_view_result.output) - except JSONDecodeError: - pytest.fail("Invalid JSON response from JSON-RPC Character process.") - - # Alice expresses her desire to participate in data sharing with nucypher - # by saving her public key somewhere Bob and Enrico can find it. - side_channel.save_alice_pubkey(alice_view_response['result']['alice_verifying_key']) - - """ - Scene 2: Bob installs nucypher, examines his configuration and expresses his - interest to participate in data retrieval by posting his public keys somewhere public (side-channel). - """ - bob_init_args = ('bob', 'init', - '--network', TEMPORARY_DOMAIN, - '--config-root', bob_config_root) - if federated: - bob_init_args += ('--federated-only', ) - else: - bob_init_args += ('--provider', TEST_PROVIDER_URI, - '--registry-filepath', str(registry_filepath), - '--checksum-address', testerchain.bob_account) - - bob_init_response = click_runner.invoke(nucypher_cli, bob_init_args, catch_exceptions=False, env=envvars) - assert bob_init_response.exit_code == 0 - - # Alice uses her configuration file to run the character "view" command - bob_configuration_file_location = os.path.join(bob_config_root, BobConfiguration.generate_filename()) - bob_view_args = ('bob', 'public-keys', - '--json-ipc', - '--mock-networking', # TODO: It's absurd for this public-keys command to connect at all. 1710 - '--config-file', bob_configuration_file_location) - - bob_view_result = click_runner.invoke(nucypher_cli, bob_view_args, catch_exceptions=False, env=envvars) - assert bob_view_result.exit_code == 0 - bob_view_response = json.loads(bob_view_result.output) - - # Bob interacts with the sidechannel - bob_public_keys = MockSideChannel.BobPublicKeys(bob_view_response['result']['bob_encrypting_key'], - bob_view_response['result']['bob_verifying_key']) - - side_channel.save_bob_public_keys(bob_public_keys) - - """ - Scene 3: Alice derives a policy keypair, and saves its public key to a sidechannel. - """ - - random_label = random_policy_label.decode() # Unicode string - - derive_args = ('alice', 'derive-policy-pubkey', - '--mock-networking', - '--json-ipc', - '--config-file', alice_configuration_file_location, - '--label', random_label) - - derive_response = click_runner.invoke(nucypher_cli, derive_args, catch_exceptions=False, env=envvars) - assert derive_response.exit_code == 0 - - derive_response = json.loads(derive_response.output) - assert derive_response['result']['label'] == random_label - - # Alice and the sidechannel: at Tinagre - policy = MockSideChannel.PolicyAndLabel(encrypting_key=derive_response['result']['policy_encrypting_key'], - label=derive_response['result']['label']) - side_channel.save_policy(policy=policy) - - """ - Scene 4: Enrico encrypts some data for some policy public key and saves it to a side channel. - """ - def enrico_encrypts(): - - # Fetch! - policy = side_channel.fetch_policy() - - enrico_args = ('enrico', - 'encrypt', - '--json-ipc', - '--policy-encrypting-key', policy.encrypting_key, - '--message', PLAINTEXT) - - encrypt_result = click_runner.invoke(nucypher_cli, enrico_args, catch_exceptions=False, env=envvars) - assert encrypt_result.exit_code == 0 - encrypt_result = json.loads(encrypt_result.output) - encrypted_message = encrypt_result['result']['message_kit'] # type: str - - side_channel.save_message_kit(message_kit=encrypted_message) - return encrypt_result - - def _alice_decrypts(encrypt_result): - """ - alice forgot what exactly she encrypted for bob. - she decrypts it just to make sure. - """ - policy = side_channel.fetch_policy() - alice_signing_key = side_channel.fetch_alice_pubkey() - message_kit = encrypt_result['result']['message_kit'] - - decrypt_args = ( - 'alice', 'decrypt', - '--mock-networking', - '--json-ipc', - '--config-file', alice_configuration_file_location, - '--message-kit', message_kit, - '--label', policy.label, - ) - - if federated: - decrypt_args += ('--federated-only',) - - decrypt_response_fail = click_runner.invoke(nucypher_cli, decrypt_args[0:7], catch_exceptions=False, env=envvars) - assert decrypt_response_fail.exit_code == 2 - - decrypt_response = click_runner.invoke(nucypher_cli, decrypt_args, catch_exceptions=False, env=envvars) - decrypt_result = json.loads(decrypt_response.output) - for cleartext in decrypt_result['result']['cleartexts']: - assert b64decode(cleartext.encode()).decode() == PLAINTEXT - - # replenish the side channel - side_channel.save_policy(policy=policy) - side_channel.save_alice_pubkey(alice_signing_key) - return encrypt_result - - """ - Scene 5: Alice grants access to Bob: - We catch up with Alice later on, but before she has learned about existing Ursulas... - """ - if federated: - teacher = list(ursulas)[0] - else: - teacher = list(ursulas)[1] - - teacher_uri = teacher.seed_node_metadata(as_teacher_uri=True) - - # Some Ursula is running somewhere - def _run_teacher(_encrypt_result): - start_pytest_ursula_services(ursula=teacher) - return teacher_uri - - def _grant(teacher_uri): - - # Alice fetched Bob's public keys from the side channel - bob_keys = side_channel.fetch_bob_public_keys() - bob_encrypting_key = bob_keys.bob_encrypting_key - bob_verifying_key = bob_keys.bob_verifying_key - - grant_args = ('alice', 'grant', - '--mock-networking', - '--json-ipc', - '--network', TEMPORARY_DOMAIN, - '--teacher', teacher_uri, - '--config-file', alice_configuration_file_location, - '--m', 2, - '--n', 3, - '--expiration', (maya.now() + datetime.timedelta(days=3)).iso8601(), - '--label', random_label, - '--bob-encrypting-key', bob_encrypting_key, - '--bob-verifying-key', bob_verifying_key) - - if federated: - grant_args += ('--federated-only',) - else: - grant_args += ('--provider', TEST_PROVIDER_URI, - '--rate', Web3.toWei(9, 'gwei')) - - grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars) - assert grant_result.exit_code == 0 - - grant_result = json.loads(grant_result.output) - - # TODO: Expand test to consider manual treasure map handing - # # Alice puts the Treasure Map somewhere Bob can get it. - # side_channel.save_treasure_map(treasure_map=grant_result['result']['treasure_map']) - - return grant_result - - def _bob_retrieves(_grant_result): - """ - Scene 6: Bob retrieves encrypted data from the side channel and uses nucypher to re-encrypt it - """ - - # Bob interacts with a sidechannel - ciphertext_message_kit = side_channel.fetch_message_kit() - - policy = side_channel.fetch_policy() - policy_encrypting_key, label = policy - - alice_signing_key = side_channel.fetch_alice_pubkey() - - retrieve_args = ('bob', 'retrieve', - '--mock-networking', - '--json-ipc', - '--teacher', teacher_uri, - '--config-file', bob_configuration_file_location, - '--message-kit', ciphertext_message_kit, - '--label', label, - '--policy-encrypting-key', policy_encrypting_key, - '--alice-verifying-key', alice_signing_key) - - # TODO: Remove - Federated not used for retrieve any more - # if federated: - # retrieve_args += ('--federated-only',) - - retrieve_response = click_runner.invoke(nucypher_cli, retrieve_args, catch_exceptions=False, env=envvars) - assert retrieve_response.exit_code == 0 - - retrieve_response = json.loads(retrieve_response.output) - for cleartext in retrieve_response['result']['cleartexts']: - assert b64decode(cleartext.encode()).decode() == PLAINTEXT - - return - - # Run the Callbacks - d = threads.deferToThread(enrico_encrypts) # scene 4 - d.addCallback(_alice_decrypts) # scene 5 (uncertainty) - d.addCallback(_run_teacher) # scene 6 (preamble) - d.addCallback(_grant) # scene 7 - d.addCallback(_bob_retrieves) # scene 8 - - return d diff --git a/tests/acceptance/cli/test_decentralized_cli_lifecycle.py b/tests/acceptance/cli/test_decentralized_cli_lifecycle.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/acceptance/cli/test_federated_cli_lifecycle.py b/tests/acceptance/cli/test_federated_cli_lifecycle.py new file mode 100644 index 000000000..b309aebe7 --- /dev/null +++ b/tests/acceptance/cli/test_federated_cli_lifecycle.py @@ -0,0 +1,130 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + +import json +from base64 import b64decode +from collections import namedtuple +from json import JSONDecodeError + +import datetime +import maya +import os +import pytest +import pytest_twisted as pt +import shutil +from twisted.internet import threads +from web3 import Web3 + +from nucypher.cli.main import nucypher_cli +from nucypher.config.characters import AliceConfiguration, BobConfiguration +from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN +from nucypher.crypto.kits import UmbralMessageKit +from nucypher.utilities.logging import GlobalLoggerSettings +from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, TEST_PROVIDER_URI +from tests.utils.ursula import start_pytest_ursula_services, MOCK_KNOWN_URSULAS_CACHE + +PLAINTEXT = "I'm bereaved, not a sap!" + + +class MockSideChannel: + + PolicyAndLabel = namedtuple('PolicyAndLabel', ['encrypting_key', 'label']) + BobPublicKeys = namedtuple('BobPublicKeys', ['bob_encrypting_key', 'bob_verifying_key']) + + class NoMessageKits(Exception): + pass + + class NoPolicies(Exception): + pass + + def __init__(self): + self.__message_kits = [] + self.__policies = [] + self.__alice_public_keys = [] + self.__bob_public_keys = [] + + def save_message_kit(self, message_kit: str) -> None: + self.__message_kits.append(message_kit) + + def fetch_message_kit(self) -> UmbralMessageKit: + if self.__message_kits: + message_kit = self.__message_kits.pop() + return message_kit + raise self.NoMessageKits + + def save_policy(self, policy: PolicyAndLabel): + self.__policies.append(policy) + + def fetch_policy(self) -> PolicyAndLabel: + if self.__policies: + policy = self.__policies[0] + return policy + raise self.NoPolicies + + def save_alice_pubkey(self, public_key): + self.__alice_public_keys.append(public_key) + + def fetch_alice_pubkey(self): + policy = self.__alice_public_keys.pop() + return policy + + def save_bob_public_keys(self, public_keys: BobPublicKeys): + self.__bob_public_keys.append(public_keys) + + def fetch_bob_public_keys(self) -> BobPublicKeys: + policy = self.__bob_public_keys.pop() + return policy + + +@pt.inlineCallbacks +def test_federated_cli_lifecycle(click_runner, + testerchain, + random_policy_label, + federated_ursulas, + custom_filepath, + custom_filepath_2): + yield _cli_lifecycle(click_runner, + testerchain, + random_policy_label, + federated_ursulas, + custom_filepath, + custom_filepath_2) + + # for port in _ports_to_remove: + # del MOCK_KNOWN_URSULAS_CACHE[port] + # MOCK_KNOWN_URSULAS_CACHE + + +@pt.inlineCallbacks +def test_decentralized_cli_lifecycle(click_runner, + testerchain, + random_policy_label, + blockchain_ursulas, + custom_filepath, + custom_filepath_2, + agency_local_registry): + + yield _cli_lifecycle(click_runner, + testerchain, + random_policy_label, + blockchain_ursulas, + custom_filepath, + custom_filepath_2, + agency_local_registry.filepath) + + + From 92fb447c03ca9ab33cb0d849a584a177adea1796 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:28:16 -0700 Subject: [PATCH 111/167] We no longer need to connect to default domains, and removing it saves us an otherwise circular import with... --- nucypher/characters/base.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 160e90b23..742c157cf 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -16,41 +16,33 @@ along with nucypher. If not, see . """ import contextlib from contextlib import suppress +from typing import ClassVar, Dict, List, Optional, Set, Union + +from cryptography.exceptions import InvalidSignature +from eth_keys import KeyAPI as EthKeyAPI +from eth_utils import to_canonical_address, to_checksum_address from constant_sorrow import default_constant_splitter from constant_sorrow.constants import (DO_NOT_SIGN, NO_BLOCKCHAIN_CONNECTION, NO_CONTROL_PROTOCOL, NO_DECRYPTION_PERFORMED, NO_NICKNAME, NO_SIGNING_POWER, SIGNATURE_IS_ON_CIPHERTEXT, SIGNATURE_TO_FOLLOW, STRANGER) -from cryptography.exceptions import InvalidSignature -from eth_keys import KeyAPI as EthKeyAPI -from eth_utils import to_canonical_address, to_checksum_address -from typing import ClassVar, Dict, List, Optional, Set, Union - from nucypher.acumen.nicknames import nickname_from_seed -from umbral.keys import UmbralPublicKey -from umbral.signing import Signature - from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry from nucypher.blockchain.eth.signers import Signer from nucypher.characters.control.controllers import CLIController, JSONRPCController from nucypher.config.keyring import NucypherKeyring -from nucypher.config.node import CharacterConfiguration from nucypher.crypto.api import encrypt_and_sign from nucypher.crypto.kits import UmbralMessageKit -from nucypher.crypto.powers import ( - CryptoPower, - SigningPower, - DecryptingPower, - NoSigningPower, - CryptoPowerUp, - DelegatingPower, TransactingPower, NoTransactingPower -) -from nucypher.crypto.signing import signature_splitter, StrangerStamp, SignatureStamp from nucypher.crypto.powers import (CryptoPower, CryptoPowerUp, DecryptingPower, DelegatingPower, NoSigningPower, SigningPower) +from nucypher.crypto.powers import ( + TransactingPower, NoTransactingPower +) from nucypher.crypto.signing import SignatureStamp, StrangerStamp, signature_splitter from nucypher.network.middleware import RestMiddleware from nucypher.network.nodes import Learner +from umbral.keys import UmbralPublicKey +from umbral.signing import Signature class Character(Learner): @@ -161,10 +153,6 @@ class Character(Learner): else: self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups) - # Fleet and Blockchain Connection (Everyone) - if not domains: - domains = {CharacterConfiguration.DEFAULT_DOMAIN} - # # Self-Character # From 9ae5c88aa5c6228fdf09ab3c8f6655a3c31f265c Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:29:16 -0700 Subject: [PATCH 112/167] ...the config.node module, where it's nice to be able to be able to set a default known node class. --- nucypher/config/node.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nucypher/config/node.py b/nucypher/config/node.py index 73f84e6dc..9eace388e 100644 --- a/nucypher/config/node.py +++ b/nucypher/config/node.py @@ -30,6 +30,8 @@ from eth_utils.address import is_checksum_address from tempfile import TemporaryDirectory from twisted.logger import Logger from typing import Callable, List, Set, Union + +from nucypher.characters.lawful import Ursula from umbral.signing import Signature from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory @@ -61,6 +63,9 @@ class CharacterConfiguration(BaseConfiguration): DEFAULT_NETWORK_MIDDLEWARE = RestMiddleware TEMP_CONFIGURATION_DIR_PREFIX = 'tmp-nucypher' + # When we begin to support other threshold schemes, this will be one of the concepts that makes us want a factory. + known_node_class = Ursula + # Gas DEFAULT_GAS_STRATEGY = 'fast' From 1c7fc6884842ed8b1b160b0e584baba92ce65369 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:34:52 -0700 Subject: [PATCH 113/167] Minimal disenchantment logic. #2127. --- nucypher/characters/base.py | 3 +++ nucypher/characters/control/controllers.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 742c157cf..731ba4d20 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -543,3 +543,6 @@ class Character(Learner): self.controller = controller return controller + + def disenchant(self): + Learner.stop_learning_loop(self) diff --git a/nucypher/characters/control/controllers.py b/nucypher/characters/control/controllers.py index 151c1f928..bed9bfbe7 100644 --- a/nucypher/characters/control/controllers.py +++ b/nucypher/characters/control/controllers.py @@ -63,6 +63,7 @@ class CharacterControllerBase(ABC): response = method(**params) # < ---- INLET response_data = serializer.dump(response) + self.stop_character() return response_data @@ -144,6 +145,9 @@ class CLIController(CharacterControlServer): self.emitter.ipc(response=response, request_id=start.epoch, duration=maya.now() - start) return response + def stop_character(self): + self.interface.character.disenchant() + class JSONRPCController(CharacterControlServer): From 5f1c8f30e6e0b8d788e279702a874ff1fa96d727 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:35:07 -0700 Subject: [PATCH 114/167] Felix needs to not learn. --- nucypher/characters/chaotic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nucypher/characters/chaotic.py b/nucypher/characters/chaotic.py index 8ae791fd0..cd0918a1e 100644 --- a/nucypher/characters/chaotic.py +++ b/nucypher/characters/chaotic.py @@ -145,6 +145,11 @@ class Felix(Character, NucypherTokenActor): r = f'{class_name}(checksum_address={self.checksum_address}, db_filepath={self.db_filepath})' return r + def start_learning_loop(self, now=False): + """ + Felix needs to not even be a Learner, but since it is at the moment, it certainly needs not to learn. + """ + def make_web_app(self): from flask import request from flask_sqlalchemy import SQLAlchemy From 05abdb6d8ba28ec4c841a6e3fa55bf0c81cd9635 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:37:00 -0700 Subject: [PATCH 115/167] Reimplementing --teacher-uri in simpler form. --- nucypher/cli/utils.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nucypher/cli/utils.py b/nucypher/cli/utils.py index f8b9bd3d6..d91ca4cc8 100644 --- a/nucypher/cli/utils.py +++ b/nucypher/cli/utils.py @@ -74,7 +74,7 @@ def make_cli_character(character_config, # Handle Teachers # TODO: Is this still relevant? Is it better to DRY this up by doing it later? - teacher_nodes = list() + sage_nodes = list() # if load_preferred_teachers: # teacher_nodes = load_seednodes(emitter, # teacher_uris=[teacher_uri] if teacher_uri else None, @@ -89,7 +89,15 @@ def make_cli_character(character_config, # # Produce Character - CHARACTER = character_config(known_nodes=teacher_nodes, + if teacher_uri: + maybe_sage_node = character_config.known_node_class.from_teacher_uri(teacher_uri=teacher_uri, + min_stake=0, # TODO: Where to get this? + federated_only=character_config.federated_only, + network_middleware=character_config.network_middleware, + registry=character_config.registry) + sage_nodes.append(maybe_sage_node) + + CHARACTER = character_config(known_nodes=sage_nodes, network_middleware=character_config.network_middleware, **config_args) From b026c120e3096c11f4f04340fb7b9002a656fd2f Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:37:21 -0700 Subject: [PATCH 116/167] Only try to find canonical seednode urls if we have a learning domain. --- nucypher/network/nodes.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index b0ad318fc..ad4c051c2 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -243,25 +243,26 @@ class Learner: if self.done_seeding: raise RuntimeError("Already finished seeding. Why try again? Is this a thread safety problem?") - canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(tuple(self.learning_domains)[0], ()) # TODO: Are we done with multiple domains? - discovered = [] - for uri in canonical_sage_uris: - try: - maybe_sage_node = self.node_class.from_teacher_uri(teacher_uri=uri, - min_stake=0, # TODO: Where to get this? - federated_only=self.federated_only, - network_middleware=self.network_middleware, - registry=self.registry) - except NodeSeemsToBeDown: - self.unresponsive_seed_nodes.add(uri) - else: - if maybe_sage_node is UNKNOWN_VERSION: - continue + if self.learning_domains: + canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(tuple(self.learning_domains)[0], ()) # TODO: Are we done with multiple domains? + + for uri in canonical_sage_uris: + try: + maybe_sage_node = self.node_class.from_teacher_uri(teacher_uri=uri, + min_stake=0, # TODO: Where to get this? + federated_only=self.federated_only, + network_middleware=self.network_middleware, + registry=self.registry) + except NodeSeemsToBeDown: + self.unresponsive_seed_nodes.add(uri) else: - new_node = self.remember_node(maybe_sage_node, record_fleet_state=False) - discovered.append(new_node) + if maybe_sage_node is UNKNOWN_VERSION: + continue + else: + new_node = self.remember_node(maybe_sage_node, record_fleet_state=False) + discovered.append(new_node) for seednode_metadata in self._seed_nodes: From ae74bc81e9b15e4531341ff125b9d36e5bd2f1ae Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:38:53 -0700 Subject: [PATCH 117/167] This error handling no longer makes sense; sprouts don't have these exceptions. --- nucypher/network/nodes.py | 49 ++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index ad4c051c2..25895b8a8 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -760,30 +760,31 @@ class Learner: self.log.info(f"Verification Failed - " f"Cannot establish connection to {sprout}.") - except sprout.StampNotSigned: - self.log.warn(f'Verification Failed - ' - f'{sprout} stamp is unsigned.') - - except sprout.NotStaking: - self.log.warn(f'Verification Failed - ' - f'{sprout} has no active stakes in the current period ' - f'({self.staking_agent.get_current_period()}') - - except sprout.InvalidWorkerSignature: - self.log.warn(f'Verification Failed - ' - f'{sprout} has an invalid wallet signature for {sprout.decentralized_identity_evidence}') - - except sprout.UnbondedWorker: - self.log.warn(f'Verification Failed - ' - f'{sprout} is not bonded to a Staker.') - - except sprout.Invalidsprout: - self.log.warn(sprout.invalid_metadata_message.format(sprout)) - - except sprout.SuspiciousActivity: - message = f"Suspicious Activity: Discovered sprout with bad signature: {sprout}." \ - f"Propagated by: {current_teacher}" - self.log.warn(message) + # TODO: This whole section is weird; sprouts down have any of these things. + # except sprout.StampNotSigned: + # self.log.warn(f'Verification Failed - ' + # f'{sprout} stamp is unsigned.') + # + # except sprout.NotStaking: + # self.log.warn(f'Verification Failed - ' + # f'{sprout} has no active stakes in the current period ' + # f'({self.staking_agent.get_current_period()}') + # + # except sprout.InvalidWorkerSignature: + # self.log.warn(f'Verification Failed - ' + # f'{sprout} has an invalid wallet signature for {sprout.decentralized_identity_evidence}') + # + # except sprout.UnbondedWorker: + # self.log.warn(f'Verification Failed - ' + # f'{sprout} is not bonded to a Staker.') + # + # # except sprout.Invalidsprout: + # # self.log.warn(sprout.invalid_metadata_message.format(sprout)) + # + # except sprout.SuspiciousActivity: + # message = f"Suspicious Activity: Discovered sprout with bad signature: {sprout}." \ + # f"Propagated by: {current_teacher}" + # self.log.warn(message) # Is cycling happening in the right order? From fb119720b55328791a421b82e1ec74b7c3a64118 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:39:57 -0700 Subject: [PATCH 118/167] Divide cli lifecycle tests into federated and decentralized in order to keep Ursula cache separated. --- .../control/test_rpc_control_blockchain.py | 2 + .../cli/test_decentralized_cli_lifecycle.py | 36 ++++++ .../cli/test_federated_cli_lifecycle.py | 106 ++---------------- 3 files changed, 45 insertions(+), 99 deletions(-) diff --git a/tests/acceptance/characters/control/test_rpc_control_blockchain.py b/tests/acceptance/characters/control/test_rpc_control_blockchain.py index 4b010822a..ec24d3b6f 100644 --- a/tests/acceptance/characters/control/test_rpc_control_blockchain.py +++ b/tests/acceptance/characters/control/test_rpc_control_blockchain.py @@ -85,6 +85,7 @@ def test_alice_rpc_character_control_create_policy(alice_rpc_test_client, create assert rpc_response.success is True assert rpc_response.id == response_id + def test_alice_rpc_character_control_bad_input(alice_rpc_test_client, create_policy_control_request): alice_rpc_test_client.__class__.MESSAGE_ID = 0 @@ -94,6 +95,7 @@ def test_alice_rpc_character_control_bad_input(alice_rpc_test_client, create_pol response = alice_rpc_test_client.send(request={'bogus': 'input'}, malformed=True) assert response.error_code == -32600 + def test_alice_rpc_character_control_derive_policy_encrypting_key(alice_rpc_test_client): method_name = 'derive_policy_encrypting_key' request_data = {'method': method_name, 'params': {'label': 'test'}} diff --git a/tests/acceptance/cli/test_decentralized_cli_lifecycle.py b/tests/acceptance/cli/test_decentralized_cli_lifecycle.py index e69de29bb..30485175b 100644 --- a/tests/acceptance/cli/test_decentralized_cli_lifecycle.py +++ b/tests/acceptance/cli/test_decentralized_cli_lifecycle.py @@ -0,0 +1,36 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" +import pytest_twisted as pt + +from tests.acceptance.cli.lifecycle import run_entire_cli_lifecycle + + +@pt.inlineCallbacks +def test_decentralized_cli_lifecycle(click_runner, + testerchain, + random_policy_label, + blockchain_ursulas, + custom_filepath, + custom_filepath_2, + agency_local_registry): + yield run_entire_cli_lifecycle(click_runner, + testerchain, + random_policy_label, + blockchain_ursulas, + custom_filepath, + custom_filepath_2, + agency_local_registry.filepath) diff --git a/tests/acceptance/cli/test_federated_cli_lifecycle.py b/tests/acceptance/cli/test_federated_cli_lifecycle.py index b309aebe7..0b31baf06 100644 --- a/tests/acceptance/cli/test_federated_cli_lifecycle.py +++ b/tests/acceptance/cli/test_federated_cli_lifecycle.py @@ -14,80 +14,9 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ - -import json -from base64 import b64decode -from collections import namedtuple -from json import JSONDecodeError - -import datetime -import maya -import os -import pytest import pytest_twisted as pt -import shutil -from twisted.internet import threads -from web3 import Web3 -from nucypher.cli.main import nucypher_cli -from nucypher.config.characters import AliceConfiguration, BobConfiguration -from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN -from nucypher.crypto.kits import UmbralMessageKit -from nucypher.utilities.logging import GlobalLoggerSettings -from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, TEST_PROVIDER_URI -from tests.utils.ursula import start_pytest_ursula_services, MOCK_KNOWN_URSULAS_CACHE - -PLAINTEXT = "I'm bereaved, not a sap!" - - -class MockSideChannel: - - PolicyAndLabel = namedtuple('PolicyAndLabel', ['encrypting_key', 'label']) - BobPublicKeys = namedtuple('BobPublicKeys', ['bob_encrypting_key', 'bob_verifying_key']) - - class NoMessageKits(Exception): - pass - - class NoPolicies(Exception): - pass - - def __init__(self): - self.__message_kits = [] - self.__policies = [] - self.__alice_public_keys = [] - self.__bob_public_keys = [] - - def save_message_kit(self, message_kit: str) -> None: - self.__message_kits.append(message_kit) - - def fetch_message_kit(self) -> UmbralMessageKit: - if self.__message_kits: - message_kit = self.__message_kits.pop() - return message_kit - raise self.NoMessageKits - - def save_policy(self, policy: PolicyAndLabel): - self.__policies.append(policy) - - def fetch_policy(self) -> PolicyAndLabel: - if self.__policies: - policy = self.__policies[0] - return policy - raise self.NoPolicies - - def save_alice_pubkey(self, public_key): - self.__alice_public_keys.append(public_key) - - def fetch_alice_pubkey(self): - policy = self.__alice_public_keys.pop() - return policy - - def save_bob_public_keys(self, public_keys: BobPublicKeys): - self.__bob_public_keys.append(public_keys) - - def fetch_bob_public_keys(self) -> BobPublicKeys: - policy = self.__bob_public_keys.pop() - return policy +from tests.acceptance.cli.lifecycle import run_entire_cli_lifecycle @pt.inlineCallbacks @@ -97,34 +26,13 @@ def test_federated_cli_lifecycle(click_runner, federated_ursulas, custom_filepath, custom_filepath_2): - yield _cli_lifecycle(click_runner, - testerchain, - random_policy_label, - federated_ursulas, - custom_filepath, - custom_filepath_2) + yield run_entire_cli_lifecycle(click_runner, + testerchain, + random_policy_label, + federated_ursulas, + custom_filepath, + custom_filepath_2) # for port in _ports_to_remove: # del MOCK_KNOWN_URSULAS_CACHE[port] # MOCK_KNOWN_URSULAS_CACHE - - -@pt.inlineCallbacks -def test_decentralized_cli_lifecycle(click_runner, - testerchain, - random_policy_label, - blockchain_ursulas, - custom_filepath, - custom_filepath_2, - agency_local_registry): - - yield _cli_lifecycle(click_runner, - testerchain, - random_policy_label, - blockchain_ursulas, - custom_filepath, - custom_filepath_2, - agency_local_registry.filepath) - - - From e20a7a3a7b1175a925226857aab03f77d13395b7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:40:17 -0700 Subject: [PATCH 119/167] Felix doesn't use a network domain. --- tests/acceptance/cli/test_felix.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/acceptance/cli/test_felix.py b/tests/acceptance/cli/test_felix.py index 864480f69..58e959bb7 100644 --- a/tests/acceptance/cli/test_felix.py +++ b/tests/acceptance/cli/test_felix.py @@ -66,7 +66,6 @@ def test_run_felix(click_runner, testerchain, agency_local_registry): '--registry-filepath', str(agency_local_registry.filepath), '--checksum-address', testerchain.client.accounts[0], '--config-root', MOCK_CUSTOM_INSTALLATION_PATH_2, - '--network', TEMPORARY_DOMAIN, '--provider', TEST_PROVIDER_URI) _original_read_function = LocalContractRegistry.read From a7303702ab77a10445936089031dcd3b6ad5a891 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:40:41 -0700 Subject: [PATCH 120/167] Skipping this test until we get the DB (#2099) worked out. --- tests/acceptance/cli/ursula/test_run_ursula.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/cli/ursula/test_run_ursula.py b/tests/acceptance/cli/ursula/test_run_ursula.py index bcd129356..b669cd034 100644 --- a/tests/acceptance/cli/ursula/test_run_ursula.py +++ b/tests/acceptance/cli/ursula/test_run_ursula.py @@ -147,7 +147,7 @@ def test_federated_ursula_learns_via_cli(click_runner, federated_ursulas): yield d - +@pytest.mark.skip("Let's put this on ice until we get 2099 and Treasure Island working together.") @pt.inlineCallbacks def test_persistent_node_storage_integration(click_runner, custom_filepath, From 86398eb43a3e53631a581af02ac1b5151ffd68d5 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 20:42:27 -0700 Subject: [PATCH 121/167] Don't ship blockchain_bob with known_nodes; he can use his domain hardcoded nodes. Also, don't start his learning loop after the fact. This caused this test to pass even when he was unable to actually find the nodes in join_policy. --- .../cli/ursula/test_stake_via_allocation_contract.py | 1 - tests/fixtures.py | 2 +- tests/mock/performance_mocks.py | 1 - tests/utils/middleware.py | 4 ++-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py b/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py index 6843fcb53..7d6800130 100644 --- a/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py +++ b/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py @@ -555,7 +555,6 @@ def test_collect_rewards_integration(click_runner, assert arrangement.ursula == ursula # Bob learns about the new staker and joins the policy - blockchain_bob.start_learning_loop() blockchain_bob.remember_node(node=ursula) blockchain_bob.join_policy(random_policy_label, bytes(blockchain_alice.stamp)) diff --git a/tests/fixtures.py b/tests/fixtures.py index c26c59cd5..6ec3bb1ed 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -211,7 +211,7 @@ def bob_blockchain_test_config(blockchain_ursulas, testerchain, test_registry): provider_uri=TEST_PROVIDER_URI, test_registry=test_registry, checksum_address=testerchain.bob_account, - known_nodes=blockchain_ursulas) + ) yield config config.cleanup() diff --git a/tests/mock/performance_mocks.py b/tests/mock/performance_mocks.py index 058927e83..dd6610e40 100644 --- a/tests/mock/performance_mocks.py +++ b/tests/mock/performance_mocks.py @@ -20,7 +20,6 @@ from unittest.mock import patch from nucypher.network.server import make_rest_app from tests.mock.serials import good_serials -from tests.utils.ursula import MOCK_KNOWN_URSULAS_CACHE from umbral.config import default_params from umbral.keys import UmbralPublicKey from umbral.signing import Signature diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 6e22cb73e..437ea5f12 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -93,9 +93,9 @@ class MockRestMiddleware(RestMiddleware): class TEACHER_NODES: @classmethod - def get(_cls, item, default): + def get(_cls, item, _default): if item is TEMPORARY_DOMAIN: - nodes = tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values()) + nodes = tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values())[0:2] else: nodes = tuple() return nodes From f82e9f12393eea13e38bd574137fcb74b0d7a2b1 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 21:23:26 -0700 Subject: [PATCH 122/167] Moving stop_character upward to capture other controller cases. --- nucypher/characters/control/controllers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nucypher/characters/control/controllers.py b/nucypher/characters/control/controllers.py index bed9bfbe7..1f2cfbe9d 100644 --- a/nucypher/characters/control/controllers.py +++ b/nucypher/characters/control/controllers.py @@ -116,6 +116,9 @@ class CharacterControlServer(CharacterControllerBase): if hasattr(method, '_schema') } + def stop_character(self): + self.interface.character.disenchant() + @abstractmethod def make_control_transport(self): return NotImplemented @@ -145,9 +148,6 @@ class CLIController(CharacterControlServer): self.emitter.ipc(response=response, request_id=start.epoch, duration=maya.now() - start) return response - def stop_character(self): - self.interface.character.disenchant() - class JSONRPCController(CharacterControlServer): From d103664d4d0889a9785a7dc36538b5402a7b459c Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 21:23:57 -0700 Subject: [PATCH 123/167] We only want to record one fleet state per iteration; this loop previously recorded two. --- nucypher/network/nodes.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 25895b8a8..011c2a4d5 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -234,7 +234,7 @@ class Learner: def known_nodes(self): return self.__known_nodes - def load_seednodes(self, read_storage: bool = True): + def load_seednodes(self, read_storage: bool = True, record_fleet_state=False): """ Engage known nodes from storages and pre-fetch hardcoded seednode certificates for node learning. @@ -291,9 +291,11 @@ class Learner: discovered.extend(nodes_restored_from_storage) - if discovered: + if discovered and record_fleet_state: self.known_nodes.record_fleet_state() + return discovered + def read_nodes_from_storage(self) -> None: stored_nodes = self.node_storage.all(federated_only=self.federated_only) # TODO: #466 @@ -389,7 +391,6 @@ class Learner: return self.learning_deferred else: self.log.info("Starting Learning Loop.") - self.cycle_teacher_node() learning_deferreds = list() @@ -437,8 +438,6 @@ class Learner: self.teacher_nodes.extend(nodes_we_know_about) def cycle_teacher_node(self): - if not self.done_seeding: - self.load_seednodes() if not self.teacher_nodes: self.select_teacher_nodes() try: @@ -650,6 +649,12 @@ class Learner: TODO: Does this (and related methods) belong on FleetSensor for portability? """ + remembered = [] + + if not self.done_seeding: + remembered_seednodes = self.load_seednodes(record_fleet_state=False) + remembered.extend(remembered_seednodes) + self._learning_round += 1 current_teacher = self.current_teacher_node() # Will raise if there's no available teacher. @@ -741,7 +746,7 @@ class Learner: # somewhere more performant, like mature() or verify_node(). sprouts = self.node_class.batch_from_bytes(node_payload) - remembered = [] + for sprout in sprouts: fail_fast = True # TODO NRN try: From fe908a1e8315a14307aa808f2b616fa8f126c685 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 22:25:23 -0700 Subject: [PATCH 124/167] Add a measure of maturation time; if this increases dramatically, that will represent a breakage. --- .../learning/test_discovery_phases.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 07825099c..6eb050841 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -128,6 +128,18 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, """ Large-scale map placement with a middleware that simulates network latency. """ + # The nodes who match the map distribution criteria. + nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) + + + preparation_started = datetime.now() + for node in nodes_we_expect_to_have_the_map: + node.mature() + maturation_time = datetime.now() - preparation_started + + # # # Loop through and instantiate actual rest apps so as not to pollute the time measurement (doesn't happen in real world). + for node in nodes_we_expect_to_have_the_map: + highperf_mocked_alice.network_middleware.client.parse_node_or_host_and_port(node) # Causes rest app to be made (happens JIT in other testS) highperf_mocked_alice.network_middleware = SluggishLargeFleetMiddleware() @@ -136,9 +148,6 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, started = datetime.now() with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: - # The nodes who match the map distribution criteria. - nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) - # returns instantly. policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) @@ -193,3 +202,7 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, # But with debuggers and other processes running on laptops, we give a little leeway. assert initial_blocking_duration.total_seconds() < 3 assert complete_distribution_time.total_seconds() < 10 + + # Takes about .8 on jMyles' laptop; still the bulk of init time. + # Assuming this is 10 percent of the fleet, that's another 8 second savings for first policy enactment. + assert maturation_time.total_seconds() < 2 \ No newline at end of file From 679022c42a07be13bd641deb9ee88ecefa103d55 Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 22:26:10 -0700 Subject: [PATCH 125/167] Another test that was passing for the wrong reason. It now works regardless of whether the future Ursula is a hardcoded seednode or a user-specified one. --- nucypher/network/nodes.py | 2 ++ .../learning/test_learning_upgrade.py | 19 +++++++++++-------- tests/utils/middleware.py | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 011c2a4d5..7d41affb9 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -276,6 +276,8 @@ class Learner: federated_only=self.federated_only) # TODO: 466 if seed_node is False: self.unresponsive_seed_nodes.add(seednode_metadata) + elif seed_node is UNKNOWN_VERSION: + continue # TODO: Bucket this? We already emitted a warning. else: self.unresponsive_seed_nodes.discard(seednode_metadata) new_node = self.remember_node(seed_node, record_fleet_state=False) diff --git a/tests/integration/learning/test_learning_upgrade.py b/tests/integration/learning/test_learning_upgrade.py index eb8ff9b50..7e18277ce 100644 --- a/tests/integration/learning/test_learning_upgrade.py +++ b/tests/integration/learning/test_learning_upgrade.py @@ -28,14 +28,18 @@ from tests.utils.middleware import MockRestMiddleware def test_emit_warning_upon_new_version(lonely_ursula_maker, caplog): - teacher, learner, new_node = lonely_ursula_maker(quantity=3) + seed_node, teacher, new_node = lonely_ursula_maker(quantity=3, + domains={"no hardcodes"}, + know_each_other=True) + learner, _bystander = lonely_ursula_maker(quantity=2, domains={"no hardcodes"}) + learner.learning_domains = {"no hardcodes"} learner.remember_node(teacher) teacher.remember_node(learner) teacher.remember_node(new_node) - new_node.TEACHER_VERSION = learner.LEARNER_VERSION + 1 - + learner._seed_nodes = [seed_node.seed_node_metadata()] + seed_node.TEACHER_VERSION = learner.LEARNER_VERSION + 1 warnings = [] def warning_trapper(event): @@ -44,20 +48,19 @@ def test_emit_warning_upon_new_version(lonely_ursula_maker, caplog): globalLogPublisher.addObserver(warning_trapper) - # First we'll get a warning, because we're loading a seednode with a version from the future. learner.load_seednodes() assert len(warnings) == 1 - assert warnings[0]['log_format'] == learner.unknown_version_message.format(new_node, - new_node.TEACHER_VERSION, + assert warnings[0]['log_format'] == learner.unknown_version_message.format(seed_node, + seed_node.TEACHER_VERSION, learner.LEARNER_VERSION) # We don't use the above seednode as a teacher, but when our teacher tries to tell us about it, we get another of the same warning. learner.learn_from_teacher_node() assert len(warnings) == 2 - assert warnings[1]['log_format'] == learner.unknown_version_message.format(new_node, - new_node.TEACHER_VERSION, + assert warnings[1]['log_format'] == learner.unknown_version_message.format(seed_node, + seed_node.TEACHER_VERSION, learner.LEARNER_VERSION) # Now let's go a little further: make the version totally unrecognizable. diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 437ea5f12..0a0d34ac0 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -58,7 +58,7 @@ class _TestMiddlewareClient(NucypherMiddlewareClient): raise RuntimeError( "Can't find an Ursula with port {} - did you spin up the right test ursulas?".format(port)) - def parse_node_or_host_and_port(self, node, host, port): + def parse_node_or_host_and_port(self, node=None, host=None, port=None): if node: if any((host, port)): raise ValueError("Don't pass host and port if you are passing the node.") From 9ba8e1dd355b18d42af90c7bad1abf604dda57dd Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 9 Jul 2020 22:29:45 -0700 Subject: [PATCH 126/167] Lifecycle logic - shared in federated and blockchain modules. --- tests/acceptance/cli/lifecycle.py | 355 ++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 tests/acceptance/cli/lifecycle.py diff --git a/tests/acceptance/cli/lifecycle.py b/tests/acceptance/cli/lifecycle.py new file mode 100644 index 000000000..dd53632ca --- /dev/null +++ b/tests/acceptance/cli/lifecycle.py @@ -0,0 +1,355 @@ +import datetime +import json +import os +import shutil +from base64 import b64decode +from collections import namedtuple +from json import JSONDecodeError + +import maya +import pytest +from twisted.internet import threads +from web3 import Web3 + +from nucypher.cli.main import nucypher_cli +from nucypher.config.characters import AliceConfiguration, BobConfiguration +from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN +from nucypher.crypto.kits import UmbralMessageKit +from nucypher.utilities.logging import GlobalLoggerSettings +from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, TEST_PROVIDER_URI +from tests.utils.ursula import start_pytest_ursula_services + +PLAINTEXT = "I'm bereaved, not a sap!" + + +class MockSideChannel: + PolicyAndLabel = namedtuple('PolicyAndLabel', ['encrypting_key', 'label']) + BobPublicKeys = namedtuple('BobPublicKeys', ['bob_encrypting_key', 'bob_verifying_key']) + + class NoMessageKits(Exception): + pass + + class NoPolicies(Exception): + pass + + def __init__(self): + self.__message_kits = [] + self.__policies = [] + self.__alice_public_keys = [] + self.__bob_public_keys = [] + + def save_message_kit(self, message_kit: str) -> None: + self.__message_kits.append(message_kit) + + def fetch_message_kit(self) -> UmbralMessageKit: + if self.__message_kits: + message_kit = self.__message_kits.pop() + return message_kit + raise self.NoMessageKits + + def save_policy(self, policy: PolicyAndLabel): + self.__policies.append(policy) + + def fetch_policy(self) -> PolicyAndLabel: + if self.__policies: + policy = self.__policies[0] + return policy + raise self.NoPolicies + + def save_alice_pubkey(self, public_key): + self.__alice_public_keys.append(public_key) + + def fetch_alice_pubkey(self): + policy = self.__alice_public_keys.pop() + return policy + + def save_bob_public_keys(self, public_keys: BobPublicKeys): + self.__bob_public_keys.append(public_keys) + + def fetch_bob_public_keys(self) -> BobPublicKeys: + policy = self.__bob_public_keys.pop() + return policy + + +def run_entire_cli_lifecycle(click_runner, + testerchain, + random_policy_label, + ursulas, + custom_filepath, + custom_filepath_2, + registry_filepath=None): + """ + This is an end to end integration test that runs each cli call + in it's own process using only CLI character control entry points, + and a mock side channel that runs in the control process + """ + federated = list(ursulas)[0].federated_only + + # Boring Setup Stuff + alice_config_root = str(custom_filepath) + bob_config_root = str(custom_filepath_2) + envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD} + + # A side channel exists - Perhaps a dApp + side_channel = MockSideChannel() + + shutil.rmtree(str(custom_filepath), ignore_errors=True) + shutil.rmtree(str(custom_filepath_2), ignore_errors=True) + + """ + Scene 1: Alice Installs nucypher to a custom filepath and examines her configuration + """ + + # Alice performs an installation for the first time + alice_init_args = ('alice', 'init', + '--network', TEMPORARY_DOMAIN, + '--config-root', alice_config_root) + if federated: + alice_init_args += ('--federated-only',) + else: + alice_init_args += ('--provider', TEST_PROVIDER_URI, + '--pay-with', testerchain.alice_account, + '--registry-filepath', str(registry_filepath)) + + alice_init_response = click_runner.invoke(nucypher_cli, alice_init_args, catch_exceptions=False, env=envvars) + assert alice_init_response.exit_code == 0 + + # Prevent previous global logger settings set by aboce command from writing non-IPC messages to stdout + GlobalLoggerSettings.stop_console_logging() + + # Alice uses her configuration file to run the character "view" command + alice_configuration_file_location = os.path.join(alice_config_root, AliceConfiguration.generate_filename()) + alice_view_args = ('alice', 'public-keys', + '--json-ipc', + '--config-file', alice_configuration_file_location) + + alice_view_result = click_runner.invoke(nucypher_cli, + alice_view_args, + input=INSECURE_DEVELOPMENT_PASSWORD, + catch_exceptions=False, + env=envvars) + + assert alice_view_result.exit_code == 0 + + try: + alice_view_response = json.loads(alice_view_result.output) + except JSONDecodeError: + pytest.fail("Invalid JSON response from JSON-RPC Character process.") + + # Alice expresses her desire to participate in data sharing with nucypher + # by saving her public key somewhere Bob and Enrico can find it. + side_channel.save_alice_pubkey(alice_view_response['result']['alice_verifying_key']) + + """ + Scene 2: Bob installs nucypher, examines his configuration and expresses his + interest to participate in data retrieval by posting his public keys somewhere public (side-channel). + """ + bob_init_args = ('bob', 'init', + '--network', TEMPORARY_DOMAIN, + '--config-root', bob_config_root) + if federated: + bob_init_args += ('--federated-only',) + else: + bob_init_args += ('--provider', TEST_PROVIDER_URI, + '--registry-filepath', str(registry_filepath), + '--checksum-address', testerchain.bob_account) + + bob_init_response = click_runner.invoke(nucypher_cli, bob_init_args, catch_exceptions=False, env=envvars) + assert bob_init_response.exit_code == 0 + + # Alice uses her configuration file to run the character "view" command + bob_configuration_file_location = os.path.join(bob_config_root, BobConfiguration.generate_filename()) + bob_view_args = ('bob', 'public-keys', + '--json-ipc', + '--mock-networking', # TODO: It's absurd for this public-keys command to connect at all. 1710 + '--lonely', # TODO: This needs to be implied by `public-keys`. + '--config-file', bob_configuration_file_location) + + bob_view_result = click_runner.invoke(nucypher_cli, bob_view_args, catch_exceptions=False, env=envvars) + assert bob_view_result.exit_code == 0 + bob_view_response = json.loads(bob_view_result.output) + + # Bob interacts with the sidechannel + bob_public_keys = MockSideChannel.BobPublicKeys(bob_view_response['result']['bob_encrypting_key'], + bob_view_response['result']['bob_verifying_key']) + + side_channel.save_bob_public_keys(bob_public_keys) + + """ + Scene 3: Alice derives a policy keypair, and saves its public key to a sidechannel. + """ + + random_label = random_policy_label.decode() # Unicode string + + derive_args = ('alice', 'derive-policy-pubkey', + '--mock-networking', + '--json-ipc', + '--config-file', alice_configuration_file_location, + '--label', random_label) + + derive_response = click_runner.invoke(nucypher_cli, derive_args, catch_exceptions=False, env=envvars) + assert derive_response.exit_code == 0 + + derive_response = json.loads(derive_response.output) + assert derive_response['result']['label'] == random_label + + # Alice and the sidechannel: at Tinagre + policy = MockSideChannel.PolicyAndLabel(encrypting_key=derive_response['result']['policy_encrypting_key'], + label=derive_response['result']['label']) + side_channel.save_policy(policy=policy) + + """ + Scene 4: Enrico encrypts some data for some policy public key and saves it to a side channel. + """ + + def enrico_encrypts(): + + # Fetch! + policy = side_channel.fetch_policy() + + enrico_args = ('enrico', + 'encrypt', + '--json-ipc', + '--policy-encrypting-key', policy.encrypting_key, + '--message', PLAINTEXT) + + encrypt_result = click_runner.invoke(nucypher_cli, enrico_args, catch_exceptions=False, env=envvars) + assert encrypt_result.exit_code == 0 + encrypt_result = json.loads(encrypt_result.output) + encrypted_message = encrypt_result['result']['message_kit'] # type: str + + side_channel.save_message_kit(message_kit=encrypted_message) + return encrypt_result + + def _alice_decrypts(encrypt_result): + """ + alice forgot what exactly she encrypted for bob. + she decrypts it just to make sure. + """ + policy = side_channel.fetch_policy() + alice_signing_key = side_channel.fetch_alice_pubkey() + message_kit = encrypt_result['result']['message_kit'] + + decrypt_args = ( + 'alice', 'decrypt', + '--mock-networking', + '--json-ipc', + '--config-file', alice_configuration_file_location, + '--message-kit', message_kit, + '--label', policy.label, + ) + + if federated: + decrypt_args += ('--federated-only',) + + decrypt_response_fail = click_runner.invoke(nucypher_cli, decrypt_args[0:7], catch_exceptions=False, + env=envvars) + assert decrypt_response_fail.exit_code == 2 + + decrypt_response = click_runner.invoke(nucypher_cli, decrypt_args, catch_exceptions=False, env=envvars) + decrypt_result = json.loads(decrypt_response.output) + for cleartext in decrypt_result['result']['cleartexts']: + assert b64decode(cleartext.encode()).decode() == PLAINTEXT + + # replenish the side channel + side_channel.save_policy(policy=policy) + side_channel.save_alice_pubkey(alice_signing_key) + return encrypt_result + + """ + Scene 5: Alice grants access to Bob: + We catch up with Alice later on, but before she has learned about existing Ursulas... + """ + if federated: + teacher = list(ursulas)[0] + else: + teacher = list(ursulas)[1] + + teacher_uri = teacher.seed_node_metadata(as_teacher_uri=True) + + # Some Ursula is running somewhere + def _run_teacher(_encrypt_result): + # start_pytest_ursula_services(ursula=teacher) + return teacher_uri + + def _grant(teacher_uri): + + # Alice fetched Bob's public keys from the side channel + bob_keys = side_channel.fetch_bob_public_keys() + bob_encrypting_key = bob_keys.bob_encrypting_key + bob_verifying_key = bob_keys.bob_verifying_key + + grant_args = ('alice', 'grant', + '--mock-networking', + '--json-ipc', + '--network', TEMPORARY_DOMAIN, + '--teacher', teacher_uri, + '--config-file', alice_configuration_file_location, + '--m', 2, + '--n', 3, + '--expiration', (maya.now() + datetime.timedelta(days=3)).iso8601(), + '--label', random_label, + '--bob-encrypting-key', bob_encrypting_key, + '--bob-verifying-key', bob_verifying_key) + + if federated: + grant_args += ('--federated-only',) + else: + grant_args += ('--provider', TEST_PROVIDER_URI, + '--rate', Web3.toWei(9, 'gwei')) + + grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars) + assert grant_result.exit_code == 0 + + grant_result = json.loads(grant_result.output) + + # TODO: Expand test to consider manual treasure map handing + # # Alice puts the Treasure Map somewhere Bob can get it. + # side_channel.save_treasure_map(treasure_map=grant_result['result']['treasure_map']) + + return grant_result + + def _bob_retrieves(_grant_result): + """ + Scene 6: Bob retrieves encrypted data from the side channel and uses nucypher to re-encrypt it + """ + + # Bob interacts with a sidechannel + ciphertext_message_kit = side_channel.fetch_message_kit() + + policy = side_channel.fetch_policy() + policy_encrypting_key, label = policy + + alice_signing_key = side_channel.fetch_alice_pubkey() + + retrieve_args = ('bob', 'retrieve', + '--mock-networking', + '--json-ipc', + '--teacher', teacher_uri, + '--config-file', bob_configuration_file_location, + '--message-kit', ciphertext_message_kit, + '--label', label, + '--policy-encrypting-key', policy_encrypting_key, + '--alice-verifying-key', alice_signing_key) + + # TODO: Remove - Federated not used for retrieve any more + # if federated: + # retrieve_args += ('--federated-only',) + + retrieve_response = click_runner.invoke(nucypher_cli, retrieve_args, catch_exceptions=False, env=envvars) + assert retrieve_response.exit_code == 0 + + retrieve_response = json.loads(retrieve_response.output) + for cleartext in retrieve_response['result']['cleartexts']: + assert b64decode(cleartext.encode()).decode() == PLAINTEXT + + return + + # Run the Callbacks + d = threads.deferToThread(enrico_encrypts) # scene 4 + d.addCallback(_alice_decrypts) # scene 5 (uncertainty) + d.addCallback(_run_teacher) # scene 6 (preamble) + d.addCallback(_grant) # scene 7 + d.addCallback(_bob_retrieves) # scene 8 + + return d From 7f032152d93fd053e5688a63f5ed53f8bdf9eafc Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 14 Jul 2020 16:09:14 -0700 Subject: [PATCH 127/167] Ensuring that Enrico doesn't have to deal with known_node class. --- nucypher/characters/base.py | 17 +++++++++++------ nucypher/characters/lawful.py | 7 ++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 731ba4d20..55b4e12d3 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -107,13 +107,10 @@ class Character(Learner): if hasattr(self, '_interface_class'): # TODO: have argument about meaning of 'lawful' # and whether maybe only Lawful characters have an interface self.interface = self._interface_class(character=self) + + if is_me: - if not known_node_class: - # Once in a while, in tests or demos, we init a plain Character who doesn't already know about its node class. - from nucypher.characters.lawful import Ursula - known_node_class = Ursula - # If we're federated only, we assume that all other nodes in our domain are as well. - known_node_class.set_federated_mode(federated_only) + self._set_known_node_class(known_node_class, federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. @@ -340,6 +337,14 @@ class Character(Learner): return cls(is_me=False, crypto_power=crypto_power, *args, **kwargs) + def _set_known_node_class(self, known_node_class, federated_only): + if not known_node_class: + # Once in a while, in tests or demos, we init a plain Character who doesn't already know about its node class. + from nucypher.characters.lawful import Ursula + known_node_class = Ursula + # If we're federated only, we assume that all other nodes in our domain are as well. + known_node_class.set_federated_mode(federated_only) + def store_metadata(self, filepath: str) -> str: """ Save this node to the disk. diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 02e6cf30b..f03c0c83b 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1624,7 +1624,7 @@ class Enrico(Character): # Encrico never uses the blockchain, hence federated_only) kwargs['federated_only'] = True - kwargs['known_node_class'] = Ursula + kwargs['known_node_class'] = None super().__init__(*args, **kwargs) if controller: @@ -1658,6 +1658,11 @@ class Enrico(Character): raise TypeError("This Enrico doesn't know which policy encrypting key he used. Oh well.") return self._policy_pubkey + def _set_known_node_class(self, *args, **kwargs): + """ + Enrico doesn't init nodes, so it doesn't care what class they are. + """ + def make_web_controller(drone_enrico, crash_on_error: bool = False): app_name = bytes(drone_enrico.stamp).hex()[:6] From eaa6b94a49e5e545d667e22ec8178fc5105472d0 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 14 Jul 2020 16:10:26 -0700 Subject: [PATCH 128/167] Starting Bob's learning loop here (which is in keeping with reality) is needed now that we don't hardcode Bob's fixture nodes. --- .../characters/test_ursula_prepares_to_act_as_worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py index e28bb4571..b88fb5253 100644 --- a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py +++ b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py @@ -155,6 +155,7 @@ def test_blockchain_ursulas_reencrypt(blockchain_ursulas, blockchain_alice, bloc message_kit, signature = enrico.encrypt_message(message) + blockchain_bob.start_learning_loop(now=True) blockchain_bob.join_policy(label, bytes(blockchain_alice.stamp)) plaintext = blockchain_bob.retrieve(message_kit, alice_verifying_key=blockchain_alice.stamp, label=label, From 52a7bf3a8a4af78e175d96398ee7d5149dc9bd19 Mon Sep 17 00:00:00 2001 From: jMyles Date: Tue, 14 Jul 2020 16:36:29 -0700 Subject: [PATCH 129/167] Lookup transacting_power in the typical public way. --- tests/acceptance/blockchain/actors/test_staker.py | 4 +++- tests/acceptance/cli/lifecycle.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/blockchain/actors/test_staker.py b/tests/acceptance/blockchain/actors/test_staker.py index bf1a6f8a6..3e977f7a5 100644 --- a/tests/acceptance/blockchain/actors/test_staker.py +++ b/tests/acceptance/blockchain/actors/test_staker.py @@ -21,6 +21,7 @@ from eth_tester.exceptions import TransactionFailed from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent from nucypher.blockchain.eth.token import NU, Stake +from nucypher.crypto.powers import TransactingPower from tests.constants import FEE_RATE_RANGE, INSECURE_DEVELOPMENT_PASSWORD from tests.utils.ursula import make_decentralized_ursulas @@ -146,7 +147,8 @@ def test_staker_collects_staking_reward(testerchain, # ...wait out the lock period... for _ in range(token_economics.minimum_locked_periods): testerchain.time_travel(periods=1) - ursula.transacting_power.activate(password=INSECURE_DEVELOPMENT_PASSWORD) + transacting_power = ursula._crypto_power.power_ups(TransactingPower) + transacting_power.activate(password=INSECURE_DEVELOPMENT_PASSWORD) ursula.commit_to_next_period() # ...wait more... diff --git a/tests/acceptance/cli/lifecycle.py b/tests/acceptance/cli/lifecycle.py index dd53632ca..459e53f7b 100644 --- a/tests/acceptance/cli/lifecycle.py +++ b/tests/acceptance/cli/lifecycle.py @@ -17,7 +17,6 @@ from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORAR from nucypher.crypto.kits import UmbralMessageKit from nucypher.utilities.logging import GlobalLoggerSettings from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, TEST_PROVIDER_URI -from tests.utils.ursula import start_pytest_ursula_services PLAINTEXT = "I'm bereaved, not a sap!" From a2123b5124ccfeb4058b6303426dedbaed2b15a4 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 15 Jul 2020 16:37:59 -0700 Subject: [PATCH 130/167] Return of demo fleet Ursulas spinup script. --- examples/run_demo_ursula_fleet.py | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 examples/run_demo_ursula_fleet.py diff --git a/examples/run_demo_ursula_fleet.py b/examples/run_demo_ursula_fleet.py new file mode 100644 index 000000000..72c0e0a0c --- /dev/null +++ b/examples/run_demo_ursula_fleet.py @@ -0,0 +1,70 @@ +""" +This file is part of nucypher. +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +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 . +""" +import os +from contextlib import suppress +from functools import partial + +from twisted.internet import reactor + +from constant_sorrow.constants import TEMPORARY_DOMAIN +from nucypher.characters.lawful import Ursula + +FLEET_POPULATION = 12 +DEMO_NODE_STARTING_PORT = 11500 + +ursula_maker = partial(Ursula, rest_host='127.0.0.1', + federated_only=True, + domains=[TEMPORARY_DOMAIN] + ) + + +def spin_up_federated_ursulas(quantity: int = FLEET_POPULATION): + # Ports + starting_port = DEMO_NODE_STARTING_PORT + ports = list(map(str, range(starting_port, starting_port + quantity))) + + ursulas = [] + + sage = ursula_maker( + rest_port=ports[0], + db_filepath="sage.db", + ) + + ursulas.append(sage) + for index, port in enumerate(ports[1:]): + u = ursula_maker( + rest_port=port, + seed_nodes=[sage.seed_node_metadata()], + start_learning_now=True, + db_filepath=f"{port}.db", + ) + ursulas.append(u) + for u in ursulas: + deployer = u.get_deployer() + deployer.addServices() + deployer.catalogServers(deployer.hendrix) + deployer.start() + print(f"{u}: {deployer._listening_message()}") + try: + reactor.run() # GO! + finally: + with suppress(FileNotFoundError): + os.remove("sage.db") + for u in ursulas[1:]: + with suppress(FileNotFoundError): + os.remove(f"{u.rest_interface.port}.db") + + +if __name__ == "__main__": + spin_up_federated_ursulas() From 1672160fd453e0c85f478c1ac4216b186994205b Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 15 Jul 2020 16:38:15 -0700 Subject: [PATCH 131/167] This log message was confusing. --- nucypher/characters/lawful.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index f03c0c83b..9ff90187e 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -779,7 +779,7 @@ class Bob(Character): alice_verifying_key=alice_verifying_key, *capsules_to_activate) - self.log.info(f"Found {len(complete_work_orders)} for this Capsule ({capsule}).") + self.log.debug(f"Found {len(complete_work_orders)} complete WorkOrders for this Capsule ({capsule}).") if complete_work_orders: if use_precedent_work_orders: From 96100bcd357c48d603116d331e04815ec749e31e Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 15 Jul 2020 16:38:39 -0700 Subject: [PATCH 132/167] This race condition no longer exists. --- nucypher/characters/lawful.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 9ff90187e..03b36689c 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1013,7 +1013,7 @@ class Ursula(Teacher, Character, Worker): Character.__init__(self, is_me=is_me, checksum_address=checksum_address, - start_learning_now=False, # Handled later in this function to avoid race condition + start_learning_now=start_learning_now, federated_only=self._federated_only_instances, # TODO: 'Ursula' object has no attribute '_federated_only_instances' if an is_me Ursula is not inited prior to this moment NRN crypto_power=crypto_power, From ad672d751cd605edacbbf817463ef838e546e79d Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 15 Jul 2020 16:38:55 -0700 Subject: [PATCH 133/167] Crash more quickly on learning errors in seednode resolution. --- nucypher/network/nodes.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 7d41affb9..59de0780c 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -410,9 +410,10 @@ class Learner: if self._learning_task.running: self._learning_task.stop() - def handle_learning_errors(self, *args, **kwargs): - failure = args[0] - if self._abort_on_learning_error: + def handle_learning_errors(self, failure, *args, **kwargs): + _exception = failure.value + crash_right_now = getattr(_exception, "crash_right_now", False) + if self._abort_on_learning_error or crash_right_now: self.log.critical("Unhandled error during node learning. Attempting graceful crash.") reactor.callFromThread(self._crash_gracefully, failure=failure) else: @@ -654,8 +655,14 @@ class Learner: remembered = [] if not self.done_seeding: - remembered_seednodes = self.load_seednodes(record_fleet_state=False) - remembered.extend(remembered_seednodes) + try: + remembered_seednodes = self.load_seednodes(record_fleet_state=False) + except Exception as e: + # Even if we aren't aborting on learning errors, we want this to crash the process pronto. + e.crash_right_now = True + raise + else: + remembered.extend(remembered_seednodes) self._learning_round += 1 From c68af1de9c03e2135ac62a42af79317ebd3cd393 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 15 Jul 2020 16:39:19 -0700 Subject: [PATCH 134/167] It makes more sense to force this off into a thread. We're not eager anyway, so there's no reason to chance blocking the main thread. --- nucypher/network/nodes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 59de0780c..9b7253024 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -477,7 +477,9 @@ class Learner: Continually learn about new nodes. """ # TODO: Allow the user to set eagerness? 1712 - self.learn_from_teacher_node(eager=False) + # TODO: Also, if we do allow eager, don't even defer; block right here. + d = deferToThread(self.learn_from_teacher_node, eager=False) + return d def learn_about_specific_nodes(self, addresses: Set): self._node_ids_to_learn_about_immediately.update(addresses) # hmmmm From 85806613f1dd24b986e0a8e5d99174f9609420a8 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Wed, 15 Jul 2020 10:11:20 -0700 Subject: [PATCH 135/167] Allow worker to attempt period commitment as part of it's initialization. --- nucypher/blockchain/eth/actors.py | 2 +- nucypher/blockchain/eth/token.py | 2 +- nucypher/characters/lawful.py | 8 +++++--- .../acceptance/blockchain/actors/test_worker.py | 6 ++---- tests/acceptance/cli/lifecycle.py | 17 +++++++++++++++++ .../learning/test_discovery_phases.py | 2 +- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 09a96f3d0..494c339c9 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -1299,7 +1299,7 @@ class Worker(NucypherTokenActor): self.stakes = StakeList(registry=self.registry, checksum_address=self.checksum_address) self.stakes.refresh() self.work_tracker = work_tracker or WorkTracker(worker=self) - self.work_tracker.start(act_now=False) + self.work_tracker.start(act_now=start_working_now) def block_until_ready(self, poll_rate: int = None, timeout: int = None): """ diff --git a/nucypher/blockchain/eth/token.py b/nucypher/blockchain/eth/token.py index e7ad1c41e..1557289d9 100644 --- a/nucypher/blockchain/eth/token.py +++ b/nucypher/blockchain/eth/token.py @@ -533,7 +533,7 @@ class WorkTracker: self._tracking_task.stop() self.log.info(f"STOPPED WORK TRACKING") - def start(self, act_now: bool = False, requirement_func: Callable = None, force: bool = False) -> None: + def start(self, act_now: bool = True, requirement_func: Callable = None, force: bool = False) -> None: """ High-level stake tracking initialization, this function aims to be safely called at any time - For example, it is okay to call diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 03b36689c..e1884eb9c 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1047,9 +1047,11 @@ class Ursula(Teacher, Character, Worker): # Prepare a TransactingPower from worker node's transacting keys _transacting_power = TransactingPower(account=worker_address, - password=client_password, - signer=self.signer, - cache=True) + password=client_password, + signer=self.signer, + cache=True) + + self.transacting_power = _transacting_power self._crypto_power.consume_power_up(_transacting_power) self._set_checksum_address(checksum_address) diff --git a/tests/acceptance/blockchain/actors/test_worker.py b/tests/acceptance/blockchain/actors/test_worker.py index 96a078502..d0bd2551e 100644 --- a/tests/acceptance/blockchain/actors/test_worker.py +++ b/tests/acceptance/blockchain/actors/test_worker.py @@ -61,20 +61,18 @@ def test_worker_auto_commitments(mocker, commit_to_next_period=False, registry=test_registry).pop() - commit_spy.assert_not_called() - initial_period = staker.staking_agent.get_current_period() def start(): # Start running the worker start_pytest_ursula_services(ursula=ursula) - ursula.work_tracker.start(act_now=True) def time_travel(_): testerchain.time_travel(periods=1) clock.advance(WorkTracker.REFRESH_RATE+1) + return clock - def verify(_): + def verify(clock): # Verify that periods were committed on-chain automatically last_committed_period = staker.staking_agent.get_last_committed_period(staker_address=staker.checksum_address) current_period = staker.staking_agent.get_current_period() diff --git a/tests/acceptance/cli/lifecycle.py b/tests/acceptance/cli/lifecycle.py index 459e53f7b..bed72f0d8 100644 --- a/tests/acceptance/cli/lifecycle.py +++ b/tests/acceptance/cli/lifecycle.py @@ -1,3 +1,20 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + import datetime import json import os diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 6eb050841..75e6b4303 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -205,4 +205,4 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, # Takes about .8 on jMyles' laptop; still the bulk of init time. # Assuming this is 10 percent of the fleet, that's another 8 second savings for first policy enactment. - assert maturation_time.total_seconds() < 2 \ No newline at end of file + assert maturation_time.total_seconds() < 2 From 9f6fb79c808706fe5db298003452fccc3010d15f Mon Sep 17 00:00:00 2001 From: jMyles Date: Thu, 16 Jul 2020 20:18:45 -0700 Subject: [PATCH 136/167] Reducing density; this line was crazy. --- nucypher/network/nodes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 9b7253024..b119a3d11 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -246,7 +246,8 @@ class Learner: discovered = [] if self.learning_domains: - canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(tuple(self.learning_domains)[0], ()) # TODO: Are we done with multiple domains? + one_and_only_learning_domain = tuple(self.learning_domains)[0] # TODO: Are we done with multiple domains? 2144 + canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(one_and_only_learning_domain, ()) for uri in canonical_sage_uris: try: From 1cddc60e9f091c7b8a25f488d13ac7d1b3fb01e5 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 17 Jul 2020 15:36:00 -0700 Subject: [PATCH 137/167] Fixing blunderous percentage error - thanks @fjarri. --- nucypher/policy/policies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index a42940d59..a20b6c724 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -172,7 +172,7 @@ class PolicyPayloadMutex(DeferredList): self._policy_locking_queue = Queue() super().__init__(deferredList, *args, **kwargs) - self._block_until_this_many_are_complete = int(len(deferredList) / self.percent_to_complete_before_release) + self._block_until_this_many_are_complete = int(len(deferredList) * self.percent_to_complete_before_release / 100) def _cbDeferred(self, *args, **kwargs): result = super()._cbDeferred(*args, **kwargs) From e1c522ff261441524baf75cd24e783ea01e7e539 Mon Sep 17 00:00:00 2001 From: damon Date: Fri, 17 Jul 2020 14:00:41 -0700 Subject: [PATCH 138/167] use 12 ursulas for circle docker demo integration tests --- scripts/circle/docker-compose.yml | 99 +++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/scripts/circle/docker-compose.yml b/scripts/circle/docker-compose.yml index f62d053a0..ebaf25226 100644 --- a/scripts/circle/docker-compose.yml +++ b/scripts/circle/docker-compose.yml @@ -21,7 +21,7 @@ services: ports: - 11500 image: circle:nucypher - command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.1 --rest-port 11500 --lonely + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.1 --rest-port 11500 --lonely --disable-availability-check networks: nucypher_circle_net: ipv4_address: 172.29.1.1 @@ -32,7 +32,7 @@ services: image: circle:nucypher depends_on: - circleursula1 - command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.2 --rest-port 11500 --teacher 172.29.1.1:11500 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.2 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 networks: nucypher_circle_net: ipv4_address: 172.29.1.2 @@ -43,7 +43,7 @@ services: image: circle:nucypher depends_on: - circleursula2 - command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.3 --rest-port 11500 --teacher 172.29.1.1:11500 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.3 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 networks: nucypher_circle_net: ipv4_address: 172.29.1.3 @@ -53,12 +53,101 @@ services: - 11500 image: circle:nucypher depends_on: - - circleursula3 - command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.4 --rest-port 11500 --teacher 172.29.1.1:11500 + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.4 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 networks: nucypher_circle_net: ipv4_address: 172.29.1.4 container_name: circleursula4 + circleursula5: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.5 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.5 + container_name: circleursula5 + circleursula6: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.6 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.6 + container_name: circleursula6 + circleursula7: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.7 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.7 + container_name: circleursula7 + circleursula8: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.8 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.8 + container_name: circleursula8 + circleursula9: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.9 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.9 + container_name: circleursula9 + circleursula10: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.10 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.10 + container_name: circleursula10 + circleursula11: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.11 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.11 + container_name: circleursula11 + circleursula12: + ports: + - 11500 + image: circle:nucypher + depends_on: + - circleursula1 + command: nucypher ursula run --dev --federated-only --rest-host 172.29.1.12 --rest-port 11500 --disable-availability-check --teacher 172.29.1.1:11500 + networks: + nucypher_circle_net: + ipv4_address: 172.29.1.12 + container_name: circleursula12 + networks: From 7e964fe4ec474b00099d4115cebeae40e02298d6 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 17 Jul 2020 19:36:57 -0700 Subject: [PATCH 139/167] Stop Alice's publication threadpool in disenchant(). --- nucypher/characters/lawful.py | 6 +++++- tests/fixtures.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index e1884eb9c..535bee4d0 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -133,7 +133,6 @@ class Alice(Character, BlockchainPolicyAuthor): self.publication_threadpool = ThreadPool(maxthreads=120, name="Alice Policy Publication") # In the future, this value is perhaps best set to something like 3-4 times the optimal "high n", whatever we determine that to be. self.publication_threadpool.start() - reactor.addSystemEventTrigger("before", "shutdown", self.publication_threadpool.stop) # TODO: Congregate Character Stop activity. else: self.m = STRANGER_ALICE self.n = STRANGER_ALICE @@ -445,6 +444,11 @@ class Alice(Character, BlockchainPolicyAuthor): return controller + def disenchant(self): + super().disenchant() + self.publication_threadpool.stop() + + class Bob(Character): banner = BOB_BANNER diff --git a/tests/fixtures.py b/tests/fixtures.py index 6ec3bb1ed..c710f3f51 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -347,14 +347,14 @@ def random_policy_label(): def federated_alice(alice_federated_test_config): alice = alice_federated_test_config.produce() yield alice - alice.publication_threadpool.stop() + alice.disenchant() @pytest.fixture(scope="module") def blockchain_alice(alice_blockchain_test_config, testerchain): alice = alice_blockchain_test_config.produce() yield alice - alice.publication_threadpool.stop() + alice.disenchant() @pytest.fixture(scope="module") From 9d5f18ee090ba3a34f9506f6d3d9bbf5c2a88a49 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 17 Jul 2020 20:02:49 -0700 Subject: [PATCH 140/167] Crash more quickly, and with better error, if unable to get cert during seednode instantiation. --- nucypher/characters/lawful.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 535bee4d0..e195d8c6e 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1309,7 +1309,6 @@ class Ursula(Teacher, Character, Worker): host: str, port: int, certificate_filepath, - federated_only: bool, *args, **kwargs ): response_data = network_middleware.client.node_information(host, port, @@ -1387,7 +1386,12 @@ class Ursula(Teacher, Character, Worker): host, port, checksum_address = parse_node_uri(seed_uri) # Fetch the hosts TLS certificate and read the common name - certificate = network_middleware.get_certificate(host=host, port=port) + try: + certificate = network_middleware.get_certificate(host=host, port=port) + except NodeSeemsToBeDown as e: + e.args += (f"While trying to load seednode {seed_uri}",) + e.crash_right_now = True + raise real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value # Create a temporary certificate storage area From 78abb8f6ed64958d10b94a79369cb7eec6cbe3c7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sat, 18 Jul 2020 13:09:08 -0700 Subject: [PATCH 141/167] Yanking some autouses in favor of usefixtures. [skip ci] --- .../test_select_client_account_for_staking.py | 2 ++ ...ursula_local_keystore_cli_functionality.py | 2 +- tests/integration/conftest.py | 20 +++++++++---------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/integration/cli/actions/test_select_client_account_for_staking.py b/tests/integration/cli/actions/test_select_client_account_for_staking.py index eee4afd25..ff337d82c 100644 --- a/tests/integration/cli/actions/test_select_client_account_for_staking.py +++ b/tests/integration/cli/actions/test_select_client_account_for_staking.py @@ -20,8 +20,10 @@ from nucypher.cli.actions.select import select_client_account_for_staking from nucypher.cli.literature import PREALLOCATION_STAKE_ADVISORY from nucypher.config.constants import TEMPORARY_DOMAIN from tests.constants import YES +import pytest +@pytest.mark.usefixtures('mock_contract_agency') def test_select_client_account_for_staking_cli_action(test_emitter, test_registry, test_registry_source_manager, diff --git a/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py b/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py index 8a92570f7..e35c945e9 100644 --- a/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py +++ b/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py @@ -44,7 +44,7 @@ def mock_account_password_keystore(tmp_path_factory): json.dump(account.encrypt(password), open(path, 'x+t')) return account, password, keystore - +@pytest.mark.usefixtures('mock_contract_agency') def test_ursula_init_with_local_keystore_signer(click_runner, tmp_path, mocker, diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index fc9221987..5c3796714 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -54,7 +54,7 @@ from tests.utils.config import ( from tests.utils.ursula import MOCK_URSULA_STARTING_PORT -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope='function') def mock_contract_agency(monkeypatch, module_mocker, token_economics): monkeypatch.setattr(ContractAgency, 'get_agent', MockContractAgency.get_agent) module_mocker.patch.object(EconomicsFactory, 'get_economics', return_value=token_economics) @@ -63,36 +63,36 @@ def mock_contract_agency(monkeypatch, module_mocker, token_economics): mock_agency.reset() -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope='function') def mock_token_agent(mock_testerchain, token_economics, mock_contract_agency): return mock_contract_agency.get_agent(NucypherTokenAgent) -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope='function') def mock_staking_agent(mock_testerchain, token_economics, mock_contract_agency): mock_agent = mock_contract_agency.get_agent(StakingEscrowAgent) return mock_agent -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope='function') def mock_adjudicator_agent(mock_testerchain, token_economics, mock_contract_agency): mock_agent = mock_contract_agency.get_agent(AdjudicatorAgent) return mock_agent -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope='function') def mock_policy_manager_agent(mock_testerchain, token_economics, mock_contract_agency): mock_agent = mock_contract_agency.get_agent(PolicyManagerAgent) return mock_agent -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope='function') def mock_multisig_agent(mock_testerchain, token_economics, mock_contract_agency): mock_agent = mock_contract_agency.get_agent(MultiSigAgent) return mock_agent -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope='function') def mock_worklock_agent(mock_testerchain, token_economics, mock_contract_agency): economics = token_economics @@ -127,7 +127,7 @@ def mock_stdin(mocker): assert mock.empty(), "Stdin mock was not empty on teardown - some unclaimed input remained" -@pytest.fixture(scope='module', autouse=True) +@pytest.fixture(scope='module') def mock_testerchain() -> MockBlockchain: BlockchainInterfaceFactory._interfaces = dict() testerchain = _make_testerchain(mock_backend=True) @@ -140,7 +140,7 @@ def token_economics(mock_testerchain): return make_token_economics(blockchain=mock_testerchain) -@pytest.fixture(scope='module', autouse=True) +@pytest.fixture(scope='module') def mock_interface(module_mocker): mock_transaction_sender = module_mocker.patch.object(BlockchainInterface, 'sign_and_broadcast_transaction') mock_transaction_sender.return_value = MockContractAgent.FAKE_RECEIPT @@ -161,7 +161,7 @@ def test_registry_source_manager(mock_testerchain, test_registry): yield real_inventory -@pytest.fixture(scope='module', autouse=True) +@pytest.fixture(scope='module') def mock_contract_agency(module_mocker, token_economics): # Patch From 1a98838316500bb51f150c4f123f60e409d601ac Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 11:56:18 -0700 Subject: [PATCH 142/167] Still need to set known_node_class here. --- nucypher/characters/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 55b4e12d3..a5077faba 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -342,6 +342,7 @@ class Character(Learner): # Once in a while, in tests or demos, we init a plain Character who doesn't already know about its node class. from nucypher.characters.lawful import Ursula known_node_class = Ursula + self.known_node_class = known_node_class # If we're federated only, we assume that all other nodes in our domain are as well. known_node_class.set_federated_mode(federated_only) From e8fb7413291ea8350ca5ca90a75f5f6229031290 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 11:56:49 -0700 Subject: [PATCH 143/167] More useful division of error situations where a signature from a Teacher appears to be invalid. --- nucypher/characters/base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index a5077faba..de070dda5 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -458,7 +458,17 @@ class Character(Learner): if signature_to_use: is_valid = signature_to_use.verify(message, sender_verifying_key) # FIXME: Message is undefined here if not is_valid: - raise InvalidSignature("Signature for message isn't valid: {}".format(signature_to_use)) + try: + node_on_the_other_end = self.known_node_class.from_seednode_metadata(stranger.seed_node_metadata(), + network_middleware=self.network_middleware) + if node_on_the_other_end != stranger: + raise self.known_node_class.InvalidNode( + f"Expected to connect to {stranger}, got {node_on_the_other_end} instead.") + else: + raise InvalidSignature("Signature for message isn't valid: {}".format(signature_to_use)) + except (TypeError, AttributeError): + raise self.known_node_class.InvalidNode( + f"Unable to verify message from strange node at {stranger.rest_url()}") else: raise InvalidSignature("No signature provided -- signature presumed invalid.") From 12ba869470bc8dd45cc36471ae32228b33dc0ab3 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 12:37:05 -0700 Subject: [PATCH 144/167] No need for the if here - this function checks if it's running. --- nucypher/characters/lawful.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index e195d8c6e..84e9af965 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1246,8 +1246,7 @@ class Ursula(Teacher, Character, Worker): def stop(self, halt_reactor: bool = False) -> None: """Stop services""" self._availability_tracker.stop() - if self._learning_task.running: - self.stop_learning_loop() + self.stop_learning_loop() if not self.federated_only: self.work_tracker.stop() if self._arrangement_pruning_task.running: From e741a96ffc3b6423409672092170dd40f37621d0 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 12:38:38 -0700 Subject: [PATCH 145/167] Another deprecated federated_only. --- nucypher/characters/lawful.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 84e9af965..392842bb8 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1404,7 +1404,6 @@ class Ursula(Teacher, Character, Worker): port=port, network_middleware=network_middleware, certificate_filepath=temp_certificate_filepath, - federated_only=federated_only, *args, **kwargs ) From c5e95fc2c1a06e4b42116416e1998ba665e56727 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 13:18:45 -0700 Subject: [PATCH 146/167] Stopping here breaks RPC and other long-running CLI. Need to put it elsewhere. --- nucypher/characters/base.py | 1 - nucypher/characters/control/controllers.py | 1 - 2 files changed, 2 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index de070dda5..69e21c70b 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -108,7 +108,6 @@ class Character(Learner): # and whether maybe only Lawful characters have an interface self.interface = self._interface_class(character=self) - if is_me: self._set_known_node_class(known_node_class, federated_only) else: diff --git a/nucypher/characters/control/controllers.py b/nucypher/characters/control/controllers.py index 1f2cfbe9d..e9e40d190 100644 --- a/nucypher/characters/control/controllers.py +++ b/nucypher/characters/control/controllers.py @@ -63,7 +63,6 @@ class CharacterControllerBase(ABC): response = method(**params) # < ---- INLET response_data = serializer.dump(response) - self.stop_character() return response_data From 4261743594d3f980121094c202f3ac38a4ea15ac Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 13:31:42 -0700 Subject: [PATCH 147/167] Adding cancellation to keep_learning logic. --- nucypher/network/nodes.py | 115 ++++++++++++++++++++++++++------------ tests/fixtures.py | 1 + 2 files changed, 81 insertions(+), 35 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index b119a3d11..3f064d354 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -24,11 +24,12 @@ from typing import Set, Tuple, Union import maya import requests +from cryptography.exceptions import InvalidSignature from cryptography.x509 import Certificate from eth_utils import to_checksum_address from requests.exceptions import SSLError from twisted.internet import defer, reactor, task -from twisted.internet.threads import deferToThread +from twisted.internet.defer import Deferred from twisted.logger import Logger import nucypher @@ -68,7 +69,8 @@ class NodeSprout(PartiallyKwargifiedBytes): self._checksum_address = None self._nickname = None self._hash = None - self.timestamp = maya.MayaDT(self.timestamp) # Weird for this to be in init. maybe this belongs in the splitter also. + self.timestamp = maya.MayaDT( + self.timestamp) # Weird for this to be in init. maybe this belongs in the splitter also. self._repr = None def __hash__(self): @@ -107,7 +109,6 @@ class NodeSprout(PartiallyKwargifiedBytes): self._nickname = nickname_from_seed(self.checksum_address)[0] return self._nickname - def mature(self): mature_node = self.finish() @@ -199,6 +200,7 @@ class Learner: self.lonely = lonely self.done_seeding = False + self._learning_deferred = None if not node_storage: # Fallback storage backend @@ -209,7 +211,8 @@ class Learner: from nucypher.characters.lawful import Ursula self.node_class = node_class or Ursula - self.node_class.set_cert_storage_function(node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481 + self.node_class.set_cert_storage_function( + node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481 known_nodes = known_nodes or tuple() self.unresponsive_startup_nodes = list() # TODO: Buckets - Attempt to use these again later #567 @@ -221,7 +224,7 @@ class Learner: self.teacher_nodes = deque() self._current_teacher_node = None # type: Teacher - self._learning_task = task.LoopingCall(self.keep_learning_about_nodes) + self._learning_task = task.LoopingCall(self.keep_learning_about_nodes, learner=self) self._learning_round = 0 # type: int self._rounds_without_new_nodes = 0 # type: int self._seed_nodes = seed_nodes or [] @@ -246,16 +249,17 @@ class Learner: discovered = [] if self.learning_domains: - one_and_only_learning_domain = tuple(self.learning_domains)[0] # TODO: Are we done with multiple domains? 2144 + one_and_only_learning_domain = tuple(self.learning_domains)[ + 0] # TODO: Are we done with multiple domains? 2144 canonical_sage_uris = self.network_middleware.TEACHER_NODES.get(one_and_only_learning_domain, ()) for uri in canonical_sage_uris: try: maybe_sage_node = self.node_class.from_teacher_uri(teacher_uri=uri, - min_stake=0, # TODO: Where to get this? - federated_only=self.federated_only, - network_middleware=self.network_middleware, - registry=self.registry) + min_stake=0, # TODO: Where to get this? + federated_only=self.federated_only, + network_middleware=self.network_middleware, + registry=self.registry) except NodeSeemsToBeDown: self.unresponsive_seed_nodes.add(uri) else: @@ -273,8 +277,8 @@ class Learner: seednode_metadata.rest_port)) seed_node = self.node_class.from_seednode_metadata(seednode_metadata=seednode_metadata, - network_middleware=self.network_middleware, - federated_only=self.federated_only) # TODO: 466 + network_middleware=self.network_middleware, + ) if seed_node is False: self.unresponsive_seed_nodes.add(seednode_metadata) elif seed_node is UNKNOWN_VERSION: @@ -299,7 +303,6 @@ class Learner: return discovered - def read_nodes_from_storage(self) -> None: stored_nodes = self.node_storage.all(federated_only=self.federated_only) # TODO: #466 @@ -366,7 +369,8 @@ class Learner: except node.NotStaking: # TODO: Bucket this node as inactive, and potentially safe to forget. 567 - self.log.info(f'Staker:Worker {node.checksum_address}:{node.worker_address} is not actively staking, skipping.') + self.log.info( + f'Staker:Worker {node.checksum_address}:{node.worker_address} is not actively staking, skipping.') return False # TODO: What about InvalidNode? (for that matter, any SuspiciousActivity) 1714, 567 too really @@ -395,13 +399,10 @@ class Learner: else: self.log.info("Starting Learning Loop.") - learning_deferreds = list() - learner_deferred = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now) learner_deferred.addErrback(self.handle_learning_errors) - learning_deferreds.append(learner_deferred) - self.learning_deferred = defer.DeferredList(learning_deferreds) + self.learning_deferred = learner_deferred return self.learning_deferred def stop_learning_loop(self, reason=None): @@ -411,6 +412,9 @@ class Learner: if self._learning_task.running: self._learning_task.stop() + if self._learning_deferred: + self._learning_deferred.cancel() + def handle_learning_errors(self, failure, *args, **kwargs): _exception = failure.value crash_right_now = getattr(_exception, "crash_right_now", False) @@ -426,7 +430,7 @@ class Learner: def _crash_gracefully(self, failure=None): """ A facility for crashing more gracefully in the event that an exception - is unhandled in a different thread, especially inside a loop like the learning loop. + is unhandled in a different thread, especially inside a loop like the acumen loop, Alice's publication loop, or Bob's retrieval loop.. """ self._crashed = failure failure.raiseException() @@ -473,14 +477,55 @@ class Learner: self.log.info("Learning loop wasn't started; forcing start now.") self._learning_task.start(self._SHORT_LEARNING_DELAY, now=True) - def keep_learning_about_nodes(self): + def keep_learning_about_nodes(self, learner=None): """ Continually learn about new nodes. + + learner is for debugging and logging only. """ + + import datetime + + print(f"+++++++++++{self} deferring learning cycle at {datetime.datetime.now()}") + + while self._learning_deferred is not None: + print(f"^^^^^^^^^{self} waiting at {datetime.datetime.now()}") + time.sleep(.1) + + class DiscoveryCanceller: + + def __init__(self): + self.stop_now = False + + def __call__(self, learning_deferred): + self.stop_now = True + + _canceller = DiscoveryCanceller() # TODO: Allow the user to set eagerness? 1712 # TODO: Also, if we do allow eager, don't even defer; block right here. - d = deferToThread(self.learn_from_teacher_node, eager=False) - return d + + self._learning_deferred = Deferred(canceller=_canceller) + + def _discover_or_abort(_first_result): + print(f"========={self} learning at {datetime.datetime.now()}") + result = self.learn_from_teacher_node(eager=False, canceller=_canceller) + print(f"///////////{self} finished learning at {datetime.datetime.now()}") + return result + + self._learning_deferred.addCallback(_discover_or_abort) + self._learning_deferred.addErrback(self.handle_learning_errors) + + def clear_learning_deferred(result_of_last_learning_cycle): + # TODO: This is an interesting opportunity to add throttling and / or check against a canonical fleet state. #1712 #1000 + print(f"Clearing {self} at {datetime.datetime.now()}") + self._learning_deferred = None + + self._learning_deferred.addCallback(clear_learning_deferred) + + # Instead of None, we might want to pass something useful about the context. + # Alternately, it might be nice for learn_from_teacher_node to (some or all of the time) return a Deferred. + reactor.callInThread(self._learning_deferred.callback, None) + return self._learning_deferred def learn_about_specific_nodes(self, addresses: Set): self._node_ids_to_learn_about_immediately.update(addresses) # hmmmm @@ -525,7 +570,8 @@ class Learner: if not self._learning_task.running: raise RuntimeError("Learning loop is not running. Start it with start_learning().") elif not reactor.running and not learn_on_this_thread: - raise RuntimeError(f"The reactor isn't running, but you're trying to use it for discovery. You need to start the Reactor in order to use {self} this way.") + raise RuntimeError( + f"The reactor isn't running, but you're trying to use it for discovery. You need to start the Reactor in order to use {self} this way.") else: raise self.NotEnoughNodes("After {} seconds and {} rounds, didn't find {} nodes".format( timeout, rounds_undertaken, number_of_nodes_to_know)) @@ -542,7 +588,8 @@ class Learner: if not learn_on_this_thread: # Get a head start by firing the looping call now. If it's very fast, maybe we'll have enough nodes on the first iteration. - self._learning_task() + if self._learning_task.running: + self._learning_task() while True: if self._crashed: @@ -553,10 +600,11 @@ class Learner: self.log.info("Learned about all nodes after {} rounds.".format(rounds_undertaken)) return True - if not self._learning_task.running: - self.log.warn("Blocking to learn about nodes, but learning loop isn't running.") if learn_on_this_thread: self.learn_from_teacher_node(eager=True) + elif not self._learning_task.running: + raise RuntimeError( + "Tried to block while discovering nodes on another thread, but the learning task isn't running.") if (maya.now() - start).seconds > timeout: @@ -564,8 +612,6 @@ class Learner: if len(still_unknown) <= allow_missing: return False - 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( @@ -649,7 +695,7 @@ class Learner: else: raise self.InvalidSignature("No signature provided -- signature presumed invalid.") - def learn_from_teacher_node(self, eager=False): + def learn_from_teacher_node(self, eager=False, canceller=None): """ Sends a request to node_url to find out about known nodes. @@ -721,7 +767,6 @@ class Learner: f"{current_teacher} is serving {teacher_domains}, but we are learning {learner_domains}") return # This node is not serving any of our domains. - # # Deserialize # @@ -733,9 +778,11 @@ class Learner: try: self.verify_from(current_teacher, node_payload, signature=signature) - except current_teacher.InvalidSignature: - self.suspicious_activities_witnessed['vladimirs'].append(('Node payload improperly signed', node_payload, signature)) - self.log.warn(f"Invalid signature ({signature}) received from teacher {current_teacher} for payload {node_payload}") + except InvalidSignature as e: + self.suspicious_activities_witnessed['vladimirs'].append( + ('Node payload improperly signed', node_payload, signature)) + self.log.warn( + f"Invalid signature ({signature}) received from teacher {current_teacher} for payload {node_payload}") # End edge case handling. fleet_state_checksum_bytes, fleet_state_updated_bytes, node_payload = FleetSensor.snapshot_splitter( @@ -803,7 +850,6 @@ class Learner: # f"Propagated by: {current_teacher}" # self.log.warn(message) - # Is cycling happening in the right order? current_teacher.update_snapshot(checksum=checksum, updated=maya.MayaDT(int.from_bytes(fleet_state_updated_bytes, byteorder="big")), @@ -811,7 +857,6 @@ class Learner: ################### - learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new." self.log.info(learning_round_log_message.format(self._learning_round, current_teacher, diff --git a/tests/fixtures.py b/tests/fixtures.py index c710f3f51..17819eab2 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -16,6 +16,7 @@ along with nucypher. If not, see . """ import contextlib +import inspect import json import os import random From 4a616d143df444b2f80af877ac8942ec8baf68c7 Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 13:32:23 -0700 Subject: [PATCH 148/167] Flagging test bob's origins for inspection on other threads. --- tests/fixtures.py | 12 ++++++++---- .../integration/characters/test_bob_handles_frags.py | 4 ++++ .../test_bob_joins_policy_and_retrieves.py | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 17819eab2..e8bc15d05 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -360,15 +360,19 @@ def blockchain_alice(alice_blockchain_test_config, testerchain): @pytest.fixture(scope="module") def federated_bob(bob_federated_test_config): + frames = inspect.stack(3) bob = bob_federated_test_config.produce() - return bob + # Since Bob is sometimes "left hanging" at the end of tests, this is an invaluable piece of information for debugging problems like #2150. + bob._FOR_TEST = frames[1].frame.f_locals['request'].module + yield bob + bob.disenchant() @pytest.fixture(scope="module") def blockchain_bob(bob_blockchain_test_config, testerchain): - _bob = bob_blockchain_test_config.produce() - return _bob - + bob = bob_blockchain_test_config.produce() + yield bob + bob.disenchant() @pytest.fixture(scope="module") def federated_ursulas(ursula_federated_test_config): diff --git a/tests/integration/characters/test_bob_handles_frags.py b/tests/integration/characters/test_bob_handles_frags.py index 189439503..c59f3430b 100644 --- a/tests/integration/characters/test_bob_handles_frags.py +++ b/tests/integration/characters/test_bob_handles_frags.py @@ -85,6 +85,10 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f abort_on_learning_error=True, federated_only=True) + import inspect + frame = inspect.currentframe() + bob._FOR_TEST = frame.f_code.co_name + # Again, let's assume that he received the TreasureMap via a side channel. hrac, treasure_map = enacted_federated_policy.hrac(), enacted_federated_policy.treasure_map map_id = treasure_map.public_id() diff --git a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py index cfa862b20..9bdd722ed 100644 --- a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py +++ b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py @@ -74,6 +74,10 @@ def test_bob_joins_policy_and_retrieves(federated_alice, known_nodes=a_couple_of_ursulas, ) + import inspect + frame = inspect.currentframe() + bob._FOR_TEST = frame.f_code.co_name + # Bob has only connected to 2 nodes. assert sum(node.verified_node for node in bob.known_nodes) == 2 From 1534bc07a96339498b667a70bb75374e28642ebd Mon Sep 17 00:00:00 2001 From: jMyles Date: Mon, 20 Jul 2020 13:36:33 -0700 Subject: [PATCH 149/167] Stopping Bob and Ursula at appropriate times. --- tests/fixtures.py | 4 ++++ tests/integration/characters/test_bob_handles_frags.py | 4 ++-- .../characters/test_bob_joins_policy_and_retrieves.py | 2 ++ tests/integration/characters/test_character_serialization.py | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index e8bc15d05..9e1d80d18 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -386,6 +386,9 @@ def federated_ursulas(ursula_federated_test_config): for port in _ports_to_remove: del MOCK_KNOWN_URSULAS_CACHE[port] + for u in _ursulas: + u.stop() + @pytest.fixture(scope="function") def lonely_ursula_maker(ursula_federated_test_config): @@ -403,6 +406,7 @@ def lonely_ursula_maker(ursula_federated_test_config): def clean(self): for ursula in self._made: + ursula.stop() del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] _maker = _PartialUrsulaMaker() yield _maker diff --git a/tests/integration/characters/test_bob_handles_frags.py b/tests/integration/characters/test_bob_handles_frags.py index c59f3430b..8da206cff 100644 --- a/tests/integration/characters/test_bob_handles_frags.py +++ b/tests/integration/characters/test_bob_handles_frags.py @@ -119,11 +119,11 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f 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 + bob.block_until_specific_nodes_are_known(unknown_nodes) # ...and he now has no more unknown_nodes. assert len(bob.known_nodes) == len(treasure_map) + bob.disenchant() def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_policy, federated_bob, diff --git a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py index 9bdd722ed..ff4d8233b 100644 --- a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py +++ b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py @@ -175,6 +175,8 @@ def test_bob_joins_policy_and_retrieves(federated_alice, label=policy.label, ) + bob.disenchant() + def test_treasure_map_serialization(enacted_federated_policy, federated_bob): treasure_map = enacted_federated_policy.treasure_map diff --git a/tests/integration/characters/test_character_serialization.py b/tests/integration/characters/test_character_serialization.py index 0e1984e60..e4e6e536a 100644 --- a/tests/integration/characters/test_character_serialization.py +++ b/tests/integration/characters/test_character_serialization.py @@ -23,3 +23,4 @@ def test_serialize_ursula(federated_ursulas): ursula_as_bytes = bytes(ursula) ursula_object = Ursula.from_bytes(ursula_as_bytes) assert ursula == ursula_object + ursula.stop() From 790719f41068325e6f20f8d3b79bbfb79fca8281 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 22 Jul 2020 16:15:18 -0700 Subject: [PATCH 150/167] Moving the canceller to a top-level class and re-using it. --- nucypher/characters/lawful.py | 2 +- nucypher/network/nodes.py | 64 +++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 392842bb8..0e2b5584a 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -445,11 +445,11 @@ class Alice(Character, BlockchainPolicyAuthor): return controller def disenchant(self): + print(f"disenchanting {self}") super().disenchant() self.publication_threadpool.stop() - class Bob(Character): banner = BOB_BANNER _interface_class = BobInterface diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 3f064d354..08c2c0dbe 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -37,7 +37,7 @@ from bytestring_splitter import BytestringSplitter, BytestringSplittingError, Pa VariableLengthBytestring from constant_sorrow import constant_or_bytes from constant_sorrow.constants import (CERTIFICATE_NOT_SAVED, FLEET_STATES_MATCH, NEVER_SEEN, NOT_SIGNED, - NO_KNOWN_NODES, NO_STORAGE_AVAILIBLE, UNKNOWN_FLEET_STATE, UNKNOWN_VERSION) + NO_KNOWN_NODES, NO_STORAGE_AVAILIBLE, UNKNOWN_FLEET_STATE, UNKNOWN_VERSION, RELAX) from nucypher.acumen.nicknames import nickname_from_seed from nucypher.acumen.perception import FleetSensor, icon_from_checksum from nucypher.blockchain.economics import EconomicsFactory @@ -121,6 +121,18 @@ class NodeSprout(PartiallyKwargifiedBytes): return self # To reduce the awkwardity of renaming; this is always the weird part of polymorphism for me. +class DiscoveryCanceller: + + def __init__(self): + self.stop_now = False + + def __call__(self, learning_deferred): + if self.stop_now: + assert False + self.stop_now = True + learning_deferred.callback(RELAX) + + class Learner: """ Any participant in the "learning loop" - a class inheriting from @@ -182,6 +194,7 @@ class Learner: self.log = Logger("learning-loop") # type: Logger + self.learning_deferred = Deferred() self.learning_domains = domains if not self.federated_only: default_middleware = self.__DEFAULT_MIDDLEWARE_CLASS(registry=self.registry) @@ -201,6 +214,7 @@ class Learner: self.lonely = lonely self.done_seeding = False self._learning_deferred = None + self._discovery_canceller = DiscoveryCanceller() if not node_storage: # Fallback storage backend @@ -399,7 +413,7 @@ class Learner: else: self.log.info("Starting Learning Loop.") - learner_deferred = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now) + learner_deferred = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now) # TODO: now=now? This block is always False, no? learner_deferred.addErrback(self.handle_learning_errors) self.learning_deferred = learner_deferred @@ -414,6 +428,11 @@ class Learner: if self._learning_deferred: self._learning_deferred.cancel() + self._learning_deferred = RELAX + elif self._learning_deferred is RELAX: + assert False + + # self.learning_deferred.cancel() def handle_learning_errors(self, failure, *args, **kwargs): _exception = failure.value @@ -488,39 +507,30 @@ class Learner: print(f"+++++++++++{self} deferring learning cycle at {datetime.datetime.now()}") - while self._learning_deferred is not None: - print(f"^^^^^^^^^{self} waiting at {datetime.datetime.now()}") - time.sleep(.1) + # while self._learning_deferred is not None: + # print(f"^^^^^^^^^{self} waiting at {datetime.datetime.now()}") + # time.sleep(.1) - class DiscoveryCanceller: - - def __init__(self): - self.stop_now = False - - def __call__(self, learning_deferred): - self.stop_now = True - - _canceller = DiscoveryCanceller() # TODO: Allow the user to set eagerness? 1712 # TODO: Also, if we do allow eager, don't even defer; block right here. - self._learning_deferred = Deferred(canceller=_canceller) + self._learning_deferred = Deferred(canceller=self._discovery_canceller) def _discover_or_abort(_first_result): print(f"========={self} learning at {datetime.datetime.now()}") - result = self.learn_from_teacher_node(eager=False, canceller=_canceller) + result = self.learn_from_teacher_node(eager=False, canceller=self._discovery_canceller) print(f"///////////{self} finished learning at {datetime.datetime.now()}") return result self._learning_deferred.addCallback(_discover_or_abort) self._learning_deferred.addErrback(self.handle_learning_errors) - def clear_learning_deferred(result_of_last_learning_cycle): - # TODO: This is an interesting opportunity to add throttling and / or check against a canonical fleet state. #1712 #1000 - print(f"Clearing {self} at {datetime.datetime.now()}") - self._learning_deferred = None - - self._learning_deferred.addCallback(clear_learning_deferred) + # def clear_learning_deferred(result_of_last_learning_cycle): + # # TODO: This is an interesting opportunity to add throttling and / or check against a canonical fleet state. #1712 #1000 + # print(f"Clearing {self} at {datetime.datetime.now()}") + # self._learning_deferred = None + # + # self._learning_deferred.addCallback(clear_learning_deferred) # Instead of None, we might want to pass something useful about the context. # Alternately, it might be nice for learn_from_teacher_node to (some or all of the time) return a Deferred. @@ -586,10 +596,10 @@ class Learner: start = maya.now() starting_round = self._learning_round - if not learn_on_this_thread: - # Get a head start by firing the looping call now. If it's very fast, maybe we'll have enough nodes on the first iteration. - if self._learning_task.running: - self._learning_task() + # if not learn_on_this_thread: + # # Get a head start by firing the looping call now. If it's very fast, maybe we'll have enough nodes on the first iteration. + # # if self._learning_task.running: + # # self._learning_task() while True: if self._crashed: @@ -727,6 +737,8 @@ class Learner: # # Request # + if canceller and canceller.stop_now: + return RELAX try: response = self.network_middleware.get_nodes_via_rest(node=current_teacher, From 695c1c4bd8fe14ecba7c78e3444bd412ce625bbf Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 22 Jul 2020 16:15:48 -0700 Subject: [PATCH 151/167] Struggling with some ALreadyCalled issues - turning debugging on for the defer module gives some useful insight. --- tests/fixtures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index 9e1d80d18..68242d56e 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -31,6 +31,8 @@ import pytest from click.testing import CliRunner from eth_utils import to_checksum_address from sqlalchemy.engine import create_engine +from twisted.internet import defer +from twisted.internet.defer import Deferred from twisted.logger import Logger from web3 import Web3 @@ -115,6 +117,7 @@ from umbral.signing import Signer test_logger = Logger("test-logger") +defer.setDebugging(True) # # Temporary From d06deba742eeab914c0ea09e7c1cfb62154ee37a Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 22 Jul 2020 16:16:30 -0700 Subject: [PATCH 152/167] Some debugging breadcrumbs for Ursula. --- tests/fixtures.py | 16 ++++++++++++---- .../learning/test_firstula_circumstances.py | 2 +- tests/utils/ursula.py | 5 ++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 68242d56e..26b2d85f1 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -363,9 +363,9 @@ def blockchain_alice(alice_blockchain_test_config, testerchain): @pytest.fixture(scope="module") def federated_bob(bob_federated_test_config): - frames = inspect.stack(3) bob = bob_federated_test_config.produce() # Since Bob is sometimes "left hanging" at the end of tests, this is an invaluable piece of information for debugging problems like #2150. + frames = inspect.stack(3) bob._FOR_TEST = frames[1].frame.f_locals['request'].module yield bob bob.disenchant() @@ -403,13 +403,20 @@ def lonely_ursula_maker(ursula_federated_test_config): _made = [] def __call__(self, *args, **kwargs): - ursula = self._partial(*args, **kwargs) - self._made.extend(ursula) - return ursula + ursulas = self._partial(*args, **kwargs) + self._made.extend(ursulas) + frames = inspect.stack(3) + for ursula in ursulas: + try: + ursula._FOR_TEST = frames[1].frame.f_code.co_name + except KeyError as e: + raise + return ursulas def clean(self): for ursula in self._made: ursula.stop() + for ursula in self._made: del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port] _maker = _PartialUrsulaMaker() yield _maker @@ -983,6 +990,7 @@ def fleet_of_highperf_mocked_ursulas(ursula_federated_test_config, request): _ursulas = make_federated_ursulas(ursula_config=ursula_federated_test_config, quantity=quantity, know_each_other=False) all_ursulas = {u.checksum_address: u for u in _ursulas} + for ursula in _ursulas: ursula.known_nodes._nodes = all_ursulas ursula.known_nodes.checksum = b"This is a fleet state checksum..".hex() diff --git a/tests/integration/learning/test_firstula_circumstances.py b/tests/integration/learning/test_firstula_circumstances.py index fc3931e8e..b041076d9 100644 --- a/tests/integration/learning/test_firstula_circumstances.py +++ b/tests/integration/learning/test_firstula_circumstances.py @@ -42,7 +42,7 @@ def test_get_cert_from_running_seed_node(lonely_ursula_maker): node_deployer.addServices() node_deployer.catalogServers(node_deployer.hendrix) - node_deployer.start() + node_deployer.start() # If this port happens not to be open, we'll get an error here. THis might be one of the few sane places to reintroduce a check. certificate_as_deployed = node_deployer.cert.to_cryptography() diff --git a/tests/utils/ursula.py b/tests/utils/ursula.py index ea1fed925..52ceb516a 100644 --- a/tests/utils/ursula.py +++ b/tests/utils/ursula.py @@ -17,6 +17,7 @@ along with nucypher. If not, see . import contextlib +import inspect import socket @@ -65,12 +66,14 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration, starting_port = max(MOCK_KNOWN_URSULAS_CACHE.keys()) + 1 federated_ursulas = set() + frames = inspect.stack(3) + for port in range(starting_port, starting_port+quantity): ursula = ursula_config.produce(rest_port=port + 100, db_filepath=MOCK_URSULA_DB_FILEPATH, **ursula_overrides) - + ursula._FOR_TEST = frames[1].frame.f_locals['request'].module federated_ursulas.add(ursula) # Store this Ursula in our global testing cache. From 9c577a5b968c36714df8f354f6584550c72cd205 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 22 Jul 2020 16:17:49 -0700 Subject: [PATCH 153/167] Loading seednodes here is enough. --- .../learning/test_firstula_circumstances.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/integration/learning/test_firstula_circumstances.py b/tests/integration/learning/test_firstula_circumstances.py index b041076d9..52b76d729 100644 --- a/tests/integration/learning/test_firstula_circumstances.py +++ b/tests/integration/learning/test_firstula_circumstances.py @@ -51,15 +51,16 @@ def test_get_cert_from_running_seed_node(lonely_ursula_maker): network_middleware=RestMiddleware()).pop() assert not any_other_ursula.known_nodes - def start_lonely_learning_loop(): - any_other_ursula.log.info( - "Known nodes when starting learning loop were: {}".format(any_other_ursula.known_nodes)) - any_other_ursula.start_learning_loop() - result = any_other_ursula.block_until_specific_nodes_are_known(set([firstula.checksum_address]), - timeout=2) - assert result - - yield deferToThread(start_lonely_learning_loop) + # def start_lonely_learning_loop(): + # any_other_ursula.log.info( + # "Known nodes when starting learning loop were: {}".format(any_other_ursula.known_nodes)) + # any_other_ursula.start_learning_loop() + # result = any_other_ursula.block_until_specific_nodes_are_known(set([firstula.checksum_address]), + # timeout=2) + # assert result + # + # yield deferToThread(start_lonely_learning_loop) + yield deferToThread(any_other_ursula.load_seednodes) assert firstula in any_other_ursula.known_nodes firstula_as_learned = any_other_ursula.known_nodes[firstula.checksum_address] From ad05f4a09afe620de5cb4cbb8f64d7b9f463cfa9 Mon Sep 17 00:00:00 2001 From: jMyles Date: Wed, 22 Jul 2020 16:18:29 -0700 Subject: [PATCH 154/167] This makes more sense, because you might block the main thread in a test, and then the Ursula can't process the request. --- tests/integration/characters/test_bob_handles_frags.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/characters/test_bob_handles_frags.py b/tests/integration/characters/test_bob_handles_frags.py index 8da206cff..70a1a7b58 100644 --- a/tests/integration/characters/test_bob_handles_frags.py +++ b/tests/integration/characters/test_bob_handles_frags.py @@ -119,7 +119,8 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f bob.start_learning_loop() # ...and block until the unknown_nodes have all been found. - bob.block_until_specific_nodes_are_known(unknown_nodes) + 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(treasure_map) From 513dbdfe396368e4c97de62692c9e9230c2f6c6d Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 24 Jul 2020 15:56:43 -0700 Subject: [PATCH 155/167] This might happen to a stranger who isn't a node and doesn't have a rest_url. --- nucypher/characters/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 69e21c70b..570a1febc 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -465,9 +465,8 @@ class Character(Learner): f"Expected to connect to {stranger}, got {node_on_the_other_end} instead.") else: raise InvalidSignature("Signature for message isn't valid: {}".format(signature_to_use)) - except (TypeError, AttributeError): - raise self.known_node_class.InvalidNode( - f"Unable to verify message from strange node at {stranger.rest_url()}") + except (TypeError, AttributeError) as e: + raise InvalidSignature(f"Unable to verify message from stranger: {stranger}") else: raise InvalidSignature("No signature provided -- signature presumed invalid.") From ff044297b5b5f9e98180c9d9a17d9c6c0602d46b Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 24 Jul 2020 16:00:19 -0700 Subject: [PATCH 156/167] make_federated_ursulas gets called from a few places of varying depths in the stack - accounting for that. --- tests/unit/test_character_sign_and_verify.py | 2 -- tests/utils/middleware.py | 5 +++-- tests/utils/ursula.py | 13 ++++++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_character_sign_and_verify.py b/tests/unit/test_character_sign_and_verify.py index b9a072005..7bf35e5b7 100644 --- a/tests/unit/test_character_sign_and_verify.py +++ b/tests/unit/test_character_sign_and_verify.py @@ -27,8 +27,6 @@ from nucypher.crypto.powers import (CryptoPower, NoSigningPower, SigningPower) """ Chapter 1: SIGNING """ - - def test_actor_without_signing_power_cannot_sign(): """ We can create a Character with no real CryptoPower to speak of. diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 0a0d34ac0..2f8c6d1a2 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -52,10 +52,11 @@ class _TestMiddlewareClient(NucypherMiddlewareClient): return mock_client def _get_ursula_by_port(self, port): + mkuc = MOCK_KNOWN_URSULAS_CACHE try: - return MOCK_KNOWN_URSULAS_CACHE[port] + return mkuc[port] except KeyError: - raise RuntimeError( + raise RuntimeError( "Can't find an Ursula with port {} - did you spin up the right test ursulas?".format(port)) def parse_node_or_host_and_port(self, node=None, host=None, port=None): diff --git a/tests/utils/ursula.py b/tests/utils/ursula.py index 52ceb516a..8f23035dc 100644 --- a/tests/utils/ursula.py +++ b/tests/utils/ursula.py @@ -67,13 +67,24 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration, federated_ursulas = set() frames = inspect.stack(3) + # This gets called from various places. + for frame in range(1, 10): + try: + test_name = frames[frame].frame.f_locals['request'].module + break + except KeyError: + continue for port in range(starting_port, starting_port+quantity): ursula = ursula_config.produce(rest_port=port + 100, db_filepath=MOCK_URSULA_DB_FILEPATH, **ursula_overrides) - ursula._FOR_TEST = frames[1].frame.f_locals['request'].module + try: + ursula._FOR_TEST = test_name + except UnboundLocalError: + raise RuntimeError("Unable to find a test name to assign to this ursula in the first 10 frames.") + federated_ursulas.add(ursula) # Store this Ursula in our global testing cache. From 7bea1de3cf61986f38b934ef24d98d3167ce6571 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 24 Jul 2020 16:00:35 -0700 Subject: [PATCH 157/167] Tests were hanging. --- tests/unit/test_character_sign_and_verify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_character_sign_and_verify.py b/tests/unit/test_character_sign_and_verify.py index 7bf35e5b7..2cc15e495 100644 --- a/tests/unit/test_character_sign_and_verify.py +++ b/tests/unit/test_character_sign_and_verify.py @@ -106,6 +106,7 @@ def test_anybody_can_verify(): cleartext = somebody.verify_from(hearsay_alice, message, signature, decrypt=False) assert cleartext is constants.NO_DECRYPTION_PERFORMED + alice.disenchant() """ From d18b2e0834d937fa78659d112cb649c261489bb2 Mon Sep 17 00:00:00 2001 From: jMyles Date: Fri, 24 Jul 2020 19:29:38 -0700 Subject: [PATCH 158/167] Big fixup for the remaining errors blocking integration.learning. --- nucypher/policy/policies.py | 7 ++- .../learning/test_discovery_phases.py | 62 +++++++++++-------- tests/mock/performance_mocks.py | 2 +- tests/utils/ursula.py | 9 ++- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index a20b6c724..fd4ba0772 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -295,20 +295,21 @@ class Policy(ABC): # TODO: Optionally, block. This is increasingly important. raise RuntimeError("Alice hasn't learned of any nodes. Thus, she can't push the TreasureMap.") - responses = list() self.log.debug(f"Pushing {self.treasure_map} to all known nodes from {self.alice}") treasure_map_id = self.treasure_map.public_id() self.alice.block_until_number_of_known_nodes_is(8, timeout=2, learn_on_this_thread=True) + + publication_deferreds = list() for node in self.bob.matching_nodes_among(self.alice.known_nodes): - responses.append(deferToThreadPool(reactor, self.alice.publication_threadpool, + publication_deferreds.append(deferToThreadPool(reactor, self.alice.publication_threadpool, network_middleware.put_treasure_map_on_node, node=node, map_id=treasure_map_id, map_payload=bytes(self.treasure_map) )) - self.publishing_mutex = PolicyPayloadMutex(responses, percent_to_complete_before_release=10) + self.publishing_mutex = PolicyPayloadMutex(publication_deferreds, percent_to_complete_before_release=10) # return self.publishing_mutex # I dunno.. return this? Why not just use the composed version? def credential(self, with_treasure_map=True): diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 75e6b4303..8c912ee01 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -21,7 +21,8 @@ from unittest.mock import patch import maya import pytest import pytest_twisted -from twisted.internet.threads import deferToThread +from twisted.internet import defer, reactor +from twisted.internet.threads import deferToThread, blockingCallFromThread from nucypher.characters.lawful import Ursula from tests.utils.middleware import SluggishLargeFleetMiddleware @@ -127,15 +128,13 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, highperf_mocked_bob): """ Large-scale map placement with a middleware that simulates network latency. + + In three parts. """ # The nodes who match the map distribution criteria. - nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(highperf_mocked_alice.known_nodes) - + nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(fleet_of_highperf_mocked_ursulas) preparation_started = datetime.now() - for node in nodes_we_expect_to_have_the_map: - node.mature() - maturation_time = datetime.now() - preparation_started # # # Loop through and instantiate actual rest apps so as not to pollute the time measurement (doesn't happen in real world). for node in nodes_we_expect_to_have_the_map: @@ -148,12 +147,16 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, started = datetime.now() with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation: + + # PART I: The function returns sychronously and quickly. + + defer.setDebugging(False) # Debugging messes up the timing here; comment this line out if you actually need it. # returns instantly. policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) nodes_that_have_the_map_when_we_return = [] - for ursula in fleet_of_highperf_mocked_ursulas: + for ursula in nodes_we_expect_to_have_the_map: if policy.treasure_map in list(ursula.treasure_maps.values()): nodes_that_have_the_map_when_we_return.append(ursula) @@ -162,26 +165,36 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, assert len( nodes_that_have_the_map_when_we_return) <= 5 # Maybe a couple finished already, especially if this is a lightning fast computer. But more than five is weird. + defer.setDebugging(True) + + # PART II: We block for a little while to ensure that the distribution is going well. + # Wait until about ten percent of the distribution has occurred. # We do it in a deferred here in the test because it will block the entire process, but in the real-world, we can do this on the granting thread. - yield deferToThread(policy.publishing_mutex.block_for_a_little_while) - initial_blocking_duration = datetime.now() - started - # Here we'll just count the nodes that have the map. In the real world, we can do a sanity check - # to make sure things haven't gone sideways. + def count_recipients_after_block(): + policy.publishing_mutex.block_for_a_little_while() + little_while_ended_at = datetime.now() - nodes_that_have_the_map_when_we_unblock = [] + # Here we'll just count the nodes that have the map. In the real world, we can do a sanity check + # to make sure things haven't gone sideways. - for ursula in fleet_of_highperf_mocked_ursulas: - if policy.treasure_map in list(ursula.treasure_maps.values()): - nodes_that_have_the_map_when_we_unblock.append(ursula) + nodes_that_have_the_map_when_we_unblock = sum(policy.treasure_map in list(u.treasure_maps.values()) for u in nodes_we_expect_to_have_the_map) - approximate_number_of_nodes_we_expect_to_have_the_map_already = len(nodes_we_expect_to_have_the_map) / 5 - assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx( - approximate_number_of_nodes_we_expect_to_have_the_map_already, .5) + return nodes_that_have_the_map_when_we_unblock, little_while_ended_at - # The rest of the distributions is continuing in the background. + d = deferToThread(count_recipients_after_block) + yield d + nodes_that_have_the_map_when_we_unblock, little_while_ended_at = d.result + # The number of nodes having the map is at least the minimum to have unblocked. + assert nodes_that_have_the_map_when_we_unblock >= policy.publishing_mutex._block_until_this_many_are_complete + + # The number of nodes having the map is approximately the number you'd expect from full utilization of Alice's publication threadpool. + assert nodes_that_have_the_map_when_we_unblock == pytest.approx(highperf_mocked_alice.publication_threadpool.max, .1) + + # PART III: Having made proper assertions about the publication call and the first block, we allow the rest to + # happen in the background and then ensure that each phase was timely. successful_responses = [] def find_successful_responses(map_publication_responses): @@ -197,12 +210,11 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, # We have the same number of successful responses as nodes we expected to have the map. assert len(successful_responses) == len(nodes_we_expect_to_have_the_map) + # TODO: Assert that no nodes outside those expected received the map. + + partial_blocking_duration = little_while_ended_at - started # Before Treasure Island (1741), this process took about 3 minutes. + assert partial_blocking_duration.total_seconds() < 3 + assert complete_distribution_time.total_seconds() < 10 # On CI, we expect these times to be even less. (Around 1 and 3.5 seconds, respectively) # But with debuggers and other processes running on laptops, we give a little leeway. - assert initial_blocking_duration.total_seconds() < 3 - assert complete_distribution_time.total_seconds() < 10 - - # Takes about .8 on jMyles' laptop; still the bulk of init time. - # Assuming this is 10 percent of the fleet, that's another 8 second savings for first policy enactment. - assert maturation_time.total_seconds() < 2 diff --git a/tests/mock/performance_mocks.py b/tests/mock/performance_mocks.py index dd6610e40..2ae4f0167 100644 --- a/tests/mock/performance_mocks.py +++ b/tests/mock/performance_mocks.py @@ -29,7 +29,7 @@ mock_cert_storage = patch("nucypher.config.storages.ForgetfulNodeStorage.store_n mock_message_verification = patch('nucypher.characters.lawful.Alice.verify_from', new=lambda *args, **kwargs: None) -def fake_keep_learning(learner, *args, **kwargs): +def fake_keep_learning(selfish, learner=None, *args, **kwargs): return None diff --git a/tests/utils/ursula.py b/tests/utils/ursula.py index 8f23035dc..dd23bfa54 100644 --- a/tests/utils/ursula.py +++ b/tests/utils/ursula.py @@ -73,7 +73,14 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration, test_name = frames[frame].frame.f_locals['request'].module break except KeyError: - continue + try: + if frames[frame].function.startswith("test"): + test_name = frames[frame].function + break + else: + continue + except AttributeError: + continue for port in range(starting_port, starting_port+quantity): From a3baea5600806b77f1d5fa7b7f86ef270c285fc8 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 17:49:13 -0700 Subject: [PATCH 159/167] Cleaning logged message for #724 / #2156. --- nucypher/characters/control/emitters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nucypher/characters/control/emitters.py b/nucypher/characters/control/emitters.py index 4be6d700e..1c792768f 100644 --- a/nucypher/characters/control/emitters.py +++ b/nucypher/characters/control/emitters.py @@ -248,7 +248,9 @@ class WebEmitter: message = f"{drone_character} [{str(response_code)} - {error_message}] | ERROR: {str(e)}" logger = getattr(drone_character.log, log_level) - logger(message) + # See #724 / 2156 + message_cleaned_for_logger = message.replace("{", "<^<").replace("}", ">^>") + logger(message_cleaned_for_logger) if drone_character.crash_on_error: raise e return drone_character.sink(str(e), status=response_code) From 7d7608086a09d4a58c97a7ab4c805b4b50db904a Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 17:54:34 -0700 Subject: [PATCH 160/167] Slow, debugger-friendly version of the instantiation of our discovery looping call. --- nucypher/network/nodes.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 08c2c0dbe..c4f4487d3 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -238,7 +238,17 @@ class Learner: self.teacher_nodes = deque() self._current_teacher_node = None # type: Teacher - self._learning_task = task.LoopingCall(self.keep_learning_about_nodes, learner=self) + self._learning_task = task.LoopingCall(self.keep_learning_about_nodes) + + # Some debugging shit. + # Very slow, but provides useful info when trying to track down a stray Character. + # Seems mostly useful for Bob or federated Ursulas, but perhaps useful for other Characters as well. + # import inspect + # frames = inspect.stack(3) + # self._learning_task = task.LoopingCall(self.keep_learning_about_nodes, learner=self, frames=frames) + # self._init_frames = frames + ####### + self._learning_round = 0 # type: int self._rounds_without_new_nodes = 0 # type: int self._seed_nodes = seed_nodes or [] @@ -496,12 +506,13 @@ class Learner: self.log.info("Learning loop wasn't started; forcing start now.") self._learning_task.start(self._SHORT_LEARNING_DELAY, now=True) - def keep_learning_about_nodes(self, learner=None): + def keep_learning_about_nodes(self, learner=None, frames=None): """ Continually learn about new nodes. learner is for debugging and logging only. """ + self.log.info("WAAAAAAMP") import datetime From fbc4d58c320f617dfba8bebb6a0651f1015d9f9b Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 17:57:55 -0700 Subject: [PATCH 161/167] Temporary tweaks and out-comments to get cancellation working on a (logically) sub-optimal way. --- nucypher/network/nodes.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index c4f4487d3..b34f8d687 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -130,7 +130,7 @@ class DiscoveryCanceller: if self.stop_now: assert False self.stop_now = True - learning_deferred.callback(RELAX) + # learning_deferred.callback(RELAX) class Learner: @@ -436,13 +436,14 @@ class Learner: if self._learning_task.running: self._learning_task.stop() - if self._learning_deferred: - self._learning_deferred.cancel() - self._learning_deferred = RELAX - elif self._learning_deferred is RELAX: + if self._learning_deferred is RELAX: assert False - # self.learning_deferred.cancel() + if self._learning_deferred is not None: + # self._learning_deferred.cancel() # TODO: The problem here is that this might already be called. + self._discovery_canceller(self._learning_deferred) + + # self.learning_deferred.cancel() # TODO: The problem here is that there's no way to get a canceller into the LoopingCall. def handle_learning_errors(self, failure, *args, **kwargs): _exception = failure.value @@ -525,7 +526,7 @@ class Learner: # TODO: Allow the user to set eagerness? 1712 # TODO: Also, if we do allow eager, don't even defer; block right here. - self._learning_deferred = Deferred(canceller=self._discovery_canceller) + self._learning_deferred = Deferred(canceller=self._discovery_canceller) # TODO: No longer relevant. def _discover_or_abort(_first_result): print(f"========={self} learning at {datetime.datetime.now()}") @@ -721,6 +722,8 @@ class Learner: Sends a request to node_url to find out about known nodes. TODO: Does this (and related methods) belong on FleetSensor for portability? + + TODO: A lot of other code can be simplified if this is converted to async def. That's a project, though. """ remembered = [] @@ -756,6 +759,11 @@ class Learner: nodes_i_need=self._node_ids_to_learn_about_immediately, announce_nodes=announce_nodes, fleet_checksum=self.known_nodes.checksum) + except RuntimeError as e: + if canceller and canceller.stop_now: + # Race condition that seems limited to tests. + # TODO: Sort this out. + return RELAX except NodeSeemsToBeDown as e: unresponsive_nodes.add(current_teacher) self.log.info("Bad Response from teacher: {}:{}.".format(current_teacher, e)) From 50ddae77a17e8fdb6daa0d44fa30b2db3c64f4db Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 17:58:39 -0700 Subject: [PATCH 162/167] Lots of Character stoppages; these continually running loops went unnoticed before. --- .../cli/test_ursula_local_keystore_cli_functionality.py | 1 + tests/integration/config/test_character_configuration.py | 7 +++++++ tests/integration/config/test_configuration_persistence.py | 2 ++ tests/integration/config/test_keyring_integration.py | 3 ++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py b/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py index e35c945e9..83f907e12 100644 --- a/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py +++ b/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py @@ -108,3 +108,4 @@ def test_ursula_init_with_local_keystore_signer(click_runner, # Show that we can produce the exact same signer as pre-config... assert pre_config_signer.path == ursula.signer.path + ursula.stop() diff --git a/tests/integration/config/test_character_configuration.py b/tests/integration/config/test_character_configuration.py index 5baf0576e..8e377fcc2 100644 --- a/tests/integration/config/test_character_configuration.py +++ b/tests/integration/config/test_character_configuration.py @@ -92,6 +92,10 @@ def test_federated_development_character_configurations(character, configuration assert another_character not in _characters _characters.append(another_character) + if character is Alice: + for alice in _characters: + alice.disenchant() + @pytest.mark.parametrize('configuration_class', all_configurations) def test_default_character_configuration_preservation(configuration_class, testerchain, test_registry_source_manager): @@ -173,6 +177,9 @@ def test_ursula_development_configuration(federated_only=True): assert ursula not in ursulas ursulas.append(ursula) + for ursula in ursulas: + ursula.stop() + @pytest.mark.skip("See #2016") def test_destroy_configuration(config, diff --git a/tests/integration/config/test_configuration_persistence.py b/tests/integration/config/test_configuration_persistence.py index c567d838a..566b35305 100644 --- a/tests/integration/config/test_configuration_persistence.py +++ b/tests/integration/config/test_configuration_persistence.py @@ -75,6 +75,7 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir): assert policy_pubkey == bob_policy.public_key # ... and Alice and her configuration disappear. + alice.disenchant() del alice del alice_config @@ -117,3 +118,4 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir): # Both policies must share the same public key (i.e., the policy public key) assert policy_pubkey == roberto_policy.public_key + new_alice.disenchant() diff --git a/tests/integration/config/test_keyring_integration.py b/tests/integration/config/test_keyring_integration.py index ee127c413..a31794c15 100644 --- a/tests/integration/config/test_keyring_integration.py +++ b/tests/integration/config/test_keyring_integration.py @@ -74,7 +74,8 @@ def test_characters_use_keyring(tmpdir): rest=False, keyring_root=tmpdir) keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) - Alice(federated_only=True, start_learning_now=False, keyring=keyring) + a = Alice(federated_only=True, start_learning_now=False, keyring=keyring) Bob(federated_only=True, start_learning_now=False, keyring=keyring) Ursula(federated_only=True, start_learning_now=False, keyring=keyring, rest_host='127.0.0.1', rest_port=12345) + a.disenchant() # To stop Alice's publication threadpool. TODO: Maybe only start it at first enactment? From 029da78b0404c60cc56e18eb9aef154a432f464b Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 17:59:16 -0700 Subject: [PATCH 163/167] Much more leeway in the number of nodes that have the map when we unblock (as this varies widely just on my laptop, depending on what else I'm running in the background. --- tests/integration/learning/test_discovery_phases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 8c912ee01..ac2545c88 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -191,7 +191,7 @@ def test_mass_treasure_map_placement(fleet_of_highperf_mocked_ursulas, assert nodes_that_have_the_map_when_we_unblock >= policy.publishing_mutex._block_until_this_many_are_complete # The number of nodes having the map is approximately the number you'd expect from full utilization of Alice's publication threadpool. - assert nodes_that_have_the_map_when_we_unblock == pytest.approx(highperf_mocked_alice.publication_threadpool.max, .1) + assert nodes_that_have_the_map_when_we_unblock == pytest.approx(highperf_mocked_alice.publication_threadpool.max, .6) # PART III: Having made proper assertions about the publication call and the first block, we allow the rest to # happen in the background and then ensure that each phase was timely. From be71b934d9b69c9151cf78e39a5d522279195a07 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Mon, 20 Jul 2020 19:08:11 -0700 Subject: [PATCH 164/167] master -> main --- .circleci/config.yml | 8 ++++---- Makefile | 2 +- README.md | 4 ++-- docs/source/conf.py | 10 +++++----- docs/source/guides/contribution_guide.rst | 8 ++++---- docs/source/guides/federated_testnet_guide.rst | 2 +- docs/source/index.rst | 4 ++-- nucypher/blockchain/eth/registry.py | 2 +- tests/contracts/test_contracts_upgradeability.py | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a2a096df2..8c0132a86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -126,7 +126,7 @@ workflows: tags: only: /v[0-9]+.*/ branches: - only: master + only: main requires: - test_build - publish_docker_experimental: @@ -137,7 +137,7 @@ workflows: tags: only: /.*/ branches: - only: master + only: main - request_publication_approval: type: approval requires: @@ -172,7 +172,7 @@ workflows: filters: branches: only: - - master + - main jobs: - pipenv_install_36: filters: @@ -311,7 +311,7 @@ workflows: tags: only: /v[0-9]+.*/ branches: - only: master + only: main requires: - test_build diff --git a/Makefile b/Makefile index 55ef47e9b..3e106ab80 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ release: clean # previous dry-run runs *without* --allow-dirty which ensures it's really just the release notes # file that we are allowing to sit here dirty, waiting to get included in the release commit. bumpversion --allow-dirty $(bump) - git push upstream master && git push upstream v$(UPCOMING_VERSION) + git push upstream main && git push upstream v$(UPCOMING_VERSION) # Restore the original system setting for commit signing git config commit.gpgSign "$(CURRENT_SIGN_SETTING)" diff --git a/README.md b/README.md index e925920fb..062f613a2 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ [![pypi](https://img.shields.io/pypi/v/nucypher.svg?style=flat)](https://pypi.org/project/nucypher/) [![pyversions](https://img.shields.io/pypi/pyversions/nucypher.svg)](https://pypi.org/project/nucypher/) -[![codecov](https://codecov.io/gh/nucypher/nucypher/branch/master/graph/badge.svg)](https://codecov.io/gh/nucypher/nucypher) -[![circleci](https://img.shields.io/circleci/project/github/nucypher/nucypher.svg?logo=circleci)](https://circleci.com/gh/nucypher/nucypher/tree/master) +[![codecov](https://codecov.io/gh/nucypher/nucypher/branch/main/graph/badge.svg)](https://codecov.io/gh/nucypher/nucypher) +[![circleci](https://img.shields.io/circleci/project/github/nucypher/nucypher.svg?logo=circleci)](https://circleci.com/gh/nucypher/nucypher/tree/main) [![discord](https://img.shields.io/discord/411401661714792449.svg?logo=discord)](https://discord.gg/7rmXa3S) [![Documentation Status](https://readthedocs.org/projects/nucypher/badge/?version=latest)](https://nucypher.readthedocs.io/en/latest/) [![license](https://img.shields.io/pypi/l/nucypher.svg)](https://www.gnu.org/licenses/gpl-3.0.html) diff --git a/docs/source/conf.py b/docs/source/conf.py index b4ea0f93f..1a8cd36c6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -75,8 +75,8 @@ templates_path = ['.templates'] source_suffix = '.rst' -# The master toctree document. -master_doc = 'index' +# The main toctree document. +main_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -161,7 +161,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'NuCypher.tex', 'NuCypher Documentation', + (main_doc, 'NuCypher.tex', 'NuCypher Documentation', 'NuCypher', 'manual'), ] @@ -171,7 +171,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'nucypher', 'NuCypher Documentation', + (main_doc, 'nucypher', 'NuCypher Documentation', [author], 1) ] @@ -182,7 +182,7 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'NuCypher', 'NuCypher Documentation', + (main_doc, 'NuCypher', 'NuCypher Documentation', author, 'NuCypher', 'A proxy re-encryption network to empower privacy in decentralized systems.', 'Miscellaneous'), ] diff --git a/docs/source/guides/contribution_guide.rst b/docs/source/guides/contribution_guide.rst index a2705cad9..9721604a2 100644 --- a/docs/source/guides/contribution_guide.rst +++ b/docs/source/guides/contribution_guide.rst @@ -113,7 +113,7 @@ Pull Request Conflicts ---------------------- As an effort to preserve authorship and a cohesive commit history, we prefer if proposed contributions -are rebased over master (or appropriate branch) when a merge conflict arises, +are rebased over `main` (or appropriate branch) when a merge conflict arises, instead of making a merge commit back into the contributors fork. Generally speaking the preferred process of doing so is with an `interactive rebase`: @@ -129,7 +129,7 @@ Generally speaking the preferred process of doing so is with an `interactive reb $ git remote update ... (some upstream changes are reported) -2. Initiate an interactive rebase over ``nucypher/nucypher@master`` +2. Initiate an interactive rebase over ``nucypher/nucypher@main`` .. note:: @@ -138,7 +138,7 @@ Generally speaking the preferred process of doing so is with an `interactive reb .. code-block:: bash - $ git rebase -i upstream/master + $ git rebase -i upstream/main ... (edit & save rebase TODO list) 3. Resolve Conflicts @@ -216,7 +216,7 @@ Issuing a New Release .. important:: - Ensure your local tree is based on ``master`` and has no uncommitted changes. + Ensure your local tree is based on ``main`` and has no uncommitted changes. 1. Decide what part of the version to bump. The version string follows the format ``{major}.{minor}.{patch}-{stage}.{devnum}``, diff --git a/docs/source/guides/federated_testnet_guide.rst b/docs/source/guides/federated_testnet_guide.rst index b52c22761..95639e242 100644 --- a/docs/source/guides/federated_testnet_guide.rst +++ b/docs/source/guides/federated_testnet_guide.rst @@ -86,7 +86,7 @@ Install ``nucypher`` with ``git`` and ``pip3`` into your virtual environment. .. note:: - We recommend NuFT nodes install directly from master to help ensure your node is using pre-released features and hotfixes + We recommend NuFT nodes install directly from main to help ensure your node is using pre-released features and hotfixes Re-activate your environment after installing diff --git a/docs/source/index.rst b/docs/source/index.rst index 232d4f057..45c585647 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,10 +12,10 @@ NuCypher :target: https://pypi.org/project/nucypher/ .. image:: https://img.shields.io/circleci/project/github/nucypher/nucypher.svg?logo=circleci - :target: https://circleci.com/gh/nucypher/nucypher/tree/master + :target: https://circleci.com/gh/nucypher/nucypher/tree/main :alt: CircleCI build status -.. image:: https://codecov.io/gh/nucypher/nucypher/branch/master/graph/badge.svg +.. image:: https://codecov.io/gh/nucypher/nucypher/branch/main/graph/badge.svg :target: https://codecov.io/gh/nucypher/nucypher .. image:: https://img.shields.io/discord/411401661714792449.svg?logo=discord diff --git a/nucypher/blockchain/eth/registry.py b/nucypher/blockchain/eth/registry.py index dd5f4b036..c6a2c8207 100644 --- a/nucypher/blockchain/eth/registry.py +++ b/nucypher/blockchain/eth/registry.py @@ -74,7 +74,7 @@ class GithubRegistrySource(CanonicalRegistrySource): is_primary = True def get_publication_endpoint(self) -> str: - url = f'{self._BASE_URL}/master/nucypher/blockchain/eth/contract_registry/{self.network}/{self.registry_name}' + url = f'{self._BASE_URL}/main/nucypher/blockchain/eth/contract_registry/{self.network}/{self.registry_name}' return url def fetch_latest_publication(self) -> Union[str, bytes]: diff --git a/tests/contracts/test_contracts_upgradeability.py b/tests/contracts/test_contracts_upgradeability.py index 963584ad3..cb73d39e3 100644 --- a/tests/contracts/test_contracts_upgradeability.py +++ b/tests/contracts/test_contracts_upgradeability.py @@ -31,7 +31,7 @@ from tests.constants import INSECURE_DEVELOPMENT_PASSWORD USER = "nucypher" REPO = "nucypher" -BRANCH = "master" +BRANCH = "main" GITHUB_SOURCE_LINK = f"https://api.github.com/repos/{USER}/{REPO}/contents/nucypher/blockchain/eth/sol/source?ref={BRANCH}" From 39f0dc4d6b7fdd321e9dca1110aa3d5cfd5c1ebe Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 18:55:11 -0700 Subject: [PATCH 165/167] Some more debug-level logs instead of prints. --- nucypher/characters/base.py | 1 + nucypher/characters/lawful.py | 3 ++- nucypher/network/nodes.py | 6 +++--- nucypher/policy/policies.py | 5 ++++- tests/fixtures.py | 3 ++- tests/integration/learning/test_firstula_circumstances.py | 1 + 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/nucypher/characters/base.py b/nucypher/characters/base.py index 570a1febc..f123db90f 100644 --- a/nucypher/characters/base.py +++ b/nucypher/characters/base.py @@ -559,4 +559,5 @@ class Character(Learner): return controller def disenchant(self): + self.log.debug(f"Disenchanting {self}") Learner.stop_learning_loop(self) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 0e2b5584a..cb0873a59 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -445,7 +445,7 @@ class Alice(Character, BlockchainPolicyAuthor): return controller def disenchant(self): - print(f"disenchanting {self}") + self.log.debug(f"disenchanting {self}") super().disenchant() self.publication_threadpool.stop() @@ -1245,6 +1245,7 @@ class Ursula(Teacher, Character, Worker): def stop(self, halt_reactor: bool = False) -> None: """Stop services""" + self.log.debug(f"---------Stopping {self}") self._availability_tracker.stop() self.stop_learning_loop() if not self.federated_only: diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index b34f8d687..dda4f3dd2 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -517,7 +517,7 @@ class Learner: import datetime - print(f"+++++++++++{self} deferring learning cycle at {datetime.datetime.now()}") + self.log.debug(f"+++++++++++{self} deferring learning cycle at {datetime.datetime.now()}") # while self._learning_deferred is not None: # print(f"^^^^^^^^^{self} waiting at {datetime.datetime.now()}") @@ -529,9 +529,9 @@ class Learner: self._learning_deferred = Deferred(canceller=self._discovery_canceller) # TODO: No longer relevant. def _discover_or_abort(_first_result): - print(f"========={self} learning at {datetime.datetime.now()}") + self.log.debug(f"========={self} learning at {datetime.datetime.now()}") result = self.learn_from_teacher_node(eager=False, canceller=self._discovery_canceller) - print(f"///////////{self} finished learning at {datetime.datetime.now()}") + self.log.debug(f"///////////{self} finished learning at {datetime.datetime.now()}") return result self._learning_deferred.addCallback(_discover_or_abort) diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index fd4ba0772..ee8ee81e6 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -166,6 +166,7 @@ class BlockchainArrangement(Arrangement): class PolicyPayloadMutex(DeferredList): + log = Logger("Policy") def __init__(self, deferredList, percent_to_complete_before_release=5, *args, **kwargs): self.percent_to_complete_before_release = percent_to_complete_before_release @@ -185,7 +186,9 @@ class PolicyPayloadMutex(DeferredList): """ https://www.youtube.com/watch?v=OkSLswPSq2o """ - return self._policy_locking_queue.get() + _ = self._policy_locking_queue.get() # Interesting opportuntiy to pass some data, like the list of contacted nodes above. + self.log.debug(f"{self.finishedCount} nodes were contacted while blocking for a little while.") + return class Policy(ABC): diff --git a/tests/fixtures.py b/tests/fixtures.py index 26b2d85f1..498552bcd 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -117,7 +117,7 @@ from umbral.signing import Signer test_logger = Logger("test-logger") -defer.setDebugging(True) +# defer.setDebugging(True) # # Temporary @@ -387,6 +387,7 @@ def federated_ursulas(ursula_federated_test_config): yield _ursulas for port in _ports_to_remove: + test_logger.debug(f"Removing {port} ({MOCK_KNOWN_URSULAS_CACHE[port]}).") del MOCK_KNOWN_URSULAS_CACHE[port] for u in _ursulas: diff --git a/tests/integration/learning/test_firstula_circumstances.py b/tests/integration/learning/test_firstula_circumstances.py index 52b76d729..241149129 100644 --- a/tests/integration/learning/test_firstula_circumstances.py +++ b/tests/integration/learning/test_firstula_circumstances.py @@ -30,6 +30,7 @@ def test_proper_seed_node_instantiation(lonely_ursula_maker): any_other_ursula = _lonely_ursula_maker(seed_nodes=[firstula_as_seed_node], domains=["useless domain"]).pop() assert not any_other_ursula.known_nodes + # print(f"**********************Starting {any_other_ursula} loop") any_other_ursula.start_learning_loop(now=True) assert firstula in any_other_ursula.known_nodes From bf4e8adb26ee4876ae6ebe138889e6845beaab31 Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 19:21:14 -0700 Subject: [PATCH 166/167] Making mature() a bit safer against reentrancy situations. Might still need a mutex. --- nucypher/network/nodes.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index dda4f3dd2..449825837 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -72,6 +72,7 @@ class NodeSprout(PartiallyKwargifiedBytes): self.timestamp = maya.MayaDT( self.timestamp) # Weird for this to be in init. maybe this belongs in the splitter also. self._repr = None + self._is_finishing = False def __hash__(self): if not self._hash: @@ -110,14 +111,20 @@ class NodeSprout(PartiallyKwargifiedBytes): return self._nickname def mature(self): + # TODO: Something is fishy here. Another race condition maybe. Sometimes raises AttributeError because Ursula doesn't have _finished_values. + # This might need a mutex. + if self._is_finishing: + return self + self._is_finishing = True # Prevent reentrance. + mature_node = self.finish() + self.__class__ = mature_node.__class__ + self.__dict__ = mature_node.__dict__ # As long as we're doing egregious workarounds, here's another one. # TODO: 1481 filepath = mature_node._cert_store_function(certificate=mature_node.certificate) mature_node.certificate_filepath = filepath - self.__class__ = mature_node.__class__ - self.__dict__ = mature_node.__dict__ return self # To reduce the awkwardity of renaming; this is always the weird part of polymorphism for me. From 424ed29abe907fc59e17a18385a2029aa2c4df5e Mon Sep 17 00:00:00 2001 From: jMyles Date: Sun, 26 Jul 2020 20:13:47 -0700 Subject: [PATCH 167/167] Stopping character after CLI action. --- nucypher/characters/control/controllers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nucypher/characters/control/controllers.py b/nucypher/characters/control/controllers.py index e9e40d190..6659b609c 100644 --- a/nucypher/characters/control/controllers.py +++ b/nucypher/characters/control/controllers.py @@ -147,6 +147,11 @@ class CLIController(CharacterControlServer): self.emitter.ipc(response=response, request_id=start.epoch, duration=maya.now() - start) return response + def _perform_action(self, *args, **kwargs) -> dict: + response_data = super()._perform_action(*args, **kwargs) + self.stop_character() + return response_data + class JSONRPCController(CharacterControlServer):