Merge pull request #1741 from nucypher/†reasure-ïsland-@ss-pi®ate-ß̆çh

Treasure Island...
pull/2233/head
K Prasch 2020-09-08 06:25:31 -07:00 committed by GitHub
commit 704a361fc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 2334 additions and 1547 deletions

View File

@ -1,6 +1,4 @@
version: 2.1
orbs:
codecov: codecov/codecov@1.0.5
workflows:
version: 2
test_build_deploy:
@ -434,13 +432,6 @@ commands:
name: "load docker"
command: docker load < ~/docker-dev/dev-docker-build.tar
codecov:
description: "Upload Coverage Report To codecov.io"
steps:
- codecov/upload:
flags: all
file: reports/coverage.xml
capture_test_results:
description: "Store and Upload test results; Follow-up step for tests"
steps:
@ -452,7 +443,6 @@ commands:
- store_artifacts:
path: test-names.txt
destination: tests
- codecov
build_and_save_test_docker:
description: "Build dev docker image for running tests against docker"

View File

@ -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

View File

@ -167,6 +167,7 @@ Whitepapers
api/nucypher.network
api/nucypher.datastore
api/nucypher.crypto
api/nucypher.acumen
.. toctree::
:maxdepth: 1

View File

@ -94,6 +94,7 @@ BOB = Bob(known_nodes=[ursula],
learn_on_same_thread=True)
ALICE.start_learning_loop(now=True)
ALICE.block_until_number_of_known_nodes_is(8, timeout=30, learn_on_this_thread=True) # In case the fleet isn't fully spun up yet, as sometimes happens on CI.
policy = ALICE.grant(BOB,
label,
@ -101,11 +102,13 @@ policy = ALICE.grant(BOB,
expiration=policy_end_datetime)
assert policy.public_key == policy_pubkey
policy.publishing_mutex.block_until_complete()
# Alice puts her public key somewhere for Bob to find later...
alices_pubkey_bytes_saved_for_posterity = bytes(ALICE.stamp)
# ...and then disappears from the internet.
ALICE.disenchant()
del ALICE
#####################
@ -167,3 +170,5 @@ for counter, plaintext in enumerate(finnegans_wake):
# We show that indeed this is the passage originally encrypted by Enrico.
assert plaintext == delivered_cleartexts[0]
print("Retrieved: {}".format(delivered_cleartexts[0]))
BOB.disenchant()

View File

@ -141,6 +141,7 @@ policy = alicia.grant(bob=doctor_strange,
m=m,
n=n,
expiration=policy_end_datetime)
policy.publishing_mutex.block_until_complete()
print("Done!")
# For the demo, we need a way to share with Bob some additional info

View File

@ -0,0 +1,68 @@
"""
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 <https://www.gnu.org/licenses/>.
"""
import os
from contextlib import suppress
from functools import partial
from pathlib import Path
from twisted.internet import reactor
from nucypher.characters.lawful import Ursula
from nucypher.config.constants import APP_DIR, TEMPORARY_DOMAIN
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=f"{Path(APP_DIR.user_cache_dir) / '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"{Path(APP_DIR.user_cache_dir) / 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()

View File

View File

@ -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 .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 = """
<div class="nucypher-nickname-icon" style="border-color:{color};">
<div class="small">{number_of_nodes} nodes</div>
<div class="symbols">
<span class="single-symbol" style="color: {color}">{symbol}&#xFE0E;</span>
</div>
<br/>
<span class="small-address">{fleet_state_checksum}</span>
</div>
""".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(),
}

View File

@ -15,26 +15,24 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
import traceback
import click
import csv
import maya
import json
import os
import sys
import time
from web3.types import TxReceipt
from constant_sorrow.constants import FULL, WORKER_NOT_RUNNING
from decimal import Decimal
from web3.types import TxReceipt
import traceback
import click
import maya
from eth_tester.exceptions import TransactionFailed as TestTransactionFailed
from eth_utils import to_canonical_address, to_checksum_address
from typing import Dict, Iterable, List, Optional, Tuple
from web3 import Web3
from web3.exceptions import ValidationError
from constant_sorrow.constants import FULL, 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,
@ -82,7 +80,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
from nucypher.types import NuNits, Period
from nucypher.utilities.logging import Logger
from typing import Callable
@ -527,7 +524,6 @@ class ContractAdministrator(NucypherTokenActor):
class Allocator:
class AllocationInputError(Exception):
"""Raised when the allocation data file doesn't have the correct format"""
@ -638,7 +634,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). "
@ -693,7 +689,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)
@ -738,7 +735,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,
@ -1061,7 +1059,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) - stake.final_locked_period
additional_periods = datetime_to_period(datetime=expiration,
seconds_per_period=self.economics.seconds_per_period) - stake.final_locked_period
if additional_periods <= 0:
raise ValueError(f"New expiration {expiration} must be at least 1 period from the "
f"current stake's end period ({stake.final_locked_period}).")
@ -1436,6 +1435,7 @@ class Worker(NucypherTokenActor):
class UnbondedWorker(WorkerError):
"""Raised when the Worker is not bonded to a Staker in the StakingEscrow contract."""
crash_right_now = True
def __init__(self,
is_me: bool,
@ -1450,7 +1450,6 @@ class Worker(NucypherTokenActor):
self.is_me = is_me
self._checksum_address = None # Stake Address
self.__worker_address = worker_address
# Agency
@ -1469,10 +1468,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=start_working_now)
def block_until_ready(self, poll_rate: int = None, timeout: int = None):
"""
@ -1522,9 +1523,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)
@ -1681,6 +1684,7 @@ class Wallet:
"""
Account management abstraction on top of blockchain providers and external signers
"""
class UnknownAccount(KeyError):
pass
@ -1751,7 +1755,6 @@ class Wallet:
class StakeHolder(Staker):
banner = STAKEHOLDER_BANNER
#
@ -1973,7 +1976,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
@ -2020,7 +2024,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)

View File

@ -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 typing import Callable, Optional, Union
@ -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)

View File

@ -557,7 +557,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

View File

@ -16,31 +16,33 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
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 umbral.keys import UmbralPublicKey
from umbral.signing import Signature
from nucypher.acumen.nicknames import nickname_from_seed
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, 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.nicknames import nickname_from_seed
from nucypher.network.nodes import Learner
from umbral.keys import UmbralPublicKey
from umbral.signing import Signature
class Character(Learner):
@ -74,7 +76,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
@ -101,13 +107,9 @@ 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.
@ -147,12 +149,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}
#
# Self-Character
#
@ -175,7 +171,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)
@ -207,25 +204,13 @@ 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)
except NoTransactingPower:
self._checksum_address = checksum_address
else:
self._set_checksum_address(checksum_address)
#
# Nicknames
@ -237,7 +222,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
@ -288,6 +278,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
@ -296,7 +287,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
@ -343,6 +334,15 @@ 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
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)
def store_metadata(self, filepath: str) -> str:
"""
Save this node to the disk.
@ -455,7 +455,16 @@ 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) as e:
raise InvalidSignature(f"Unable to verify message from stranger: {stranger}")
else:
raise InvalidSignature("No signature provided -- signature presumed invalid.")
@ -485,7 +494,30 @@ 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:
# TODO: And why not return here then?
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 signing 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)
@ -495,12 +527,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
@ -516,8 +550,12 @@ 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
def disenchant(self):
self.log.debug(f"Disenchanting {self}")
Learner.stop_learning_loop(self)

View File

@ -148,6 +148,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

View File

@ -115,6 +115,9 @@ class CharacterControlServer(CharacterControllerBase):
if hasattr(method, '_schema')
}
def stop_character(self):
self.interface.character.disenchant()
@abstractmethod
def make_control_transport(self):
return NotImplemented
@ -144,6 +147,14 @@ 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:
try:
response_data = super()._perform_action(*args, **kwargs)
finally:
self.log.debug(f"Finished action '{kwargs['action']}', stopping {self.interface.character}")
self.stop_character()
return response_data
class JSONRPCController(CharacterControlServer):

View File

@ -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)

View File

