Skip to content
Snippets Groups Projects
node.py 8.61 KiB
Newer Older
'''
Created on 21 févr. 2015

@author: inso
'''

from ucoinpy.documents.peer import Peer, BMAEndpoint, Endpoint
from ucoinpy.api import bma
inso's avatar
inso committed
from ucoinpy.api.bma import ConnectionHandler
from requests.exceptions import RequestException
inso's avatar
inso committed
from ...tools.exceptions import InvalidNodeCurrency, PersonNotFoundError
from ..person import Person
from PyQt5.QtCore import QObject, pyqtSignal
    A node is a peer seend from the client point of view.
    This node can have multiple states :
    - ONLINE : The node is available for requests
    - OFFLINE: The node is disconnected
    - DESYNCED : The node is online but is desynced from the network
    - CORRUPTED : The node is corrupted, some weird behaviour is going on
    '''

    ONLINE = 1
    OFFLINE = 2
    DESYNCED = 3
    CORRUPTED = 4

inso's avatar
inso committed
    def __init__(self, currency, endpoints, uid, pubkey, block, state):
        self._endpoints = endpoints
inso's avatar
inso committed
        self._uid = uid
        self._pubkey = pubkey
        self._block = block
        self._state = state
inso's avatar
inso committed
        self._neighbours = []
inso's avatar
inso committed
        self._currency = currency

    @classmethod
    def from_address(cls, currency, address, port):
        '''
        Factory method to get a node from a given address

        :param str currency: The node currency. None if we don't know\
         the currency it should have, for example if its the first one we add
        :param str address: The node address
        :param int port: The node port
        '''
inso's avatar
inso committed
        peer_data = bma.network.Peering(ConnectionHandler(address, port)).get()

        peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['raw'],
                                                  peer_data['signature']))

        if currency is not None:
            if peer.currency != currency:
                raise InvalidNodeCurrency(peer.currency, currency)

inso's avatar
inso committed
        node = cls(peer.currency, peer.endpoints, "", peer.pubkey, 0, Node.ONLINE)
inso's avatar
inso committed
        node.refresh_state()
        return node

    @classmethod
    def from_peer(cls, currency, peer):
        '''
        Factory method to get a node from a peer document.

        :param str currency: The node currency. None if we don't know\
         the currency it should have, for example if its the first one we add
        :param peer: The peer document
        '''
inso's avatar
inso committed
        if currency is not None:
            if peer.currency != currency:
                raise InvalidNodeCurrency(peer.currency, currency)

inso's avatar
inso committed
        node = cls(peer.currency, peer.endpoints, "", "", 0, Node.ONLINE)
inso's avatar
inso committed
        node.refresh_state()
        return node

    @classmethod
    def from_json(cls, currency, data):
        endpoints = []
inso's avatar
inso committed
        uid = ""
        pubkey = ""

        for endpoint_data in data['endpoints']:
            endpoints.append(Endpoint.from_inline(endpoint_data))

inso's avatar
inso committed
        if currency in data:
            currency = data['currency']
inso's avatar
inso committed

inso's avatar
inso committed
        if uid in data:
            uid = data['uid']

        if pubkey in data:
            pubkey = data['pubkey']

        node = cls(currency, endpoints, uid, pubkey, 0, Node.ONLINE)
inso's avatar
inso committed
        node.refresh_state()
inso's avatar
inso committed
    def jsonify(self):
inso's avatar
inso committed
        data = {'pubkey': self._pubkey,
inso's avatar
inso committed
                'uid': self._uid,
inso's avatar
inso committed
                'currency': self._currency}
inso's avatar
inso committed
        endpoints = []
        for e in self._endpoints:
            endpoints.append(e.inline())
        data['endpoints'] = endpoints
        return data

    @property
    def pubkey(self):
        return self._pubkey

    @property
    def endpoint(self) -> BMAEndpoint:
        return next((e for e in self._endpoints if type(e) is BMAEndpoint))

    @property
    def block(self):
        return self._block

    @property
    def state(self):
        return self._state

inso's avatar
inso committed
    @property
    def currency(self):
        return self._currency

inso's avatar
inso committed
    @property
    def neighbours(self):
        return self._neighbours

inso's avatar
inso committed
    @property
    def uid(self):
        return self._uid

inso's avatar
inso committed
    def check_sync(self, block):
        if self._block < block:
            self._state = Node.DESYNCED
        else:
            self._state = Node.ONLINE

inso's avatar
inso committed
    def _request_uid(self):
        uid = ""
        try:
            data = bma.wot.Lookup(self.endpoint.conn_handler(), self.pubkey).get()
            timestamp = 0
            for result in data['results']:
                if result["pubkey"] == self.pubkey:
                    uids = result['uids']
                    for uid in uids:
                        if uid["meta"]["timestamp"] > timestamp:
                            timestamp = uid["meta"]["timestamp"]
                            uid = uid["uid"]
        except ValueError as e:
            if '404' in str(e):
                logging.debug("Error : node uid not found : {0}".format(self.pubkey))
                uid = ""
        return uid

inso's avatar
inso committed
    def refresh_state(self):
inso's avatar
inso committed
        emit_change = False
        try:
            informations = bma.network.Peering(self.endpoint.conn_handler()).get()
            block = bma.blockchain.Current(self.endpoint.conn_handler()).get()
inso's avatar
inso committed
            peers_data = bma.network.peering.Peers(self.endpoint.conn_handler()).get()
            neighbours = []
            for p in peers_data:
                peer = Peer.from_signed_raw("{0}{1}\n".format(p['value']['raw'],
                                                            p['value']['signature']))
                neighbours.append(peer.endpoints)

            block_number = block["number"]
            node_pubkey = informations["pubkey"]
            node_currency = informations["currency"]
        except ValueError as e:
            if '404' in e:
                block_number = 0
        except RequestException:
            self._state = Node.OFFLINE
inso's avatar
inso committed
            emit_change = True
        # If not is offline, do not refresh last data
        if self._state != Node.OFFLINE:
            # If not changed its currency, consider it corrupted
            if node_currency != self._currency:
                self.state = Node.CORRUPTED
                emit_change = True
            else:
inso's avatar
inso committed
                node_uid = self._request_uid()

                if block_number != self._block:
                    self._block = block_number
                    emit_change = True

                if node_pubkey != self._pubkey:
                    self._pubkey = node_pubkey
                    emit_change = True

inso's avatar
inso committed
                if node_uid != self._uid:
                    self._uid = node_uid
                    emit_change = True

                logging.debug(neighbours)
                new_inlines = [e.inline() for n in neighbours for e in n]
                last_inlines = [e.inline() for n in self._neighbours for e in n]

                hash_new_neighbours = hash(tuple(frozenset(sorted(new_inlines))))
                hash_last_neighbours = hash(tuple(frozenset(sorted(last_inlines))))
                if hash_new_neighbours != hash_last_neighbours:
                    self._neighbours = neighbours
                    emit_change = True
inso's avatar
inso committed

        if emit_change:
            self.changed.emit()

inso's avatar
inso committed
    def peering_traversal(self, found_nodes,
inso's avatar
inso committed
                          traversed_pubkeys, interval):
        logging.debug("Read {0} peering".format(self.pubkey))
        traversed_pubkeys.append(self.pubkey)
inso's avatar
inso committed
        self.refresh_state()
        if self.pubkey not in [n.pubkey for n in found_nodes]:
            # if node is corrupted remove it
            if self._state != Node.CORRUPTED:
                found_nodes.append(self)
inso's avatar
inso committed
            logging.debug(self.neighbours)
inso's avatar
inso committed
            for n in self.neighbours:
inso's avatar
inso committed
                e = next(e for e in n if type(e) is BMAEndpoint)
                peering = bma.network.Peering(e.conn_handler()).get()
inso's avatar
inso committed
                peer = Peer.from_signed_raw("{0}{1}\n".format(peering['raw'],
                                                            peering['signature']))
inso's avatar
inso committed
                node = Node.from_peer(self._currency, peer)
                logging.debug(traversed_pubkeys)
inso's avatar
inso committed
                logging.debug("Traversing : next to read : {0} : {1}".format(node.pubkey,
                              (node.pubkey not in traversed_pubkeys)))
                if node.pubkey not in traversed_pubkeys:
inso's avatar
inso committed
                    node.peering_traversal(found_nodes,
inso's avatar
inso committed
                                        traversed_pubkeys, interval)
                    time.sleep(interval)
        except RequestException as e:
            self._state = Node.OFFLINE

    def __str__(self):
        return ','.join([str(self.pubkey), str(self.endpoint.server), str(self.endpoint.port), str(self.block),
                         str(self.currency), str(self.state), str(self.neighbours)])