import logging import time import asyncio from PyQt5.QtCore import QLocale, QDateTime from ..core.registry import Identity, BlockchainState from ..tools.decorators import asyncify from ..tools.exceptions import NoPeerAvailable from cutecoin.gui.views.wot import NODE_STATUS_HIGHLIGHTED, NODE_STATUS_OUT, ARC_STATUS_STRONG, ARC_STATUS_WEAK class Graph(object): def __init__(self, app, community, graph=None): """ Init Graph instance :param cutecoin.core.app.Application app: Application instance :param cutecoin.core.community.Community community: Community instance :param dict graph: Dict of the graph :return: """ self.app = app self.community = community self.signature_validity = 0 self.ARC_STATUS_STRONG_time = 0 # graph empty if None parameter self._graph = graph or (dict() and (graph is None)) @asyncio.coroutine def refresh_signature_validity(self): parameters = yield from self.community.parameters() self.signature_validity = parameters['sigValidity'] # arc considered strong during 75% of signature validity time self.ARC_STATUS_STRONG_time = int(self.signature_validity * 0.75) def set(self, graph): """ Set the graph from dict :param dict graph: :return: """ self._graph = graph def get(self): """ Return the graph dict :return: """ return self._graph @asyncio.coroutine def get_shortest_path_between_members(self, from_identity, to_identity): """ Return path list of nodes from from_identity to to_identity :param identity from_identity: :param identity to_identity: :return: """ path = list() # if from_identity has no certifications, we can not make a path certifier_list = yield from from_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) certified_list = yield from from_identity.unique_valid_certified_by(self.app.identities_registry, self.community) print (certifier_list, certified_list) if not certifier_list and not certified_list: logging.debug('from_identity has no certifications : can not calculate wot path') return path # if to_identity has no certifications, we can not make a path certifier_list = yield from to_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) certified_list = yield from to_identity.unique_valid_certified_by(self.app.identities_registry, self.community) if not certifier_list and not certified_list: logging.debug('to_identity has no certifications : can not calculate wot path') return path logging.debug("path between %s to %s..." % (from_identity.uid, to_identity.uid)) if from_identity.pubkey not in self._graph.keys(): self.add_identity(from_identity) certifier_list = yield from from_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) yield from self.add_certifier_list(certifier_list, from_identity, to_identity) certified_list = yield from from_identity.unique_valid_certified_by(self.app.identities_registry, self.community) yield from self.add_certified_list(certified_list, from_identity, to_identity) if to_identity.pubkey not in self._graph.keys(): # recursively feed graph searching for account node... yield from self.explore_to_find_member(to_identity, self._graph[from_identity.pubkey]['connected'], list()) if len(self._graph[from_identity.pubkey]['connected']) > 0 and to_identity.pubkey in self._graph: # calculate path of nodes between identity and to_identity path = yield from self.find_shortest_path(self._graph[from_identity.pubkey], self._graph[to_identity.pubkey]) if path: logging.debug([node['text'] for node in path]) else: logging.debug('no wot path') return path @asyncio.coroutine def explore_to_find_member(self, identity, connected=None, done=None): """ Scan graph recursively to find identity :param identity identity: identity instance to find :param list connected: Optional, default=None, Pubkey list of the connected nodes around the current scanned node :param list done: Optional, default=None, List of node already scanned :return: False when the identity is added in the graph """ # functions keywords args are persistent... Need to reset it with None trick connected = connected or (list() and (connected is None)) done = done or (list() and (done is None)) logging.debug("search %s in " % identity.uid) logging.debug([self._graph[pubkey]['text'] for pubkey in connected]) # for each pubkey connected... for pubkey in tuple(connected): # capture node connected node = self._graph[pubkey] if node['id'] in tuple(done): continue identity_selected = identity.from_handled_data(node['text'], node['id'], BlockchainState.VALIDATED) certifier_list = yield from identity_selected.unique_valid_certifiers_of(self.app.identities_registry, self.community) yield from self.add_certifier_list(certifier_list, identity_selected, identity) if identity.pubkey in tuple(self._graph.keys()): return True certified_list = yield from identity_selected.unique_valid_certified_by(self.app.identities_registry, self.community) yield from self.add_certified_list(certified_list, identity_selected, identity) if identity.pubkey in tuple(self._graph.keys()): return True if node['id'] not in tuple(done): done.append(node['id']) if len(done) >= len(self._graph): return False found = yield from self.explore_to_find_member(identity, self._graph[identity_selected.pubkey]['connected'], done) if found: return True return False @asyncio.coroutine def find_shortest_path(self, start, end, path=None): """ Find recursively the shortest path between two nodes :param dict start: Start node :param dict end: End node :param list path: Optional, default=None, List of nodes :return: """ path = path or (list() and (path is None)) path = path + [start] if start['id'] == end['id']: return path if start['id'] not in self._graph.keys(): return None shortest = None for pubkey in tuple(self._graph[start['id']]['connected']): node = self._graph[pubkey] if node not in path: newpath = yield from self.find_shortest_path(node, end, path) if newpath: if not shortest or len(newpath) < len(shortest): shortest = newpath return shortest @asyncio.coroutine def add_certifier_list(self, certifier_list, identity, identity_account): """ Add list of certifiers to graph :param list certifier_list: List of certifiers from api :param identity identity: identity instance which is certified :param identity identity_account: Account identity instance :return: """ if self.community: try: yield from self.refresh_signature_validity() # add certifiers of uid for certifier in tuple(certifier_list): # add only valid certification... if (time.time() - certifier['cert_time']) > self.signature_validity: continue # new node if certifier['identity'].pubkey not in self._graph.keys(): node_status = 0 is_member = yield from certifier['identity'].is_member(self.community) if certifier['identity'].pubkey == identity_account.pubkey: node_status += NODE_STATUS_HIGHLIGHTED if is_member is False: node_status += NODE_STATUS_OUT self._graph[certifier['identity'].pubkey] = { 'id': certifier['identity'].pubkey, 'arcs': list(), 'text': certifier['identity'].uid, 'tooltip': certifier['identity'].pubkey, 'status': node_status, 'connected': [identity.pubkey] } # keep only the latest certification if self._graph[certifier['identity'].pubkey]['arcs']: if certifier['cert_time'] < self._graph[certifier['identity'].pubkey]['arcs'][0]['cert_time']: continue # display validity status if (time.time() - certifier['cert_time']) > self.ARC_STATUS_STRONG_time: arc_status = ARC_STATUS_WEAK else: arc_status = ARC_STATUS_STRONG arc = { 'id': identity.pubkey, 'status': arc_status, 'tooltip': QLocale.toString( QLocale(), QDateTime.fromTime_t(certifier['cert_time'] + self.signature_validity).date(), QLocale.dateFormat(QLocale(), QLocale.ShortFormat) ), 'cert_time': certifier['cert_time'] } current_block_number = self.community.network.current_blockid.number if current_block_number and certifier['block_number']: current_validations = current_block_number - certifier['block_number'] + 1 else: current_validations = 0 members_pubkeys = yield from self.community.members_pubkeys() max_validation = self.community.network.fork_window(members_pubkeys) + 1 # Current validation can be negative if self.community.network.current_blockid.number # is not refreshed yet if max_validation > current_validations >= 0: if self.app.preferences['expert_mode']: arc['validation_text'] = "{0}/{1}".format(current_validations, max_validation) else: validation = current_validations / max_validation * 100 arc['validation_text'] = "{0} %".format(QLocale().toString(float(validation), 'f', 0)) else: arc['validation_text'] = None # add arc to certifier self._graph[certifier['identity'].pubkey]['arcs'].append(arc) # if certifier node not in identity nodes if certifier['identity'].pubkey not in tuple(self._graph[identity.pubkey]['connected']): # add certifier node to identity node self._graph[identity.pubkey]['connected'].append(certifier['identity'].pubkey) except NoPeerAvailable as e: logging.debug(str(e)) @asyncio.coroutine def add_certified_list(self, certified_list, identity, identity_account): """ Add list of certified from api to graph :param list certified_list: List of certified from api :param identity identity: identity instance which is certifier :param identity identity_account: Account identity instance :return: """ if self.community: try: yield from self.refresh_signature_validity() # add certified by uid for certified in tuple(certified_list): # add only valid certification... if (time.time() - certified['cert_time']) > self.signature_validity: continue if certified['identity'].pubkey not in self._graph.keys(): node_status = 0 is_member = yield from certified['identity'].is_member(self.community) if certified['identity'].pubkey == identity_account.pubkey: node_status += NODE_STATUS_HIGHLIGHTED if is_member is False: node_status += NODE_STATUS_OUT self._graph[certified['identity'].pubkey] = { 'id': certified['identity'].pubkey, 'arcs': list(), 'text': certified['identity'].uid, 'tooltip': certified['identity'].pubkey, 'status': node_status, 'connected': [identity.pubkey] } # display validity status if (time.time() - certified['cert_time']) > self.ARC_STATUS_STRONG_time: arc_status = ARC_STATUS_WEAK else: arc_status = ARC_STATUS_STRONG arc = { 'id': certified['identity'].pubkey, 'status': arc_status, 'tooltip': QLocale.toString( QLocale(), QDateTime.fromTime_t(certified['cert_time'] + self.signature_validity).date(), QLocale.dateFormat(QLocale(), QLocale.ShortFormat) ), 'cert_time': certified['cert_time'] } current_block_number = self.community.network.current_blockid.number if current_block_number and certified['block_number']: current_validations = current_block_number - certified['block_number'] + 1 else: current_validations = 0 members_pubkeys = yield from self.community.members_pubkeys() max_validations = self.community.network.fork_window(members_pubkeys) + 1 if max_validations > current_validations >= 0: if self.app.preferences['expert_mode']: arc['validation_text'] = "{0}/{1}".format(current_validations, max_validations) else: validation = current_validations / max_validations * 100 validation = 100 if validation > 100 else validation arc['validation_text'] = "{0} %".format(QLocale().toString(float(validation), 'f', 0)) else: arc['validation_text'] = None # replace old arc if this one is more recent new_arc = True index = 0 for a in self._graph[identity.pubkey]['arcs']: # if same arc already exists... if a['id'] == arc['id']: # if arc more recent, dont keep old one... if arc['cert_time'] >= a['cert_time']: self._graph[identity.pubkey]['arcs'][index] = arc new_arc = False index += 1 # if arc not in graph... if new_arc: # add arc in graph self._graph[identity.pubkey]['arcs'].append(arc) # if certified node not in identity nodes if certified['identity'].pubkey not in tuple(self._graph[identity.pubkey]['connected']): # add certified node to identity node self._graph[identity.pubkey]['connected'].append(certified['identity'].pubkey) except NoPeerAvailable as e: logging.debug(str(e)) def add_identity(self, identity, status=None, arcs=None, connected=None): """ Add identity as a new node in graph :param identity identity: identity instance :param int status: Optional, default=None, Node status (see cutecoin.gui.views.wot) :param list arcs: Optional, default=None, List of arcs (certified by identity) :param list connected: Optional, default=None, Public key list of the connected nodes around the identity :return: """ # functions keywords args are persistent... Need to reset it with None trick status = status or (0 and (status is None)) arcs = arcs or (list() and (arcs is None)) connected = connected or (list() and (connected is None)) self._graph[identity.pubkey] = { 'id': identity.pubkey, 'arcs': arcs, 'text': identity.uid, 'tooltip': identity.pubkey, 'status': status, 'connected': connected }