@ -120,7 +120,10 @@ class AliceInterface(CharacterPublicInterface):
n=n,
value=value,
rate=rate,
expiration=expiration)
expiration=expiration,
discover_on_this_thread=True)
new_policy.publishing_mutex.block_until_success_is_reasonably_likely()
response_data = {'treasure_map': new_policy.treasure_map,
'policy_encrypting_key': new_policy.public_key,

View File

@ -14,37 +14,36 @@ 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 <https://www.gnu.org/licenses/>.
"""
import contextlib
import json
import random
import time
from base64 import b64decode, b64encode
from collections import OrderedDict
from datetime import datetime
from functools import partial
from json.decoder import JSONDecodeError
from queue import Queue
from random import shuffle
from typing import Dict, Iterable, List, Set, Tuple, Union
import maya
import time
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 cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate, NameOID, load_pem_x509_certificate
from datetime import datetime
from eth_utils import to_checksum_address
from flask import Response, request
from functools import partial
from json.decoder import JSONDecodeError
from twisted.internet import reactor, stdio, threads
from twisted.internet.task import LoopingCall
from typing import Dict, Iterable, List, Set, Tuple, Union
from umbral import pre
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature
import nucypher
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, UNKNOWN_VERSION, READY
from nucypher.acumen.nicknames import nickname_from_seed
from nucypher.acumen.perception import FleetSensor
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor, Worker
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
@ -70,12 +69,16 @@ from nucypher.datastore.keypairs import HostingKeypair
from nucypher.datastore.models import PolicyArrangement
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, Teacher
from nucypher.network.protocols import InterfaceInfo, parse_node_uri
from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app
from nucypher.network.trackers import AvailabilityTracker
from nucypher.utilities.logging import Logger
from umbral import pre
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature
class Alice(Character, BlockchainPolicyAuthor):
@ -88,7 +91,7 @@ class Alice(Character, BlockchainPolicyAuthor):
# Mode
is_me: bool = True,
federated_only: bool = False,
signer = None,
signer=None,
# Ownership
checksum_address: str = None,
@ -119,6 +122,9 @@ class Alice(Character, BlockchainPolicyAuthor):
if is_me:
self.m = m
self.n = n
self._policy_queue = Queue()
self._policy_queue.put(READY)
else:
self.m = STRANGER_ALICE
self.n = STRANGER_ALICE
@ -191,7 +197,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.
"""
@ -261,6 +267,7 @@ class Alice(Character, BlockchainPolicyAuthor):
discover_on_this_thread: bool = True,
timeout: int = None,
publish_treasure_map: bool = True,
block_until_success_is_reasonably_likely: bool = True,
**policy_params):
timeout = timeout or self.timeout
@ -297,11 +304,17 @@ class Alice(Character, BlockchainPolicyAuthor):
self.log.debug(f"Making arrangements for {policy} ... ")
policy.make_arrangements(network_middleware=self.network_middleware,
handpicked_ursulas=handpicked_ursulas)
handpicked_ursulas=handpicked_ursulas,
discover_on_this_thread=discover_on_this_thread)
# 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)
if publish_treasure_map and block_until_success_is_reasonably_likely:
policy.publishing_mutex.block_until_success_is_reasonably_likely()
return policy # Now with TreasureMap affixed!
def get_policy_encrypting_key_from_label(self, label: bytes) -> UmbralPublicKey:
@ -535,7 +548,12 @@ class Bob(Character):
if not self.known_nodes and not self._learning_task.running:
# Quick sanity check - if we don't know of *any* Ursulas, and we have no
# plans to learn about any more, than this function will surely fail.
raise self.NotEnoughTeachers
if not self.done_seeding:
self.learn_from_teacher_node()
# If we still don't know of any nodes, we gotta bail.
if not self.known_nodes:
raise self.NotEnoughTeachers("Can't retrieve without knowing about any nodes at all. Pass a teacher or seed node.")
treasure_map = self.get_treasure_map_from_known_ursulas(self.network_middleware,
map_id)
@ -563,36 +581,48 @@ 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.
"""
from nucypher.policy.collections import TreasureMap
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:
try:
treasure_map = TreasureMap.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
if self.federated_only:
from nucypher.policy.collections import TreasureMap as _MapClass
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} ")
from nucypher.policy.collections import SignedTreasureMap as _MapClass
return treasure_map
start = maya.now()
# Spend no more than half the timeout finding the nodes. 8 nodes is arbitrary. Come at me.
self.block_until_number_of_known_nodes_is(8, timeout=timeout/2, learn_on_this_thread=True)
while True:
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, self.NotEnoughNodes):
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:
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()
if (start - maya.now()).seconds > timeout:
raise _MapClass.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_id} ")
def work_orders_for_capsules(self,
*capsules,
@ -692,16 +722,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 SignedTreasureMap 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:
@ -746,7 +778,7 @@ class Bob(Character):
alice_verifying_key=alice_verifying_key,
*capsules_to_activate)
self.log.info(f"Found {len(complete_work_orders)} 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:
@ -781,6 +813,10 @@ class Bob(Character):
# None of the Capsules for this particular WorkOrder need to be activated. Move on to the next one.
continue
# OK, so we're going to need to do some network activity for this retrieval. Let's make sure we've seeded.
if not self.done_seeding:
self.learn_from_teacher_node()
# We don't have enough CFrags yet. Let's get another one from a WorkOrder.
try:
self.get_reencrypted_cfrags(work_order, retain_cfrags=retain_cfrags)
@ -835,6 +871,42 @@ class Bob(Character):
return cleartexts
def matching_nodes_among(self,
nodes: FleetSensor,
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.
# 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?
# 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]
while len(target_nodes) < no_less_than:
target_nodes = []
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.
# Not enough matching nodes. Fine, we'll just publish to the first few.
try:
# TODO: This is almost certainly happening in a test. If it does happen in production, it's a bit of a problem. Need to fix #2124 to mitigate.
target_nodes = list(nodes._nodes.values())[0:6]
return target_nodes
except IndexError:
raise self.NotEnoughNodes("There aren't enough nodes on the network to enact this policy. Unless this is day one of the network and nodes are still getting spun up, something is bonkers.")
# 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):
app_name = bytes(drone_bob.stamp).hex()[:6]
@ -879,7 +951,6 @@ class Bob(Character):
class Ursula(Teacher, Character, Worker):
banner = URSULA_BANNER
_alice_class = Alice
@ -917,7 +988,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,
@ -949,8 +1021,9 @@ 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
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
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,
abort_on_learning_error=abort_on_learning_error,
known_nodes=known_nodes,
@ -959,9 +1032,8 @@ class Ursula(Teacher, Character, Worker):
**character_kwargs)
if is_me:
# In-Memory TreasureMap tracking
self._stored_treasure_maps = dict()
self._stored_treasure_maps = dict() # TODO: Something more persistent (See PR #2132)
# Learner
self._start_learning_now = start_learning_now
@ -982,25 +1054,35 @@ 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,
password=client_password,
signer=self.signer,
cache=True)
self._crypto_power.consume_power_up(self.transacting_power)
_transacting_power = TransactingPower(account=worker_address,
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)
# 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,
is_me=is_me,
registry=self.registry,
checksum_address=checksum_address,
worker_address=worker_address,
work_tracker=work_tracker,
start_working_now=start_working_now,
block_until_ready=block_until_ready)
try:
Worker.__init__(self,
is_me=is_me,
registry=self.registry,
checksum_address=checksum_address,
worker_address=worker_address,
work_tracker=work_tracker,
start_working_now=start_working_now,
block_until_ready=block_until_ready)
except (Exception, self.WorkerError): # FIXME
# TODO: Do not announce self to "other nodes" until this init is finished.
# It's not possible to finish constructing this node.
self.stop(halt_reactor=False)
raise
if not crypto_power or (TLSHostingPower not in crypto_power):
@ -1123,10 +1205,11 @@ 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')
# TODO: block until specific nodes are known here?
# if learning: # TODO: Include learning startup here with the rest of the services?
# 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...
@ -1177,17 +1260,22 @@ 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"""
self._availability_tracker.stop()
if self._learning_task.running:
"""
Stop services for partially or fully initialized characters.
# CAUTION #
"""
self.log.debug(f"---------Stopping {self}")
# Handles the shutdown of a partially initialized character.
with contextlib.suppress(AttributeError): # TODO: Is this acceptable here, what are alternatives?
self._availability_tracker.stop()
self.stop_learning_loop()
if not self.federated_only:
self.work_tracker.stop()
if self._arrangement_pruning_task.running:
self._arrangement_pruning_task.stop()
if not self.federated_only:
self.work_tracker.stop()
if self._arrangement_pruning_task.running:
self._arrangement_pruning_task.stop()
if halt_reactor:
reactor.stop()
@ -1245,7 +1333,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,
@ -1289,7 +1376,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))
@ -1323,7 +1410,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
@ -1332,12 +1424,10 @@ class Ursula(Teacher, Character, Worker):
# Load the host as a potential seed node
potential_seed_node = cls.from_rest_url(
registry=registry,
host=real_host,
port=port,
network_middleware=network_middleware,
certificate_filepath=temp_certificate_filepath,
federated_only=federated_only,
*args,
**kwargs
)
@ -1376,7 +1466,7 @@ class Ursula(Teacher, Character, Worker):
def from_bytes(cls,
ursula_as_bytes: bytes,
version: int = INCLUDED_IN_BYTESTRING,
registry: BaseContractRegistry = None,
fail_fast=False,
) -> 'Ursula':
if version is INCLUDED_IN_BYTESTRING:
@ -1398,11 +1488,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):
@ -1432,7 +1532,6 @@ class Ursula(Teacher, Character, Worker):
@classmethod
def batch_from_bytes(cls,
ursulas_as_bytes: Iterable[bytes],
registry: BaseContractRegistry = None,
fail_fast: bool = False,
) -> List['Ursula']:
@ -1445,13 +1544,22 @@ class Ursula(Teacher, Character, Worker):
for version, node_bytes in versions_and_node_bytes:
try:
sprout = cls.from_bytes(node_bytes,
version=version,
registry=registry)
version=version)
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
@ -1534,9 +1642,9 @@ class Enrico(Character):
def __init__(self, policy_encrypting_key=None, controller: bool = True, *args, **kwargs):
self._policy_pubkey = policy_encrypting_key
# Encrico never uses the blockchain, hence federated_only)
# Enrico 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:
@ -1570,6 +1678,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]

View File

@ -14,8 +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 <https://www.gnu.org/licenses/>.
"""
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.exceptions import DevelopmentInstallationRequired
from copy import copy
@ -41,6 +40,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,
@ -61,15 +64,18 @@ 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.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_ready=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(),
@ -81,7 +87,6 @@ class Vladimir(Ursula):
interface_signature=target_ursula._interface_signature,
#########
)
return vladimir
@classmethod
@ -100,6 +105,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):
"""
@ -174,3 +195,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)

View File

@ -52,7 +52,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.processes import get_geth_provider_process
@ -89,7 +90,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 +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):
@ -135,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:
@ -151,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,
@ -172,6 +179,7 @@ group_config_options = group_options(
pay_with=option_pay_with,
registry_filepath=option_registry_filepath,
middleware=option_middleware,
lonely=option_lonely,
)
@ -285,8 +293,8 @@ class AliceCharacterOptions:
teacher_uri=self.teacher_uri,
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:

View File

@ -17,7 +17,7 @@
import base64
import click
import ipfshttpclient
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import BobInterface
@ -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:
@ -95,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(
@ -108,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)
@ -131,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:
@ -140,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}
@ -158,7 +168,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,
)
@ -310,6 +321,7 @@ def retrieve(general_config,
raise click.BadArgumentUsage(f'{required_fields} are required flags to retrieve')
if ipfs:
import ipfshttpclient
# TODO: #2108
emitter.message(f"Connecting to IPFS Gateway {ipfs}")
ipfs_client = ipfshttpclient.connect(ipfs)

View File

@ -17,7 +17,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import click
import ipfshttpclient
from umbral.keys import UmbralPublicKey
from nucypher.characters.control.interfaces import EnricoInterface
@ -78,6 +77,7 @@ def encrypt(general_config, policy_encrypting_key, message, file, ipfs):
# Handle Ciphertext
# TODO: This might be crossing the bridge of being application code
if ipfs:
import ipfshttpclient
emitter.message(f"Connecting to IPFS Gateway {ipfs}")
ipfs_client = ipfshttpclient.connect(ipfs)
cid = ipfs_client.add_str(response['message_kit'])

View File

@ -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:
@ -124,6 +127,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:
@ -245,7 +249,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,
)
@ -253,9 +258,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
@ -278,9 +282,8 @@ 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,
start_learning_now=load_seednodes)
return ursula_config, URSULA
@ -293,7 +296,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
)
@ -388,10 +390,15 @@ def run(general_config, character_options, config_file, interactive, dry_run, me
metrics_prefix=metrics_prefix,
listen_address=metrics_listen_address)
return URSULA.run(emitter=emitter,
start_reactor=not dry_run,
interactive=interactive,
prometheus_config=prometheus_config)
# TODO should we just not call run at all for "dry_run"
try:
URSULA.run(emitter=emitter,
start_reactor=not dry_run,
interactive=interactive,
prometheus_config=prometheus_config)
finally:
if dry_run:
URSULA.stop()
@ursula.command(name='save-metadata')

View File

@ -48,6 +48,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)

View File

@ -30,7 +30,7 @@ from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
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):

View File

@ -22,7 +22,6 @@ import click
import os
import shutil
from constant_sorrow.constants import NO_CONTROL_PROTOCOL
from nacl.exceptions import CryptoError
from nucypher.blockchain.eth.interfaces import (
BlockchainDeployerInterface,
@ -33,7 +32,6 @@ from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContr
from nucypher.characters.base import Character
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.actions.auth import get_nucypher_password, unlock_nucypher_keyring
from nucypher.utilities.seednodes import load_seednodes
from nucypher.cli.literature import (
CONNECTING_TO_BLOCKCHAIN,
ETHERSCAN_FLAG_DISABLED_WARNING,
@ -57,8 +55,7 @@ def make_cli_character(character_config,
emitter,
unlock_keyring: bool = True,
teacher_uri: str = None,
min_stake: int = 0,
load_preferred_teachers: bool = True,
min_stake: int = 0, # We not using this anymore? Where is it hooked up?
**config_args) -> Character:
#
@ -73,22 +70,23 @@ def make_cli_character(character_config,
password=get_nucypher_password(confirm=False))
# Handle Teachers
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)
# TODO: Is this still relevant? Is it better to DRY this up by doing it later?
sage_nodes = list()
#
# Character Init
#
# 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)

View File

@ -29,6 +29,8 @@ from constant_sorrow.constants import (
from eth_utils.address import is_checksum_address
from tempfile import TemporaryDirectory
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. #571
known_node_class = Ursula
# Gas
DEFAULT_GAS_STRATEGY = 'fast'
@ -93,6 +98,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 +158,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 +407,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)

View File

@ -29,6 +29,7 @@ from cryptography.x509 import Certificate, NameOID
from eth_utils import is_checksum_address
from typing import Any, Callable, Set, Tuple, Union
from nucypher.acumen.nicknames import nickname_from_seed
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
@ -135,7 +136,8 @@ class NodeStorage(ABC):
public_pem_bytes = certificate.public_bytes(self.TLS_CERTIFICATE_ENCODING)
certificate_file.write(public_pem_bytes)
self.log.debug(f"Saved TLS certificate for {checksum_address}: {certificate_filepath}")
nickname, pairs = nickname_from_seed(checksum_address)
self.log.debug(f"Saved TLS certificate for {nickname} {checksum_address}: {certificate_filepath}")
return certificate_filepath

View File

@ -36,6 +36,7 @@ class NucypherMiddlewareClient:
library = requests
timeout = 1.2
def __init__(self, registry=None, *args, **kwargs):
self.registry = registry
@ -144,6 +145,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)
@ -187,7 +192,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",

