mirror of https://github.com/nucypher/nucypher.git
Merge pull request #1741 from nucypher/†reasure-ïsland-@ss-pi®ate-ß̆çh
Treasure Island...pull/2233/head
commit
704a361fc3
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -167,6 +167,7 @@ Whitepapers
|
|||
api/nucypher.network
|
||||
api/nucypher.datastore
|
||||
api/nucypher.crypto
|
||||
api/nucypher.acumen
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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}︎</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(),
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}︎</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,
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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', ),
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}'
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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()
|
||||
|
||||
|
||||
"""
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue