Merge pull request #366 from jMyles/learning-loop

Learning Loop Part IV and Working n==3 Federated Demo.
pull/370/merge
Justin Holmes 2018-07-21 17:49:21 -07:00 committed by GitHub
commit ecce0963da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 523 additions and 219 deletions

6
.gitignore vendored
View File

@ -19,4 +19,8 @@ _temp_test_datastore
chains
*logs*
*.ipynb*
.ethash
.ethash
examples/examples-runtime-cruft/*
examples/finnegans-wake.txt
mypy_reports/
reports/

View File

@ -3,11 +3,12 @@
# WIP w/ hendrix@3.0.0
import binascii
import datetime
import logging
import sys
import maya
from sandbox_resources import SandboxRestMiddleware
from nucypher.characters import Alice, Bob, Ursula
from nucypher.data_sources import DataSource
@ -15,29 +16,43 @@ from nucypher.data_sources import DataSource
from nucypher.network.middleware import RestMiddleware
from umbral.keys import UmbralPublicKey
URSULA = Ursula.from_rest_url(network_middleware=RestMiddleware(),
host="localhost",
port=3601,
federated_only=True)
root = logging.getLogger()
root.setLevel(logging.DEBUG)
network_middleware = SandboxRestMiddleware([URSULA])
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
root.addHandler(ch)
teacher_dht_port = 3552
teacher_rest_port = 3652
with open("examples-runtime-cruft/node-metadata-{}".format(teacher_rest_port), "r") as f:
f.seek(0)
teacher_bytes = binascii.unhexlify(f.read())
URSULA = Ursula.from_bytes(teacher_bytes, federated_only=True)
print("Will learn from {}".format(URSULA))
# network_middleware = SandboxRestMiddleware([URSULA])
#########
# Alice #
#########
ALICE = Alice(network_middleware=network_middleware,
ALICE = Alice(network_middleware=RestMiddleware(),
known_nodes=(URSULA,), # in lieu of seed nodes
federated_only=True) # TODO: 289
federated_only=True,
always_be_learning=True) # TODO: 289
# Here are our Policy details.
policy_end_datetime = maya.now() + datetime.timedelta(days=5)
m = 1
n = 1
m = 2
n = 3
label = b"secret/files/and/stuff"
# Alice grants to Bob.
BOB = Bob(known_nodes=(URSULA,), federated_only=True)
BOB = Bob(known_nodes=(URSULA,), federated_only=True, always_be_learning=True)
ALICE.start_learning_loop(now=True)
policy = ALICE.grant(BOB, label, m=m, n=n,
expiration=policy_end_datetime)
@ -52,15 +67,16 @@ del ALICE
#####################
# some time passes. #
# ... #
# #
# ... #
# And now for Bob. #
#####################
# Bob wants to join the policy so that he can receive any future
# data shared on it.
# He needs a few piece of knowledge to do that.
# He needs a few pieces of knowledge to do that.
BOB.join_policy(label, # The label - he needs to know what data he's after.
alices_pubkey_bytes_saved_for_posterity, # To verify the signature, he'll need Alice's public key.
verify_sig=True, # And yes, he usually wants to verify that signature.
# He can also bootstrap himself onto the network more quickly
# by providing a list of known nodes at this time.
node_list=[("localhost", 3601)]
@ -136,8 +152,8 @@ for counter, plaintext in enumerate(finnegans_wake):
# and the DataSource which produced it.
alice_pubkey_restored_from_ancient_scroll = UmbralPublicKey.from_bytes(alices_pubkey_bytes_saved_for_posterity)
delivered_cleartexts = BOB.retrieve(message_kit=message_kit,
data_source=datasource_as_understood_by_bob,
alice_verifying_key=alice_pubkey_restored_from_ancient_scroll)
data_source=datasource_as_understood_by_bob,
alice_verifying_key=alice_pubkey_restored_from_ancient_scroll)
# We show that indeed this is the passage originally encrypted by the DataSource.
assert plaintext == delivered_cleartexts[0]

View File

@ -5,40 +5,69 @@
# WIP w/ hendrix@tags/3.3.0rc1
import os
import os, sys
from cryptography.hazmat.primitives.asymmetric import ec
import asyncio
from contextlib import suppress
from hendrix.deploy.tls import HendrixDeployTLS
from hendrix.facilities.services import ExistingKeyTLSContextFactory
from nucypher.characters import Ursula
from OpenSSL.crypto import X509
from OpenSSL.SSL import TLSv1_2_METHOD
from nucypher.crypto.api import generate_self_signed_certificate
DB_NAME = "non-mining-proxy-node"
DB_NAME = "examples-runtime-cruft/db"
STARTING_PORT = 3501
_URSULA = Ursula(dht_port=3501,
rest_port=3601,
rest_host="localhost",
dht_host="localhost",
db_name=DB_NAME,
federated_only=True)
_URSULA.dht_listen()
CURVE = ec.SECP256R1
cert, private_key = generate_self_signed_certificate(_URSULA.stamp.fingerprint().decode(), CURVE)
import logging, binascii
import sys
deployer = HendrixDeployTLS("start",
{"wsgi":_URSULA.rest_app, "https_port": _URSULA.rest_interface.port},
key=private_key,
cert=X509.from_cryptography(cert),
context_factory=ExistingKeyTLSContextFactory,
context_factory_kwargs={"curve_name": "prime256v1",
"sslmethod": TLSv1_2_METHOD})
root = logging.getLogger()
root.setLevel(logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
root.addHandler(ch)
def spin_up_ursula(dht_port, rest_port, db_name, teachers=()):
metadata_file = "examples-runtime-cruft/node-metadata-{}".format(rest_port)
asyncio.set_event_loop(asyncio.new_event_loop()) # Ugh. Awful. But needed until we shed the DHT.
_URSULA = Ursula(dht_port=dht_port,
rest_port=rest_port,
rest_host="localhost",
dht_host="localhost",
db_name=db_name,
federated_only=True,
known_nodes=teachers,
)
_URSULA.dht_listen()
try:
with open(metadata_file, "w") as f:
f.write(bytes(_URSULA).hex())
_URSULA.start_learning_loop()
_URSULA.get_deployer().run()
finally:
os.remove(db_name)
os.remove(metadata_file)
if __name__ == "__main__":
try:
teacher_dht_port = sys.argv[2]
teacher_rest_port = int(teacher_dht_port) + 100
with open("examples-runtime-cruft/node-metadata-{}".format(teacher_rest_port), "r") as f:
f.seek(0)
teacher_bytes = binascii.unhexlify(f.read())
teacher = Ursula.from_bytes(teacher_bytes, federated_only=True)
teachers = (teacher, )
print("Will learn from {}".format(teacher))
except (IndexError, FileNotFoundError):
teachers = ()
dht_port = sys.argv[1]
rest_port = int(dht_port) + 100
db_name = DB_NAME + str(rest_port)
spin_up_ursula(dht_port, rest_port, db_name, teachers=teachers)
try:
deployer.run()
finally:
os.remove(DB_NAME)

View File

@ -12,9 +12,9 @@ import kademlia
import maya
import time
from eth_keys import KeyAPI as EthKeyAPI
from eth_keys.datatypes import Signature
from kademlia.network import Server
from kademlia.utils import digest
from twisted.internet import task, threads
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from constant_sorrow import constants, default_constant_splitter
@ -32,7 +32,6 @@ from nucypher.network.middleware import RestMiddleware
from nucypher.network.nodes import VerifiableNode
from nucypher.network.protocols import InterfaceInfo
from nucypher.network.server import NucypherDHTServer, NucypherSeedOnlyDHTServer, ProxyRESTServer
from twisted.internet import task, threads
from umbral.keys import UmbralPublicKey
from umbral.signing import Signature
@ -47,7 +46,9 @@ class Character:
_default_crypto_powerups = None
_stamp = None
_SECONDS_DELAY_BETWEEN_LEARNING = 2
_SHORT_LEARNING_DELAY = 5
_LONG_LEARNING_DELAY = 90
_ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN = 10
from nucypher.network.protocols import SuspiciousActivity # Ship this exception with every Character.
@ -71,7 +72,7 @@ class Character:
checksum_address: bytes = None,
always_be_learning=False,
start_learning_on_same_thread=False,
known_nodes: Set = (),
known_nodes: tuple = (),
abort_on_learning_error: bool = False,
):
"""
@ -136,6 +137,7 @@ class Character:
self._current_teacher_node = None
self._learning_task = task.LoopingCall(self.keep_learning_about_nodes)
self._learning_round = 0
self._rounds_without_new_nodes = 0
if always_be_learning:
self.start_learning_loop(now=start_learning_on_same_thread)
@ -234,8 +236,8 @@ class Character:
def remember_node(self, node):
# TODO: 334
listeners = self._learning_listeners.pop(node.canonical_public_address, ())
address = node.canonical_public_address
listeners = self._learning_listeners.pop(node.checksum_public_address, ())
address = node.checksum_public_address
self._known_nodes[address] = node
self.log.info("Remembering {}, popping {} listeners.".format(node.checksum_public_address, len(listeners)))
@ -247,7 +249,7 @@ class Character:
if self._learning_task.running:
return False
else:
d = self._learning_task.start(interval=self._SECONDS_DELAY_BETWEEN_LEARNING, now=now)
d = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now)
d.addErrback(self.handle_learning_errors)
return d
@ -300,7 +302,7 @@ class Character:
"Learning loop isn't started; can't learn about nodes now. You can ovverride this with force=True.")
elif force:
self.log.info("Learning loop wasn't started; forcing start now.")
self._learning_task.start(self._SECONDS_DELAY_BETWEEN_LEARNING, now=True)
self._learning_task.start(self._SHORT_LEARNING_DELAY, now=True)
def keep_learning_about_nodes(self):
"""
@ -312,25 +314,58 @@ class Character:
self._node_ids_to_learn_about_immediately.update(canonical_addresses) # hmmmm
self.learn_about_nodes_now()
def block_until_nodes_are_known(self, canonical_addresses: Set, timeout=10, allow_missing=0,
learn_on_this_thread=False):
# TODO: Dehydrate these next two methods.
def block_until_number_of_known_nodes_is(self, number_of_nodes_to_know: int,
timeout=10,
learn_on_this_thread=False):
start = maya.now()
starting_round = self._learning_round
while True:
rounds_undertaken = self._learning_round - starting_round
if len(self._known_nodes) >= number_of_nodes_to_know:
if rounds_undertaken:
self.log.info("Learned about enough nodes after {} rounds.".format(rounds_undertaken))
return True
if not self._learning_task.running:
self.log.warning("Blocking to learn about nodes, but learning loop isn't running.")
if learn_on_this_thread:
self.learn_from_teacher_node(eager=True)
rounds_undertaken = self._learning_round - starting_round
if (maya.now() - start).seconds < timeout:
if canonical_addresses.issubset(self._known_nodes):
self.log.info("Learned about all nodes after {} rounds.".format(rounds_undertaken))
return True
if (maya.now() - start).seconds > timeout:
if not self._learning_task.running:
raise self.NotEnoughUrsulas(
"We didn't discover any nodes because the learning loop isn't running. Start it with start_learning().")
else:
time.sleep(.1)
raise self.NotEnoughUrsulas("After {} seconds and {} rounds, didn't find {} nodes".format(
timeout, rounds_undertaken, number_of_nodes_to_know))
else:
time.sleep(.1)
def block_until_specific_nodes_are_known(self,
canonical_addresses: Set,
timeout=10,
allow_missing=0,
learn_on_this_thread=False):
start = maya.now()
starting_round = self._learning_round
while True:
rounds_undertaken = self._learning_round - starting_round
if canonical_addresses.issubset(self._known_nodes):
if rounds_undertaken:
self.log.info("Learned about all nodes after {} rounds.".format(rounds_undertaken))
return True
if not self._learning_task.running:
self.log.warning("Blocking to learn about nodes, but learning loop isn't running.")
if learn_on_this_thread:
self.learn_from_teacher_node(eager=True)
if (maya.now() - start).seconds > timeout:
still_unknown = canonical_addresses.difference(self._known_nodes)
if len(still_unknown) <= allow_missing:
@ -342,31 +377,46 @@ class Character:
raise self.NotEnoughUrsulas("After {} seconds and {} rounds, didn't find these {} nodes: {}".format(
timeout, rounds_undertaken, len(still_unknown), still_unknown))
else:
time.sleep(.1)
def learn_from_teacher_node(self, eager=True):
"""
Sends a request to node_url to find out about known nodes.
"""
self._learning_round += 1
current_teacher = self.current_teacher_node()
try:
current_teacher = self.current_teacher_node()
except self.NotEnoughUrsulas as e:
self.log.warning("Can't learn right now: {}".format(e.args[0]))
return
rest_address = current_teacher.rest_interface.host
port = current_teacher.rest_interface.port
# TODO: Do we really want to try to learn about all these nodes instantly? Hearing this traffic might give insight to an attacker.
response = self.network_middleware.get_nodes_via_rest(rest_address,
port, node_ids=self._node_ids_to_learn_about_immediately)
if response.status_code != 200:
raise RuntimeError
signature, nodes = signature_splitter(response.content, return_remainder=True)
node_list = Ursula.batch_from_bytes(nodes, federated_only=self.federated_only) # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node.
if VerifiableNode in self.__class__.__bases__:
announce_nodes = [self]
else:
announce_nodes = None
self.log.info("Learning round {}. Teacher: {} knew about {} nodes.".format(self._learning_round,
current_teacher.checksum_public_address,
len(node_list)))
response = self.network_middleware.get_nodes_via_rest(rest_address,
port,
nodes_i_need=self._node_ids_to_learn_about_immediately,
announce_nodes=announce_nodes)
if response.status_code != 200:
raise RuntimeError("Bad response from teacher: {} - {}".format(response, response.content
))
signature, nodes = signature_splitter(response.content, return_remainder=True)
node_list = Ursula.batch_from_bytes(nodes,
federated_only=self.federated_only) # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node.
new_nodes = []
for node in node_list:
if node.checksum_public_address in self._known_nodes:
if node.checksum_public_address in self._known_nodes or node.checksum_public_address == self.checksum_public_address:
continue # TODO: 168 Check version and update if required.
try:
@ -380,10 +430,37 @@ class Character:
"Propagated by: {}:{}".format(current_teacher.checksum_public_address,
rest_address, port)
self.log.warning(message)
self.log.info("Prevously unknown node: {}".format(node.checksum_public_address))
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
self.remember_node(node)
new_nodes.append(node)
self._adjust_learning(new_nodes)
self.log.info("Learning round {}. Teacher: {} knew about {} nodes, {} were new.".format(self._learning_round,
current_teacher.checksum_public_address,
len(node_list),
len(new_nodes)),
)
def _adjust_learning(self, node_list):
"""
Takes a list of new nodes, adjusts learning accordingly.
Currently, simply slows down learning loop when no new nodes have been discovered in a while.
TODO: Do other important things - scrub, bucket, etc.
"""
if node_list:
self._rounds_without_new_nodes = 0
self._learning_task.interval = self._SHORT_LEARNING_DELAY
else:
self._rounds_without_new_nodes += 1
if self._rounds_without_new_nodes > self._ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN:
self.log.info("After {} rounds with no new nodes, it's time to slow down to {} seconds.".format(
self._ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN,
self._LONG_LEARNING_DELAY))
self._learning_task.interval = self._LONG_LEARNING_DELAY
def _push_certain_newly_discovered_nodes_here(self, queue_to_push, node_addresses):
"""
@ -500,13 +577,14 @@ class Character:
if signature_to_use:
is_valid = signature_to_use.verify(message, sender_pubkey_sig)
if not is_valid:
raise mystery_stranger.InvalidSignature("Signature for message isn't valid: {}".format(signature_to_use))
raise mystery_stranger.InvalidSignature(
"Signature for message isn't valid: {}".format(signature_to_use))
else:
raise self.InvalidSignature("No signature provided -- signature presumed invalid.")
return cleartext
"""
Next we have decrypt(), sign(), and generate_self_signed_certificate() - these use the private
Next we have decrypt() and sign() - these use the private
keys of their respective powers; any character who has these powers can use these functions.
If they don't have the correct Power, the appropriate PowerUpError is raised.
@ -518,10 +596,6 @@ class Character:
def sign(self, message):
return self._crypto_power.power_ups(SigningPower).sign(message)
def generate_self_signed_certificate(self):
signing_power = self._crypto_power.power_ups(SigningPower)
return signing_power.generate_self_signed_cert(self.stamp.fingerprint().decode())
"""
And finally, some miscellaneous but generally-applicable abilities:
"""
@ -633,7 +707,7 @@ class Alice(Character, PolicyAuthor):
return policy
def grant(self, bob, uri, m=None, n=None, expiration=None, deposit=None, ursulas=None):
def grant(self, bob, uri, m=None, n=None, expiration=None, deposit=None, handpicked_ursulas=None):
if not m:
# TODO: get m from config #176
raise NotImplementedError
@ -649,6 +723,8 @@ class Alice(Character, PolicyAuthor):
deposit = self.network_middleware.get_competitive_rate()
if deposit == NotImplemented:
deposit = constants.NON_PAYMENT(b"0000000")
if handpicked_ursulas is None:
handpicked_ursulas = set()
policy = self.create_policy(bob, uri, m, n)
@ -658,10 +734,26 @@ class Alice(Character, PolicyAuthor):
# Users may decide to inject some market strategies here.
#
# TODO: 289
# If we're federated only, we need to block to make sure we have enough nodes.
if self.federated_only and len(self._known_nodes) < n:
good_to_go = self.block_until_number_of_known_nodes_is(n, learn_on_this_thread=True)
if not good_to_go:
raise ValueError(
"To make a Policy in federated mode, you need to know about\
all the Ursulas you need (in this case, {}); there's no other way to\
know which nodes to use. Either pass them here or when you make\
the Policy, or run the learning loop on a network with enough Ursulas.".format(self.n))
if len(handpicked_ursulas) < n:
number_of_ursulas_needed = n - len(handpicked_ursulas)
new_ursulas = random.sample(list(self._known_nodes.values()), number_of_ursulas_needed)
handpicked_ursulas.update(new_ursulas)
policy.make_arrangements(network_middleware=self.network_middleware,
deposit=deposit,
expiration=expiration,
ursulas=ursulas,
handpicked_ursulas=handpicked_ursulas,
)
# REST call happens here, as does population of TreasureMap.
@ -746,14 +838,14 @@ class Bob(Character):
if block:
if new_thread:
return threads.deferToThread(self.block_until_nodes_are_known, unknown_ursulas,
return threads.deferToThread(self.block_until_specific_nodes_are_known, unknown_ursulas,
timeout=timeout,
allow_missing=allow_missing)
else:
self.block_until_nodes_are_known(unknown_ursulas,
timeout=timeout,
allow_missing=allow_missing,
learn_on_this_thread=True)
self.block_until_specific_nodes_are_known(unknown_ursulas,
timeout=timeout,
allow_missing=allow_missing,
learn_on_this_thread=True)
return unknown_ursulas, known_ursulas
@ -854,15 +946,14 @@ class Bob(Character):
for counter, capsule in enumerate(work_order.capsules):
# TODO: Ursula is actually supposed to sign this. See #141.
# TODO: Maybe just update the work order here instead of setting it anew.
work_orders_by_ursula = self._saved_work_orders[bytes(work_order.ursula.canonical_public_address)]
work_orders_by_ursula = self._saved_work_orders[work_order.ursula.checksum_public_address]
work_orders_by_ursula[capsule] = work_order
return cfrags
def get_ursula(self, ursula_id):
return self._ursulas[ursula_id]
def join_policy(self, label, alice_pubkey_sig,
node_list=None, verify_sig=True):
def join_policy(self, label, alice_pubkey_sig, node_list=None):
if node_list:
self._node_ids_to_learn_about_immediately.update(node_list)
treasure_map = self.get_treasure_map(alice_pubkey_sig, label)
@ -876,7 +967,7 @@ class Bob(Character):
verifying=alice_verifying_key)
hrac, map_id = self.construct_hrac_and_map_id(alice_verifying_key, data_source.label)
self.follow_treasure_map(map_id=map_id)
self.follow_treasure_map(map_id=map_id, block=True)
work_orders = self.generate_work_orders(map_id, message_kit.capsule)
@ -886,15 +977,16 @@ class Bob(Character):
cfrags = self.get_reencrypted_cfrags(work_order)
message_kit.capsule.attach_cfrag(cfrags[0])
try:
delivered_cleartext = self.verify_from(data_source,
message_kit,
decrypt=True,
delegator_signing_key=alice_verifying_key)
except self.InvalidSignature as e:
raise RuntimeError(e)
else:
cleartexts.append(delivered_cleartext)
verified, delivered_cleartext = self.verify_from(data_source,
message_kit,
decrypt=True,
delegator_signing_key=alice_verifying_key)
if verified:
cleartexts.append(delivered_cleartext)
else:
raise RuntimeError(
"Not verified - replace this with real message.") # TODO: Actually raise an error in verify_from instead of here 358
return cleartexts
@ -908,6 +1000,7 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
InterfaceInfo)
_dht_server_class = NucypherDHTServer
_alice_class = Alice
# TODO: Maybe this wants to be a registry, so that, for example, TLSHostingPower still can enjoy default status, but on a different class
_default_crypto_powerups = [SigningPower, EncryptingPower]
class NotFound(Exception):
@ -930,7 +1023,11 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
federated_only=False,
checksum_address=None,
always_be_learning=None,
crypto_power=None
crypto_power=None,
tls_curve=None,
tls_private_key=None, # Obviously config here. 361
known_nodes=(),
**character_kwargs
):
VerifiableNode.__init__(self, interface_signature=interface_signature)
@ -946,13 +1043,18 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
always_be_learning=always_be_learning,
federated_only=federated_only,
crypto_power=crypto_power,
abort_on_learning_error=abort_on_learning_error)
abort_on_learning_error=abort_on_learning_error,
known_nodes=known_nodes,
**character_kwargs)
if not federated_only:
Miner.__init__(self, miner_agent=miner_agent, is_me=is_me, checksum_address=checksum_address)
blockchain_power = BlockchainPower(blockchain=self.blockchain, account=self.checksum_public_address)
self._crypto_power.consume_power_up(blockchain_power)
ProxyRESTServer.__init__(self, host=rest_host, port=rest_port, db_name=db_name)
ProxyRESTServer.__init__(self, host=rest_host, port=rest_port,
db_name=db_name,
tls_private_key=tls_private_key, tls_curve=tls_curve
)
if is_me is True:
# TODO: 340
@ -1035,7 +1137,7 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
@classmethod
def from_bytes(cls, ursula_as_bytes, federated_only=False):
signature, identity_evidence, verifying_key, encrypting_key, public_address, rest_info, dht_info = cls._internal_splitter(
ursula_as_bytes)
ursula_as_bytes)
stranger_ursula_from_public_keys = cls.from_public_keys(
{SigningPower: verifying_key, EncryptingPower: encrypting_key},
interface_signature=signature,
@ -1055,7 +1157,8 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
stranger_ursulas = []
ursulas_attrs = cls._internal_splitter.repeat(ursulas_as_bytes)
for (signature, identity_evidence, verifying_key, encrypting_key, public_address, rest_info, dht_info) in ursulas_attrs:
for (signature, identity_evidence, verifying_key, encrypting_key, public_address, rest_info,
dht_info) in ursulas_attrs:
stranger_ursula_from_public_keys = cls.from_public_keys(
{SigningPower: verifying_key, EncryptingPower: encrypting_key},
interface_signature=signature,
@ -1071,11 +1174,9 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
return stranger_ursulas
def __bytes__(self):
message = self.canonical_public_address + self.rest_interface
interface_info = VariableLengthBytestring(self.rest_interface)
if self.dht_interface:
message += self.dht_interface
interface_info += VariableLengthBytestring(self.dht_interface)
identity_evidence = VariableLengthBytestring(self._evidence_of_decentralized_identity)

View File

@ -125,7 +125,7 @@ def generate_self_signed_certificate(common_name, curve, private_key=None, days_
cert = cert.serial_number(x509.random_serial_number())
cert = cert.not_valid_before(now)
cert = cert.not_valid_after(now + datetime.timedelta(days=days_valid))
# TODO: What domain here? Not localhost presumably - ENS? #146
# TODO: What are we going to do about domain name here? 179
cert = cert.add_extension(x509.SubjectAlternativeName([x509.DNSName(u"localhost")]), critical=False)
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
return cert, private_key

View File

@ -5,7 +5,7 @@ from eth_keys.datatypes import PublicKey, Signature as EthSignature
from eth_utils import keccak
from nucypher.keystore import keypairs
from nucypher.keystore.keypairs import SigningKeypair, EncryptingKeypair
from nucypher.keystore.keypairs import SigningKeypair, EncryptingKeypair, HostingKeypair
from umbral import pre
from umbral.keys import UmbralPublicKey, UmbralPrivateKey, UmbralKeyingMaterial
@ -131,6 +131,7 @@ class BlockchainPower(CryptoPowerUp):
class KeyPairBasedPower(CryptoPowerUp):
confers_public_key = True
_keypair_class = keypairs.Keypair
_default_private_key_class = UmbralPrivateKey
def __init__(self,
pubkey: UmbralPublicKey = None,
@ -146,17 +147,17 @@ class KeyPairBasedPower(CryptoPowerUp):
# UmbralPublicKey if they provided such a thing.
if pubkey:
try:
key_to_pass_to_keypair = pubkey.as_umbral_pubkey()
public_key = pubkey.as_umbral_pubkey()
except AttributeError:
try:
key_to_pass_to_keypair = UmbralPublicKey.from_bytes(pubkey)
public_key = UmbralPublicKey.from_bytes(pubkey)
except TypeError:
key_to_pass_to_keypair = pubkey
public_key = pubkey
self.keypair = self._keypair_class(
public_key=public_key)
else:
# They didn't even pass pubkey_bytes. We'll generate a keypair.
key_to_pass_to_keypair = UmbralPrivateKey.gen_key()
self.keypair = self._keypair_class(
umbral_key=key_to_pass_to_keypair)
# They didn't even pass a public key. We have no choice but to generate a keypair.
self.keypair = self._keypair_class(generate_keys_if_needed=True)
def __getattr__(self, item):
if item in self.provides:
@ -184,7 +185,7 @@ class DerivedKeyBasedPower(CryptoPowerUp):
class SigningPower(KeyPairBasedPower):
_keypair_class = SigningKeypair
not_found_error = NoSigningPower
provides = ("sign", "generate_self_signed_cert", "get_signature_stamp")
provides = ("sign", "get_signature_stamp")
class EncryptingPower(KeyPairBasedPower):
@ -193,6 +194,11 @@ class EncryptingPower(KeyPairBasedPower):
provides = ("decrypt",)
class TLSHostingPower(KeyPairBasedPower):
_keypair_class = HostingKeypair
provides = ("get_deployer",)
class DelegatingPower(DerivedKeyBasedPower):
def __init__(self):

View File

@ -1,14 +1,19 @@
import sha3
from typing import Union
import sha3
from OpenSSL.SSL import TLSv1_2_METHOD
from OpenSSL.crypto import X509
from cryptography.hazmat.primitives.asymmetric import ec
from constant_sorrow import constants
from hendrix.deploy.tls import HendrixDeployTLS
from hendrix.facilities.services import ExistingKeyTLSContextFactory
from nucypher.crypto import api as API
from nucypher.crypto.api import generate_self_signed_certificate
from constant_sorrow.constants import PUBLIC_ONLY
from umbral.keys import UmbralPrivateKey, UmbralPublicKey
from umbral import pre
from umbral.config import default_curve
from nucypher.crypto.kits import MessageKit
from nucypher.crypto.signing import SignatureStamp, StrangerStamp
from umbral import pre
from umbral.config import default_curve
from umbral.keys import UmbralPrivateKey, UmbralPublicKey
from umbral.signing import Signature, Signer
@ -17,29 +22,31 @@ class Keypair(object):
A parent Keypair class for all types of Keypairs.
"""
_private_key_source = UmbralPrivateKey.gen_key
_public_key_method = "get_pubkey"
def __init__(self,
umbral_key: Union[UmbralPrivateKey, UmbralPublicKey] = None,
private_key=None,
public_key=None,
generate_keys_if_needed=True):
"""
Initalizes a Keypair object with an Umbral key object.
:param umbral_key: An UmbralPrivateKey or UmbralPublicKey
:param generate_keys_if_needed: Generate keys or not?
"""
try:
self.pubkey = umbral_key.get_pubkey()
self._privkey = umbral_key
except NotImplementedError:
self.pubkey = umbral_key
self._privkey = PUBLIC_ONLY
except AttributeError:
# They didn't pass anything we recognize as a valid key.
if generate_keys_if_needed:
self._privkey = UmbralPrivateKey.gen_key()
self.pubkey = self._privkey.get_pubkey()
else:
raise ValueError(
"Either pass a valid key as umbral_key or, if you want to generate keys, set generate_keys_if_needed to True.")
if private_key and public_key:
raise ValueError("Pass either private_key or public_key - not both.")
elif private_key:
self.pubkey = getattr(private_key, self._public_key_method)()
self._privkey = private_key
elif public_key:
self.pubkey = public_key
self._privkey = constants.PUBLIC_ONLY
elif generate_keys_if_needed:
self._privkey = self._private_key_source()
self.pubkey = getattr(self._privkey, self._public_key_method)()
else:
raise ValueError(
"Either pass a valid key or, if you want to generate keys, set generate_keys_if_needed to True.")
def serialize_pubkey(self, as_b64=False) -> bytes:
"""
@ -103,14 +110,49 @@ class SigningKeypair(Keypair):
signature_der_bytes = API.ecdsa_sign(message, self._privkey)
return Signature.from_bytes(signature_der_bytes, der_encoded=True)
def generate_self_signed_cert(self, common_name):
cryptography_key = self._privkey.to_cryptography_privkey()
return generate_self_signed_certificate(common_name, default_curve(), cryptography_key)
def get_signature_stamp(self):
if self._privkey == PUBLIC_ONLY:
if self._privkey == constants.PUBLIC_ONLY:
return StrangerStamp(verifying_key=self.pubkey)
else:
signer = Signer(self._privkey)
return SignatureStamp(verifying_key=self.pubkey, signer=signer)
class HostingKeypair(Keypair):
"""
A keypair for TLS'ing.
"""
_private_key_source = ec.generate_private_key
_public_key_method = "public_key"
_DEFAULT_CURVE = ec.SECP384R1
def __init__(self,
common_name,
private_key: Union[UmbralPrivateKey, UmbralPublicKey] = None,
certificate=None,
curve=None,
generate_keys_if_needed=True):
self.curve = curve or self._DEFAULT_CURVE
if not certificate:
self._certificate, private_key = generate_self_signed_certificate(common_name=common_name,
private_key=private_key,
curve=self.curve)
else:
self._certificate = certificate
super().__init__(private_key=private_key)
def generate_self_signed_cert(self, common_name):
cryptography_key = self._privkey.to_cryptography_privkey()
return generate_self_signed_certificate(common_name, default_curve(), cryptography_key)
def get_deployer(self, rest_app, port):
return HendrixDeployTLS("start",
key=self._privkey,
cert=X509.from_cryptography(self._certificate),
context_factory=ExistingKeyTLSContextFactory,
context_factory_kwargs={"curve_name": self.curve.name,
"sslmethod": TLSv1_2_METHOD},
options={"wsgi": rest_app, "https_port": port})

View File

@ -1,12 +1,27 @@
import requests
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from umbral.fragments import CapsuleFrag
class RestMiddleware:
def consider_arrangement(self, ursula, arrangement=None):
pass
def consider_arrangement(self, arrangement):
node = arrangement.ursula
port = node.rest_interface.port
address = node.rest_interface.host
response = requests.post("https://{}:{}/consider_arrangement".format(address, port), bytes(arrangement), verify=False)
if not response.status_code == 200:
raise RuntimeError("Bad response: {}".format(response.content))
return response
def enact_policy(self, ursula, id, payload):
port = ursula.rest_interface.port
address = ursula.rest_interface.host
response = requests.post('https://{}:{}/kFrag/{}'.format(address, port, id.hex()), payload, verify=False)
if not response.status_code == 200:
raise RuntimeError("Bad response: {}".format(response.content))
return True, ursula.stamp.as_umbral_pubkey()
def reencrypt(self, work_order):
ursula_rest_response = self.send_work_order_payload_to_ursula(work_order)
@ -40,10 +55,17 @@ class RestMiddleware:
def node_information(self, host, port):
return requests.get("https://{}:{}/public_information".format(host, port), verify=False) # TODO: TLS-only.
def get_nodes_via_rest(self, address, port, node_ids=None):
if node_ids:
def get_nodes_via_rest(self, address, port, announce_nodes=None, nodes_i_need=None):
if nodes_i_need:
# TODO: This needs to actually do something.
# Include node_ids in the request; if the teacher node doesn't know about the
# nodes matching these ids, then it will ask other nodes via the DHT or whatever.
raise NotImplementedError
response = requests.get("https://{}:{}/list_nodes".format(address, port), verify=False) # TODO: TLS-only.
pass
if announce_nodes:
response = requests.post("https://{}:{}/node_metadata".format(address, port),
verify=False,
data=bytes().join(bytes(n) for n in announce_nodes)) # TODO: TLS-only.
else:
response = requests.get("https://{}:{}/node_metadata".format(address, port),
verify=False) # TODO: TLS-only.
return response

View File

@ -1,7 +1,9 @@
from nucypher.crypto.powers import BlockchainPower, SigningPower, EncryptingPower, PowerUpError, NoSigningPower
from nucypher.blockchain.eth.actors import only_me
from nucypher.crypto.powers import BlockchainPower, SigningPower, EncryptingPower, NoSigningPower
from constant_sorrow import constants
from nucypher.network.protocols import SuspiciousActivity
from eth_keys.datatypes import Signature as EthSignature
import maya
class VerifiableNode:
@ -11,7 +13,7 @@ class VerifiableNode:
verified_interface = False
_verified_node = False
def __init__(self, interface_signature):
def __init__(self, interface_signature=constants.NOT_SIGNED.bool_value(False)):
self._interface_signature_object = interface_signature
class InvalidNode(SuspiciousActivity):

View File

@ -136,7 +136,7 @@ class InterfaceInfo:
def __init__(self, host, port):
self.host = host
self.port = port
self.port = int(port)
@classmethod
def from_bytes(cls, url_string):

View File

@ -6,23 +6,22 @@ from typing import ClassVar
import kademlia
from apistar import http, Route, App
from apistar.http import Response
from constant_sorrow import constants
from kademlia.crawling import NodeSpiderCrawl
from kademlia.network import Server
from kademlia.utils import digest
from bytestring_splitter import VariableLengthBytestring, BytestringSplitter
from bytestring_splitter import VariableLengthBytestring
from constant_sorrow import constants
from hendrix.experience import crosstown_traffic
from nucypher.config.configs import NetworkConfiguration
from nucypher.crypto.constants import PUBLIC_ADDRESS_LENGTH, PUBLIC_KEY_LENGTH
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import EncryptingPower, SigningPower
from nucypher.crypto.powers import SigningPower, TLSHostingPower
from nucypher.keystore.keypairs import HostingKeypair
from nucypher.keystore.threading import ThreadedSession
from nucypher.network.protocols import NucypherSeedOnlyProtocol, NucypherHashProtocol, InterfaceInfo
from nucypher.network.storage import SeedOnlyStorage
from umbral import pre
from umbral.fragments import KFrag
from umbral.keys import UmbralPublicKey
from umbral.signing import Signature
class NucypherDHTServer(Server):
@ -104,11 +103,21 @@ class NucypherSeedOnlyDHTServer(NucypherDHTServer):
class ProxyRESTServer:
def __init__(self, host=None, port=None, db_name=None, *args, **kwargs):
def __init__(self,
host=None,
port=None,
db_name=None,
tls_private_key=None,
tls_curve=None,
*args, **kwargs):
self.rest_interface = InterfaceInfo(host=host, port=port)
self.db_name = db_name
self._rest_app = None
tls_hosting_keypair = HostingKeypair(common_name=self.checksum_public_address,
private_key=tls_private_key, curve=tls_curve)
tls_hosting_power = TLSHostingPower(keypair=tls_hosting_keypair)
self._crypto_power.consume_power_up(tls_hosting_power)
@classmethod
def from_config(cls, network_config: NetworkConfiguration = None):
@ -140,8 +149,10 @@ class ProxyRESTServer:
self.reencrypt_via_rest),
Route('/public_information', 'GET',
self.public_information),
Route('/list_nodes', 'GET',
self.list_all_active_nodes_about_which_we_know),
Route('/node_metadata', 'GET',
self.all_known_nodes),
Route('/node_metadata', 'POST',
self.node_metadata_exchange),
Route('/consider_arrangement',
'POST',
self.consider_arrangement),
@ -188,14 +199,38 @@ class ProxyRESTServer:
return response
def list_all_active_nodes_about_which_we_know(self, request: http.Request):
def all_known_nodes(self, request: http.Request):
headers = {'Content-Type': 'application/octet-stream'}
# TODO: mm hmmph *slowly exhales* fffff. Some 227 right here.
ursulas_as_bytes = bytes().join(bytes(n) for n in self._known_nodes.values())
ursulas_as_bytes += bytes(self)
signature = self.stamp(ursulas_as_bytes)
return Response(bytes(signature) + ursulas_as_bytes, headers=headers)
def node_metadata_exchange(self, request: http.Request, query_params: http.QueryParams):
nodes = self.batch_from_bytes(request.body, federated_only=self.federated_only)
# TODO: This logic is basically repeated in learn_from_teacher_node. Let's find a better way.
for node in nodes:
if node.checksum_public_address in self._known_nodes:
continue # TODO: 168 Check version and update if required.
@crosstown_traffic()
def learn_about_announced_nodes():
try:
node.verify_node(self.network_middleware, accept_federated_only=self.federated_only)
except node.SuspiciousActivity:
# TODO: Account for possibility that stamp, rather than interface, was bad.
message = "Suspicious Activity: Discovered node with bad signature: {}. " \
" Announced via REST." # TODO: Include data about caller?
self.log.warning(message)
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
self.remember_node(node)
# TODO: What's the right status code here? 202? Different if we already knew about the node?
return self.all_known_nodes(request)
def consider_arrangement(self, request: http.Request):
from nucypher.policy.models import Arrangement
arrangement = Arrangement.from_bytes(request.body)
@ -232,12 +267,6 @@ class ProxyRESTServer:
# TODO: What do we do if the Policy isn't signed properly?
pass
#
# alices_signature, policy_payload =BytestringSplitter(Signature)(cleartext, return_remainder=True)
# TODO: If we're not adding anything else in the payload, stop using the
# splitter here.
# kfrag = policy_payload_splitter(policy_payload)[0]
kfrag = KFrag.from_bytes(cleartext)
with ThreadedSession(self.db_engine) as session:
@ -253,6 +282,7 @@ class ProxyRESTServer:
from nucypher.policy.models import WorkOrder # Avoid circular import
id = binascii.unhexlify(id_as_hex)
work_order = WorkOrder.from_rest_payload(id, request.body)
self.log.info("Work Order from {}, signed {}".format(work_order.bob, work_order.receipt_signature))
with ThreadedSession(self.db_engine) as session:
kfrag_bytes = self.datastore.get_policy_arrangement(id.hex().encode(),
session=session).k_frag # Careful! :-)
@ -262,7 +292,9 @@ class ProxyRESTServer:
for capsule in work_order.capsules:
# TODO: Sign the result of this. See #141.
cfrag_byte_stream += VariableLengthBytestring(pre.reencrypt(kfrag, capsule))
cfrag = pre.reencrypt(kfrag, capsule)
self.log.info("Re-encrypting for Capsule {}, made CFrag {}.".format(capsule, cfrag))
cfrag_byte_stream += VariableLengthBytestring(cfrag)
# TODO: Put this in Ursula's datastore
self._work_orders.append(work_order)
@ -309,3 +341,8 @@ class ProxyRESTServer:
# TODO: Make this a proper 500 or whatever.
self.log.info("Bad TreasureMap ID; not storing {}".format(treasure_map_id))
assert False
def get_deployer(self):
deployer = self._crypto_power.power_ups(TLSHostingPower).get_deployer(rest_app=self._rest_app,
port=self.rest_interface.port)
return deployer

View File

@ -9,6 +9,7 @@ import msgpack
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from constant_sorrow import constants
from eth_utils import to_canonical_address, to_checksum_address
from nucypher.characters import Alice
from nucypher.characters import Bob, Ursula, Character
from nucypher.crypto.api import keccak_digest, encrypt_and_sign, secure_random
@ -280,9 +281,11 @@ class FederatedPolicy(Policy):
def make_arrangements(self, network_middleware,
deposit: int,
expiration: maya.MayaDT,
ursulas: Set[Ursula] = None) -> None:
if ursulas is None:
handpicked_ursulas: Set[Ursula] = None) -> None:
if handpicked_ursulas is None:
ursulas = set()
else:
ursulas = handpicked_ursulas
ursulas.update(self.ursulas)
if len(ursulas) < self.n:
@ -308,7 +311,7 @@ class TreasureMap:
(bytes, KECCAK_DIGEST_LENGTH), # hrac
(UmbralMessageKit, VariableLengthBytestring)
)
node_id_splitter = BytestringSplitter(int(PUBLIC_ADDRESS_LENGTH), Arrangement.ID_LENGTH)
node_id_splitter = BytestringSplitter((to_checksum_address, int(PUBLIC_ADDRESS_LENGTH)), Arrangement.ID_LENGTH)
class InvalidSignature(Exception):
"""Raised when the public signature (typically intended for Ursula) is not valid."""
@ -378,12 +381,12 @@ class TreasureMap:
if self.destinations == constants.NO_DECRYPTION_PERFORMED:
return constants.NO_DECRYPTION_PERFORMED
else:
return bytes().join(ursula_id + arrangement_id for ursula_id, arrangement_id in self.destinations.items())
return bytes().join(to_canonical_address(ursula_id) + arrangement_id for ursula_id, arrangement_id in self.destinations.items())
def add_arrangement(self, arrangement):
if self.destinations == constants.NO_DECRYPTION_PERFORMED:
raise TypeError("This TreasureMap is encrypted. You can't add another node without decrypting it.")
self.destinations[arrangement.ursula.canonical_public_address] = arrangement.id
self.destinations[arrangement.ursula.checksum_public_address] = arrangement.id
def public_id(self):
"""
@ -505,11 +508,7 @@ class WorkOrderHistory:
assert False
def __getitem__(self, item):
if isinstance(item, bytes):
return self.by_ursula.setdefault(item, {})
else:
raise TypeError(
"If you want to lookup a WorkOrder by Ursula, you need to pass bytes of her signing public key.")
return self.by_ursula.setdefault(item, {})
def __setitem__(self, key, value):
assert False

View File

@ -87,7 +87,7 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
bob.start_learning_loop()
# ...and block until the unknown_nodes have all been found.
yield threads.deferToThread(bob.block_until_nodes_are_known, unknown_nodes)
yield threads.deferToThread(bob.block_until_specific_nodes_are_known, unknown_nodes)
# ...and he now has no more unknown_nodes.
print(len(bob._known_nodes))

View File

@ -136,6 +136,14 @@ def test_anybody_can_encrypt():
assert ciphertext is not None
def test_node_deployer(ursulas):
for ursula in ursulas:
deployer = ursula.get_deployer()
assert deployer.options['https_port'] == ursula.rest_interface.port
assert deployer.application == ursula.rest_app
"""
What follows are various combinations of signing and encrypting, to match
real-world scenarios.

View File

@ -1,16 +1,11 @@
import pytest
from nucypher.characters import Ursula
from nucypher.crypto.api import secure_random
from nucypher.crypto.powers import SigningPower, CryptoPower
from eth_keys.datatypes import Signature as EthSignature
@pytest.mark.usesfixtures('testerchain')
def test_ursula_generates_self_signed_cert():
ursula = Ursula(is_me=False, rest_port=5000, rest_host="not used", federated_only=True)
cert, cert_private_key = ursula.generate_self_signed_certificate()
public_key_numbers = ursula.public_key(SigningPower).to_cryptography_pubkey().public_numbers()
assert cert.public_key().public_numbers() == public_key_numbers
from tests.utilities import make_ursulas, MockRestMiddleware
@pytest.mark.skip
@ -18,6 +13,27 @@ def test_federated_ursula_substantiates_stamp():
assert False
def test_new_ursula_announces_herself():
ursula_here, ursula_there = make_ursulas(2,
know_each_other=False,
network_middleware=MockRestMiddleware())
# Neither Ursula knows about the other.
assert ursula_here._known_nodes == ursula_there._known_nodes == {}
ursula_here.remember_node(ursula_there)
# OK, now, ursula_here knows about ursula_there, but not vice-versa.
assert ursula_there in ursula_here._known_nodes.values()
assert not ursula_here in ursula_there._known_nodes.values()
# But as ursula_here learns, she'll announce herself to ursula_there.
ursula_here.learn_from_teacher_node()
assert ursula_there in ursula_here._known_nodes.values()
assert ursula_here in ursula_there._known_nodes.values()
def test_blockchain_ursula_substantiates_stamp(mining_ursulas):
first_ursula = list(mining_ursulas)[0]
signature_as_bytes = first_ursula._evidence_of_decentralized_identity

View File

@ -81,7 +81,7 @@ def enacted_federated_policy(idle_federated_policy, ursulas):
idle_federated_policy.make_arrangements(network_middleware,
deposit=deposit,
expiration=contract_end_datetime,
ursulas=ursulas)
handpicked_ursulas=ursulas)
idle_federated_policy.enact(network_middleware) # REST call happens here, as does population of TreasureMap.
return idle_federated_policy
@ -168,7 +168,6 @@ def ursulas(three_agents):
token_agent, miner_agent, policy_agent = three_agents
ether_addresses = [to_checksum_address(os.urandom(20)) for _ in range(constants.NUMBER_OF_URSULAS_IN_NETWORK)]
_ursulas = make_ursulas(ether_addresses=ether_addresses,
ursula_starting_port=int(constants.URSULA_PORT_SEED),
miner_agent=miner_agent
)
try:
@ -190,7 +189,6 @@ def mining_ursulas(three_agents):
ursula_addresses = all_yall[:int(constants.NUMBER_OF_URSULAS_IN_NETWORK)]
_ursulas = make_ursulas(ether_addresses=ursula_addresses,
ursula_starting_port=int(starting_point),
miner_agent=miner_agent,
miners=True)
try:
@ -215,7 +213,6 @@ def non_ursula_miners(three_agents):
starting_point = constants.URSULA_PORT_SEED + 500
_ursulas = make_ursulas(ether_addresses=ursula_addresses,
ursula_starting_port=int(starting_point),
miner_agent=miner_agent,
miners=True,
bare=True)

View File

@ -23,14 +23,14 @@ def test_keypair_with_umbral_keys():
assert new_keypair_from_priv._privkey.bn_key.to_bytes() == umbral_privkey.bn_key.to_bytes()
assert new_keypair_from_priv.pubkey.to_bytes() == umbral_pubkey.to_bytes()
new_keypair_from_pub = keypairs.Keypair(umbral_pubkey)
new_keypair_from_pub = keypairs.Keypair(public_key=umbral_pubkey)
assert new_keypair_from_pub.pubkey.to_bytes() == umbral_pubkey.to_bytes()
assert new_keypair_from_pub._privkey == PUBLIC_ONLY
def test_keypair_serialization():
umbral_pubkey = UmbralPrivateKey.gen_key().get_pubkey()
new_keypair = keypairs.Keypair(umbral_pubkey)
new_keypair = keypairs.Keypair(public_key=umbral_pubkey)
pubkey_bytes = new_keypair.serialize_pubkey()
assert pubkey_bytes == bytes(umbral_pubkey)
@ -41,7 +41,7 @@ def test_keypair_serialization():
def test_keypair_fingerprint():
umbral_pubkey = UmbralPrivateKey.gen_key().get_pubkey()
new_keypair = keypairs.Keypair(umbral_pubkey)
new_keypair = keypairs.Keypair(public_key=umbral_pubkey)
fingerprint = new_keypair.fingerprint()
assert fingerprint != None

View File

@ -142,7 +142,7 @@ def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_poli
enacted_federated_policy.label)
# Let's imagine he has learned about some - say, from the blockchain.
bob._known_nodes = {u.canonical_public_address: u for u in ursulas}
bob._known_nodes = {u.checksum_public_address: u for u in ursulas}
# Now try.
treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp,

View File

@ -6,12 +6,14 @@ from typing import List, Set
import maya
from apistar.test import TestClient
from constant_sorrow import constants
from eth_utils import to_checksum_address
from nucypher.blockchain.eth.agents import MinerAgent
from nucypher.characters import Ursula
#
# Setup
#
from nucypher.crypto.api import secure_random
from nucypher.network.middleware import RestMiddleware
from nucypher.policy.models import Arrangement, Policy
@ -24,8 +26,12 @@ constants.NUMBER_OF_URSULAS_IN_NETWORK(10)
_ALL_URSULAS = {}
def make_ursulas(ether_addresses: list, ursula_starting_port: int,
miner_agent=None, miners=False, bare=False) -> Set[Ursula]:
def make_ursulas(ether_addresses: list,
miner_agent=None,
miners=False,
bare=False,
know_each_other=True,
**ursula_kwargs) -> Set[Ursula]:
"""
:param ether_addresses: Ethereum addresses to create ursulas with.
:param ursula_starting_port: The port of the first created Ursula; subsequent Ursulas will increment the port number by 1.
@ -39,10 +45,18 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
:return: A list of created Ursulas
"""
if isinstance(ether_addresses, int):
ether_addresses = [to_checksum_address(secure_random(20)) for _ in range(ether_addresses)]
event_loop = asyncio.get_event_loop()
if not _ALL_URSULAS:
starting_port = constants.URSULA_PORT_SEED
else:
starting_port = max(_ALL_URSULAS.keys()) + 1
ursulas = set()
for port, ether_address in enumerate(ether_addresses, start=ursula_starting_port):
for port, ether_address in enumerate(ether_addresses, start=starting_port):
if bare:
ursula = Ursula(is_me=False, # do not attach dht server
@ -51,7 +65,8 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
checksum_address=ether_address,
always_be_learning=False,
miner_agent=miner_agent,
abort_on_learning_error=True)
abort_on_learning_error=True,
**ursula_kwargs)
ursula.is_me = True # Patch to allow execution of transacting methods in tests
@ -68,7 +83,8 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
rest_port=port+100,
always_be_learning=False,
miner_agent=miner_agent,
federated_only=federated_only)
federated_only=federated_only,
**ursula_kwargs)
ursula.attach_rest_server()
@ -79,16 +95,6 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
ursula.datastore_threadpool = MockDatastoreThreadPool()
ursula.dht_listen()
for ursula_to_teach in ursulas:
# Add other Ursulas as known nodes.
for ursula_to_learn_about in ursulas:
ursula_to_teach.remember_node(ursula_to_learn_about)
event_loop.run_until_complete(
ursula.dht_server.bootstrap(
[("127.0.0.1", ursula_starting_port + _c) for _c in range(len(ursulas))]))
ursula.publish_dht_information()
if miners is True:
# TODO: 309
# stake a random amount
@ -106,6 +112,18 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
ursulas.add(ursula)
_ALL_URSULAS[ursula.rest_interface.port] = ursula
if know_each_other and not bare:
for ursula_to_teach in ursulas:
# Add other Ursulas as known nodes.
for ursula_to_learn_about in ursulas:
ursula_to_teach.remember_node(ursula_to_learn_about)
event_loop.run_until_complete(
ursula.dht_server.bootstrap(
[("127.0.0.1", starting_port + _c) for _c in range(len(ursulas))]))
ursula.publish_dht_information()
return ursulas
@ -177,15 +195,22 @@ class MockRestMiddleware(RestMiddleware):
response = mock_client.get("http://localhost/public_information")
return response
def get_nodes_via_rest(self, address, port, node_ids):
def get_nodes_via_rest(self, address, port, announce_nodes=None, nodes_i_need=None):
mock_client = self.__get_mock_client_by_port(port)
# TODO: Better passage of node IDs here.
# if node_ids:
# node_address_bytestring = bytes().join(bytes(id) for id in node_ids)
# params = {'nodes': node_address_bytestring}
# else:
# params = None
response = mock_client.get("http://localhost/list_nodes")
if nodes_i_need:
# TODO: This needs to actually do something.
# Include node_ids in the request; if the teacher node doesn't know about the
# nodes matching these ids, then it will ask other nodes via the DHT or whatever.
pass
if announce_nodes:
response = mock_client.post("https://{}:{}/node_metadata".format(address, port),
verify=False,
data=bytes().join(bytes(n) for n in announce_nodes)) # TODO: TLS-only.
else:
response = mock_client.get("https://{}:{}/node_metadata".format(address, port),
verify=False) # TODO: TLS-only.
return response
def put_treasure_map_on_node(self, node, map_id, map_payload):