View File

@ -16,217 +16,49 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import contextlib
from collections import OrderedDict, defaultdict, deque, namedtuple
from contextlib import suppress
from typing import Set, Tuple, Union, Iterable
import binascii
import maya
import random
import requests
import datetime
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 collections import defaultdict, deque
from contextlib import suppress
from queue import Queue
from typing import Iterable
from typing import Set, Tuple, Union
import maya
import requests
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 umbral.signing import Signature
from twisted.internet import reactor, task
from twisted.internet.defer import Deferred
from twisted.logger import Logger
import nucypher
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, 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
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
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
from nucypher.utilities.logging import Logger
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 = """
<div class="nucypher-nickname-icon" style="border-color:{color};">
<div class="small">{number_of_nodes} nodes</div>
<div class="symbols">
<span class="single-symbol" style="color: {color}">{symbol}&#xFE0E;</span>
</div>
<br/>
<span class="small-address">{fleet_state_checksum}</span>
</div>
""".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(),
}
from umbral.signing import Signature
class NodeSprout(PartiallyKwargifiedBytes):
@ -237,16 +69,24 @@ 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.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._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._repr = None
self._is_finishing = False
self._finishing_mutex = Queue()
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):
@ -262,18 +102,49 @@ 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):
if self._is_finishing:
return self._finishing_mutex.get()
self._is_finishing = True # Prevent reentrance.
_finishing_mutex = self._finishing_mutex
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__
_finishing_mutex.put(self)
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
@ -294,18 +165,20 @@ 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?"
really_unknown_version_message = "Unable to glean address from node that perhaps purported to be version {}. We're only version {}."
fleet_state_icon = ""
_DEBUG_MODE = False
class NotEnoughNodes(RuntimeError):
pass
class NotEnoughTeachers(NotEnoughNodes):
pass
crash_right_now = True
class UnresponsiveTeacher(ConnectionError):
pass
@ -335,6 +208,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)
@ -353,6 +227,8 @@ class Learner:
self.lonely = lonely
self.done_seeding = False
self._learning_deferred = None
self._discovery_canceller = DiscoveryCanceller()
if not node_storage:
# Fallback storage backend
@ -363,7 +239,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
@ -376,27 +253,65 @@ class Learner:
self.teacher_nodes = deque()
self._current_teacher_node = None # type: Teacher
self._learning_task = task.LoopingCall(self.keep_learning_about_nodes)
if self._DEBUG_MODE:
# 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, os
frames = inspect.stack(3)
self._learning_task = task.LoopingCall(self.keep_learning_about_nodes, learner=self, frames=frames)
self._init_frames = frames
from tests.conftest import global_mutable_where_everybody
test_name = os.environ["PYTEST_CURRENT_TEST"]
global_mutable_where_everybody[test_name].append(self)
self._FOR_TEST = test_name
########################
self._learning_round = 0 # type: int
self._rounds_without_new_nodes = 0 # type: int
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.start_learning_loop(now=self.learn_on_same_thread)
@property
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, record_fleet_state=False):
"""
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
raise RuntimeError("Already finished seeding. Why try again? Is this a thread safety problem?")
discovered = []
if self.learning_domains:
one_and_only_learning_domain = tuple(self.learning_domains)[
0] # TODO: Are we done with multiple domains? 2144, 1580
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)
except NodeSeemsToBeDown:
self.unresponsive_seed_nodes.add(uri)
else:
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)
from nucypher.characters.lawful import Ursula
for seednode_metadata in self._seed_nodes:
self.log.debug(
@ -404,14 +319,17 @@ class Learner:
seednode_metadata.rest_host,
seednode_metadata.rest_port))
seed_node = Ursula.from_seednode_metadata(seednode_metadata=seednode_metadata,
network_middleware=self.network_middleware,
federated_only=self.federated_only) # TODO: 466
seed_node = self.node_class.from_seednode_metadata(seednode_metadata=seednode_metadata,
network_middleware=self.network_middleware,
)
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)
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.")
@ -419,16 +337,25 @@ 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()
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
discovered.extend(nodes_restored_from_storage)
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
restored_from_disk = []
for node in stored_nodes:
self.remember_node(node)
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,
@ -485,7 +412,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
@ -506,32 +434,16 @@ 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()
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.")
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 = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=False)
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):
@ -541,11 +453,21 @@ 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:
self.log.critical("Unhandled error during node learning. Attempting graceful crash.")
if self._learning_deferred is RELAX:
assert False
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
crash_right_now = getattr(_exception, "crash_right_now", False)
if self._abort_on_learning_error or crash_right_now:
reactor.callFromThread(self._crash_gracefully, failure=failure)
self.log.critical("Unhandled error during node learning. Attempting graceful crash.")
else:
self.log.warn(f"Unhandled error during node learning: {failure.getTraceback()}")
if not self._learning_task.running:
@ -554,12 +476,19 @@ 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
# When using Learner._DEBUG_MODE in tests, it may be helpful to uncomment this to be able to introspect.
# from tests.conftest import global_mutable_where_everybody
# gmwe = global_mutable_where_everybody
failure.raiseException()
# TODO: We don't actually have checksum_address at this level - maybe only Characters can crash gracefully :-) 1711
self.log.critical("{} crashed with {}".format(self.checksum_address, failure))
reactor.stop()
def select_teacher_nodes(self):
nodes_we_know_about = self.known_nodes.shuffled()
@ -570,12 +499,6 @@ 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.teacher_nodes:
self.select_teacher_nodes()
try:
@ -599,20 +522,47 @@ class Learner:
def learn_about_nodes_now(self, force=False):
if self._learning_task.running:
self._learning_task.reset()
self._learning_task()
# self._learning_task()
elif not force:
self.log.warn(
"Learning loop isn't started; can't learn about nodes now. You can override this with force=True.")
elif force:
# TODO: What if this has been stopped?
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, frames=None):
"""
Continually learn about new nodes.
learner is for debugging and logging only.
"""
# 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.
self._learning_deferred = Deferred(canceller=self._discovery_canceller) # TODO: No longer relevant.
def _discover_or_abort(_first_result):
self.log.debug(f"========={self} learning at {datetime.datetime.now()}")
result = self.learn_from_teacher_node(eager=False, canceller=self._discovery_canceller)
self.log.debug(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: Iterable):
if len(addresses) > 0:
@ -629,6 +579,10 @@ class Learner:
start = maya.now()
starting_round = self._learning_round
# if not learn_on_this_thread and self._learning_task.running:
# # 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:
@ -649,10 +603,13 @@ 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:
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))
@ -667,6 +624,13 @@ 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()
addresses = set(addresses)
while True:
if self._crashed:
return self._crashed
@ -676,10 +640,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:
@ -687,8 +652,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(
@ -772,17 +735,29 @@ 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.
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 = []
if not self.done_seeding:
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
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]
@ -794,12 +769,21 @@ class Learner:
#
# Request
#
if canceller and canceller.stop_now:
return RELAX
try:
response = self.network_middleware.get_nodes_via_rest(node=current_teacher,
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
else:
raise
except NodeSeemsToBeDown as e:
unresponsive_nodes.add(current_teacher)
self.log.info("Bad Response from teacher: {}:{}.".format(current_teacher, e))
@ -834,26 +818,26 @@ 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
#
try:
signature, node_payload = signature_splitter(response.content, return_remainder=True)
except BytestringSplittingError as e:
except BytestringSplittingError:
self.log.warn("No signature prepended to Teacher {} payload: {}".format(current_teacher, response.content))
return
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 Learner.InvalidSignature: # TODO: Ensure wev've got the right InvalidSignature exception here
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 = FleetStateTracker.snapshot_splitter(
node_payload,
return_remainder=True)
payload = FleetSensor.snapshot_splitter(node_payload, return_remainder=True)
fleet_state_checksum_bytes, fleet_state_updated_bytes, node_payload = payload
current_teacher.last_seen = maya.now()
# TODO: This is weird - let's get a stranger FleetState going. NRN
@ -871,7 +855,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:
@ -890,6 +874,7 @@ class Learner:
self.log.info(f"Verification Failed - "
f"Cannot establish connection to {sprout}.")
# 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.')
@ -907,15 +892,15 @@ class Learner:
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))
# TODO: Handle invalid sprouts
# 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?
current_teacher.update_snapshot(checksum=checksum,
updated=maya.MayaDT(int.from_bytes(fleet_state_updated_bytes, byteorder="big")),
@ -923,7 +908,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,

View File

@ -171,53 +171,12 @@ def make_rest_app(
signature = this_node.stamp(payload)
return Response(bytes(signature) + payload, headers=headers)
sprouts = _node_class.batch_from_bytes(request.data,
registry=this_node.registry)
sprouts = _node_class.batch_from_bytes(request.data)
# 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'])
@ -377,27 +336,48 @@ def make_rest_app(
@rest_app.route('/treasure_map/<treasure_map_id>', methods=['POST'])
def receive_treasure_map(treasure_map_id):
from nucypher.policy.collections import TreasureMap
# TODO: Any of the codepaths that trigger 4xx Responses here are also SuspiciousActivity.
if not this_node.federated_only:
from nucypher.policy.collections import SignedTreasureMap 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:
do_store = False
treasure_map = _MapClass.from_bytes(bytes_representation=request.data, verify=True)
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:
# TODO: If we include the policy ID in this check, does that prevent map spam? 1736
do_store = treasure_map.public_id() == treasure_map_id
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:
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()
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))
# 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:
# 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():

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
# Hardcoded bootstrapping teacher nodes keyed by network domain
TEACHER_NODES = {
'ibex': ('https://ibex.nucypher.network:9151', ),
}

View File

@ -15,26 +15,23 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from collections import OrderedDict
from typing import Optional, Tuple
import binascii
import maya
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
from cryptography.hazmat.primitives import hashes
from eth_utils import to_canonical_address, to_checksum_address
from typing import List, Optional, Tuple
from umbral.config import default_params
from umbral.curvebn import CurveBN
from umbral.keys import UmbralPublicKey
from umbral.pre import Capsule
from bytestring_splitter import BytestringKwargifier
from bytestring_splitter import BytestringSplitter, BytestringSplittingError, VariableLengthBytestring
from constant_sorrow.constants import CFRAG_NOT_RETAINED, NO_DECRYPTION_PERFORMED
from constant_sorrow.constants import NOT_SIGNED
from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH, ETH_HASH_BYTE_LENGTH
from nucypher.characters.lawful import Bob, Character
from nucypher.crypto.api import encrypt_and_sign, keccak_digest
from nucypher.crypto.api import verify_eip_191
from nucypher.crypto.constants import KECCAK_DIGEST_LENGTH, PUBLIC_ADDRESS_LENGTH
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.signing import InvalidSignature, Signature, signature_splitter
@ -43,16 +40,14 @@ from nucypher.crypto.utils import (canonical_address_from_umbral_key,
get_coordinates_as_bytes,
get_signature_recovery_value)
from nucypher.network.middleware import RestMiddleware
from umbral.config import default_params
from umbral.curvebn import CurveBN
from umbral.keys import UmbralPublicKey
from umbral.pre import Capsule
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)
)
ID_LENGTH = 32
class NowhereToBeFound(RestMiddleware.NotFound):
"""
@ -67,7 +62,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,
@ -86,11 +82,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,
@ -119,6 +131,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(
@ -158,7 +174,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:
"""
@ -166,19 +182,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()
@ -214,10 +223,14 @@ 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)
try:
return self.public_id() == other.public_id()
except AttributeError:
raise TypeError(f"Can't compare {other} to a TreasureMap (it needs to implement public_id() )")
def __iter__(self):
return iter(self.destinations.items())
@ -229,6 +242,37 @@ class TreasureMap:
return f"{self.__class__.__name__}:{self.public_id()[:6]}"
class SignedTreasureMap(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):
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__()
class PolicyCredential:
"""
A portable structure that contains information necessary for Alice or Bob
@ -260,25 +304,32 @@ 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(
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']))
if federated: # I know know. TODO: WTF. 466 and just... you know... whatever.
_MapClass = TreasureMap
else:
_MapClass = SignedTreasureMap
treasure_map = _MapClass.from_bytes(
bytes().fromhex(cred_json['treasure_map']))
return cls(alice_verifying_key, label, expiration, policy_pubkey,
treasure_map)
@ -291,7 +342,6 @@ class PolicyCredential:
class WorkOrder:
class PRETask:
input_splitter = capsule_splitter + signature_splitter # splitter for task without cfrag and signature
@ -545,8 +595,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

View File

@ -14,13 +14,20 @@ 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 <https://www.gnu.org/licenses/>.
"""
import math
import time
import random
from abc import ABC, abstractmethod
from collections import OrderedDict, deque
from queue import Queue
from typing import Callable
from typing import Generator, List, Set
import maya
from abc import ABC, abstractmethod
from twisted.internet import reactor
from twisted.internet.defer import ensureDeferred, Deferred
from twisted.python.threadpool import ThreadPool
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from constant_sorrow.constants import NOT_SIGNED, UNKNOWN_KFRAG
from typing import Generator, List, Set, Optional
@ -33,11 +40,13 @@ 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
from nucypher.utilities.logging import Logger
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
class Arrangement:
@ -124,13 +133,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
@ -161,6 +169,130 @@ class BlockchainArrangement(Arrangement):
return bytes(self.publish_transaction) + partial_payload
class NodeEngagementMutex:
"""
TODO: Does this belong on middleware?
TODO: There are a couple of ways this can break. If one fo the jobs hangs, the whole thing will hang. Also,
if there are fewer successfully completed than percent_to_complete_before_release, the partial queue will never
release.
TODO: Make registry per... I guess Policy? It's weird to be able to accidentally enact again.
"""
log = Logger("Policy")
def __init__(self,
callable_to_engage, # TODO: typing.Protocol
nodes,
network_middleware,
percent_to_complete_before_release=5,
note=None,
threadpool_size=120,
*args,
**kwargs):
self.f = callable_to_engage
self.nodes = nodes
self.network_middleware = network_middleware
self.args = args
self.kwargs = kwargs
self.completed = {}
self.failed = {}
self._started = False
self._finished = False
self.percent_to_complete_before_release = percent_to_complete_before_release
self._partial_queue = Queue()
self._completion_queue = Queue()
self._block_until_this_many_are_complete = math.ceil(
len(nodes) * self.percent_to_complete_before_release / 100)
self.nodes_contacted_during_partial_block = False
self.when_complete = Deferred() # TODO: Allow cancelling via KB Interrupt or some other way?
if note is None:
self._repr = f"{callable_to_engage} to {len(nodes)} nodes"
else:
self._repr = f"{note}: {callable_to_engage} to {len(nodes)} nodes"
self._threadpool = ThreadPool(minthreads=threadpool_size, maxthreads=threadpool_size, name=self._repr)
self.log.info(f"NEM spinning up {self._threadpool}")
def __repr__(self):
return self._repr
def block_until_success_is_reasonably_likely(self):
"""
https://www.youtube.com/watch?v=OkSLswPSq2o
"""
if len(self.completed) < self._block_until_this_many_are_complete:
completed_for_reasonable_likelihood_of_success = self._partial_queue.get() # Interesting opportuntiy to pass some data, like the list of contacted nodes above.
self.log.debug(f"{len(self.completed)} nodes were contacted while blocking for a little while.")
return completed_for_reasonable_likelihood_of_success
else:
return self.completed
def block_until_complete(self):
if self.total_disposed() < len(self.nodes):
_ = self._completion_queue.get() # Interesting opportuntiy to pass some data, like the list of contacted nodes above.
if not reactor.running and not self._threadpool.joined:
# If the reactor isn't running, the user *must* call this, because this is where we stop.
self._threadpool.stop()
def _handle_success(self, response, node):
if response.status_code == 202:
self.completed[node] = response
else:
assert False # TODO: What happens if this is a 300 or 400 level response? (A 500 response will propagate as an error and be handled in the errback chain.)
if self.nodes_contacted_during_partial_block:
self._consider_finalizing()
else:
if len(self.completed) >= self._block_until_this_many_are_complete:
contacted = tuple(self.completed.keys())
self.nodes_contacted_during_partial_block = contacted
self.log.debug(f"Blocked for a little while, completed {contacted} nodes")
self._partial_queue.put(contacted)
return response
def _handle_error(self, failure, node):
self.failed[node] = failure # TODO: Add a failfast mode?
self._consider_finalizing()
self.log.warn(f"{node} failed: {failure}")
def total_disposed(self):
return len(self.completed) + len(self.failed)
def _consider_finalizing(self):
if not self._finished:
if self.total_disposed() == len(self.nodes):
# TODO: Consider whether this can possibly hang.
self._finished = True
if reactor.running:
reactor.callInThread(self._threadpool.stop)
self._completion_queue.put(self.completed)
self.when_complete.callback(self.completed)
self.log.info(f"{self} finished.")
else:
raise RuntimeError("Already finished.")
def _engage_node(self, node):
maybe_coro = self.f(node, network_middleware=self.network_middleware, *self.args, **self.kwargs)
d = ensureDeferred(maybe_coro)
d.addCallback(self._handle_success, node)
d.addErrback(self._handle_error, node)
return d
def start(self):
if self._started:
raise RuntimeError("Already started.")
self._started = True
self.log.info(f"NEM Starting {self._threadpool}")
for node in self.nodes:
self._threadpool.callInThread(self._engage_node, node)
self._threadpool.start()
class Policy(ABC):
"""
An edict by Alice, arranged with n Ursulas, to perform re-encryption for a specific Bob
@ -196,14 +328,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.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.treasure_map = TreasureMap(m=m)
self._id = construct_policy_id(self.label, bytes(self.bob.stamp))
self.treasure_map = self._treasure_map_class(m=m)
self.expiration = expiration
self._accepted_arrangements = set() # type: Set[Arrangement]
@ -215,6 +346,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
@ -227,7 +360,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]}"
@ -252,44 +385,31 @@ 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:
async def put_treasure_map_on_node(self, node, network_middleware):
treasure_map_id = self.treasure_map.public_id()
response = network_middleware.put_treasure_map_on_node(
node=node,
map_id=treasure_map_id,
map_payload=bytes(self.treasure_map))
return response
def publish_treasure_map(self, network_middleware: RestMiddleware,
blockchain_signer: Callable = None) -> NodeEngagementMutex:
self.treasure_map.prepare_for_publication(self.bob.public_keys(DecryptingPower),
self.bob.public_keys(SigningPower),
self.alice.stamp,
self.label)
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.")
if blockchain_signer is not None:
self.treasure_map.include_blockchain_signature(blockchain_signer)
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.
self.alice.block_until_number_of_known_nodes_is(8, timeout=2, learn_on_this_thread=True)
try:
treasure_map_id = self.treasure_map.public_id()
target_nodes = self.bob.matching_nodes_among(self.alice.known_nodes)
self.publishing_mutex = NodeEngagementMutex(callable_to_engage=self.put_treasure_map_on_node,
nodes=target_nodes,
network_middleware=network_middleware)
# 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))
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 responses
self.publishing_mutex.start()
def credential(self, with_treasure_map=True):
"""
@ -306,7 +426,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:
@ -327,7 +446,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
@ -336,6 +455,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())
@ -344,14 +464,14 @@ 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:
# 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.
@ -366,11 +486,11 @@ class Policy(ABC):
self.revocation_kit = RevocationKit(self, self.alice.stamp)
self.alice.add_active_policy(self)
if publish is True:
return self.publish_treasure_map(network_middleware=network_middleware)
if publish_treasure_map is True:
return self.publish_treasure_map(network_middleware=network_middleware) # TODO: blockchain_signer?
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
@ -383,10 +503,12 @@ class Policy(ABC):
def make_arrangements(self,
network_middleware: RestMiddleware,
handpicked_ursulas: Optional[Set[Ursula]] = None,
discover_on_this_thread: bool = True,
*args, **kwargs,
) -> None:
sampled_ursulas = self.sample(handpicked_ursulas=handpicked_ursulas)
sampled_ursulas = self.sample(handpicked_ursulas=handpicked_ursulas,
discover_on_this_thread=discover_on_this_thread)
if len(sampled_ursulas) < self.n:
raise self.MoreKFragsThanArrangements(
@ -396,9 +518,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 '
@ -409,34 +531,38 @@ class Policy(ABC):
raise NotImplementedError
@abstractmethod
def sample_essential(self, quantity: int, handpicked_ursulas: Set[Ursula]) -> Set[Ursula]:
def sample_essential(self, *args, **kwargs) -> Set[Ursula]:
raise NotImplementedError
def sample(self, handpicked_ursulas: Optional[Set[Ursula]] = None) -> Set[Ursula]:
def sample(self,
handpicked_ursulas: Optional[Set[Ursula]] = None,
discover_on_this_thread: bool = False,
) -> Set[Ursula]:
selected_ursulas = set(handpicked_ursulas) if handpicked_ursulas else set()
# Calculate the target sample quantity
target_sample_quantity = self.n - len(selected_ursulas)
if target_sample_quantity > 0:
sampled_ursulas = self.sample_essential(quantity=target_sample_quantity,
handpicked_ursulas=selected_ursulas)
handpicked_ursulas=selected_ursulas,
discover_on_this_thread=discover_on_this_thread)
selected_ursulas.update(sampled_ursulas)
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.
@ -452,7 +578,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()
@ -463,8 +589,8 @@ 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:
@ -476,7 +602,11 @@ class FederatedPolicy(Policy):
"Pass them here as handpicked_ursulas.".format(self.n)
raise self.MoreKFragsThanArrangements(error) # TODO: NotEnoughUrsulas where in the exception tree is this?
def sample_essential(self, quantity: int, handpicked_ursulas: Set[Ursula]) -> Set[Ursula]:
def sample_essential(self,
quantity: int,
handpicked_ursulas: Set[Ursula],
discover_on_this_thread: bool = True) -> Set[Ursula]:
self.alice.block_until_number_of_known_nodes_is(quantity, learn_on_this_thread=discover_on_this_thread)
known_nodes = self.alice.known_nodes
if handpicked_ursulas:
# Prevent re-sampling of handpicked ursulas.
@ -496,6 +626,7 @@ class BlockchainPolicy(Policy):
A collection of n BlockchainArrangements representing a single Policy
"""
_arrangement_class = BlockchainArrangement
from nucypher.policy.collections import SignedTreasureMap as _treasure_map_class # TODO: Circular Import
class NoSuchPolicy(Exception):
pass
@ -577,7 +708,8 @@ class BlockchainPolicy(Policy):
quantity: int,
handpicked_ursulas: Set[Ursula],
learner_timeout: int = 1,
timeout: int = 10) -> Set[Ursula]: # TODO #843: Make timeout configurable
timeout: int = 10,
discover_on_this_thread: bool = False) -> Set[Ursula]: # TODO #843: Make timeout configurable
selected_ursulas = set(handpicked_ursulas)
quantity_remaining = quantity
@ -616,19 +748,14 @@ class BlockchainPolicy(Policy):
new_to_check = reservoir.draw_at_most(quantity_remaining)
to_check.update(new_to_check)
# Feed newly sampled stakers to the learner
self.alice.learn_about_specific_nodes(new_to_check)
# TODO: would be nice to wait for the learner to finish an iteration here,
# because if it hasn't, we really have nothing to do.
time.sleep(learner_timeout)
delta = maya.now() - start_time
if delta.total_seconds() >= timeout:
still_checking = ', '.join(to_check)
raise RuntimeError(f"Timed out after {timeout} seconds; "
f"need {quantity_remaining} more, still checking {still_checking}.")
self.alice.block_until_specific_nodes_are_known(to_check, learn_on_this_thread=discover_on_this_thread)
found_ursulas = list(selected_ursulas)
# Randomize the output to avoid the largest stakers always being the first in the list
@ -643,11 +770,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
@ -665,16 +792,27 @@ 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) -> NodeEngagementMutex:
"""
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
return super().enact(network_middleware, publish)
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),
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)
publisher = self.publish_treasure_map(network_middleware=network_middleware,
blockchain_signer=transacting_power.sign_message)
return publisher

View File

@ -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']:
@ -76,53 +74,3 @@ def aggregate_seednode_uris(domains: set, highest_priority: Optional[List[str]]
uris.extend(hardcoded_uris)
return uris
def load_seednodes(emitter,
min_stake: int,
federated_only: bool,
network_domains: set,
network_middleware: RestMiddleware = None,
teacher_uris: list = None,
registry: BaseContractRegistry = None,
) -> List:
"""
Aggregates seednodes URI sources into a list or teacher URIs ordered
by connection priority in the following order:
1. --teacher CLI flag
2. static-nodes.json
3. Hardcoded teachers
"""
# Heads up
emitter.message(START_LOADING_SEEDNODES, color='yellow')
from nucypher.characters.lawful import Ursula
# Aggregate URIs (Ordered by Priority)
teacher_nodes = list() # type: List[Ursula]
teacher_uris = aggregate_seednode_uris(domains=network_domains, highest_priority=teacher_uris)
if not teacher_uris:
emitter.message(NO_DOMAIN_PEERS.format(domains=','.join(network_domains)))
return teacher_nodes
# Construct Ursulas
for uri in teacher_uris:
try:
teacher_node = Ursula.from_teacher_uri(teacher_uri=uri,
min_stake=min_stake,
federated_only=federated_only,
network_middleware=network_middleware,
registry=registry)
except NodeSeemsToBeDown:
emitter.message(UNREADABLE_SEEDNODE_ADVISORY.format(uri=uri))
continue
except Teacher.NotStaking:
emitter.message(SEEDNODE_NOT_STAKING_WARNING.format(uri=uri))
continue
teacher_nodes.append(teacher_node)
if not teacher_nodes:
emitter.message(NO_DOMAIN_PEERS.format(domains=','.join(network_domains)))
return teacher_nodes

View File

@ -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
@ -42,8 +42,8 @@ services:
- 11500
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
- circleursula1
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:

View File

@ -13,6 +13,11 @@ echo "working in directory: $PWD"
# run some ursulas
docker-compose up -d
# Wait to ensure Ursulas are up.
echo "War... watisit good for?"
sleep 3
# Run demo
echo "Starting Demo"
echo "working in directory: $PWD"

View File

@ -9,6 +9,10 @@ echo "Starting Up Heartbeat Demo Test..."
# run some ursulas
docker-compose up -d
# Wait to ensure Ursulas are up.
echo "War... watisit good for?"
sleep 3
echo "running heartbeat demo"
# run alicia and bob all in one running of docker since we lack persistent disks in circle

View File

@ -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.blockchain.eth.utils import datetime_at_period
from nucypher.crypto.powers import TransactingPower
from tests.constants import FEE_RATE_RANGE, INSECURE_DEVELOPMENT_PASSWORD, DEVELOPMENT_TOKEN_AIRDROP_AMOUNT
from tests.utils.blockchain import token_airdrop
from tests.utils.ursula import make_decentralized_ursulas
@ -33,7 +34,8 @@ def test_staker_locking_tokens(testerchain, agency, staker, token_economics, moc
assert NU(token_economics.minimum_allowed_locked, 'NuNit') < staker.token_balance, "Insufficient staker balance"
staker.initialize_stake(amount=NU(token_economics.minimum_allowed_locked, 'NuNit'), # Lock the minimum amount of tokens
staker.initialize_stake(amount=NU(token_economics.minimum_allowed_locked, 'NuNit'),
# Lock the minimum amount of tokens
lock_periods=token_economics.minimum_locked_periods)
# Verify that the escrow is "approved" to receive tokens
@ -52,8 +54,8 @@ def test_staker_locking_tokens(testerchain, agency, staker, token_economics, moc
@pytest.mark.usefixtures("agency")
def test_staker_divides_stake(staker, token_economics):
stake_value = NU(token_economics.minimum_allowed_locked*5, 'NuNit')
new_stake_value = NU(token_economics.minimum_allowed_locked*2, 'NuNit')
stake_value = NU(token_economics.minimum_allowed_locked * 5, 'NuNit')
new_stake_value = NU(token_economics.minimum_allowed_locked * 2, 'NuNit')
stake_index = 0
staker.initialize_stake(amount=stake_value, lock_periods=int(token_economics.minimum_locked_periods))
@ -108,8 +110,10 @@ def test_staker_divides_stake(staker, token_economics):
economics=token_economics)
assert 4 == len(staker.stakes), 'A new stake was not added after two stake divisions'
assert expected_old_stake == staker.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'
assert expected_new_stake == staker.stakes[stake_index + 2].to_stake_info(), 'New stake values are invalid after two stake divisions'
assert expected_old_stake == staker.stakes[
stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'
assert expected_new_stake == staker.stakes[
stake_index + 2].to_stake_info(), 'New stake values are invalid after two stake divisions'
assert expected_yet_another_stake.value == staker.stakes[stake_index + 3].value, 'Third stake values are invalid'
@ -221,7 +225,6 @@ def test_staker_merges_stakes(agency, staker, token_economics):
def test_staker_manages_restaking(testerchain, test_registry, staker):
# Enable Restaking
receipt = staker.enable_restaking()
assert receipt['status'] == 1
@ -237,7 +240,7 @@ def test_staker_manages_restaking(testerchain, test_registry, staker):
assert staker.restaking_lock_enabled
with pytest.raises((TransactionFailed, ValueError)):
staker.disable_restaking()
staker.disable_restaking()
# Wait until terminal period
testerchain.time_travel(periods=2)
@ -264,8 +267,10 @@ def test_staker_collects_staking_reward(testerchain,
mock_transacting_power_activation(account=staker.checksum_address, password=INSECURE_DEVELOPMENT_PASSWORD)
staker.initialize_stake(amount=NU(token_economics.minimum_allowed_locked, 'NuNit'), # Lock the minimum amount of tokens
lock_periods=int(token_economics.minimum_locked_periods)) # ... for the fewest number of periods
staker.initialize_stake(amount=NU(token_economics.minimum_allowed_locked, 'NuNit'),
# Lock the minimum amount of tokens
lock_periods=int(
token_economics.minimum_locked_periods)) # ... for the fewest number of periods
# Get an unused address for a new worker
worker_address = testerchain.unassigned_accounts[-1]
@ -280,7 +285,8 @@ def test_staker_collects_staking_reward(testerchain,
# ...mint few tokens...
for _ in range(2):
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()
testerchain.time_travel(periods=1)
@ -322,6 +328,10 @@ def test_staker_manages_winding_down(testerchain,
commit_to_next_period=False,
registry=test_registry).pop()
# Unlock
transacting_power = ursula._crypto_power.power_ups(TransactingPower)
transacting_power.activate(password=INSECURE_DEVELOPMENT_PASSWORD)
# Enable winding down
testerchain.time_travel(periods=1)
base_duration = token_economics.minimum_locked_periods + 4
@ -345,7 +355,6 @@ def test_staker_manages_winding_down(testerchain,
def test_set_min_fee_rate(testerchain, test_registry, staker):
# Check before set
_minimum, default, maximum = FEE_RATE_RANGE
assert staker.min_fee_rate == default

View File

@ -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()

View File

@ -17,6 +17,7 @@
import os
import pytest
from eth_utils import is_checksum_address, to_checksum_address
from nucypher.blockchain.eth.actors import ContractAdministrator
@ -55,6 +56,7 @@ def test_geth_create_new_account(instant_geth_dev_node):
assert is_checksum_address(new_account)
@pytest.mark.skip('See PR #2074')
@skip_on_circleci
def test_geth_deployment_integration(instant_geth_dev_node, test_registry):
blockchain = BlockchainDeployerInterface(provider_process=instant_geth_dev_node, poa=True) # always poa here.

View File

@ -19,31 +19,63 @@ from base64 import b64encode
import pytest
from nucypher.characters.control.interfaces import AliceInterface, BobInterface, EnricoInterface
from nucypher.characters.control.interfaces import AliceInterface
from nucypher.characters.control.interfaces import BobInterface, EnricoInterface
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.policy.collections import TreasureMap
from nucypher.policy.collections import SignedTreasureMap
from tests.utils.controllers import get_fields, validate_json_rpc_response_data
def get_fields(interface, method_name):
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])
spec = getattr(interface, method_name)._schema
input_fields = [k for k, f in spec.load_fields.items() if f.required]
optional_fields = [k for k, f in spec.load_fields.items() if not f.required]
required_output_fileds = list(spec.dump_fields.keys())
return (
input_fields,
optional_fields,
required_output_fileds
)
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 validate_json_rpc_response_data(response, method_name, interface):
required_output_fields = get_fields(interface, method_name)[-1]
assert 'jsonrpc' in response.data
for output_field in required_output_fields:
assert output_field in response.content
return True
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_with_tmap(
enacted_blockchain_policy, blockchain_bob, blockchain_alice,
bob_rpc_controller, retrieve_control_request):
# So that this test can run even independently.
if not blockchain_bob.done_seeding:
blockchain_bob.learn_from_teacher_node()
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 = SignedTreasureMap(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(SignedTreasureMap.IsDisorienting):
bob_rpc_controller.send(request_data)
def test_alice_rpc_character_control_create_policy(alice_rpc_test_client, create_policy_control_request):
@ -82,6 +114,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
@ -91,6 +124,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'}}
@ -108,58 +142,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 = TreasureMap(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()
request_data['params']['treasure_map'] = tmap_64
with pytest.raises(TreasureMap.IsDisorienting):
bob_rpc_controller.send(request_data)

View File

@ -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, SignedTreasureMap
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 = SignedTreasureMap.from_bytes(map_bytes)
assert encrypted_map._hrac is not None
# Send bad data to assert error returns
@ -199,6 +199,7 @@ def test_bob_character_control_join_policy(bob_web_controller_test_client, enact
def test_bob_web_character_control_retrieve(bob_web_controller_test_client, retrieve_control_request):
method_name, params = retrieve_control_request
endpoint = f'/{method_name}'

View File

@ -56,7 +56,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
@ -69,7 +69,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...
@ -111,7 +112,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:

View File

@ -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,
@ -64,7 +65,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 +78,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 +95,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 +102,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 +120,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 +131,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
@ -157,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,

View File

@ -15,17 +15,16 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime
import json
import os
import shutil
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
@ -35,13 +34,11 @@ 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!"
class MockSideChannel:
PolicyAndLabel = namedtuple('PolicyAndLabel', ['encrypting_key', 'label'])
BobPublicKeys = namedtuple('BobPublicKeys', ['bob_encrypting_key', 'bob_verifying_key'])
@ -90,46 +87,13 @@ class MockSideChannel:
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):
def run_entire_cli_lifecycle(click_runner,
random_policy_label,
ursulas,
custom_filepath,
custom_filepath_2,
registry_filepath=None,
testerchain=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,
@ -157,7 +121,7 @@ def _cli_lifecycle(click_runner,
'--network', TEMPORARY_DOMAIN,
'--config-root', alice_config_root)
if federated:
alice_init_args += ('--federated-only', )
alice_init_args += ('--federated-only',)
else:
alice_init_args += ('--provider', TEST_PROVIDER_URI,
'--pay-with', testerchain.alice_account,
@ -200,7 +164,7 @@ def _cli_lifecycle(click_runner,
'--network', TEMPORARY_DOMAIN,
'--config-root', bob_config_root)
if federated:
bob_init_args += ('--federated-only', )
bob_init_args += ('--federated-only',)
else:
bob_init_args += ('--provider', TEST_PROVIDER_URI,
'--registry-filepath', str(registry_filepath),
@ -213,6 +177,8 @@ 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
'--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)
@ -226,7 +192,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
@ -251,6 +217,7 @@ def _cli_lifecycle(click_runner,
"""
Scene 4: Enrico encrypts some data for some policy public key and saves it to a side channel.
"""
def enrico_encrypts():
# Fetch!
@ -265,7 +232,7 @@ def _cli_lifecycle(click_runner,
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
encrypted_message = encrypt_result['result']['message_kit'] # type: str
side_channel.save_message_kit(message_kit=encrypted_message)
return encrypt_result
@ -291,7 +258,8 @@ def _cli_lifecycle(click_runner,
if federated:
decrypt_args += ('--federated-only',)
decrypt_response_fail = click_runner.invoke(nucypher_cli, decrypt_args[0:7], catch_exceptions=False, env=envvars)
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)
@ -317,7 +285,7 @@ def _cli_lifecycle(click_runner,
# Some Ursula is running somewhere
def _run_teacher(_encrypt_result):
start_pytest_ursula_services(ursula=teacher)
# start_pytest_ursula_services(ursula=teacher)
return teacher_uri
def _grant(teacher_uri):
@ -346,6 +314,7 @@ def _cli_lifecycle(click_runner,
grant_args += ('--provider', TEST_PROVIDER_URI,
'--rate', Web3.toWei(9, 'gwei'))
# TODO: Stop.
grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars)
assert grant_result.exit_code == 0
@ -395,9 +364,9 @@ def _cli_lifecycle(click_runner,
# 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
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

View File

@ -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

View File

@ -87,7 +87,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
@ -106,7 +106,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

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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,
random_policy_label,
blockchain_ursulas,
custom_filepath,
custom_filepath_2,
agency_local_registry.filepath,
testerchain=testerchain)

View File

@ -0,0 +1,32 @@
"""
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 <https://www.gnu.org/licenses/>.
"""
import pytest_twisted as pt
from tests.acceptance.cli.lifecycle import run_entire_cli_lifecycle
@pt.inlineCallbacks
def test_federated_cli_lifecycle(click_runner,
random_policy_label,
federated_ursulas,
custom_filepath,
custom_filepath_2):
yield run_entire_cli_lifecycle(click_runner,
random_policy_label,
federated_ursulas,
custom_filepath,
custom_filepath_2)

View File

@ -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

View File

@ -69,6 +69,7 @@ def test_coexisting_configurations(click_runner,
assert not custom_filepath.exists()
# Parse node addresses
# TODO: Is testerchain & Full contract deployment needed here (causes massive slowdown)?
alice, ursula, another_ursula, felix, staker, *all_yall = testerchain.unassigned_accounts
envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
@ -180,14 +181,13 @@ def test_coexisting_configurations(click_runner,
# Run an Ursula amidst the other configuration files
run_args = ('ursula', 'run',
'--dry-run',
'--interactive',
'--config-file', another_ursula_configuration_file_location)
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' * 2
Worker.BONDING_POLL_RATE = 1
Worker.BONDING_TIMEOUT = 1
with pytest.raises(Teacher.UnbondedWorker):
with pytest.raises(Teacher.UnbondedWorker): # TODO: Why is this being checked here?
# Worker init success, but not bonded.
result = click_runner.invoke(nucypher_cli, run_args, input=user_input, catch_exceptions=False)
assert result.exit_code == 0

View File

@ -23,6 +23,7 @@ import pytest
from eth_utils import to_wei
from web3 import Web3
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.actors import Bidder, Staker
from nucypher.blockchain.eth.agents import (
ContractAgency,
@ -238,6 +239,10 @@ def test_refund(click_runner, testerchain, agency_local_registry, token_economic
remaining_work = worklock_agent.get_remaining_work(checksum_address=bidder)
assert remaining_work > 0
# Unlock
transacting_power = worker._crypto_power.power_ups(TransactingPower)
transacting_power.activate(password=INSECURE_DEVELOPMENT_PASSWORD)
# Do some work
for i in range(3):
receipt = worker.commit_to_next_period()

View File

@ -29,7 +29,7 @@ from tests.constants import (
FAKE_PASSWORD_CONFIRMED, INSECURE_DEVELOPMENT_PASSWORD,
MOCK_CUSTOM_INSTALLATION_PATH,
MOCK_IP_ADDRESS, YES_ENTER)
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, select_test_port
def test_initialize_ursula_defaults(click_runner, mocker):
@ -55,13 +55,14 @@ def test_initialize_ursula_defaults(click_runner, mocker):
def test_initialize_custom_configuration_root(custom_filepath, click_runner):
deploy_port = select_test_port()
# Use a custom local filepath for configuration
init_args = ('ursula', 'init',
'--network', TEMPORARY_DOMAIN,
'--federated-only',
'--config-root', custom_filepath,
'--rest-host', MOCK_IP_ADDRESS,
'--rest-port', MOCK_URSULA_STARTING_PORT)
'--rest-port', deploy_port)
result = click_runner.invoke(nucypher_cli, init_args, input=FAKE_PASSWORD_CONFIRMED, catch_exceptions=False)
assert result.exit_code == 0
@ -139,6 +140,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,
@ -148,7 +150,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

View File

@ -36,7 +36,7 @@ from tests.constants import (
MOCK_IP_ADDRESS,
TEST_PROVIDER_URI
)
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, select_test_port
@pytest.fixture(scope='module')
@ -107,13 +107,15 @@ def test_ursula_and_local_keystore_signer_integration(click_runner,
pre_config_signer = KeystoreSigner.from_signer_uri(uri=mock_signer_uri)
assert worker_account.address in pre_config_signer.accounts
deploy_port = select_test_port()
init_args = ('ursula', 'init',
'--network', TEMPORARY_DOMAIN,
'--worker-address', worker_account.address,
'--config-root', config_root_path,
'--provider', TEST_PROVIDER_URI,
'--rest-host', MOCK_IP_ADDRESS,
'--rest-port', MOCK_URSULA_STARTING_PORT,
'--rest-port', deploy_port,
# The bit we are testing for here
'--signer', mock_signer_uri)
@ -146,15 +148,18 @@ def test_ursula_and_local_keystore_signer_integration(click_runner,
ursula = ursula_config.produce(client_password=password,
block_until_ready=False)
# Verify the keystore path is still preserved
assert isinstance(ursula.signer, KeystoreSigner)
assert isinstance(ursula.signer.path, str), "Use str"
assert ursula.signer.path == str(mock_keystore_path)
try:
# Verify the keystore path is still preserved
assert isinstance(ursula.signer, KeystoreSigner)
assert isinstance(ursula.signer.path, str), "Use str"
assert ursula.signer.path == str(mock_keystore_path)
# Show that we can produce the exact same signer as pre-config...
assert pre_config_signer.path == ursula.signer.path
# Show that we can produce the exact same signer as pre-config...
assert pre_config_signer.path == ursula.signer.path
# ...and that transactions are signed by the keytore signer
receipt = ursula.commit_to_next_period()
transaction_data = testerchain.client.w3.eth.getTransaction(receipt['transactionHash'])
assert transaction_data['from'] == worker_account.address
# ...and that transactions are signed by the keytore signer
receipt = ursula.commit_to_next_period()
transaction_data = testerchain.client.w3.eth.getTransaction(receipt['transactionHash'])
assert transaction_data['from'] == worker_account.address
finally:
ursula.stop()

View File

@ -14,14 +14,12 @@ 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 <https://www.gnu.org/licenses/>.
"""
import os
import time
from unittest import mock
import os
import pytest
import pytest_twisted as pt
import time
from twisted.internet import threads
from nucypher import utilities
@ -39,7 +37,7 @@ from tests.constants import (
TEST_PROVIDER_URI,
YES_ENTER
)
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, start_pytest_ursula_services
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, start_pytest_ursula_services, select_test_port
@mock.patch('glob.glob', return_value=list())
@ -51,7 +49,6 @@ def test_missing_configuration_file(default_filepath_mock, click_runner):
def test_ursula_rest_host_determination(click_runner, mocker):
# Patch the get_external_ip call
mocker.patch.object(utilities.networking, 'get_external_ip_from_centralized_source', return_value=MOCK_IP_ADDRESS)
mocker.patch.object(UrsulaConfiguration, 'to_configuration_file', return_value=None)
@ -77,13 +74,14 @@ def test_ursula_rest_host_determination(click_runner, mocker):
@pt.inlineCallbacks
def test_run_lone_federated_default_development_ursula(click_runner):
args = ('ursula', 'run', # Stat Ursula Command
'--debug', # Display log output; Do not attach console
'--federated-only', # Operating Mode
'--rest-port', MOCK_URSULA_STARTING_PORT, # Network Port
'--dev', # Run in development mode (ephemeral node)
'--dry-run', # Disable twisted reactor in subprocess
'--lonely' # Do not load seednodes
deploy_port = select_test_port()
args = ('ursula', 'run', # Stat Ursula Command
'--debug', # Display log output; Do not attach console
'--federated-only', # Operating Mode
'--rest-port', deploy_port, # Network Port
'--dev', # Run in development mode (ephemeral node)
'--dry-run', # Disable twisted reactor in subprocess
'--lonely' # Do not load seednodes
)
result = yield threads.deferToThread(click_runner.invoke,
@ -94,67 +92,62 @@ def test_run_lone_federated_default_development_ursula(click_runner):
time.sleep(Learner._SHORT_LEARNING_DELAY)
assert result.exit_code == 0
assert "Running" in result.output
assert "127.0.0.1:{}".format(MOCK_URSULA_STARTING_PORT) in result.output
assert "127.0.0.1:{}".format(deploy_port) in result.output
reserved_ports = (UrsulaConfiguration.DEFAULT_REST_PORT, UrsulaConfiguration.DEFAULT_DEVELOPMENT_REST_PORT)
assert MOCK_URSULA_STARTING_PORT not in reserved_ports
assert deploy_port not in reserved_ports
@pt.inlineCallbacks
def test_federated_ursula_learns_via_cli(click_runner, federated_ursulas):
# Establish a running Teacher Ursula
teacher = list(federated_ursulas)[0]
teacher_uri = teacher.seed_node_metadata(as_teacher_uri=True)
# Some Ursula is running somewhere
def run_teacher():
start_pytest_ursula_services(ursula=teacher)
return teacher_uri
def run_ursula(teacher_uri):
deploy_port = select_test_port()
def run_ursula():
i = start_pytest_ursula_services(ursula=teacher)
args = ('ursula', 'run',
'--debug', # Display log output; Do not attach console
'--federated-only', # Operating Mode
'--rest-port', MOCK_URSULA_STARTING_PORT, # Network Port
'--debug', # Display log output; Do not attach console
'--federated-only', # Operating Mode
'--rest-port', deploy_port, # Network Port
'--teacher', teacher_uri,
'--dev', # Run in development mode (ephemeral node)
'--dry-run' # Disable twisted reactor
'--dev', # Run in development mode (ephemeral node)
'--dry-run' # Disable twisted reactor
)
result = yield threads.deferToThread(click_runner.invoke,
nucypher_cli, args,
catch_exceptions=False,
input=INSECURE_DEVELOPMENT_PASSWORD + '\n')
assert result.exit_code == 0
assert "Running Ursula" in result.output
assert "127.0.0.1:{}".format(MOCK_URSULA_STARTING_PORT+101) in result.output
reserved_ports = (UrsulaConfiguration.DEFAULT_REST_PORT, UrsulaConfiguration.DEFAULT_DEVELOPMENT_REST_PORT)
assert MOCK_URSULA_STARTING_PORT not in reserved_ports
# Check that CLI Ursula reports that it remembers the teacher and saves the TLS certificate
assert teacher.checksum_address in result.output
assert f"Saved TLS certificate for {teacher.nickname}" in result.output
assert f"Remembering {teacher.nickname}" in result.output
return threads.deferToThread(click_runner.invoke,
nucypher_cli, args,
catch_exceptions=False,
input=INSECURE_DEVELOPMENT_PASSWORD + '\n')
# Run the Callbacks
d = threads.deferToThread(run_teacher)
d.addCallback(run_ursula)
d = run_ursula()
yield d
result = d.result
assert result.exit_code == 0
assert "Starting Ursula" in result.output
assert f"127.0.0.1:{deploy_port}" in result.output
reserved_ports = (UrsulaConfiguration.DEFAULT_REST_PORT, UrsulaConfiguration.DEFAULT_DEVELOPMENT_REST_PORT)
assert deploy_port not in reserved_ports
# Check that CLI Ursula reports that it remembers the teacher and saves the TLS certificate
assert teacher.checksum_address in result.output
assert f"Saved TLS certificate for {teacher.nickname}" in result.output
@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,
testerchain,
blockchain_ursulas,
agency_local_registry):
alice, ursula, another_ursula, felix, staker, *all_yall = testerchain.unassigned_accounts
filename = UrsulaConfiguration.generate_filename()
another_ursula_configuration_file_location = os.path.join(custom_filepath, filename)

View File

@ -420,6 +420,8 @@ def test_ursula_init(click_runner,
manual_worker,
testerchain):
deploy_port = select_test_port()
init_args = ('ursula', 'init',
'--network', TEMPORARY_DOMAIN,
'--worker-address', manual_worker,
@ -427,7 +429,7 @@ def test_ursula_init(click_runner,
'--provider', TEST_PROVIDER_URI,
'--registry-filepath', agency_local_registry.filepath,
'--rest-host', MOCK_IP_ADDRESS,
'--rest-port', MOCK_URSULA_STARTING_PORT)
'--rest-port', deploy_port)
result = click_runner.invoke(nucypher_cli,
init_args,
@ -558,7 +560,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))

