diff --git a/src/sakia/data/connectors/node.py b/src/sakia/data/connectors/node.py index 15fa59d84ab3837b2ec44624b4626c1f332276da..039b6428e795aba433a39ea9e1bf85adb0bd9c19 100644 --- a/src/sakia/data/connectors/node.py +++ b/src/sakia/data/connectors/node.py @@ -210,7 +210,6 @@ class NodeConnector(QObject): :param dict block_data: The block data in json format """ if not self.node.current_buid or self.node.current_buid.sha_hash != block_data['hash']: - new_state = self.node.state for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: conn_handler = next(endpoint.conn_handler(self.session, proxy=self._user_parameters.proxy())) @@ -222,17 +221,14 @@ class NodeConnector(QObject): if not previous_block: continue self.node.previous_buid = BlockUID(previous_block['number'], previous_block['hash']) - new_state = Node.ONLINE break # Do not try any more endpoint except errors.DuniterError as e: if e.ucode == errors.BLOCK_NOT_FOUND: self.node.previous_buid = BlockUID.empty() - self.node.current_buid = BlockUID.empty() # we don't change state here - new_state = Node.ONLINE break else: - new_state = Node.CORRUPTED + self.change_state_and_emit(Node.CORRUPTED) break self._logger.debug("Error in previous block reply of {0} : {1}".format(self.node.pubkey[:5], str(e))) @@ -242,8 +238,7 @@ class NodeConnector(QObject): self.node.current_ts = block_data['medianTime'] self._logger.debug("Changed block {0} -> {1}".format(self.node.current_buid.number, block_data['number'])) - self.change_state_and_emit(new_state) - + self.changed.emit() else: self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) self.change_state_and_emit(Node.OFFLINE) diff --git a/src/sakia/data/entities/node.py b/src/sakia/data/entities/node.py index f9e5e16282fe016393cb233f70af6cd660a6083b..bf0f0ba462d3d49480c7ff07ba749699a64b166a 100644 --- a/src/sakia/data/entities/node.py +++ b/src/sakia/data/entities/node.py @@ -33,7 +33,6 @@ class Node: ONLINE = 1 OFFLINE = 2 - DESYNCED = 3 CORRUPTED = 4 # The currency handled by this node diff --git a/src/sakia/data/processors/nodes.py b/src/sakia/data/processors/nodes.py index 3040f1dc582132e58a655e18479c98f84573c5df..d8bcf4e94a64ea99b18bf1f6f74d635cf2055790 100644 --- a/src/sakia/data/processors/nodes.py +++ b/src/sakia/data/processors/nodes.py @@ -29,27 +29,28 @@ class NodesProcessor: Get current buid :param str currency: """ - synced_node = self._repo.get_one(currency=currency, state=Node.ONLINE) - return synced_node.current_buid + current_buid = self._repo.current_buid(currency=currency) + return current_buid def synced_nodes(self, currency): """ Get nodes which are in the ONLINE state. """ - return self._repo.get_all(currency=currency, state=Node.ONLINE) + current_buid = self._repo.current_buid(currency=currency) + return self._repo.get_all(currency=currency, state=Node.ONLINE, current_buid=current_buid) def synced_members_nodes(self, currency): """ Get nodes which are in the ONLINE state. """ - return self._repo.get_all(currency=currency, state=Node.ONLINE, member=True) + current_buid = self._repo.current_buid(currency=currency) + return self._repo.get_all(currency=currency, state=Node.ONLINE, member=True, current_buid=current_buid) def online_nodes(self, currency): """ Get nodes which are in the ONLINE state. """ - return self._repo.get_all(currency=currency, state=Node.ONLINE) + \ - self._repo.get_all(currency=currency, state=Node.DESYNCED) + return self._repo.get_all(currency=currency, state=Node.ONLINE) def delete_node(self, node): self._repo.drop(node) diff --git a/src/sakia/data/repositories/003_add_last_state_change_property.sql b/src/sakia/data/repositories/003_add_last_state_change_property.sql index 36a7b90dd872b3365af15c5d84ac64719e24aee6..3214f7c37ec8cc4e5e31c1d309108029f64cf2e5 100644 --- a/src/sakia/data/repositories/003_add_last_state_change_property.sql +++ b/src/sakia/data/repositories/003_add_last_state_change_property.sql @@ -2,5 +2,6 @@ BEGIN TRANSACTION ; ALTER TABLE Nodes ADD COLUMN last_state_change INTEGER DEFAULT 0; UPDATE NODES SET last_state_change=strftime('%s', 'now'); +UPDATE NODES SET state=1 WHERE state=3 ; COMMIT; diff --git a/src/sakia/data/repositories/nodes.py b/src/sakia/data/repositories/nodes.py index 0d4949125dff18cdad524d53423da1db90dca183..5772f4ffba3efadc62939c5b7dfec6c38bf95547 100644 --- a/src/sakia/data/repositories/nodes.py +++ b/src/sakia/data/repositories/nodes.py @@ -1,5 +1,6 @@ import attr +from duniterpy.documents import BlockUID, block_uid from ..entities import Node @@ -109,3 +110,16 @@ class NodesRepo: self._conn.execute("""DELETE FROM nodes WHERE currency=? AND pubkey=?""", where_fields) + + def current_buid(self, currency): + c = self._conn.execute("""SELECT `current_buid`, + COUNT(`current_buid`) AS `value_occurrence` + FROM `nodes` + WHERE state == 1 AND member == 1 AND currency == ? + GROUP BY `current_buid` + ORDER BY `value_occurrence` DESC + LIMIT 1;""", (currency,)) + data = c.fetchone() + if data: + return block_uid(data[0]) + return BlockUID.empty() diff --git a/src/sakia/gui/navigation/network/table_model.py b/src/sakia/gui/navigation/network/table_model.py index 6b3d00c106b1514d02c899d91e4225a60ff588e3..be51f975a4860806c03a5a94d7b561973b89be7a 100644 --- a/src/sakia/gui/navigation/network/table_model.py +++ b/src/sakia/gui/navigation/network/table_model.py @@ -19,6 +19,10 @@ class NetworkFilterProxyModel(QSortFilterProxyModel): """ left_data = self.sourceModel().data(left, Qt.DisplayRole) right_data = self.sourceModel().data(right, Qt.DisplayRole) + + if left.column() == NetworkTableModel.columns_types.index('pubkey'): + return left_data < right_data + if left.column() == NetworkTableModel.columns_types.index('port'): left_data = int(left_data.split('\n')[0]) if left_data != '' else 0 right_data = int(right_data.split('\n')[0]) if right_data != '' else 0 @@ -29,7 +33,8 @@ class NetworkFilterProxyModel(QSortFilterProxyModel): right_data = int(right_data) if right_data != '' else 0 if left_data == right_data: pubkey_col = NetworkTableModel.columns_types.index('pubkey') - return self.lessThan(self.index(left.row(), pubkey_col), self.index(right.row(), pubkey_col)) + return self.lessThan(self.sourceModel().index(left.row(), pubkey_col), + self.sourceModel().index(right.row(), pubkey_col)) return left_data < right_data @@ -123,22 +128,26 @@ class NetworkTableModel(QAbstractTableModel): 'is_root', 'state' ) + + DESYNCED = 3 + node_colors = { Node.ONLINE: QColor('#99ff99'), Node.OFFLINE: QColor('#ff9999'), - Node.DESYNCED: QColor('#ffbd81'), + DESYNCED: QColor('#ffbd81'), Node.CORRUPTED: QColor(Qt.lightGray) } + node_icons = { Node.ONLINE: ":/icons/synchronized", Node.OFFLINE: ":/icons/offline", - Node.DESYNCED: ":/icons/forked", + DESYNCED: ":/icons/forked", Node.CORRUPTED: ":/icons/corrupted" } node_states = { Node.ONLINE: lambda: QT_TRANSLATE_NOOP("NetworkTableModel", 'Online'), Node.OFFLINE: lambda: QT_TRANSLATE_NOOP("NetworkTableModel", 'Offline'), - Node.DESYNCED: lambda: QT_TRANSLATE_NOOP("NetworkTableModel", 'Unsynchronized'), + DESYNCED: lambda: QT_TRANSLATE_NOOP("NetworkTableModel", 'Unsynchronized'), Node.CORRUPTED: lambda: QT_TRANSLATE_NOOP("NetworkTableModel", 'Corrupted') } @@ -155,8 +164,9 @@ class NetworkTableModel(QAbstractTableModel): self.network_service.node_changed.connect(self.change_node) self.network_service.node_removed.connect(self.remove_node) self.network_service.new_node_found.connect(self.add_node) + self.network_service.latest_block_changed.connect(self.init_nodes) - def data_node(self, node: Node) -> tuple: + def data_node(self, node: Node, current_buid=None) -> tuple: """ Return node data tuple :param ..core.net.node.Node node: Network node @@ -181,10 +191,16 @@ class NetworkTableModel(QAbstractTableModel): number, block_hash, block_time = node.current_buid.number, node.current_buid.sha_hash, node.current_ts else: number, block_hash, block_time = "", "", "" + state = node.state + if not current_buid: + current_buid = self.network_service.current_buid() + if node.state == Node.ONLINE and node.current_buid != current_buid: + state = NetworkTableModel.DESYNCED + return (address, port, number, block_hash, block_time, node.uid, - node.member, node.pubkey, node.software, node.version, node.root, node.state) + node.member, node.pubkey, node.software, node.version, node.root, state) - def init_nodes(self): + def init_nodes(self, current_buid=None): self.beginResetModel() self.nodes_data = [] nodes_data = [] diff --git a/src/sakia/services/network.py b/src/sakia/services/network.py index a22f135269938411a1400ee28e8acf6bd3db2640..1da6ec50b8c70a59fab9ca0a33c66350a7e090c7 100644 --- a/src/sakia/services/network.py +++ b/src/sakia/services/network.py @@ -1,10 +1,10 @@ import asyncio import logging import time -from collections import Counter from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt from duniterpy.api import errors +from duniterpy.documents import BlockUID from duniterpy.key import VerifyingKey from sakia.data.connectors import NodeConnector from sakia.data.entities import Node @@ -20,6 +20,7 @@ class NetworkService(QObject): node_changed = pyqtSignal(Node) new_node_found = pyqtSignal(Node) node_removed = pyqtSignal(Node) + latest_block_changed = pyqtSignal(BlockUID) root_nodes_changed = pyqtSignal() def __init__(self, app, currency, node_processor, connectors, blockchain_service, identities_service): @@ -99,6 +100,9 @@ class NetworkService(QObject): def commit_node(self, node): self._processor.commit_node(node) + def current_buid(self): + return self._processor.current_buid(self.currency) + async def stop_coroutines(self, closing=False): """ Stop network nodes crawling. @@ -116,53 +120,6 @@ class NetworkService(QObject): def continue_crawling(self): return self._must_crawl - def check_nodes_sync(self, node): - """ - Check nodes sync with the following rules : - 1 : The block of the majority - 2 : The more last different issuers - 3 : The more difficulty - 4 : The biggest number or timestamp - """ - online_nodes = self._processor.online_nodes(self.currency) - # rule number 1 : block of the majority - blocks = [n.current_buid.sha_hash for n in online_nodes if n.current_buid.sha_hash] - blocks_occurences = Counter(blocks) - blocks_by_occurences = {} - for key, value in blocks_occurences.items(): - the_block = [n.current_buid.sha_hash - for n in online_nodes if n.current_buid.sha_hash == key][0] - if value not in blocks_by_occurences: - blocks_by_occurences[value] = [the_block] - else: - blocks_by_occurences[value].append(the_block) - - if len(blocks_by_occurences) == 0: - for n in [n for n in online_nodes if n.state in (Node.ONLINE, Node.DESYNCED)]: - if n.state != Node.ONLINE: - n.state = Node.ONLINE - self._processor.update_node(n) - self.node_changed.emit(n) - if n == node: - node.state = n.state - else: - most_present = max(blocks_by_occurences.keys()) - - synced_block_hash = blocks_by_occurences[most_present][0] - - for n in online_nodes: - if n.current_buid.sha_hash == synced_block_hash: - if n.state != Node.ONLINE: - n.state = Node.ONLINE - self.node_changed.emit(n) - elif n.state != Node.DESYNCED: - n.state = Node.DESYNCED - self.node_changed.emit(n) - self._processor.update_node(n) - if n == node: - node.state = n.state - return node - def add_connector(self, node_connector): """ Add a nod to the network. @@ -265,8 +222,6 @@ class NetworkService(QObject): def handle_change(self): node_connector = self.sender() - - node_connector.node = self.check_nodes_sync(node_connector.node) self._processor.update_node(node_connector.node) self.node_changed.emit(node_connector.node) @@ -275,6 +230,7 @@ class NetworkService(QObject): self._logger.debug("{0} -> {1}".format(self._block_found.sha_hash[:10], current_buid.sha_hash[:10])) if self._block_found.sha_hash != current_buid.sha_hash: self._logger.debug("Latest block changed : {0}".format(current_buid.number)) + self.latest_block_changed.emit(self._block_found) # If new latest block is lower than the previously found one # or if the previously found block is different locally # than in the main chain, we declare a rollback