From 702e05f5976e957a525cd12ebb9b79ec38e1e58c Mon Sep 17 00:00:00 2001
From: Inso <insomniak.fr@gmail.com>
Date: Tue, 24 Feb 2015 23:29:51 +0100
Subject: [PATCH] Refactoring netcode so that background crawling can be
 implemented

---
 src/cutecoin/core/community.py                | 143 +++---------------
 .../core/{network => net}/__init__.py         |   0
 src/cutecoin/core/net/network.py              |  81 ++++++++++
 src/cutecoin/core/net/node.py                 | 112 ++++++++++++++
 src/cutecoin/core/network/node.py             |  50 ------
 src/cutecoin/models/network.py                |   1 +
 6 files changed, 217 insertions(+), 170 deletions(-)
 rename src/cutecoin/core/{network => net}/__init__.py (100%)
 create mode 100644 src/cutecoin/core/net/network.py
 create mode 100644 src/cutecoin/core/net/node.py
 delete mode 100644 src/cutecoin/core/network/node.py

diff --git a/src/cutecoin/core/community.py b/src/cutecoin/core/community.py
index 6242a98e..c872cf37 100644
--- a/src/cutecoin/core/community.py
+++ b/src/cutecoin/core/community.py
@@ -9,7 +9,8 @@ from ucoinpy import PROTOCOL_VERSION
 from ucoinpy.documents.peer import Peer, Endpoint, BMAEndpoint
 from ucoinpy.documents.block import Block
 from ..tools.exceptions import NoPeerAvailable
-from .network.node import Node
+from .net.node import Node
+from .net.network import Network
 import logging
 import inspect
 import hashlib