View File

@ -317,6 +317,8 @@ def test_ursula_init(click_runner,
manual_worker,
testerchain):
deploy_port = select_test_port()
init_args = ('ursula', 'init',
'--network', TEMPORARY_DOMAIN,
'--worker-address', manual_worker,
@ -324,7 +326,7 @@ def test_ursula_init(click_runner,
'--provider', TEST_PROVIDER_URI,
'--registry-filepath', agency_local_registry.filepath,
'--rest-host', MOCK_IP_ADDRESS,
'--rest-port', MOCK_URSULA_STARTING_PORT)
'--rest-port', deploy_port)
result = click_runner.invoke(nucypher_cli,
init_args,

View File

@ -16,11 +16,12 @@
"""
import pytest
from constant_sorrow.constants import NOT_SIGNED
from twisted.logger import LogLevel, globalLogPublisher
from constant_sorrow.constants import NOT_SIGNED
from nucypher.acumen.perception import FleetSensor
from nucypher.crypto.powers import TransactingPower
from nucypher.network.nodes import FleetStateTracker, Learner
from nucypher.network.nodes import Learner
from tests.utils.middleware import MockRestMiddleware
from tests.utils.ursula import make_ursula_for_staker
@ -44,7 +45,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)
@ -70,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,
@ -153,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)

View File

@ -15,16 +15,18 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime
import maya
import pytest
from hendrix.experience import crosstown_traffic
from hendrix.utils.test_utils import crosstownTaskListDecoratorFactory
from nucypher.characters.lawful import Ursula
from nucypher.acumen.nicknames import nickname_from_seed
from nucypher.acumen.perception import FleetSensor
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 tests.constants import INSECURE_DEVELOPMENT_PASSWORD
from tests.utils.middleware import MockRestMiddleware
@ -45,7 +47,7 @@ def test_all_blockchain_ursulas_know_about_all_other_ursulas(blockchain_ursulas,
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()
@ -55,6 +57,35 @@ def test_blockchain_alice_finds_ursula_via_rest(blockchain_alice, blockchain_urs
assert ursula in blockchain_alice.known_nodes
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
@pytest.mark.skip("See Issue #1075") # TODO: Issue #1075
def test_vladimir_illegal_interface_key_does_not_propagate(blockchain_ursulas):
"""
@ -124,6 +155,36 @@ 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)
@pytest.mark.skip('Needs to be restored -- no way to access treasure maps from stranger ursulas here.')
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._stored_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

