mirror of https://github.com/nucypher/nucypher.git
Refactor FleetSensor
parent
3eb3dd3caf
commit
4de9b91d2a
|
@ -53,7 +53,7 @@
|
||||||
debug:
|
debug:
|
||||||
msg:
|
msg:
|
||||||
"local nickname: {{host_nickname}}\n
|
"local nickname: {{host_nickname}}\n
|
||||||
{% if serving and 'json' in status_data %}nickname: {{status_data.json.nickname}}\n
|
{% if serving and 'json' in status_data %}nickname: {{status_data.json.nickname.text}}\n
|
||||||
staker address: {{status_data.json.staker_address}}\n
|
staker address: {{status_data.json.staker_address}}\n
|
||||||
worker address: {{status_data.json.worker_address}}\n
|
worker address: {{status_data.json.worker_address}}\n
|
||||||
rest url: https://{{status_data.json.rest_url}}\n
|
rest url: https://{{status_data.json.rest_url}}\n
|
||||||
|
|
|
@ -74,7 +74,7 @@ class NicknameCharacter:
|
||||||
self.color_hex = color_hex
|
self.color_hex = color_hex
|
||||||
self._text = color_name + " " + _SYMBOLS[symbol]
|
self._text = color_name + " " + _SYMBOLS[symbol]
|
||||||
|
|
||||||
def payload(self):
|
def to_json(self):
|
||||||
return dict(symbol=self.symbol,
|
return dict(symbol=self.symbol,
|
||||||
color_name=self.color_name,
|
color_name=self.color_name,
|
||||||
color_hex=self.color_hex)
|
color_hex=self.color_hex)
|
||||||
|
@ -103,8 +103,9 @@ class Nickname:
|
||||||
self.icon = "[" + "".join(character.symbol for character in characters) + "]"
|
self.icon = "[" + "".join(character.symbol for character in characters) + "]"
|
||||||
self.characters = characters
|
self.characters = characters
|
||||||
|
|
||||||
def payload(self):
|
def to_json(self):
|
||||||
return [character.payload() for character in self.characters]
|
return dict(text=self._text,
|
||||||
|
characters=[character.to_json() for character in self.characters])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self._text
|
return self._text
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
|
import itertools
|
||||||
import random
|
import random
|
||||||
|
import weakref
|
||||||
|
|
||||||
import maya
|
import maya
|
||||||
|
|
||||||
|
@ -30,45 +32,137 @@ from nucypher.crypto.api import keccak_digest
|
||||||
from nucypher.utilities.logging import Logger
|
from nucypher.utilities.logging import Logger
|
||||||
|
|
||||||
|
|
||||||
class FleetSensor:
|
class BaseFleetState:
|
||||||
"""
|
|
||||||
A representation of a fleet of NuCypher nodes.
|
|
||||||
"""
|
|
||||||
_checksum = NO_KNOWN_NODES.bool_value(False)
|
|
||||||
_nickname = NO_KNOWN_NODES
|
|
||||||
_tracking = False
|
|
||||||
most_recent_node_change = NO_KNOWN_NODES
|
|
||||||
snapshot_splitter = BytestringSplitter(32, 4)
|
|
||||||
log = Logger("Learning")
|
|
||||||
FleetState = namedtuple("FleetState", ("nickname", "icon", "nodes", "updated", "checksum"))
|
|
||||||
|
|
||||||
def __init__(self, domain: str):
|
def __str__(self):
|
||||||
self.domain = domain
|
if len(self) != 0:
|
||||||
self.additional_nodes_to_track = []
|
# TODO: draw the icon in color, similarly to the web version?
|
||||||
self.updated = maya.now()
|
return '{checksum} ⇀{nickname}↽ {icon}'.format(icon=self.nickname.icon,
|
||||||
self._nodes = OrderedDict()
|
nickname=self.nickname,
|
||||||
self._marked = defaultdict(list) # Beginning of bucketing.
|
checksum=self.checksum[:7])
|
||||||
self.states = OrderedDict()
|
|
||||||
|
|
||||||
def __setitem__(self, checksum_address, node_or_sprout):
|
|
||||||
if node_or_sprout.domain == self.domain:
|
|
||||||
self._nodes[checksum_address] = node_or_sprout
|
|
||||||
|
|
||||||
if self._tracking:
|
|
||||||
self.log.info("Updating fleet state after saving node {}".format(node_or_sprout))
|
|
||||||
self.record_fleet_state()
|
|
||||||
else:
|
else:
|
||||||
msg = f"Rejected node {node_or_sprout} because its domain is '{node_or_sprout.domain}' but we're only tracking '{self.domain}'"
|
return 'No Known Nodes'
|
||||||
self.log.warn(msg)
|
|
||||||
|
|
||||||
|
class ArchivedFleetState(BaseFleetState):
|
||||||
|
|
||||||
|
def __init__(self, checksum, nickname, timestamp, population):
|
||||||
|
self.checksum = checksum
|
||||||
|
self.nickname = nickname
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.population = population
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return dict(checksum=self.checksum,
|
||||||
|
nickname=self.nickname.to_json() if self.nickname is not None else 'NO_KNOWN_NODES',
|
||||||
|
timestamp=self.timestamp.rfc2822(),
|
||||||
|
population=self.population)
|
||||||
|
|
||||||
|
|
||||||
|
# Assumptions we're based on:
|
||||||
|
# - Every supplied node object, after its constructor has finished,
|
||||||
|
# has a ``.checksum_address`` and ``bytes()`` (metadata)
|
||||||
|
# - checksum address or metadata does not change for the same Python object
|
||||||
|
# - ``this_node`` (the owner of FleetSensor) may not have a checksum address initially
|
||||||
|
# (when the constructor is first called), but will have one at the time of the first
|
||||||
|
# `record_fleet_state()` call. This applies to its metadata as well.
|
||||||
|
# - The metadata of ``this_node`` **can** change.
|
||||||
|
# - For the purposes of the fleet state, nodes with different metadata are considered different,
|
||||||
|
# even if they have the same checksum address.
|
||||||
|
|
||||||
|
class FleetState(BaseFleetState):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(cls, this_node=None):
|
||||||
|
this_node_ref = weakref.ref(this_node) if this_node is not None else None
|
||||||
|
# Using empty checksum so that JSON library is not confused.
|
||||||
|
# Plus, we do need some checksum anyway. It's a legitimate state after all.
|
||||||
|
return cls(checksum=keccak_digest(b"").hex(),
|
||||||
|
nodes={},
|
||||||
|
this_node_ref=this_node_ref,
|
||||||
|
this_node_metadata=None)
|
||||||
|
|
||||||
|
def __init__(self, checksum, nodes, this_node_ref, this_node_metadata):
|
||||||
|
self.checksum = checksum
|
||||||
|
self.nickname = None if checksum is None else Nickname.from_seed(checksum, length=1)
|
||||||
|
self._nodes = nodes
|
||||||
|
self.timestamp = maya.now()
|
||||||
|
self._this_node_ref = this_node_ref
|
||||||
|
self._this_node_metadata = this_node_metadata
|
||||||
|
|
||||||
|
def archived(self):
|
||||||
|
return ArchivedFleetState(checksum=self.checksum,
|
||||||
|
nickname=self.nickname,
|
||||||
|
timestamp=self.timestamp,
|
||||||
|
population=self.population)
|
||||||
|
|
||||||
|
def with_updated_nodes(self, new_nodes, marked_nodes):
|
||||||
|
|
||||||
|
# Checking if the node already has a checksum address
|
||||||
|
# (it may be created later during the constructor)
|
||||||
|
# or if it mutated since the last check.
|
||||||
|
if self._this_node_ref is not None and getattr(self._this_node_ref(), 'finished_initializing', False):
|
||||||
|
this_node = self._this_node_ref()
|
||||||
|
this_node_metadata = bytes(this_node)
|
||||||
|
this_node_changed = self._this_node_metadata != this_node_metadata
|
||||||
|
this_node_list = [this_node]
|
||||||
|
else:
|
||||||
|
this_node_metadata = self._this_node_metadata
|
||||||
|
this_node_changed = False
|
||||||
|
this_node_list = []
|
||||||
|
|
||||||
|
new_nodes = {checksum_address: node for checksum_address, node in new_nodes.items()
|
||||||
|
if checksum_address not in marked_nodes}
|
||||||
|
|
||||||
|
remote_nodes_updated = any(
|
||||||
|
(checksum_address not in self._nodes or bytes(self._nodes[checksum_address]) != bytes(node))
|
||||||
|
and checksum_address not in marked_nodes
|
||||||
|
for checksum_address, node in new_nodes.items())
|
||||||
|
|
||||||
|
remote_nodes_slashed = any(checksum_address in self._nodes for checksum_address in marked_nodes)
|
||||||
|
|
||||||
|
if this_node_changed or remote_nodes_updated or remote_nodes_slashed:
|
||||||
|
# TODO: if nodes were kept in a Merkle tree,
|
||||||
|
# we'd have to only recalculate log(N) checksums.
|
||||||
|
# Is it worth it?
|
||||||
|
nodes = dict(self._nodes)
|
||||||
|
nodes.update(new_nodes)
|
||||||
|
for checksum_address in marked_nodes:
|
||||||
|
if checksum_address in nodes:
|
||||||
|
del nodes[checksum_address]
|
||||||
|
|
||||||
|
all_nodes_sorted = sorted(itertools.chain(this_node_list, nodes.values()),
|
||||||
|
key=lambda node: node.checksum_address)
|
||||||
|
joined_metadata = b"".join(bytes(node) for node in all_nodes_sorted)
|
||||||
|
checksum = keccak_digest(joined_metadata).hex()
|
||||||
|
else:
|
||||||
|
nodes = self._nodes
|
||||||
|
checksum = self.checksum
|
||||||
|
|
||||||
|
return FleetState(checksum=checksum,
|
||||||
|
nodes=nodes,
|
||||||
|
this_node_ref=self._this_node_ref,
|
||||||
|
this_node_metadata=this_node_metadata)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def population(self):
|
||||||
|
"""Returns the number of all known nodes, including itself, if applicable."""
|
||||||
|
return len(self) + int(self._this_node_metadata is not None)
|
||||||
|
|
||||||
def __getitem__(self, checksum_address):
|
def __getitem__(self, checksum_address):
|
||||||
return self._nodes[checksum_address]
|
return self._nodes[checksum_address]
|
||||||
|
|
||||||
|
def addresses(self):
|
||||||
|
return self._nodes.keys()
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return bool(self._nodes)
|
return len(self) != 0
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
return item in self._nodes.keys() or item in self._nodes.values()
|
if isinstance(item, str):
|
||||||
|
return item in self._nodes
|
||||||
|
else:
|
||||||
|
return item.checksum_address in self._nodes
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield from self._nodes.values()
|
yield from self._nodes.values()
|
||||||
|
@ -76,100 +170,182 @@ class FleetSensor:
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._nodes)
|
return len(self._nodes)
|
||||||
|
|
||||||
def __eq__(self, other):
|
# TODO: we only send it along with `FLEET_STATES_MATCH`, so it is essentially useless.
|
||||||
return self._nodes == other._nodes
|
# But it's hard to change now because older nodes will be looking for it.
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self._nodes.__repr__()
|
|
||||||
|
|
||||||
def population(self):
|
|
||||||
return len(self) + len(self.additional_nodes_to_track)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def checksum(self):
|
|
||||||
return self._checksum
|
|
||||||
|
|
||||||
@checksum.setter
|
|
||||||
def checksum(self, checksum_value):
|
|
||||||
self._checksum = checksum_value
|
|
||||||
self._nickname = Nickname.from_seed(checksum_value, length=1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nickname(self):
|
|
||||||
return self._nickname
|
|
||||||
|
|
||||||
@property
|
|
||||||
def icon(self) -> str:
|
|
||||||
if self.nickname is NO_KNOWN_NODES:
|
|
||||||
return str(NO_KNOWN_NODES)
|
|
||||||
return self.nickname.icon
|
|
||||||
|
|
||||||
def addresses(self):
|
|
||||||
return self._nodes.keys()
|
|
||||||
|
|
||||||
def snapshot(self):
|
def snapshot(self):
|
||||||
fleet_state_checksum_bytes = binascii.unhexlify(self.checksum)
|
checksum_bytes = binascii.unhexlify(self.checksum)
|
||||||
fleet_state_updated_bytes = self.updated.epoch.to_bytes(4, byteorder="big")
|
timestamp_bytes = self.timestamp.epoch.to_bytes(4, byteorder="big")
|
||||||
return fleet_state_checksum_bytes + fleet_state_updated_bytes
|
return checksum_bytes + timestamp_bytes
|
||||||
|
|
||||||
def record_fleet_state(self, additional_nodes_to_track=None):
|
snapshot_splitter = BytestringSplitter(32, 4)
|
||||||
if additional_nodes_to_track:
|
|
||||||
self.additional_nodes_to_track.extend(additional_nodes_to_track)
|
|
||||||
|
|
||||||
if not self._nodes:
|
@staticmethod
|
||||||
# No news here.
|
def unpack_snapshot(data):
|
||||||
return
|
checksum_bytes, timestamp_bytes, remainder = FleetState.snapshot_splitter(data, return_remainder=True)
|
||||||
sorted_nodes = self.sorted()
|
checksum = checksum_bytes.hex()
|
||||||
|
timestamp = maya.MayaDT(int.from_bytes(timestamp_bytes, byteorder="big"))
|
||||||
sorted_nodes_joined = b"".join(bytes(n) for n in sorted_nodes)
|
return checksum, timestamp, remainder
|
||||||
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,
|
|
||||||
nodes=sorted_nodes,
|
|
||||||
icon=self.icon,
|
|
||||||
updated=self.updated,
|
|
||||||
checksum=self.checksum)
|
|
||||||
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):
|
def shuffled(self):
|
||||||
nodes_we_know_about = list(self._nodes.values())
|
nodes_we_know_about = list(self._nodes.values())
|
||||||
random.shuffle(nodes_we_know_about)
|
random.shuffle(nodes_we_know_about)
|
||||||
return nodes_we_know_about
|
return nodes_we_know_about
|
||||||
|
|
||||||
def abridged_states_dict(self):
|
def to_json(self):
|
||||||
abridged_states = {}
|
return dict(nickname=self.nickname.to_json(),
|
||||||
for k, v in self.states.items():
|
updated=self.timestamp.rfc2822())
|
||||||
abridged_states[k] = self.abridged_state_details(v)
|
|
||||||
return abridged_states
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
# FIXME: should it be called at all if there are no states recorded?
|
||||||
|
if len(self) == 0:
|
||||||
|
return str(NO_KNOWN_NODES)
|
||||||
|
return self.nickname.icon
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self._nodes.items()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return self._nodes.values()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"FleetState({self.checksum}, {self._nodes}, {self._this_node_ref}, {self._this_node_metadata})"
|
||||||
|
|
||||||
|
|
||||||
|
class FleetSensor:
|
||||||
|
"""
|
||||||
|
A representation of a fleet of NuCypher nodes.
|
||||||
|
|
||||||
|
If `this_node` is provided, it will be included in the state checksum
|
||||||
|
(but not returned during iteration/lookups).
|
||||||
|
"""
|
||||||
|
log = Logger("Learning")
|
||||||
|
|
||||||
|
def __init__(self, domain: str, this_node=None):
|
||||||
|
|
||||||
|
self._domain = domain
|
||||||
|
|
||||||
|
self._current_state = FleetState.new(this_node)
|
||||||
|
self._archived_states = [self._current_state.archived()]
|
||||||
|
self.remote_states = {}
|
||||||
|
|
||||||
|
# temporary accumulator for new nodes to avoid updating the fleet state every time
|
||||||
|
self._new_nodes = {}
|
||||||
|
self._marked = set() # Beginning of bucketing.
|
||||||
|
|
||||||
|
self._auto_update_state = False
|
||||||
|
|
||||||
|
def record_node(self, node):
|
||||||
|
|
||||||
|
if node.domain == self._domain:
|
||||||
|
self._new_nodes[node.checksum_address] = node
|
||||||
|
|
||||||
|
if self._auto_update_state:
|
||||||
|
self.log.info(f"Updating fleet state after saving node {node}")
|
||||||
|
self.record_fleet_state()
|
||||||
|
else:
|
||||||
|
msg = f"Rejected node {node} because its domain is '{node.domain}' but we're only tracking '{self._domain}'"
|
||||||
|
self.log.warn(msg)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return self._current_state[item]
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self._current_state)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
"""
|
||||||
|
Checks if the node *with the same metadata* is recorded in the current state.
|
||||||
|
Does not compare ``item`` with the owner node of this FleetSensor.
|
||||||
|
"""
|
||||||
|
return item in self._current_state
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from self._current_state
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._current_state)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"FleetSensor({self._current_state.__repr__()})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_state(self):
|
||||||
|
return self._current_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def checksum(self):
|
||||||
|
# FIXME: should it be called at all if there are no states recorded?
|
||||||
|
if self._current_state.population == 0:
|
||||||
|
return NO_KNOWN_NODES
|
||||||
|
return self._current_state.checksum
|
||||||
|
|
||||||
|
@property
|
||||||
|
def population(self):
|
||||||
|
return self._current_state.population
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nickname(self):
|
||||||
|
# FIXME: should it be called at all if there are no states recorded?
|
||||||
|
if self._current_state.population == 0:
|
||||||
|
return NO_KNOWN_NODES
|
||||||
|
return self._current_state.nickname
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
# FIXME: should it be called at all if there are no states recorded?
|
||||||
|
return self._current_state.icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self):
|
||||||
|
return self._current_state.timestamp
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self._current_state.items()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return self._current_state.values()
|
||||||
|
|
||||||
|
def latest_states(self, quantity):
|
||||||
|
"""
|
||||||
|
Returns at most ``quantity`` latest archived states (including the current one),
|
||||||
|
in chronological order.
|
||||||
|
"""
|
||||||
|
return self._archived_states[-min(len(self._archived_states), quantity):]
|
||||||
|
|
||||||
|
def addresses(self):
|
||||||
|
return self._current_state.addresses()
|
||||||
|
|
||||||
|
def snapshot(self):
|
||||||
|
return self._current_state.snapshot()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def abridged_state_details(state):
|
def unpack_snapshot(data):
|
||||||
return {"nickname": str(state.nickname),
|
return FleetState.unpack_snapshot(data)
|
||||||
# FIXME: generalize in case we want to extend the number of symbols in the state nickname
|
|
||||||
"symbol": state.nickname.characters[0].symbol,
|
def record_fleet_state(self):
|
||||||
"color_hex": state.nickname.characters[0].color_hex,
|
new_state = self._current_state.with_updated_nodes(self._new_nodes, self._marked)
|
||||||
"color_name": state.nickname.characters[0].color_name,
|
self._new_nodes = {}
|
||||||
"updated": state.updated.rfc2822(),
|
self._marked = set()
|
||||||
}
|
self._current_state = new_state
|
||||||
|
|
||||||
|
# TODO: set a limit on the number of archived states?
|
||||||
|
# Two ways to collect archived states:
|
||||||
|
# 1. (current) add a state to the archive every time it changes
|
||||||
|
# 2. (possible) keep a dictionary of known states
|
||||||
|
# and bump the timestamp of a previously encountered one
|
||||||
|
if new_state.checksum != self._archived_states[-1].checksum:
|
||||||
|
self._archived_states.append(new_state.archived())
|
||||||
|
|
||||||
|
def shuffled(self):
|
||||||
|
return self._current_state.shuffled()
|
||||||
|
|
||||||
def mark_as(self, label: Exception, node: "Teacher"):
|
def mark_as(self, label: Exception, node: "Teacher"):
|
||||||
self._marked[label].append(node)
|
# TODO: for now we're not using `label` in any way, so we're just ignoring it
|
||||||
|
self._marked.add(node.checksum_address)
|
||||||
|
|
||||||
if self._nodes.get(node):
|
def record_remote_fleet_state(self, checksum_address, state_checksum, timestamp, population):
|
||||||
del self._nodes[node]
|
# TODO: really we can just create the timestamp here
|
||||||
|
|
||||||
|
nickname = Nickname.from_seed(state_checksum, length=1) # TODO: create in a single place
|
||||||
|
self.remote_states[checksum_address] = ArchivedFleetState(state_checksum, nickname, timestamp, population)
|
||||||
|
|
|
@ -73,6 +73,7 @@ class Character(Learner):
|
||||||
provider_uri: str = None,
|
provider_uri: str = None,
|
||||||
signer: Signer = None,
|
signer: Signer = None,
|
||||||
registry: BaseContractRegistry = None,
|
registry: BaseContractRegistry = None,
|
||||||
|
include_self_in_the_state: bool = False,
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
@ -191,6 +192,7 @@ class Character(Learner):
|
||||||
domain=domain,
|
domain=domain,
|
||||||
network_middleware=self.network_middleware,
|
network_middleware=self.network_middleware,
|
||||||
node_class=known_node_class,
|
node_class=known_node_class,
|
||||||
|
include_self_in_the_state=include_self_in_the_state,
|
||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -981,7 +981,7 @@ class Bob(Character):
|
||||||
# Not enough matching nodes. Fine, we'll just publish to the first few.
|
# Not enough matching nodes. Fine, we'll just publish to the first few.
|
||||||
try:
|
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.
|
# 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]
|
target_nodes = list(nodes.values())[0:6]
|
||||||
return target_nodes
|
return target_nodes
|
||||||
except IndexError:
|
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.")
|
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.")
|
||||||
|
@ -1113,6 +1113,7 @@ class Ursula(Teacher, Character, Worker):
|
||||||
known_nodes=known_nodes,
|
known_nodes=known_nodes,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
known_node_class=Ursula,
|
known_node_class=Ursula,
|
||||||
|
include_self_in_the_state=True,
|
||||||
**character_kwargs)
|
**character_kwargs)
|
||||||
|
|
||||||
if is_me:
|
if is_me:
|
||||||
|
@ -1232,7 +1233,7 @@ class Ursula(Teacher, Character, Worker):
|
||||||
decentralized_identity_evidence=decentralized_identity_evidence)
|
decentralized_identity_evidence=decentralized_identity_evidence)
|
||||||
|
|
||||||
if is_me:
|
if is_me:
|
||||||
self.known_nodes.record_fleet_state(additional_nodes_to_track=[self]) # Initial Impression
|
self.known_nodes.record_fleet_state() # Initial Impression
|
||||||
|
|
||||||
message = "THIS IS YOU: {}: {}".format(self.__class__.__name__, self)
|
message = "THIS IS YOU: {}: {}".format(self.__class__.__name__, self)
|
||||||
self.log.info(message)
|
self.log.info(message)
|
||||||
|
@ -1241,6 +1242,11 @@ class Ursula(Teacher, Character, Worker):
|
||||||
message = "Initialized Stranger {} | {}".format(self.__class__.__name__, self)
|
message = "Initialized Stranger {} | {}".format(self.__class__.__name__, self)
|
||||||
self.log.debug(message)
|
self.log.debug(message)
|
||||||
|
|
||||||
|
# FIXME: we need to know when Ursula is ready to be recorded in a fleet state.
|
||||||
|
# The first time fleet state is updated may be still within this constructor.
|
||||||
|
# Any better way to solve this?
|
||||||
|
self.finished_initializing = True
|
||||||
|
|
||||||
def __prune_datastore(self) -> None:
|
def __prune_datastore(self) -> None:
|
||||||
"""Deletes all expired arrangements, kfrags, and treasure maps in the datastore."""
|
"""Deletes all expired arrangements, kfrags, and treasure maps in the datastore."""
|
||||||
now = maya.MayaDT.from_datetime(datetime.fromtimestamp(self._datastore_pruning_task.clock.seconds()))
|
now = maya.MayaDT.from_datetime(datetime.fromtimestamp(self._datastore_pruning_task.clock.seconds()))
|
||||||
|
|
|
@ -15,7 +15,6 @@ You should have received a copy of the GNU Affero General Public License
|
||||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
import maya
|
import maya
|
||||||
from constant_sorrow.constants import NO_KNOWN_NODES
|
|
||||||
|
|
||||||
from nucypher.config.constants import SEEDNODES
|
from nucypher.config.constants import SEEDNODES
|
||||||
from nucypher.datastore.datastore import RecordNotFound
|
from nucypher.datastore.datastore import RecordNotFound
|
||||||
|
@ -23,19 +22,7 @@ from nucypher.datastore.models import Workorder
|
||||||
|
|
||||||
|
|
||||||
def build_fleet_state_status(ursula) -> str:
|
def build_fleet_state_status(ursula) -> str:
|
||||||
# Build FleetState status line
|
return str(ursula.known_nodes.current_state)
|
||||||
if ursula.known_nodes.checksum is not NO_KNOWN_NODES:
|
|
||||||
fleet_state_checksum = ursula.known_nodes.checksum[:7]
|
|
||||||
fleet_state_nickname = ursula.known_nodes.nickname
|
|
||||||
fleet_state = '{checksum} ⇀{nickname}↽ {icon}'.format(icon=fleet_state_nickname.icon,
|
|
||||||
nickname=fleet_state_nickname,
|
|
||||||
checksum=fleet_state_checksum)
|
|
||||||
elif ursula.known_nodes.checksum is NO_KNOWN_NODES:
|
|
||||||
fleet_state = 'No Known Nodes'
|
|
||||||
else:
|
|
||||||
fleet_state = 'Unknown'
|
|
||||||
|
|
||||||
return fleet_state
|
|
||||||
|
|
||||||
|
|
||||||
def paint_node_status(emitter, ursula, start_time):
|
def paint_node_status(emitter, ursula, start_time):
|
||||||
|
@ -61,7 +48,7 @@ def paint_node_status(emitter, ursula, start_time):
|
||||||
except RecordNotFound:
|
except RecordNotFound:
|
||||||
num_work_orders = 0
|
num_work_orders = 0
|
||||||
|
|
||||||
stats = ['⇀URSULA {}↽'.format(ursula.nickname_icon),
|
stats = ['⇀URSULA {}↽'.format(ursula.nickname.icon),
|
||||||
'{}'.format(ursula),
|
'{}'.format(ursula),
|
||||||
'Uptime .............. {}'.format(maya.now() - start_time),
|
'Uptime .............. {}'.format(maya.now() - start_time),
|
||||||
'Start Time .......... {}'.format(start_time.slang_time()),
|
'Start Time .......... {}'.format(start_time.slang_time()),
|
||||||
|
|
|
@ -114,8 +114,7 @@ class UrsulaCommandProtocol(LineReceiver):
|
||||||
Display information about the network-wide fleet state as the attached Ursula node sees it.
|
Display information about the network-wide fleet state as the attached Ursula node sees it.
|
||||||
"""
|
"""
|
||||||
from nucypher.cli.painting.nodes import build_fleet_state_status
|
from nucypher.cli.painting.nodes import build_fleet_state_status
|
||||||
line = '{}'.format(build_fleet_state_status(ursula=self.ursula))
|
self.emitter.echo(build_fleet_state_status(ursula=self.ursula))
|
||||||
self.emitter.echo(line)
|
|
||||||
|
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
self.emitter.echo("\nType 'help' or '?' for help")
|
self.emitter.echo("\nType 'help' or '?' for help")
|
||||||
|
|
|
@ -174,7 +174,6 @@ class Learner:
|
||||||
tracker_class = FleetSensor
|
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."
|
invalid_metadata_message = "{} has invalid metadata. The node's stake may have ended, or it is transitioning to a new interface. Ignoring."
|
||||||
fleet_state_population = None
|
|
||||||
|
|
||||||
_DEBUG_MODE = False
|
_DEBUG_MODE = False
|
||||||
|
|
||||||
|
@ -209,6 +208,7 @@ class Learner:
|
||||||
abort_on_learning_error: bool = False,
|
abort_on_learning_error: bool = False,
|
||||||
lonely: bool = False,
|
lonely: bool = False,
|
||||||
verify_node_bonding: bool = True,
|
verify_node_bonding: bool = True,
|
||||||
|
include_self_in_the_state: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.log = Logger("learning-loop") # type: Logger
|
self.log = Logger("learning-loop") # type: Logger
|
||||||
|
@ -228,7 +228,7 @@ class Learner:
|
||||||
self._learning_listeners = defaultdict(list)
|
self._learning_listeners = defaultdict(list)
|
||||||
self._node_ids_to_learn_about_immediately = set()
|
self._node_ids_to_learn_about_immediately = set()
|
||||||
|
|
||||||
self.__known_nodes = self.tracker_class(domain=domain)
|
self.__known_nodes = self.tracker_class(domain=domain, this_node=self if include_self_in_the_state else None)
|
||||||
self._verify_node_bonding = verify_node_bonding
|
self._verify_node_bonding = verify_node_bonding
|
||||||
|
|
||||||
self.lonely = lonely
|
self.lonely = lonely
|
||||||
|
@ -253,6 +253,7 @@ class Learner:
|
||||||
self.remember_node(node, eager=True)
|
self.remember_node(node, eager=True)
|
||||||
except self.UnresponsiveTeacher:
|
except self.UnresponsiveTeacher:
|
||||||
self.unresponsive_startup_nodes.append(node)
|
self.unresponsive_startup_nodes.append(node)
|
||||||
|
self.known_nodes.record_fleet_state()
|
||||||
|
|
||||||
self.teacher_nodes = deque()
|
self.teacher_nodes = deque()
|
||||||
self._current_teacher_node = None # type: Teacher
|
self._current_teacher_node = None # type: Teacher
|
||||||
|
@ -387,7 +388,7 @@ class Learner:
|
||||||
# This node is already known. We can safely return.
|
# This node is already known. We can safely return.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.known_nodes[node.checksum_address] = node # FIXME - dont always remember nodes, bucket them.
|
self.known_nodes.record_node(node) # FIXME - dont always remember nodes, bucket them.
|
||||||
|
|
||||||
if self.save_metadata:
|
if self.save_metadata:
|
||||||
self.node_storage.store_node_metadata(node=node)
|
self.node_storage.store_node_metadata(node=node)
|
||||||
|
@ -749,7 +750,7 @@ class Learner:
|
||||||
|
|
||||||
if not self.done_seeding:
|
if not self.done_seeding:
|
||||||
try:
|
try:
|
||||||
remembered_seednodes = self.load_seednodes(record_fleet_state=False)
|
remembered_seednodes = self.load_seednodes(record_fleet_state=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Even if we aren't aborting on learning errors, we want this to crash the process pronto.
|
# Even if we aren't aborting on learning errors, we want this to crash the process pronto.
|
||||||
e.crash_right_now = True
|
e.crash_right_now = True
|
||||||
|
@ -761,7 +762,7 @@ class Learner:
|
||||||
|
|
||||||
current_teacher = self.current_teacher_node() # Will raise if there's no available teacher.
|
current_teacher = self.current_teacher_node() # Will raise if there's no available teacher.
|
||||||
|
|
||||||
if Teacher in self.__class__.__bases__:
|
if isinstance(self, Teacher):
|
||||||
announce_nodes = [self]
|
announce_nodes = [self]
|
||||||
else:
|
else:
|
||||||
announce_nodes = None
|
announce_nodes = None
|
||||||
|
@ -842,18 +843,18 @@ class Learner:
|
||||||
f"Invalid signature ({signature}) received from teacher {current_teacher} for payload {node_payload}")
|
f"Invalid signature ({signature}) received from teacher {current_teacher} for payload {node_payload}")
|
||||||
|
|
||||||
# End edge case handling.
|
# End edge case handling.
|
||||||
payload = FleetSensor.snapshot_splitter(node_payload, return_remainder=True)
|
|
||||||
fleet_state_checksum_bytes, fleet_state_updated_bytes, node_payload = payload
|
fleet_state_checksum, fleet_state_updated, node_payload = FleetSensor.unpack_snapshot(node_payload)
|
||||||
|
|
||||||
current_teacher.last_seen = maya.now()
|
current_teacher.last_seen = maya.now()
|
||||||
# TODO: This is weird - let's get a stranger FleetState going. NRN
|
|
||||||
checksum = fleet_state_checksum_bytes.hex()
|
|
||||||
|
|
||||||
if constant_or_bytes(node_payload) is FLEET_STATES_MATCH:
|
if constant_or_bytes(node_payload) is FLEET_STATES_MATCH:
|
||||||
current_teacher.update_snapshot(checksum=checksum,
|
self.known_nodes.record_remote_fleet_state(
|
||||||
updated=maya.MayaDT(
|
current_teacher.checksum_address,
|
||||||
int.from_bytes(fleet_state_updated_bytes, byteorder="big")),
|
fleet_state_checksum,
|
||||||
number_of_known_nodes=self.known_nodes.population())
|
fleet_state_updated,
|
||||||
|
self.known_nodes.population)
|
||||||
|
|
||||||
return FLEET_STATES_MATCH
|
return FLEET_STATES_MATCH
|
||||||
|
|
||||||
# Note: There was previously a version check here, but that required iterating through node bytestrings twice,
|
# Note: There was previously a version check here, but that required iterating through node bytestrings twice,
|
||||||
|
@ -908,9 +909,11 @@ class Learner:
|
||||||
self.log.warn(message)
|
self.log.warn(message)
|
||||||
|
|
||||||
# Is cycling happening in the right order?
|
# Is cycling happening in the right order?
|
||||||
current_teacher.update_snapshot(checksum=checksum,
|
self.known_nodes.record_remote_fleet_state(
|
||||||
updated=maya.MayaDT(int.from_bytes(fleet_state_updated_bytes, byteorder="big")),
|
current_teacher.checksum_address,
|
||||||
number_of_known_nodes=len(sprouts))
|
fleet_state_checksum,
|
||||||
|
fleet_state_updated,
|
||||||
|
len(sprouts))
|
||||||
|
|
||||||
###################
|
###################
|
||||||
|
|
||||||
|
@ -945,13 +948,8 @@ class Teacher:
|
||||||
#
|
#
|
||||||
|
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
self.fleet_state_checksum = None
|
|
||||||
self.fleet_state_updated = None
|
|
||||||
self.last_seen = NEVER_SEEN("No Connection to Node")
|
self.last_seen = NEVER_SEEN("No Connection to Node")
|
||||||
|
|
||||||
self.fleet_state_population = UNKNOWN_FLEET_STATE
|
|
||||||
self.fleet_state_nickname = UNKNOWN_FLEET_STATE
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Identity
|
# Identity
|
||||||
#
|
#
|
||||||
|
@ -1050,23 +1048,6 @@ class Teacher:
|
||||||
payload += ursulas_as_bytes
|
payload += ursulas_as_bytes
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def update_snapshot(self, checksum, updated, number_of_known_nodes):
|
|
||||||
"""
|
|
||||||
TODO: We update the simple snapshot here, but of course if we're dealing
|
|
||||||
with an instance that is also a Learner, it has
|
|
||||||
its own notion of its FleetState, so we probably
|
|
||||||
need a reckoning of sorts here to manage that. In time. NRN
|
|
||||||
|
|
||||||
:param checksum:
|
|
||||||
:param updated:
|
|
||||||
:param number_of_known_nodes:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
self.fleet_state_nickname = Nickname.from_seed(checksum, length=1)
|
|
||||||
self.fleet_state_checksum = checksum
|
|
||||||
self.fleet_state_updated = updated
|
|
||||||
self.fleet_state_population = number_of_known_nodes
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Stamp
|
# Stamp
|
||||||
#
|
#
|
||||||
|
@ -1310,75 +1291,66 @@ class Teacher:
|
||||||
return self.timestamp.epoch.to_bytes(4, 'big')
|
return self.timestamp.epoch.to_bytes(4, 'big')
|
||||||
|
|
||||||
#
|
#
|
||||||
# Nicknames and Metadata
|
# Status metadata
|
||||||
#
|
#
|
||||||
|
|
||||||
@property
|
def _status_info_base(self, raise_invalid=True):
|
||||||
def nickname_icon(self):
|
|
||||||
return self.nickname.icon
|
|
||||||
|
|
||||||
def nickname_icon_details(self):
|
|
||||||
return dict(
|
|
||||||
node_class=self.__class__.__name__,
|
|
||||||
version=self.TEACHER_VERSION,
|
|
||||||
# FIXME: generalize in case we want to extend the number of symbols in the node nickname
|
|
||||||
first_color=self.nickname.characters[0].color_hex,
|
|
||||||
first_symbol=self.nickname.characters[0].symbol,
|
|
||||||
second_color=self.nickname.characters[1].color_hex,
|
|
||||||
second_symbol=self.nickname.characters[1].symbol,
|
|
||||||
address_first6=self.checksum_address[2:8]
|
|
||||||
)
|
|
||||||
|
|
||||||
def known_nodes_details(self, raise_invalid=True) -> dict:
|
|
||||||
abridged_nodes = {}
|
|
||||||
for checksum_address, node in self.known_nodes._nodes.items():
|
|
||||||
try:
|
|
||||||
abridged_nodes[checksum_address] = self.node_details(node=node)
|
|
||||||
except self.StampNotSigned:
|
|
||||||
if raise_invalid:
|
|
||||||
raise
|
|
||||||
self.log.error(f"encountered unsigned stamp for node with checksum: {checksum_address}")
|
|
||||||
return abridged_nodes
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def node_details(node):
|
|
||||||
"""Stranger-Safe Details"""
|
|
||||||
node.mature()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
last_seen = node.last_seen.iso8601()
|
worker_address = self.worker_address
|
||||||
|
# FIXME: how did such a node end up in `known_nodes`?
|
||||||
|
except self.StampNotSigned:
|
||||||
|
if raise_invalid:
|
||||||
|
raise
|
||||||
|
self.log.error(f"encountered unsigned stamp for node with checksum: {self.checksum_address}")
|
||||||
|
worker_address = 'UNSIGNED_STAMP'
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
nickname=self.nickname.to_json(),
|
||||||
|
staker_address=self.checksum_address,
|
||||||
|
worker_address=worker_address,
|
||||||
|
rest_url=self.rest_url(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _status_info_remote(self, known_nodes, raise_invalid=True):
|
||||||
|
info = self._status_info_base(raise_invalid=raise_invalid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
last_seen = self.last_seen.iso8601()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
last_seen = str(node.last_seen) # In case it's the constant NEVER_SEEN
|
last_seen = None # In case it's the constant NEVER_SEEN
|
||||||
|
|
||||||
fleet_icon = node.fleet_state_nickname
|
info['last_learned_from'] = last_seen
|
||||||
if fleet_icon is UNKNOWN_FLEET_STATE:
|
|
||||||
fleet_icon = "?" # TODO NRN, MN
|
# TODO: what *is* the `timestamp`, anyway? When is it created?
|
||||||
|
info['timestamp'] = self.timestamp.iso8601()
|
||||||
|
|
||||||
|
# TODO: how come we know about the node but not about its fleet state? Does it ever happen?
|
||||||
|
if self.checksum_address in known_nodes.remote_states:
|
||||||
|
info['recorded_fleet_state'] = known_nodes.remote_states[self.checksum_address].to_json()
|
||||||
else:
|
else:
|
||||||
fleet_icon = fleet_icon.icon
|
info['recorded_fleet_state'] = None
|
||||||
|
|
||||||
payload = {"icon_details": node.nickname.payload(),
|
return info
|
||||||
"rest_url": node.rest_url(),
|
|
||||||
"nickname": str(node.nickname),
|
|
||||||
"worker_address": node.worker_address,
|
|
||||||
"staker_address": node.checksum_address,
|
|
||||||
"timestamp": node.timestamp.iso8601(),
|
|
||||||
"last_seen": last_seen,
|
|
||||||
"fleet_state": node.fleet_state_checksum or 'unknown',
|
|
||||||
"fleet_state_icon": fleet_icon,
|
|
||||||
"domain": node.domain,
|
|
||||||
'version': nucypher.__version__
|
|
||||||
}
|
|
||||||
return payload
|
|
||||||
|
|
||||||
def abridged_node_details(self, raise_invalid=True) -> dict:
|
def status_info(self, raise_invalid=True, omit_known_nodes=False):
|
||||||
"""Self-Reporting"""
|
# FIXME: is anyone using `raise_invalid=True`? Or is it always `False`?
|
||||||
payload = self.node_details(node=self)
|
|
||||||
states = self.known_nodes.abridged_states_dict()
|
for node in self.known_nodes:
|
||||||
known = self.known_nodes_details(raise_invalid=raise_invalid)
|
node.mature()
|
||||||
payload.update({'states': states, 'known_nodes': known})
|
|
||||||
if not self.federated_only:
|
info = self._status_info_base()
|
||||||
payload.update({
|
|
||||||
"balances": dict(eth=float(self.eth_balance), nu=float(self.token_balance.to_tokens())),
|
info['domain'] = self.domain
|
||||||
"missing_commitments": self.missing_commitments,
|
info['version'] = nucypher.__version__
|
||||||
"last_committed_period": self.last_committed_period})
|
|
||||||
return payload
|
latest_states = self.known_nodes.latest_states(5)
|
||||||
|
|
||||||
|
info['fleet_state'] = latest_states[-1].to_json()
|
||||||
|
info['previous_fleet_states'] = [state.to_json() for state in latest_states[:-1]]
|
||||||
|
|
||||||
|
if not omit_known_nodes:
|
||||||
|
info['known_nodes'] = [node._status_info_remote(self.known_nodes, raise_invalid=raise_invalid)
|
||||||
|
for node in self.known_nodes.values()]
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
|
@ -173,7 +173,7 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) ->
|
||||||
# TODO: Is this every something we don't want to do?
|
# TODO: Is this every something we don't want to do?
|
||||||
this_node.start_learning_loop()
|
this_node.start_learning_loop()
|
||||||
|
|
||||||
if this_node.known_nodes.checksum is NO_KNOWN_NODES:
|
if not this_node.known_nodes:
|
||||||
return Response(b"", headers=headers, status=204)
|
return Response(b"", headers=headers, status=204)
|
||||||
|
|
||||||
known_nodes_bytestring = this_node.bytestring_of_known_nodes()
|
known_nodes_bytestring = this_node.bytestring_of_known_nodes()
|
||||||
|
@ -422,30 +422,28 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) ->
|
||||||
|
|
||||||
@rest_app.route('/status/', methods=['GET'])
|
@rest_app.route('/status/', methods=['GET'])
|
||||||
def status():
|
def status():
|
||||||
if request.args.get('json'):
|
|
||||||
payload = this_node.abridged_node_details(raise_invalid=False)
|
return_json = request.args.get('json') == 'true'
|
||||||
response = jsonify(payload)
|
omit_known_nodes = request.args.get('omit_known_nodes') == 'true'
|
||||||
return response
|
|
||||||
|
status_info = this_node.status_info(raise_invalid=False,
|
||||||
|
omit_known_nodes=omit_known_nodes)
|
||||||
|
|
||||||
|
if return_json:
|
||||||
|
return jsonify(status_info)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
headers = {"Content-Type": "text/html", "charset": "utf-8"}
|
headers = {"Content-Type": "text/html", "charset": "utf-8"}
|
||||||
previous_states = list(reversed(this_node.known_nodes.states.values()))[:5]
|
|
||||||
# Mature every known node before rendering.
|
|
||||||
for node in this_node.known_nodes:
|
|
||||||
node.mature()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = status_template.render(this_node=this_node,
|
content = status_template.render(status_info)
|
||||||
known_nodes=this_node.known_nodes,
|
|
||||||
previous_states=previous_states,
|
|
||||||
domain=domain,
|
|
||||||
version=nucypher.__version__,
|
|
||||||
checksum_address=this_node.checksum_address)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
text_error = mako_exceptions.text_error_template().render()
|
text_error = mako_exceptions.text_error_template().render()
|
||||||
html_error = mako_exceptions.html_error_template().render()
|
html_error = mako_exceptions.html_error_template().render()
|
||||||
log.debug("Template Rendering Exception:\n" + text_error)
|
log.debug("Template Rendering Exception:\n" + text_error)
|
||||||
return Response(response=html_error, headers=headers, status=500)
|
return Response(response=html_error, headers=headers, status=500)
|
||||||
|
|
||||||
return Response(response=content, headers=headers)
|
return Response(response=content, headers=headers)
|
||||||
|
|
||||||
return rest_app
|
return rest_app
|
||||||
|
|
|
@ -6,9 +6,6 @@ def hex_to_rgb(color_hex):
|
||||||
b = int(color_hex[5:7], 16)
|
b = int(color_hex[5:7], 16)
|
||||||
return r, g, b
|
return r, g, b
|
||||||
|
|
||||||
def rgb_to_hex(r, g, b):
|
|
||||||
return f"#{r:02x}{g:02x}{b:02x}"
|
|
||||||
|
|
||||||
def contrast_color(color_hex):
|
def contrast_color(color_hex):
|
||||||
r, g, b = hex_to_rgb(color_hex)
|
r, g, b = hex_to_rgb(color_hex)
|
||||||
# As defined in https://www.w3.org/WAI/ER/WD-AERT/#color-contrast
|
# As defined in https://www.w3.org/WAI/ER/WD-AERT/#color-contrast
|
||||||
|
@ -20,23 +17,25 @@ def contrast_color(color_hex):
|
||||||
return "white"
|
return "white"
|
||||||
|
|
||||||
def character_span(character):
|
def character_span(character):
|
||||||
return f'<span class="symbol" style="color: {contrast_color(character.color_hex)}; background-color: {character.color_hex}">{character.symbol}</span>'
|
color = character['color_hex']
|
||||||
|
symbol = character['symbol']
|
||||||
|
return f'<span class="symbol" style="color: {contrast_color(color)}; background-color: {color}">{symbol}</span>'
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="fleet_state_icon(checksum, nickname, population)">
|
<%def name="fleet_state_icon(state)">
|
||||||
%if not checksum:
|
%if not state:
|
||||||
NO FLEET STATE AVAILABLE
|
<span style="color: #CCCCCC">—</span>
|
||||||
%else:
|
%else:
|
||||||
<table class="state-info" title="${nickname}">
|
<table class="state-info" title="${state['nickname']['text']}">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
## Need to compose these spans as strings to avoid introducing whitespaces
|
## Need to compose these spans as strings to avoid introducing whitespaces
|
||||||
<span class="state-icon">${"".join(character_span(character) for character in nickname.characters)}</span>
|
<span class="state-icon">${"".join(character_span(character) for character in state['nickname']['characters'])}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span>${population} nodes</span>
|
<span>${state['population']} nodes</span>
|
||||||
<br/>
|
<br/>
|
||||||
<span class="checksum">${checksum[0:8]}</span>
|
<span class="checksum">${state['checksum'][0:8]}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -44,30 +43,20 @@ NO FLEET STATE AVAILABLE
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
<%def name="fleet_state_icon_from_state(state)">
|
|
||||||
${fleet_state_icon(state.checksum, state.nickname, len(state.nodes))}
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
|
|
||||||
<%def name="fleet_state_icon_from_known_nodes(state)">
|
|
||||||
${fleet_state_icon(state.checksum, state.nickname, state.population())}
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
|
|
||||||
<%def name="node_info(node)">
|
<%def name="node_info(node)">
|
||||||
<div>
|
<div>
|
||||||
<table class="node-info">
|
<table class="node-info">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
## Need to compose these spans as strings to avoid introducing whitespaces
|
## Need to compose these spans as strings to avoid introducing whitespaces
|
||||||
<span class="node-icon">${"".join(character_span(character) for character in node.nickname.characters)}</span>
|
<span class="node-icon">${"".join(character_span(character) for character in node['nickname']['characters'])}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://${node.rest_url()}/status">
|
<a href="https://${node['rest_url']}/status">
|
||||||
<span class="nickname">${ node.nickname }</span>
|
<span class="nickname">${ node['nickname']['text'] }</span>
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<span class="checksum">${ node.checksum_address }</span>
|
<span class="checksum">${ node['staker_address'] }</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -75,7 +64,7 @@ ${fleet_state_icon(state.checksum, state.nickname, state.population())}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
<%def name="main()">
|
<%def name="main(status_info)">
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -168,7 +157,7 @@ ${fleet_state_icon(state.checksum, state.nickname, state.population())}
|
||||||
<table class="this-node-info">
|
<table class="this-node-info">
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><div class="this-node">${node_info(this_node)}</div></td>
|
<td><div class="this-node">${node_info(status_info)}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><div style="margin-bottom: 1em"></div></td>
|
<td><div style="margin-bottom: 1em"></div></td>
|
||||||
|
@ -176,48 +165,54 @@ ${fleet_state_icon(state.checksum, state.nickname, state.population())}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><i>Running:</i></td>
|
<td><i>Running:</i></td>
|
||||||
<td>v${ version }</td>
|
<td>v${ status_info['version'] }</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><i>Domain:</i></td>
|
<td><i>Domain:</i></td>
|
||||||
<td>${ domain }</td>
|
<td>${ status_info['domain'] }</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><i>Fleet state:</i></td>
|
<td><i>Fleet state:</i></td>
|
||||||
<td>${fleet_state_icon_from_known_nodes(this_node.known_nodes)}</td>
|
<td>${fleet_state_icon(status_info['fleet_state'])}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><i>Previous states:</i></td>
|
<td><i>Previous states:</i></td>
|
||||||
<td>
|
<td>
|
||||||
%for state in previous_states:
|
%for state in status_info['previous_fleet_states']:
|
||||||
${fleet_state_icon_from_state(state)}
|
${fleet_state_icon(state)}
|
||||||
%endfor
|
%endfor
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3>${len(known_nodes)} ${"known node" if len(known_nodes) == 1 else "known nodes"}:</h3>
|
%if 'known_nodes' in status_info:
|
||||||
|
<h3>${len(status_info['known_nodes'])} ${"known node" if len(status_info['known_nodes']) == 1 else "known nodes"}:</h3>
|
||||||
|
|
||||||
<table class="known-nodes">
|
<table class="known-nodes">
|
||||||
<thead>
|
<thead>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>Launched</td>
|
<td>Launched</td>
|
||||||
<td>Last Seen</td>
|
<td style="padding-right: 1em">Last Learned From</td>
|
||||||
<td>Fleet State</td>
|
<td>Fleet State</td>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
%for node in known_nodes:
|
%for node in status_info['known_nodes']:
|
||||||
<tr>
|
<tr>
|
||||||
<td>${node_info(node)}</td>
|
<td>${node_info(node)}</td>
|
||||||
<td>${ node.timestamp }</td>
|
<td>${node['timestamp']}</td>
|
||||||
<td>${ node.last_seen }</td>
|
<td>
|
||||||
<td>${fleet_state_icon(node.fleet_state_checksum,
|
%if node['last_learned_from']:
|
||||||
node.fleet_state_nickname,
|
${node['last_learned_from']}
|
||||||
node.fleet_state_population)}</td>
|
%else:
|
||||||
|
<span style="color: #CCCCCC">—</span>
|
||||||
|
%endif
|
||||||
|
</td>
|
||||||
|
<td>${fleet_state_icon(node['recorded_fleet_state'])}</td>
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
%endif
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -188,7 +188,7 @@ class AvailabilityTracker:
|
||||||
return
|
return
|
||||||
|
|
||||||
def sample(self, quantity: int) -> list:
|
def sample(self, quantity: int) -> list:
|
||||||
population = tuple(self._ursula.known_nodes._nodes.values())
|
population = tuple(self._ursula.known_nodes.values())
|
||||||
ursulas = random.sample(population=population, k=quantity)
|
ursulas = random.sample(population=population, k=quantity)
|
||||||
return ursulas
|
return ursulas
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ class UrsulaInfoMetricsCollector(BaseMetricsCollector):
|
||||||
'host': str(self.ursula.rest_interface),
|
'host': str(self.ursula.rest_interface),
|
||||||
'domain': self.ursula.domain,
|
'domain': self.ursula.domain,
|
||||||
'nickname': str(self.ursula.nickname),
|
'nickname': str(self.ursula.nickname),
|
||||||
'nickname_icon': self.ursula.nickname_icon,
|
'nickname_icon': self.ursula.nickname.icon,
|
||||||
'fleet_state': str(self.ursula.known_nodes.checksum),
|
'fleet_state': str(self.ursula.known_nodes.checksum),
|
||||||
'known_nodes': str(len(self.ursula.known_nodes))
|
'known_nodes': str(len(self.ursula.known_nodes))
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,9 +46,11 @@ def test_ursula_html_renders(ursula, client):
|
||||||
assert str(ursula.nickname).encode() in response.data
|
assert str(ursula.nickname).encode() in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_decentralized_json_status_endpoint(ursula, client):
|
@pytest.mark.parametrize('omit_known_nodes', [False, True])
|
||||||
response = client.get('/status/?json=true')
|
def test_decentralized_json_status_endpoint(ursula, client, omit_known_nodes):
|
||||||
|
omit_known_nodes_str = 'true' if omit_known_nodes else 'false'
|
||||||
|
response = client.get(f'/status/?json=true&omit_known_nodes={omit_known_nodes_str}')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
json_status = response.get_json()
|
json_status = response.get_json()
|
||||||
status = ursula.abridged_node_details()
|
status = ursula.status_info(omit_known_nodes=omit_known_nodes)
|
||||||
assert json_status == status
|
assert json_status == status
|
||||||
|
|
|
@ -118,9 +118,6 @@ def test_vladimir_illegal_interface_key_does_not_propagate(blockchain_ursulas):
|
||||||
# Indeed, Ursula noticed that something was up.
|
# Indeed, Ursula noticed that something was up.
|
||||||
vladimir in other_ursula.suspicious_activities_witnessed['vladimirs']
|
vladimir in other_ursula.suspicious_activities_witnessed['vladimirs']
|
||||||
|
|
||||||
# She marked him as Invalid...
|
|
||||||
vladimir in other_ursula.known_nodes._marked[vladimir.InvalidNode]
|
|
||||||
|
|
||||||
# ...and booted him from known_nodes
|
# ...and booted him from known_nodes
|
||||||
vladimir not in other_ursula.known_nodes
|
vladimir not in other_ursula.known_nodes
|
||||||
|
|
||||||
|
@ -143,7 +140,8 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali
|
||||||
# Ideally, a fishy node shouldn't be present in `known_nodes`,
|
# Ideally, a fishy node shouldn't be present in `known_nodes`,
|
||||||
# but I guess we're testing the case when it became fishy somewhere between we learned about it
|
# but I guess we're testing the case when it became fishy somewhere between we learned about it
|
||||||
# and the proposal arrangement.
|
# and the proposal arrangement.
|
||||||
blockchain_alice.known_nodes[vladimir.checksum_address] = vladimir
|
blockchain_alice.known_nodes.record_node(vladimir)
|
||||||
|
blockchain_alice.known_nodes.record_fleet_state()
|
||||||
|
|
||||||
with pytest.raises(vladimir.InvalidNode):
|
with pytest.raises(vladimir.InvalidNode):
|
||||||
idle_blockchain_policy._propose_arrangement(address=vladimir.checksum_address,
|
idle_blockchain_policy._propose_arrangement(address=vladimir.checksum_address,
|
||||||
|
|
|
@ -954,8 +954,8 @@ def fleet_of_highperf_mocked_ursulas(ursula_federated_test_config, request):
|
||||||
all_ursulas = {u.checksum_address: u for u in _ursulas}
|
all_ursulas = {u.checksum_address: u for u in _ursulas}
|
||||||
|
|
||||||
for ursula in _ursulas:
|
for ursula in _ursulas:
|
||||||
ursula.known_nodes._nodes = all_ursulas
|
ursula.known_nodes.current_state._nodes = all_ursulas
|
||||||
ursula.known_nodes.checksum = b"This is a fleet state checksum..".hex()
|
ursula.known_nodes.current_state.checksum = b"This is a fleet state checksum..".hex()
|
||||||
yield _ursulas
|
yield _ursulas
|
||||||
|
|
||||||
for ursula in _ursulas:
|
for ursula in _ursulas:
|
||||||
|
@ -972,7 +972,7 @@ def highperf_mocked_alice(fleet_of_highperf_mocked_ursulas):
|
||||||
save_metadata=False,
|
save_metadata=False,
|
||||||
reload_metadata=False)
|
reload_metadata=False)
|
||||||
|
|
||||||
with mock_cert_storage, mock_verify_node, mock_record_fleet_state, mock_message_verification, mock_keep_learning:
|
with mock_cert_storage, mock_verify_node, mock_message_verification, mock_keep_learning:
|
||||||
alice = config.produce(known_nodes=list(fleet_of_highperf_mocked_ursulas)[:1])
|
alice = config.produce(known_nodes=list(fleet_of_highperf_mocked_ursulas)[:1])
|
||||||
yield alice
|
yield alice
|
||||||
# TODO: Where does this really, truly belong?
|
# TODO: Where does this really, truly belong?
|
||||||
|
|
|
@ -24,7 +24,8 @@ def test_new_federated_ursula_announces_herself(lonely_ursula_maker):
|
||||||
ursula_in_a_house, ursula_with_a_mouse = lonely_ursula_maker(quantity=2, domain="useless_domain")
|
ursula_in_a_house, ursula_with_a_mouse = lonely_ursula_maker(quantity=2, domain="useless_domain")
|
||||||
|
|
||||||
# Neither Ursula knows about the other.
|
# Neither Ursula knows about the other.
|
||||||
assert ursula_in_a_house.known_nodes == ursula_with_a_mouse.known_nodes
|
assert ursula_with_a_mouse not in ursula_in_a_house.known_nodes
|
||||||
|
assert ursula_in_a_house not in ursula_with_a_mouse.known_nodes
|
||||||
|
|
||||||
ursula_in_a_house.remember_node(ursula_with_a_mouse)
|
ursula_in_a_house.remember_node(ursula_with_a_mouse)
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,7 @@ def test_learner_ignores_stored_nodes_from_other_domains(lonely_ursula_maker, tm
|
||||||
|
|
||||||
# Once pest made its way into learner, learner taught passed it to other mainnet nodes.
|
# Once pest made its way into learner, learner taught passed it to other mainnet nodes.
|
||||||
|
|
||||||
learner.known_nodes._nodes[pest.checksum_address] = pest # This used to happen anyway.
|
learner.known_nodes.record_node(pest) # This used to happen anyway.
|
||||||
other_staker._current_teacher_node = learner
|
other_staker._current_teacher_node = learner
|
||||||
other_staker.learn_from_teacher_node() # And once it did, the node from the wrong domain spread.
|
other_staker.learn_from_teacher_node() # And once it did, the node from the wrong domain spread.
|
||||||
assert pest not in other_staker.known_nodes # But not anymore.
|
assert pest not in other_staker.known_nodes # But not anymore.
|
||||||
|
|
|
@ -51,7 +51,7 @@ def test_get_cert_from_running_seed_node(lonely_ursula_maker):
|
||||||
network_middleware=RestMiddleware()).pop()
|
network_middleware=RestMiddleware()).pop()
|
||||||
assert not any_other_ursula.known_nodes
|
assert not any_other_ursula.known_nodes
|
||||||
|
|
||||||
yield deferToThread(any_other_ursula.load_seednodes)
|
yield deferToThread(lambda: any_other_ursula.load_seednodes(record_fleet_state=True))
|
||||||
assert firstula in any_other_ursula.known_nodes
|
assert firstula in any_other_ursula.known_nodes
|
||||||
|
|
||||||
firstula_as_learned = any_other_ursula.known_nodes[firstula.checksum_address]
|
firstula_as_learned = any_other_ursula.known_nodes[firstula.checksum_address]
|
||||||
|
|
|
@ -65,19 +65,30 @@ def test_old_state_is_preserved(federated_ursulas, lonely_ursula_maker):
|
||||||
some_ursula_in_the_fleet = list(federated_ursulas)[0]
|
some_ursula_in_the_fleet = list(federated_ursulas)[0]
|
||||||
lonely_learner.remember_node(some_ursula_in_the_fleet)
|
lonely_learner.remember_node(some_ursula_in_the_fleet)
|
||||||
checksum_after_learning_one = lonely_learner.known_nodes.checksum
|
checksum_after_learning_one = lonely_learner.known_nodes.checksum
|
||||||
|
assert some_ursula_in_the_fleet in lonely_learner.known_nodes
|
||||||
|
assert some_ursula_in_the_fleet.checksum_address in lonely_learner.known_nodes
|
||||||
|
assert len(lonely_learner.known_nodes) == 1
|
||||||
|
assert lonely_learner.known_nodes.population == 2
|
||||||
|
|
||||||
another_ursula_in_the_fleet = list(federated_ursulas)[1]
|
another_ursula_in_the_fleet = list(federated_ursulas)[1]
|
||||||
lonely_learner.remember_node(another_ursula_in_the_fleet)
|
lonely_learner.remember_node(another_ursula_in_the_fleet)
|
||||||
checksum_after_learning_two = lonely_learner.known_nodes.checksum
|
checksum_after_learning_two = lonely_learner.known_nodes.checksum
|
||||||
|
assert some_ursula_in_the_fleet in lonely_learner.known_nodes
|
||||||
|
assert another_ursula_in_the_fleet in lonely_learner.known_nodes
|
||||||
|
assert some_ursula_in_the_fleet.checksum_address in lonely_learner.known_nodes
|
||||||
|
assert another_ursula_in_the_fleet.checksum_address in lonely_learner.known_nodes
|
||||||
|
assert len(lonely_learner.known_nodes) == 2
|
||||||
|
assert lonely_learner.known_nodes.population == 3
|
||||||
|
|
||||||
assert checksum_after_learning_one != checksum_after_learning_two
|
assert checksum_after_learning_one != checksum_after_learning_two
|
||||||
|
|
||||||
proper_first_state = sorted([some_ursula_in_the_fleet, lonely_learner], key=lambda n: n.checksum_address)
|
first_state = lonely_learner.known_nodes._archived_states[-2]
|
||||||
assert lonely_learner.known_nodes.states[checksum_after_learning_one].nodes == proper_first_state
|
assert first_state.population == 2
|
||||||
|
assert first_state.checksum == checksum_after_learning_one
|
||||||
|
|
||||||
proper_second_state = sorted([some_ursula_in_the_fleet, another_ursula_in_the_fleet, lonely_learner],
|
second_state = lonely_learner.known_nodes._archived_states[-1]
|
||||||
key=lambda n: n.checksum_address)
|
assert second_state.population == 3
|
||||||
assert lonely_learner.known_nodes.states[checksum_after_learning_two].nodes == proper_second_state
|
assert second_state.checksum == checksum_after_learning_two
|
||||||
|
|
||||||
|
|
||||||
def test_state_is_recorded_after_learning(federated_ursulas, lonely_ursula_maker):
|
def test_state_is_recorded_after_learning(federated_ursulas, lonely_ursula_maker):
|
||||||
|
@ -87,22 +98,27 @@ def test_state_is_recorded_after_learning(federated_ursulas, lonely_ursula_maker
|
||||||
"""
|
"""
|
||||||
_lonely_ursula_maker = partial(lonely_ursula_maker, quantity=1)
|
_lonely_ursula_maker = partial(lonely_ursula_maker, quantity=1)
|
||||||
lonely_learner = _lonely_ursula_maker().pop()
|
lonely_learner = _lonely_ursula_maker().pop()
|
||||||
|
states = lonely_learner.known_nodes._archived_states
|
||||||
|
|
||||||
# This Ursula doesn't know about any nodes.
|
# This Ursula doesn't know about any nodes.
|
||||||
assert len(lonely_learner.known_nodes) == 0
|
assert len(lonely_learner.known_nodes) == 0
|
||||||
|
|
||||||
some_ursula_in_the_fleet = list(federated_ursulas)[0]
|
some_ursula_in_the_fleet = list(federated_ursulas)[0]
|
||||||
lonely_learner.remember_node(some_ursula_in_the_fleet)
|
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.
|
assert len(states) == 2 # Saved a fleet state when we remembered this node.
|
||||||
|
|
||||||
|
# The first fleet state is just us and the one about whom we learned, which is part of the fleet.
|
||||||
|
assert states[-1].population == 2
|
||||||
|
|
||||||
# The rest of the fucking owl.
|
# The rest of the fucking owl.
|
||||||
lonely_learner.learn_from_teacher_node()
|
lonely_learner.learn_from_teacher_node()
|
||||||
|
|
||||||
states = list(lonely_learner.known_nodes.states.values())
|
# There are two new states: one created after seednodes are loaded, to select a teacher,
|
||||||
assert len(states) == 2
|
# and the second after we get the rest of the nodes from the seednodes.
|
||||||
|
assert len(states) == 4
|
||||||
|
|
||||||
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.
|
# When we ran learn_from_teacher_node, we also loaded the rest 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.
|
assert states[-1].population == len(federated_ursulas) + 1
|
||||||
|
|
||||||
|
|
||||||
def test_teacher_records_new_fleet_state_upon_hearing_about_new_node(federated_ursulas, lonely_ursula_maker):
|
def test_teacher_records_new_fleet_state_upon_hearing_about_new_node(federated_ursulas, lonely_ursula_maker):
|
||||||
|
@ -110,18 +126,23 @@ def test_teacher_records_new_fleet_state_upon_hearing_about_new_node(federated_u
|
||||||
lonely_learner = _lonely_ursula_maker().pop()
|
lonely_learner = _lonely_ursula_maker().pop()
|
||||||
|
|
||||||
some_ursula_in_the_fleet = list(federated_ursulas)[0]
|
some_ursula_in_the_fleet = list(federated_ursulas)[0]
|
||||||
|
|
||||||
lonely_learner.remember_node(some_ursula_in_the_fleet)
|
lonely_learner.remember_node(some_ursula_in_the_fleet)
|
||||||
|
|
||||||
|
states = some_ursula_in_the_fleet.known_nodes._archived_states
|
||||||
|
|
||||||
teacher_states_before = list(some_ursula_in_the_fleet.known_nodes.states.values())
|
states_before = len(states)
|
||||||
lonely_learner.learn_from_teacher_node()
|
lonely_learner.learn_from_teacher_node()
|
||||||
teacher_states_after = list(some_ursula_in_the_fleet.known_nodes.states.values())
|
states_after = len(states)
|
||||||
|
|
||||||
# We added one fleet state.
|
# FIXME: some kind of a timeout is required here to wait for the learning to end
|
||||||
len(teacher_states_after) == len(teacher_states_before) + 1
|
return
|
||||||
|
|
||||||
|
# `some_ursula_in_the_fleet` learned about `lonely_learner`
|
||||||
|
assert states_before + 1 == states_after
|
||||||
|
|
||||||
# The current fleet state of the Teacher...
|
# The current fleet state of the Teacher...
|
||||||
teacher_fleet_state_checksum = some_ursula_in_the_fleet.fleet_state_checksum
|
teacher_fleet_state_checksum = some_ursula_in_the_fleet.fleet_state_checksum
|
||||||
|
|
||||||
# ...is the same as the learner, because both have learned about everybody at this point.
|
# ...is the same as the learner, because both have learned about everybody at this point.
|
||||||
teacher_fleet_state_checksum in lonely_learner.known_nodes.states
|
assert teacher_fleet_state_checksum == states[-1].checksum
|
||||||
|
|
|
@ -35,7 +35,7 @@ def test_alice_can_grant_even_when_the_first_nodes_she_tries_are_down(federated_
|
||||||
m, n = 2, 3
|
m, n = 2, 3
|
||||||
policy_end_datetime = maya.now() + datetime.timedelta(days=5)
|
policy_end_datetime = maya.now() + datetime.timedelta(days=5)
|
||||||
label = b"this_is_the_path_to_which_access_is_being_granted"
|
label = b"this_is_the_path_to_which_access_is_being_granted"
|
||||||
federated_alice.known_nodes._nodes = {}
|
federated_alice.known_nodes.current_state._nodes = {}
|
||||||
|
|
||||||
federated_alice.network_middleware = NodeIsDownMiddleware()
|
federated_alice.network_middleware = NodeIsDownMiddleware()
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ def test_alice_can_grant_even_when_the_first_nodes_she_tries_are_down(federated_
|
||||||
|
|
||||||
|
|
||||||
def test_node_has_changed_cert(federated_alice, federated_ursulas):
|
def test_node_has_changed_cert(federated_alice, federated_ursulas):
|
||||||
federated_alice.known_nodes._nodes = {}
|
federated_alice.known_nodes.current_state._nodes = {}
|
||||||
federated_alice.network_middleware = NodeIsDownMiddleware()
|
federated_alice.network_middleware = NodeIsDownMiddleware()
|
||||||
federated_alice.network_middleware.client.certs_are_broken = True
|
federated_alice.network_middleware.client.certs_are_broken = True
|
||||||
|
|
||||||
|
|
|
@ -159,8 +159,7 @@ def do_not_create_cert(*args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def simple_remember(ursula, node, *args, **kwargs):
|
def simple_remember(ursula, node, *args, **kwargs):
|
||||||
address = node.checksum_address
|
ursula.known_nodes.record_node(node)
|
||||||
ursula.known_nodes[address] = node
|
|
||||||
|
|
||||||
|
|
||||||
class NotARestApp:
|
class NotARestApp:
|
||||||
|
|
|
@ -32,8 +32,15 @@ from nucypher.utilities.networking import (
|
||||||
from tests.constants import MOCK_IP_ADDRESS
|
from tests.constants import MOCK_IP_ADDRESS
|
||||||
|
|
||||||
|
|
||||||
|
MOCK_NETWORK = 'holodeck'
|
||||||
|
|
||||||
|
|
||||||
class Dummy: # Teacher
|
class Dummy: # Teacher
|
||||||
certificate_filepath = None
|
|
||||||
|
def __init__(self, checksum_address):
|
||||||
|
self.checksum_address = checksum_address
|
||||||
|
self.certificate_filepath = None
|
||||||
|
self.domain = MOCK_NETWORK
|
||||||
|
|
||||||
class GoodResponse:
|
class GoodResponse:
|
||||||
status_code = 200
|
status_code = 200
|
||||||
|
@ -45,7 +52,7 @@ class Dummy: # Teacher
|
||||||
content = 'DUMMY 404'
|
content = 'DUMMY 404'
|
||||||
|
|
||||||
def mature(self):
|
def mature(self):
|
||||||
return Dummy()
|
return self
|
||||||
|
|
||||||
def verify_node(self, *args, **kwargs):
|
def verify_node(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
@ -53,6 +60,9 @@ class Dummy: # Teacher
|
||||||
def rest_url(self):
|
def rest_url(self):
|
||||||
return MOCK_IP_ADDRESS
|
return MOCK_IP_ADDRESS
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return self.checksum_address.encode()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_requests(mocker):
|
def mock_requests(mocker):
|
||||||
|
@ -66,14 +76,9 @@ def mock_client(mocker):
|
||||||
yield mocker.patch.object(NucypherMiddlewareClient, 'invoke_method', return_value=Dummy.GoodResponse)
|
yield mocker.patch.object(NucypherMiddlewareClient, 'invoke_method', return_value=Dummy.GoodResponse)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def mock_network():
|
|
||||||
return 'holodeck'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_default_teachers(mocker, mock_network):
|
def mock_default_teachers(mocker):
|
||||||
teachers = {mock_network: (MOCK_IP_ADDRESS, )}
|
teachers = {MOCK_NETWORK: (MOCK_IP_ADDRESS, )}
|
||||||
mocker.patch.dict(RestMiddleware.TEACHER_NODES, teachers)
|
mocker.patch.dict(RestMiddleware.TEACHER_NODES, teachers)
|
||||||
|
|
||||||
|
|
||||||
|
@ -82,31 +87,33 @@ def test_get_external_ip_from_centralized_source(mock_requests):
|
||||||
mock_requests.assert_called_once_with(url=CENTRALIZED_IP_ORACLE_URL)
|
mock_requests.assert_called_once_with(url=CENTRALIZED_IP_ORACLE_URL)
|
||||||
|
|
||||||
|
|
||||||
def test_get_external_ip_from_empty_known_nodes(mock_requests, mock_network):
|
def test_get_external_ip_from_empty_known_nodes(mock_requests):
|
||||||
sensor = FleetSensor(domain=mock_network)
|
sensor = FleetSensor(domain=MOCK_NETWORK)
|
||||||
assert len(sensor) == 0
|
assert len(sensor) == 0
|
||||||
get_external_ip_from_known_nodes(known_nodes=sensor)
|
get_external_ip_from_known_nodes(known_nodes=sensor)
|
||||||
# skipped because there are no known nodes
|
# skipped because there are no known nodes
|
||||||
mock_requests.assert_not_called()
|
mock_requests.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_get_external_ip_from_known_nodes_with_one_known_node(mock_requests, mock_network):
|
def test_get_external_ip_from_known_nodes_with_one_known_node(mock_requests):
|
||||||
sensor = FleetSensor(domain=mock_network)
|
sensor = FleetSensor(domain=MOCK_NETWORK)
|
||||||
sensor._nodes['0xdeadbeef'] = Dummy()
|
sensor.record_node(Dummy('0xdeadbeef'))
|
||||||
|
sensor.record_fleet_state()
|
||||||
assert len(sensor) == 1
|
assert len(sensor) == 1
|
||||||
get_external_ip_from_known_nodes(known_nodes=sensor)
|
get_external_ip_from_known_nodes(known_nodes=sensor)
|
||||||
# skipped because there are too few known nodes
|
# skipped because there are too few known nodes
|
||||||
mock_requests.assert_not_called()
|
mock_requests.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_get_external_ip_from_known_nodes(mock_client, mock_network):
|
def test_get_external_ip_from_known_nodes(mock_client):
|
||||||
|
|
||||||
# Setup FleetSensor
|
# Setup FleetSensor
|
||||||
sensor = FleetSensor(domain=mock_network)
|
sensor = FleetSensor(domain=MOCK_NETWORK)
|
||||||
sample_size = 3
|
sample_size = 3
|
||||||
sensor._nodes['0xdeadbeef'] = Dummy()
|
sensor.record_node(Dummy('0xdeadbeef'))
|
||||||
sensor._nodes['0xdeadllama'] = Dummy()
|
sensor.record_node(Dummy('0xdeadllama'))
|
||||||
sensor._nodes['0xdeadmouse'] = Dummy()
|
sensor.record_node(Dummy('0xdeadmouse'))
|
||||||
|
sensor.record_fleet_state()
|
||||||
assert len(sensor) == sample_size
|
assert len(sensor) == sample_size
|
||||||
|
|
||||||
# First sampled node replies
|
# First sampled node replies
|
||||||
|
@ -120,19 +127,20 @@ def test_get_external_ip_from_known_nodes(mock_client, mock_network):
|
||||||
assert mock_client.call_count == sample_size
|
assert mock_client.call_count == sample_size
|
||||||
|
|
||||||
|
|
||||||
def test_get_external_ip_from_known_nodes_client(mocker, mock_client, mock_network):
|
def test_get_external_ip_from_known_nodes_client(mocker, mock_client):
|
||||||
|
|
||||||
# Setup FleetSensor
|
# Setup FleetSensor
|
||||||
sensor = FleetSensor(domain=mock_network)
|
sensor = FleetSensor(domain=MOCK_NETWORK)
|
||||||
sample_size = 3
|
sample_size = 3
|
||||||
sensor._nodes['0xdeadbeef'] = Dummy()
|
sensor.record_node(Dummy('0xdeadbeef'))
|
||||||
sensor._nodes['0xdeadllama'] = Dummy()
|
sensor.record_node(Dummy('0xdeadllama'))
|
||||||
sensor._nodes['0xdeadmouse'] = Dummy()
|
sensor.record_node(Dummy('0xdeadmouse'))
|
||||||
|
sensor.record_fleet_state()
|
||||||
assert len(sensor) == sample_size
|
assert len(sensor) == sample_size
|
||||||
|
|
||||||
# Setup HTTP Client
|
# Setup HTTP Client
|
||||||
mocker.patch.object(Ursula, 'from_teacher_uri', return_value=Dummy())
|
mocker.patch.object(Ursula, 'from_teacher_uri', return_value=Dummy('0xdeadpork'))
|
||||||
teacher_uri = RestMiddleware.TEACHER_NODES[mock_network][0]
|
teacher_uri = RestMiddleware.TEACHER_NODES[MOCK_NETWORK][0]
|
||||||
|
|
||||||
get_external_ip_from_known_nodes(known_nodes=sensor, sample_size=sample_size)
|
get_external_ip_from_known_nodes(known_nodes=sensor, sample_size=sample_size)
|
||||||
assert mock_client.call_count == 1 # first node responded
|
assert mock_client.call_count == 1 # first node responded
|
||||||
|
@ -142,22 +150,22 @@ def test_get_external_ip_from_known_nodes_client(mocker, mock_client, mock_netwo
|
||||||
assert endpoint == f'https://{teacher_uri}/ping'
|
assert endpoint == f'https://{teacher_uri}/ping'
|
||||||
|
|
||||||
|
|
||||||
def test_get_external_ip_default_teacher_unreachable(mocker, mock_network):
|
def test_get_external_ip_default_teacher_unreachable(mocker):
|
||||||
for error in NodeSeemsToBeDown:
|
for error in NodeSeemsToBeDown:
|
||||||
# Default seednode is down
|
# Default seednode is down
|
||||||
mocker.patch.object(Ursula, 'from_teacher_uri', side_effect=error)
|
mocker.patch.object(Ursula, 'from_teacher_uri', side_effect=error)
|
||||||
ip = get_external_ip_from_default_teacher(network=mock_network)
|
ip = get_external_ip_from_default_teacher(network=MOCK_NETWORK)
|
||||||
assert ip is None
|
assert ip is None
|
||||||
|
|
||||||
|
|
||||||
def test_get_external_ip_from_default_teacher(mocker, mock_client, mock_requests, mock_network):
|
def test_get_external_ip_from_default_teacher(mocker, mock_client, mock_requests):
|
||||||
|
|
||||||
mock_client.return_value = Dummy.GoodResponse
|
mock_client.return_value = Dummy.GoodResponse
|
||||||
teacher_uri = RestMiddleware.TEACHER_NODES[mock_network][0]
|
teacher_uri = RestMiddleware.TEACHER_NODES[MOCK_NETWORK][0]
|
||||||
mocker.patch.object(Ursula, 'from_teacher_uri', return_value=Dummy())
|
mocker.patch.object(Ursula, 'from_teacher_uri', return_value=Dummy('0xdeadbeef'))
|
||||||
|
|
||||||
# "Success"
|
# "Success"
|
||||||
ip = get_external_ip_from_default_teacher(network=mock_network)
|
ip = get_external_ip_from_default_teacher(network=MOCK_NETWORK)
|
||||||
assert ip == MOCK_IP_ADDRESS
|
assert ip == MOCK_IP_ADDRESS
|
||||||
|
|
||||||
# Check that the correct endpoint and function is targeted
|
# Check that the correct endpoint and function is targeted
|
||||||
|
@ -181,16 +189,17 @@ def test_get_external_ip_default_unknown_network():
|
||||||
determine_external_ip_address(known_nodes=sensor, network=unknown_domain)
|
determine_external_ip_address(known_nodes=sensor, network=unknown_domain)
|
||||||
|
|
||||||
|
|
||||||
def test_get_external_ip_cascade_failure(mocker, mock_network, mock_requests):
|
def test_get_external_ip_cascade_failure(mocker, mock_requests):
|
||||||
first = mocker.patch('nucypher.utilities.networking.get_external_ip_from_known_nodes', return_value=None)
|
first = mocker.patch('nucypher.utilities.networking.get_external_ip_from_known_nodes', return_value=None)
|
||||||
second = mocker.patch('nucypher.utilities.networking.get_external_ip_from_default_teacher', return_value=None)
|
second = mocker.patch('nucypher.utilities.networking.get_external_ip_from_default_teacher', return_value=None)
|
||||||
third = mocker.patch('nucypher.utilities.networking.get_external_ip_from_centralized_source', return_value=None)
|
third = mocker.patch('nucypher.utilities.networking.get_external_ip_from_centralized_source', return_value=None)
|
||||||
|
|
||||||
sensor = FleetSensor(domain=mock_network)
|
sensor = FleetSensor(domain=MOCK_NETWORK)
|
||||||
sensor._nodes['0xdeadbeef'] = Dummy()
|
sensor.record_node(Dummy('0xdeadbeef'))
|
||||||
|
sensor.record_fleet_state()
|
||||||
|
|
||||||
with pytest.raises(UnknownIPAddress, match='External IP address detection failed'):
|
with pytest.raises(UnknownIPAddress, match='External IP address detection failed'):
|
||||||
determine_external_ip_address(network=mock_network, known_nodes=sensor)
|
determine_external_ip_address(network=MOCK_NETWORK, known_nodes=sensor)
|
||||||
|
|
||||||
first.assert_called_once()
|
first.assert_called_once()
|
||||||
second.assert_called_once()
|
second.assert_called_once()
|
||||||
|
|
Loading…
Reference in New Issue