@@ -75,13 +76,12 @@ class Community(object):
     classdocs
     '''
 
-    def __init__(self, currency, peers):
+    def __init__(self, currency, network):
         '''
         A community is a group of nodes using the same currency.
         '''
         self.currency = currency
-        self.peers = [p for p in peers if p.currency == currency]
-        self._nodes = [Node.from_peer(p) for p in peers]
+        self._network = network
         self._cache = Cache(self)
 
         self._cache.refresh()
@@ -97,47 +97,11 @@ class Community(object):
 
     @classmethod
     def load(cls, json_data):
-        peers = []
-
-        currency = json_data['currency']
-
-        for data in json_data['peers']:
-            for e in data['endpoints']:
-                if Endpoint.from_inline(e) is not None:
-                    endpoint = Endpoint.from_inline(e)
-                    try:
-                        peering = bma.network.Peering(endpoint.conn_handler())
-                        peer_data = peering.get()
-                        peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['raw'],
-                                                          peer_data['signature']))
-                        peers.append(peer)
-                        break
-                    except:
-                        pass
-
-        community = cls(currency, peers)
-        logging.debug("Creating community")
-        community.peers = community.peering()
-        logging.debug("{0} peers found".format(len(community.peers)))
-        logging.debug([peer.pubkey for peer in community.peers])
-        return community
-
-    @classmethod
-    def without_network(cls, json_data):
-        peers = []
-
         currency = json_data['currency']
 
-        for data in json_data['peers']:
-            endpoints = []
-            for e in data['endpoints']:
-                endpoints.append(Endpoint.from_inline(e))
-            peer = Peer(PROTOCOL_VERSION, currency, data['pubkey'],
-                        "0-DA39A3EE5E6B4B0D3255BFEF95601890AFD80709",
-                        endpoints, None)
-            peers.append(peer)
+        network = Network.from_json(currency, json_data['peers'])
 
-        community = cls(currency, peers)
+        community = cls(currency, network)
         return community
 
     def load_cache(self, json_data):
@@ -206,48 +170,9 @@ class Community(object):
             if '404' in e:
                 return 0
 
-    def _peering_traversal(self, peer, found_peers, traversed_pubkeys):
-        logging.debug("Read {0} peering".format(peer.pubkey))
-        traversed_pubkeys.append(peer.pubkey)
-        if peer.currency == self.currency and \
-            peer.pubkey not in [p.pubkey for p in found_peers]:
-            found_peers.append(peer)
-        try:
-            e = next(e for e in peer.endpoints if type(e) is BMAEndpoint)
-            next_peers = bma.network.peering.Peers(e.conn_handler()).get()
-            for p in next_peers:
-                next_peer = Peer.from_signed_raw("{0}{1}\n".format(p['value']['raw'],
-                                                            p['value']['signature']))
-                logging.debug(traversed_pubkeys)
-                logging.debug("Traversing : next to read : {0} : {1}".format(next_peer.pubkey,
-                              (next_peer.pubkey not in traversed_pubkeys)))
-                if next_peer.pubkey not in traversed_pubkeys:
-                    self._peering_traversal(next_peer, found_peers, traversed_pubkeys)
-        except Timeout:
-            pass
-        except ConnectionError:
-            pass
-        except ValueError:
-            pass
-        except RequestException as e:
-            pass
-
-    def peering(self):
-        peers = []
-        traversed_pubkeys = []
-        for p in self.peers:
-            logging.debug(traversed_pubkeys)
-            logging.debug("Peering : next to read : {0} : {1}".format(p.pubkey,
-                          (p.pubkey not in traversed_pubkeys)))
-            if p.pubkey not in traversed_pubkeys:
-                self._peering_traversal(p, peers, traversed_pubkeys)
-
-        logging.debug("Peers found : {0}".format(peers))
-        return peers
-
     @property
     def nodes(self):
-        return self._nodes
+        return self._network.all_nodes
 
     def get_block(self, number=None):
         if number is None:
@@ -287,10 +212,10 @@ class Community(object):
         if cached:
             return self._cache.request(request, req_args, get_args)
         else:
-            for peer in self.peers.copy():
-                e = next(e for e in peer.endpoints if type(e) is BMAEndpoint)
+            nodes = self._network.online_nodes
+            for node in nodes:
                 try:
-                    req = request(e.conn_handler(), **req_args)
+                    req = request(node.endpoint.conn_handler(), **req_args)
                     data = req.get(**get_args)
 
                     if inspect.isgenerator(data):
@@ -306,40 +231,33 @@ class Community(object):
                     else:
                         raise
                 except RequestException:
-                    # Move the timeout peer to the end
-                    self.peers.remove(peer)
-                    self.peers.append(peer)
                     continue
 
-        raise NoPeerAvailable(self.currency, len(self.peers))
+        raise NoPeerAvailable(self.currency, len(nodes))
 
     def post(self, request, req_args={}, post_args={}):
-        for peer in self.peers:
-            e = next(e for e in peer.endpoints if type(e) is BMAEndpoint)
-            logging.debug("Trying to connect to : " + peer.pubkey)
-            req = request(e.conn_handler(), **req_args)
+        nodes = self._network.online_nodes
+        for node in nodes:
+            req = request(node.endpoint.conn_handler(), **req_args)
+            logging.debug("Trying to connect to : " + node.pubkey)
+            req = request(node.endpoint.conn_handler(), **req_args)
             try:
                 req.post(**post_args)
                 return
             except ValueError as e:
                 raise
             except RequestException:
-                # Move the timeout peer to the end
-                self.peers.remove(peer)
-                self.peers.append(peer)
                 continue
-            except:
-                raise
-        raise NoPeerAvailable(self.currency, len(self.peers))
+        raise NoPeerAvailable(self.currency, len(nodes))
 
     def broadcast(self, request, req_args={}, post_args={}):
         tries = 0
         ok = False
         value_error = None
-        for peer in self.peers:
-            e = next(e for e in peer.endpoints if type(e) is BMAEndpoint)
-            logging.debug("Trying to connect to : " + peer.pubkey)
-            req = request(e.conn_handler(), **req_args)
+        nodes = self._network.online_nodes
+        for node in nodes:
+            logging.debug("Trying to connect to : " + node.pubkey)
+            req = request(node.endpoint.conn_handler(), **req_args)
             try:
                 req.post(**post_args)
                 ok = True
@@ -348,32 +266,17 @@ class Community(object):
                 continue
             except RequestException:
                 tries = tries + 1
-                # Move the timeout peer to the end
-                self.peers.remove(peer)
-                self.peers.append(peer)
                 continue
-            except:
-                raise
 
         if not ok:
             raise value_error
 
         if tries == len(self.peers):
-            raise NoPeerAvailable(self.currency, len(self.peers))
-
-    def jsonify_peers_list(self):
-        data = []
-        for peer in self.peers:
-            endpoints_data = []
-            for e in peer.endpoints:
-                endpoints_data.append(e.inline())
-            data.append({'endpoints': endpoints_data,
-                         'pubkey': peer.pubkey})
-        return data
+            raise NoPeerAvailable(self.currency, len(nodes))
 
     def jsonify(self):
         data = {'currency': self.currency,
-                'peers': self.jsonify_peers_list()}
+                'peers': self.network.jsonify()}
         return data
 
     def get_parameters(self):
diff --git a/src/cutecoin/core/network/__init__.py b/src/cutecoin/core/net/__init__.py
similarity index 100%
rename from src/cutecoin/core/network/__init__.py
rename to src/cutecoin/core/net/__init__.py
diff --git a/src/cutecoin/core/net/network.py b/src/cutecoin/core/net/network.py
new file mode 100644
index 00000000..07d4af18
--- /dev/null
+++ b/src/cutecoin/core/net/network.py
@@ -0,0 +1,81 @@
+'''
+Created on 24 févr. 2015
+
+@author: inso
+'''
+
+from ucoinpy.documents.peer import Peer, BMAEndpoint
+from ucoinpy.api import bma
+
+from .node import Node
+
+import logging
+import time
+
+from PyQt5.QtCore import QObject, pyqtSignal
+
+
+class Network(QObject):
+    '''
+    classdocs
+    '''
+    nodes_changed = pyqtSignal()
+
+    def __init__(self, currency, nodes):
+        '''
+        Constructor
+        '''
+        self.currency = currency
+        self._nodes = nodes
+        #TODO: Crawl nodes at startup
+
+    @classmethod
+    def from_json(cls, currency, json_data):
+        nodes = []
+        for data in json_data:
+            node = Node.from_json(currency, data)
+            nodes.append(node)
+        block_max = max([n.block for n in nodes])
+        for node in nodes:
+            node.check_sync(currency, block_max)
+        return cls(currency, nodes)
+
+    def jsonify(self):
+        data = []
+        for node in self.nodes:
+            data.append(node.jsonify())
+        return data
+
+    @property
+    def online_nodes(self):
+        return [n for n in self._nodes if n.state == Node.ONLINE]
+
+    @property
+    def all_nodes(self):
+        return self._nodes.copy()
+
+    def perpetual_crawling(self):
+        while self.must_crawl:
+            self.crawling(interval=10)
+
+    def crawling(self, interval=0):
+        nodes = []
+        traversed_pubkeys = []
+        for n in self._nodes:
+            logging.debug(traversed_pubkeys)
+            logging.debug("Peering : next to read : {0} : {1}".format(n.pubkey,
+                          (n.pubkey not in traversed_pubkeys)))
+            if n.pubkey not in traversed_pubkeys:
+                n.peering_traversal(self.currency, nodes,
+                                    traversed_pubkeys, interval)
+                time.sleep(interval)
+
+        block_max = max([n.block for n in nodes])
+        for node in [n for n in nodes if n.state == Node.ONLINE]:
+            node.check_sync(block_max)
+
+        #TODO: Offline nodes for too long have to be removed
+        #TODO: Corrupted nodes should maybe be removed faster ?
+
+        logging.debug("Nodes found : {0}".format(nodes))
+        return nodes
diff --git a/src/cutecoin/core/net/node.py b/src/cutecoin/core/net/node.py
new file mode 100644
index 00000000..340c7342
--- /dev/null
+++ b/src/cutecoin/core/net/node.py
@@ -0,0 +1,112 @@
+'''
+Created on 21 févr. 2015
+
+@author: inso
+'''
+
+from ucoinpy.documents.peer import Peer, BMAEndpoint, Endpoint
+from ucoinpy.api import bma
+from requests.exceptions import RequestException
+from ...core.person import Person
+from ...tools.exceptions import PersonNotFoundError
+import logging
+import time
+
+
+class Node(object):
+    '''
+    classdocs
+    '''
+
+    ONLINE = 1
+    OFFLINE = 2
+    DESYNCED = 3
+    CORRUPTED = 4
+
+    def __init__(self, endpoints, pubkey, block, state):
+        '''
+        Constructor
+        '''
+        self._endpoints = endpoints
+        self._pubkey = pubkey
+        self._block = block
+        self._state = state
+
+    @classmethod
+    def from_peer(cls, currency, peer):
+        node = cls(peer.endpoints, "", 0, Node.ONLINE)
+        node.refresh_state(currency)
+        return node
+
+    @classmethod
+    def from_json(cls, currency, data):
+        endpoints = []
+        for endpoint_data in data['endpoints']:
+            endpoints.append(Endpoint.from_inline(endpoint_data))
+
+        node = cls(endpoints, "", 0, Node.ONLINE)
+        node.refresh_state(currency)
+        return node
+
+    @property
+    def pubkey(self):
+        return self._pubkey
+
+    @property
+    def endpoint(self):
+        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
+
+    def check_sync(self, currency, block):
+        if self._block < block:
+            self._state = Node.DESYNCED
+        else:
+            self._state = Node.ONLINE
+
+    def refresh_state(self, currency):
+        try:
+            informations = bma.network.Peering(self.endpoint.conn_handler()).get()
+            block = bma.blockchain.Current(self.endpoint.conn_handler()).get()
+            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
+
+        if node_currency != currency:
+            self.state = Node.CORRUPTED
+
+        self._block = block_number
+        self._pubkey = node_pubkey
+
+    def peering_traversal(self, currency, found_nodes, traversed_pubkeys, interval):
+        logging.debug("Read {0} peering".format(self.pubkey))
+        traversed_pubkeys.append(self.pubkey)
+        self.refresh_state(currency)
+        if self.pubkey not in [n.pubkey for n in found_nodes]:
+            found_nodes.append(self)
+
+        try:
+            next_peers = bma.network.peering.Peers(self.endpoint.conn_handler()).get()
+            for p in next_peers:
+                next_peer = Peer.from_signed_raw("{0}{1}\n".format(p['value']['raw'],
+                                                            p['value']['signature']))
+                logging.debug(traversed_pubkeys)
+                logging.debug("Traversing : next to read : {0} : {1}".format(next_peer.pubkey,
+                              (next_peer.pubkey not in traversed_pubkeys)))
+                next_node = Node.from_peer(next_peer)
+                if next_node.pubkey not in traversed_pubkeys:
+                    next_node.peering_traversal(currency, found_nodes, traversed_pubkeys)
+                    time.sleep(interval)
+        except RequestException as e:
+            self._state = Node.OFFLINE
diff --git a/src/cutecoin/core/network/node.py b/src/cutecoin/core/network/node.py
deleted file mode 100644
index 10e6f391..00000000
--- a/src/cutecoin/core/network/node.py
+++ /dev/null
@@ -1,50 +0,0 @@
-'''
-Created on 21 févr. 2015
-
-@author: inso
-'''
-
-from ucoinpy.documents.peer import Peer, BMAEndpoint
-from ucoinpy.api import bma
-from ...core.person import Person
-from ...tools.exceptions import PersonNotFoundError
-
-
-class Node(object):
-    '''
-    classdocs
-    '''
-
-    def __init__(self, endpoint, pubkey, block):
-        '''
-        Constructor
-        '''
-        self._endpoint = endpoint
-        self._pubkey = pubkey
-        self._block = block
-
-    @classmethod
-    def from_peer(cls, peer):
-        e = next((e for e in peer.endpoints if type(e) is BMAEndpoint))
-        informations = bma.network.Peering(e.conn_handler()).get()
-        try:
-            block = bma.blockchain.Current(e.conn_handler()).get()
-            block_number = block["number"]
-        except ValueError as e:
-            if '404' in e:
-                block_number = 0
-        node_pubkey = informations["pubkey"]
-
-        return cls(e, node_pubkey, block_number)
-
-    @property
-    def pubkey(self):
-        return self._pubkey
-
-    @property
-    def endpoint(self):
-        return self._endpoint
-
-    @property
-    def block(self):
-        return self._block
diff --git a/src/cutecoin/models/network.py b/src/cutecoin/models/network.py
index dc72f09a..907853e6 100644
--- a/src/cutecoin/models/network.py
+++ b/src/cutecoin/models/network.py
@@ -117,6 +117,7 @@ class NetworkTableModel(QAbstractTableModel):
         node = self.nodes[row]
         if role == Qt.DisplayRole:
             return self.data_node(node)[col]
+        #TODO: Display colors depending on node state
 
     def flags(self, index):
         return Qt.ItemIsSelectable | Qt.ItemIsEnabled
-- 
GitLab