View File

@ -14,6 +14,8 @@ 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 <https://www.gnu.org/licenses/>.
"""
import argparse
from collections import defaultdict
import pytest
@ -36,9 +38,15 @@ TEACHER_NODES = dict()
# Prevent halting the reactor via health checks during tests
AvailabilityTracker._halt_reactor = lambda *a, **kw: True
# Global test character cache
global_mutable_where_everybody = defaultdict(list)
##########################################
from nucypher.network.nodes import Learner
Learner._DEBUG_MODE = False
@pytest.fixture(autouse=True, scope='session')
def __very_pretty_and_insecure_scrypt_do_not_use():
"""
@ -92,6 +100,16 @@ def pytest_addoption(parser):
default=False,
help="run tests even if they are marked as nightly")
# class SetLearnerDebugMode((argparse.Action)):
# def __call__(self, *args, **kwargs):
# from nucypher.network.nodes import Learner
# Learner._DEBUG_MODE = True
# parser.addoption("--track-character-lifecycles",
# action=SetLearnerDebugMode,
# default=False,
# help="Track characters in a global... mutable... where everybody...")
def pytest_configure(config):
message = "{0}: mark test as {0} to run (skipped by default, use '{1}' to include these tests)"
@ -128,3 +146,44 @@ def pytest_collection_modifyitems(config, items):
GlobalLoggerSettings.set_log_level(log_level_name)
GlobalLoggerSettings.start_text_file_logging()
GlobalLoggerSettings.start_json_file_logging()
# global_mutable_where_everybody = defaultdict(list)
@pytest.fixture(scope='module', autouse=True)
def check_character_state_after_test(request):
from nucypher.network.nodes import Learner
yield
if Learner._DEBUG_MODE:
gmwe = global_mutable_where_everybody
module_name = request.module.__name__
test_learners = global_mutable_where_everybody.get(module_name, [])
# Those match the module name exactly; maybe there are some that we got by frame.
for maybe_frame, learners in global_mutable_where_everybody.items():
if f"{module_name}.py" in maybe_frame:
test_learners.extend(learners)
crashed = [learner for learner in test_learners if learner._crashed]
if any(crashed):
failure_message = ""
for learner in crashed:
failure_message += learner._crashed.getBriefTraceback()
pytest.fail(f"Some learners crashed:{failure_message}")
still_running = [learner for learner in test_learners if learner._learning_task.running]
if any(still_running):
offending_tests = set()
for learner in still_running:
offending_tests.add(learner._FOR_TEST)
try: # TODO: Deal with stop vs disenchant. Currently stop is only for Ursula.
learner.stop()
except AttributeError:
learner.disenchant()
pytest.fail(f"Learners remaining: {still_running}. Offending tests: {offending_tests} ")
still_tracking = [learner for learner in test_learners if hasattr(learner, 'work_tracker') and learner.work_tracker._tracking_task.running]
for tracker in still_tracking:
tracker.work_tracker.stop()

View File

@ -15,25 +15,20 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
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 eth_utils import to_checksum_address
from io import StringIO
from functools import partial
from typing import Tuple
from umbral import pre
from umbral.curvebn import CurveBN
from umbral.keys import UmbralPrivateKey
from umbral.signing import Signer
import maya
import pytest
from click.testing import CliRunner
from eth_utils import to_checksum_address
from web3 import Web3
from nucypher.blockchain.economics import BaseEconomics, StandardTokenEconomics
@ -108,10 +103,16 @@ 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
from umbral import pre
from umbral.curvebn import CurveBN
from umbral.keys import UmbralPrivateKey
from umbral.signing import Signer
test_logger = Logger("test-logger")
# defer.setDebugging(True)
#
# Temporary
@ -186,6 +187,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")
@ -200,12 +203,12 @@ def alice_blockchain_test_config(blockchain_ursulas, testerchain, test_registry)
@pytest.fixture(scope="module")
def bob_blockchain_test_config(blockchain_ursulas, testerchain, test_registry):
def bob_blockchain_test_config(testerchain, test_registry):
config = make_bob_test_configuration(federated=False,
provider_uri=TEST_PROVIDER_URI,
test_registry=test_registry,
checksum_address=testerchain.bob_account,
known_nodes=blockchain_ursulas)
)
yield config
config.cleanup()
@ -239,7 +242,8 @@ def enacted_federated_policy(idle_federated_policy, federated_ursulas):
idle_federated_policy.make_arrangements(network_middleware, handpicked_ursulas=federated_ursulas)
# REST call happens here, as does population of TreasureMap.
responses = idle_federated_policy.enact(network_middleware)
idle_federated_policy.enact(network_middleware)
idle_federated_policy.publishing_mutex.block_until_complete()
return idle_federated_policy
@ -252,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,
@ -277,6 +281,7 @@ def enacted_blockchain_policy(idle_blockchain_policy, blockchain_ursulas):
network_middleware, handpicked_ursulas=list(blockchain_ursulas))
idle_blockchain_policy.enact(network_middleware) # REST call happens here, as does population of TreasureMap.
idle_blockchain_policy.publishing_mutex.block_until_complete()
return idle_blockchain_policy
@ -339,41 +344,81 @@ 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.disenchant()
@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.disenchant()
@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()
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):
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)
# Since we mutate this list in some tests, it's not enough to remember and remove the Ursulas; we have to remember them by port.
# The same is true of blockchain_ursulas below.
_ports_to_remove = [ursula.rest_interface.port for ursula in _ursulas]
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:
u.stop()
@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):
ursulas = self._partial(*args, **kwargs)
self._made.extend(ursulas)
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
_maker.clean()
#
# Blockchain
#
def make_token_economics(blockchain):
# Get current blocktime
now = blockchain.w3.eth.getBlock(block_identifier='latest').timestamp
@ -389,7 +434,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,
@ -629,6 +674,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,
@ -642,8 +689,12 @@ 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 port in _ports_to_remove:
del MOCK_KNOWN_URSULAS_CACHE[port]
@pytest.fixture(scope="module")
def idle_staker(testerchain, agency):
@ -837,7 +888,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,
@ -917,6 +969,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:
@ -927,10 +980,14 @@ 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()
return _ursulas
yield _ursulas
for ursula in _ursulas:
del MOCK_KNOWN_URSULAS_CACHE[ursula.rest_interface.port]
@pytest.fixture(scope="module")
@ -945,7 +1002,9 @@ 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()
@pytest.fixture(scope="module")
@ -958,10 +1017,13 @@ 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
#
# CLI
#

View File

@ -17,6 +17,9 @@
import pytest
from nucypher.characters.control.interfaces import BobInterface
from tests.utils.controllers import validate_json_rpc_response_data
def test_alice_rpc_character_control_create_policy(alice_rpc_test_client, create_policy_control_request):
alice_rpc_test_client.__class__.MESSAGE_ID = 0
@ -91,4 +94,6 @@ def test_bob_rpc_character_control_retrieve(bob_rpc_controller, retrieve_control
method_name, params = retrieve_control_request
request_data = {'method': method_name, 'params': params}
response = bob_rpc_controller.send(request_data)
assert 'jsonrpc' in response.data
assert validate_json_rpc_response_data(response=response,
method_name=method_name,
interface=BobInterface)

View File

@ -121,6 +121,7 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
# ...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,

View File

@ -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 - at most - 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
@ -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)
@ -157,6 +171,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

View File

@ -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()

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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

View File

@ -30,8 +30,8 @@ from nucypher.config.constants import (
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD,
TEMPORARY_DOMAIN
)
from tests.utils.ursula import select_test_port
from tests.constants import MOCK_IP_ADDRESS
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT
@pytest.fixture(scope='module')
@ -59,13 +59,15 @@ def test_ursula_init_with_local_keystore_signer(click_runner,
# Good signer...
pre_config_signer = KeystoreSigner.from_signer_uri(uri=mock_signer_uri)
deploy_port = select_test_port()
init_args = ('ursula', 'init',
'--network', TEMPORARY_DOMAIN,
'--worker-address', worker_account.address,
'--config-root', custom_filepath,
'--provider', mock_testerchain.provider_uri,
'--rest-host', MOCK_IP_ADDRESS,
'--rest-port', MOCK_URSULA_STARTING_PORT,
'--rest-port', deploy_port,
# The bit we are testing here
'--signer', mock_signer_uri)
@ -108,3 +110,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()

View File

@ -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
@ -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,

View File

@ -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()

View File

@ -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, db_filepath=tempfile.mkdtemp())
a.disenchant() # To stop Alice's publication threadpool. TODO: Maybe only start it at first enactment?

View File

@ -14,19 +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 <https://www.gnu.org/licenses/>.
"""
import time
from datetime import datetime
from unittest.mock import patch
import maya
import pytest
import time
from flask import Response
from umbral.keys import UmbralPublicKey
from unittest.mock import patch
import pytest_twisted
from twisted.internet import defer
from twisted.internet.defer import ensureDeferred
from twisted.internet.threads import deferToThread
from nucypher.characters.lawful import Ursula
from tests.utils.ursula import MOCK_KNOWN_URSULAS_CACHE
from umbral.keys import UmbralPublicKey
from nucypher.datastore.base import RecordField
from nucypher.network.nodes import Teacher
from nucypher.policy.collections import TreasureMap
from tests.mock.performance_mocks import (
NotAPublicKey,
NotARestApp,
@ -41,6 +43,10 @@ from tests.mock.performance_mocks import (
mock_stamp_call,
mock_verify_node
)
from tests.utils.middleware import SluggishLargeFleetMiddleware
from tests.utils.ursula import MOCK_KNOWN_URSULAS_CACHE
from umbral.keys import UmbralPublicKey
from flask import Response
"""
Node Discovery happens in phases. The first step is for a network actor to learn about the mere existence of a Node.
@ -51,7 +57,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).
@ -66,11 +72,13 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice):
# TODO: Consider changing this - #1449
assert VerificationTracker.node_verifications == 1
_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 = 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_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:
@ -86,7 +94,9 @@ def test_alice_can_learn_about_a_whole_bunch_of_ursulas(highperf_mocked_alice):
VerificationTracker.node_verifications = 0 # Cleanup
@pytest.mark.parametrize('fleet_of_highperf_mocked_ursulas', [100], indirect=True)
_POLICY_PRESERVER = []
def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas,
highperf_mocked_alice,
highperf_mocked_bob):
@ -125,3 +135,80 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas,
# TODO: Make some assertions about policy.
total_verified = sum(node.verified_node for node in highperf_mocked_alice.known_nodes)
assert total_verified == 30
_POLICY_PRESERVER.append(policy)
# @pytest_twisted.inlineCallbacks # TODO: Why does this, in concert with yield policy.publishing_mutex.when_complete, hang?
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.
In three parts.
"""
# The nodes who match the map distribution criteria.
nodes_we_expect_to_have_the_map = highperf_mocked_bob.matching_nodes_among(fleet_of_highperf_mocked_ursulas)
Teacher.verify_node = lambda *args, **kwargs: None
# # # 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:
# Causes rest app to be made (happens JIT in other testS)
highperf_mocked_alice.network_middleware.client.parse_node_or_host_and_port(node)
def _partial_rest_app(node):
def faster_receive_map(treasure_map_id, *args, **kwargs):
node.treasure_maps[treasure_map_id] = True
return Response(bytes(b"Sure, we stored it."), status=202)
return faster_receive_map
node.rest_app._actual_rest_app.view_functions._view_functions_registry['receive_treasure_map'] = _partial_rest_app(node)
highperf_mocked_alice.network_middleware = SluggishLargeFleetMiddleware()
policy = _POLICY_PRESERVER.pop()
with patch('umbral.keys.UmbralPublicKey.__eq__', lambda *args, **kwargs: True), mock_metadata_validation:
started = datetime.now()
# 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.
policy.publish_treasure_map(network_middleware=highperf_mocked_alice.network_middleware) # returns quickly.
# defer.setDebugging(True)
# PART II: We block for a little while to ensure that the distribution is going well.
nodes_that_have_the_map_when_we_unblock = policy.publishing_mutex.block_until_success_is_reasonably_likely()
little_while_ended_at = datetime.now()
# The number of nodes having the map is at least the minimum to have unblocked.
assert len(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.
# TODO: This line fails sometimes because the loop goes too fast.
assert len(nodes_that_have_the_map_when_we_unblock) == pytest.approx(policy.publishing_mutex._block_until_this_many_are_complete, .2)
# 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.
# This will block until the distribution is complete.
policy.publishing_mutex.block_until_complete()
complete_distribution_time = datetime.now() - started
partial_blocking_duration = little_while_ended_at - started
# Before Treasure Island (1741), this process took about 3 minutes.
if partial_blocking_duration.total_seconds() > 10:
pytest.fail(
f"Took too long ({partial_blocking_duration}) to contact {len(policy.publishing_mutex.nodes_contacted_during_partial_block)} nodes ({complete_distribution_time} total.)")
# TODO: Assert that no nodes outside those expected received the map.
assert complete_distribution_time.total_seconds() < 20
# But with debuggers and other processes running on laptops, we give a little leeway.
# We have the same number of successful responses as nodes we expected to have the map.
assert len(policy.publishing_mutex.completed) == len(nodes_we_expect_to_have_the_map)
nodes_that_got_the_map = sum(
policy.treasure_map.public_id() in u.treasure_maps for u in nodes_we_expect_to_have_the_map)
assert nodes_that_got_the_map == len(nodes_we_expect_to_have_the_map)

View File

@ -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()

View File

@ -23,34 +23,27 @@ 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)
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
# print(f"**********************Starting {any_other_ursula} loop")
any_other_ursula.start_learning_loop(now=True)
assert firstula in any_other_ursula.known_nodes
@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()
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()
@ -59,15 +52,7 @@ def test_get_cert_from_running_seed_node(ursula_federated_test_config):
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)
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]

View File

@ -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.
@ -71,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.
@ -99,22 +80,20 @@ 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
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()
@ -122,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 # 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, 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.

View File

@ -15,31 +15,31 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
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 nucypher.network.nicknames import nickname_from_seed
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)
teacher, learner, new_node = nodes
def test_emit_warning_upon_new_version(lonely_ursula_maker, caplog):
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):
@ -47,11 +47,20 @@ def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog):
warnings.append(event)
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(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) == 1
assert warnings[0]['log_format'] == learner.unknown_version_message.format(new_node,
new_node.TEACHER_VERSION,
assert len(warnings) == 2
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.
@ -76,8 +85,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 +100,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)

View File

@ -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(TreasureMap.NowhereToBeFound):
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):

View File

@ -0,0 +1,90 @@
"""
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 <https://www.gnu.org/licenses/>.
"""
import pytest
from nucypher.crypto.api import keccak_digest
from tests.utils.middleware import MockRestMiddleware
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
"""
treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id())
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,
enacted_federated_policy):
"""
The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it.
"""
enacted_federated_policy.publishing_mutex.block_until_complete()
treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id())
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
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.
# Bob will automatically load seednodes when getting the 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()

View File

@ -27,7 +27,7 @@ from nucypher.datastore.models import PolicyArrangement
from tests.utils.ursula import make_federated_ursulas
def test_alice_enacts_policies_in_policy_group_via_rest(enacted_federated_policy):
def test_alice_enacts_policies_in_policy_group_via_rest(enacted_federated_policy, reduced_memory_page_lmdb):
"""
Now that Alice has made a PolicyGroup, she can enact its policies, using Ursula's Public Key to encrypt each offer
and transmitting them via REST.
@ -40,8 +40,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()

View File

@ -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()
@ -43,7 +39,7 @@ def test_one_node_stores_a_bunch_of_others(federated_ursulas, ursula_federated_t
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.")

View File

@ -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,
@ -51,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
@ -66,6 +70,8 @@ def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_poli
that Bob can retrieve it with only the information about which he is privy pursuant to the PolicyGroup.
"""
bob = enacted_federated_policy.bob
_previous_domains = bob.learning_domains
bob.learning_domains = [] # Bob has no knowledge of the network.
# Of course, in the real world, Bob has sufficient information to reconstitute a PolicyGroup, gleaned, we presume,
# through a side-channel with Alice.
@ -75,8 +81,10 @@ def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_poli
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])
# Bob finds out about one Ursula (in the real world, a seed node, hardcoded based on his learning domain)
bob.done_seeding = False
bob.learning_domains = _previous_domains
# ...and then learns about the rest of the network.
bob.learn_from_teacher_node(eager=True)
@ -93,7 +101,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):

View File

@ -17,26 +17,26 @@
import tempfile
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 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")
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
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)
"""
@ -49,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
@ -386,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
@ -400,6 +91,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.
@ -551,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

1
tests/mock/serials.py Normal file

File diff suppressed because one or more lines are too long

View File

@ -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.
@ -108,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()
"""

View File

@ -16,13 +16,33 @@
"""
import json
from io import StringIO
from typing import Union
import nucypher
def get_fields(interface, method_name):
spec = getattr(interface, method_name)._schema
input_fields = [k for k, f in spec.load_fields.items() if f.required]
optional_fields = [k for k, f in spec.load_fields.items() if not f.required]
required_output_fileds = list(spec.dump_fields.keys())
return (
input_fields,
optional_fields,
required_output_fileds
)
def validate_json_rpc_response_data(response, method_name, interface):
required_output_fields = get_fields(interface, method_name)[-1]
assert 'jsonrpc' in response.data
for output_field in required_output_fields:
assert output_field in response.content
return True
class TestRPCResponse:
"""A mock RPC response object"""

View File

@ -14,7 +14,8 @@ 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 <https://www.gnu.org/licenses/>.
"""
import time
import random
import requests
import socket
@ -23,10 +24,15 @@ 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
class BadTestUrsulas(RuntimeError):
crash_right_now = True
class _TestMiddlewareClient(NucypherMiddlewareClient):
timeout = None
@ -51,13 +57,14 @@ 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 BadTestUrsulas(
"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.")
@ -89,6 +96,16 @@ class MockRestMiddleware(RestMiddleware):
class NotEnoughMockUrsulas(Ursula.NotEnoughUrsulas):
pass
class TEACHER_NODES:
@classmethod
def get(_cls, item, _default):
if item is TEMPORARY_DOMAIN:
nodes = tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values())[0:2]
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):
ursula = self.client._get_ursula_by_port(port)
@ -112,6 +129,17 @@ 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, node, *args, **kwargs):
time.sleep(random.randrange(5, 15) / 100)
result = super().put_treasure_map_on_node(node=node, *args, **kwargs)
time.sleep(random.randrange(5, 15) / 100)
return result
class _MiddlewareClientWithConnectionProblems(_TestMiddlewareClient):
def __init__(self, *args, **kwargs):

View File

@ -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:

View File

@ -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
@ -46,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)
@ -64,6 +65,7 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration,
starting_port = max(MOCK_KNOWN_URSULAS_CACHE.keys()) + 1
federated_ursulas = set()
for port in range(starting_port, starting_port+quantity):
ursula = ursula_config.produce(rest_port=port + 100,
@ -73,7 +75,6 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration,
federated_ursulas.add(ursula)
# Store this Ursula in our global testing cache.
port = ursula.rest_interface.port
MOCK_KNOWN_URSULAS_CACHE[port] = ursula
@ -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)
@ -160,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()